Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b45a902
security review github action
shashank-factory Dec 29, 2025
7da0757
chore: remove formatting-only changes
shashank-factory Dec 29, 2025
f346d30
security skills scanning
shashank-factory Jan 2, 2026
105eba0
fix: remove invalid git tool identifiers from security review
shashank-factory Jan 2, 2026
752a9c2
chore: remove formatting-only changes from security review feature
shashank-factory Jan 2, 2026
5f6d3c5
fix: address review feedback for security scan
shashank-factory Jan 3, 2026
409e980
fix: correct skills paths from security/ to skills/ in Factory-AI/ski…
shashank-factory Jan 3, 2026
bd513b4
feat: change @droid security-review command to @droid security
shashank-factory Jan 3, 2026
f248d11
feat: show 'running a security check' message for security commands
shashank-factory Jan 3, 2026
9b65808
feat: consolidate review output to single tracking comment
shashank-factory Jan 3, 2026
a8ef7db
feat: add modular composite actions for parallel review workflow
shashank-factory Jan 3, 2026
a6b7426
fix: allow parallel automatic code and security reviews
shashank-factory Jan 3, 2026
91d0aa3
fix: simplify detect step to use prepare outputs
shashank-factory Jan 3, 2026
cd4e10a
fix: each job obtains its own GitHub token via OIDC
shashank-factory Jan 3, 2026
e239e45
fix: use heredoc format for multi-line GITHUB_OUTPUT
shashank-factory Jan 3, 2026
91ebe00
fix: use core.setOutput instead of manual GITHUB_OUTPUT writing
shashank-factory Jan 3, 2026
3cd3339
fix: exclude MCP tools from --enabled-tools flag
shashank-factory Jan 3, 2026
e4fa2be
feat: separate inline comments to finalize step to avoid overlaps
shashank-factory Jan 3, 2026
93491a2
fix: remove github_pr___submit_review from combine step to prevent du…
shashank-factory Jan 3, 2026
eb47cb5
fix: use 'Code review completed' title for combined summary
shashank-factory Jan 3, 2026
7422343
feat: add @droid review/security command triggers for parallel workflow
shashank-factory Jan 3, 2026
57c8669
updated prompt
shashank-factory Jan 7, 2026
8ca3f06
formatted
shashank-factory Jan 7, 2026
f73f7ea
added github workflow
shashank-factory Jan 7, 2026
c10a055
merged dev
shashank-factory Jan 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 22 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

This GitHub Action powers the Factory **Droid** app. It watches your pull requests for the two supported commands and runs a full Droid Exec session to help you ship faster:

* `@droid fill` — turns a bare pull request into a polished description that matches your template or our opinionated fallback.
* `@droid review` — performs an automated code review, surfaces potential bugs, and leaves inline comments directly on the diff.
- `@droid fill` — turns a bare pull request into a polished description that matches your template or our opinionated fallback.
- `@droid review` — performs an automated code review, surfaces potential bugs, and leaves inline comments directly on the diff.

Everything runs inside GitHub Actions using your Factory API key, so the bot never leaves your repository and operates with the permissions you grant.

Expand All @@ -18,11 +18,11 @@ Everything runs inside GitHub Actions using your Factory API key, so the bot nev
## Installation

1. **Install the Droid GitHub App**
* Install from the Factory dashboard and grant it access to the repositories where you want Droid to operate.
- Install from the Factory dashboard and grant it access to the repositories where you want Droid to operate.
2. **Create a Factory API Key**
* Generate a token at [https://app.factory.ai/settings/api-keys](https://app.factory.ai/settings/api-keys) and save it as `FACTORY_API_KEY` in your repository or organization secrets.
- Generate a token at [https://app.factory.ai/settings/api-keys](https://app.factory.ai/settings/api-keys) and save it as `FACTORY_API_KEY` in your repository or organization secrets.
3. **Add the Action Workflows**
* Create two workflow files under `.github/workflows/` to separate on-demand tagging from automatic PR reviews.
- Create two workflow files under `.github/workflows/` to separate on-demand tagging from automatic PR reviews.

`droid.yml` (responds to explicit `@droid` mentions):

Expand Down Expand Up @@ -105,26 +105,28 @@ Once committed, tagging `@droid fill` or `@droid review` on an open PR will trig
## Using the Commands

### `@droid fill`
* Place the command in the PR description or in a top-level comment.
* Droid searches for common PR template locations (`.github/pull_request_template.md`, etc.). When a template exists, it fills the sections; otherwise it writes a structured summary (overview, changes, testing, rollout).
* The original request is replaced with the generated description so reviewers can merge immediately.

- Place the command in the PR description or in a top-level comment.
- Droid searches for common PR template locations (`.github/pull_request_template.md`, etc.). When a template exists, it fills the sections; otherwise it writes a structured summary (overview, changes, testing, rollout).
- The original request is replaced with the generated description so reviewers can merge immediately.

### `@droid review`
* Mention `@droid review` in a PR comment.
* Droid inspects the diff, prioritizes potential bugs or high-impact issues, and leaves inline comments directly on the changed lines.
* A short summary comment is posted in the original thread highlighting the findings and linking to any inline feedback.

- Mention `@droid review` in a PR comment.
- Droid inspects the diff, prioritizes potential bugs or high-impact issues, and leaves inline comments directly on the changed lines.
- A short summary comment is posted in the original thread highlighting the findings and linking to any inline feedback.

## Configuration Essentials

| Input | Purpose |
| --- | --- |
| `factory_api_key` | **Required.** Grants Droid Exec permission to run via Factory. |
| `github_token` | Optional override if you prefer a custom GitHub App/token. By default the installed app token is used. |
| `review_model` | Optional. Override the model used for code review (e.g., `claude-sonnet-4-5-20250929`, `gpt-5.1-codex`). Only applies to review flows. |
| `fill_model` | Optional. Override the model used for PR description fill (e.g., `claude-sonnet-4-5-20250929`, `gpt-5.1-codex`). Only applies to fill flows. |
| Input | Purpose |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `factory_api_key` | **Required.** Grants Droid Exec permission to run via Factory. |
| `github_token` | Optional override if you prefer a custom GitHub App/token. By default the installed app token is used. |
| `review_model` | Optional. Override the model used for code review (e.g., `claude-sonnet-4-5-20250929`, `gpt-5.1-codex`). Only applies to review flows. |
| `fill_model` | Optional. Override the model used for PR description fill (e.g., `claude-sonnet-4-5-20250929`, `gpt-5.1-codex`). Only applies to fill flows. |

## Troubleshooting & Support

* Check the workflow run linked from the Droid tracking comment for execution logs.
* Verify that the workflow file and repository allow the GitHub App to run (branch protections can block bots).
* Need more detail? Start with the [Setup Guide](./docs/setup.md) or [FAQ](./docs/faq.md).
- Check the workflow run linked from the Droid tracking comment for execution logs.
- Verify that the workflow file and repository allow the GitHub App to run (branch protections can block bots).
- Need more detail? Start with the [Setup Guide](./docs/setup.md) or [FAQ](./docs/faq.md).
76 changes: 76 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,38 @@ inputs:
description: "Automatically run the review flow for pull request contexts without requiring an explicit @droid review command. Only supported for PR-related events."
required: false
default: "false"
automatic_security_review:
description: "Automatically run the security review flow for pull request contexts without requiring an explicit @droid security-review command. Only supported for PR-related events."
required: false
default: "false"
security_model:
description: "Override the model used for security review (e.g., 'claude-sonnet-4-5-20250929', 'gpt-5.1-codex'). Only applies to security review flows."
required: false
default: ""
security_severity_threshold:
description: "Minimum severity to report in security reviews (critical, high, medium, low). Findings below this threshold will be filtered out."
required: false
default: "medium"
security_block_on_critical:
description: "Submit REQUEST_CHANGES review when critical severity findings are detected."
required: false
default: "true"
security_block_on_high:
description: "Submit REQUEST_CHANGES review when high severity findings are detected."
required: false
default: "false"
security_notify_team:
description: "GitHub team to @mention on critical findings (e.g., '@org/security-team')."
required: false
default: ""
security_scan_schedule:
description: "Enable scheduled security scans. Set to 'true' for schedule events to trigger full repository scans."
required: false
default: "false"
security_scan_days:
description: "Number of days of commits to scan for scheduled security scans. Only applies when security_scan_schedule is enabled."
required: false
default: "7"
review_model:
description: "Override the model used for code review (e.g., 'claude-sonnet-4-5-20250929', 'gpt-5.1-codex'). Only applies to review flows."
required: false
Expand Down Expand Up @@ -131,6 +163,14 @@ runs:
DEFAULT_WORKFLOW_TOKEN: ${{ github.token }}
TRACK_PROGRESS: ${{ inputs.track_progress }}
AUTOMATIC_REVIEW: ${{ inputs.automatic_review }}
AUTOMATIC_SECURITY_REVIEW: ${{ inputs.automatic_security_review }}
SECURITY_MODEL: ${{ inputs.security_model }}
SECURITY_SEVERITY_THRESHOLD: ${{ inputs.security_severity_threshold }}
SECURITY_BLOCK_ON_CRITICAL: ${{ inputs.security_block_on_critical }}
SECURITY_BLOCK_ON_HIGH: ${{ inputs.security_block_on_high }}
SECURITY_NOTIFY_TEAM: ${{ inputs.security_notify_team }}
SECURITY_SCAN_SCHEDULE: ${{ inputs.security_scan_schedule }}
SECURITY_SCAN_DAYS: ${{ inputs.security_scan_days }}
REVIEW_MODEL: ${{ inputs.review_model }}
FILL_MODEL: ${{ inputs.fill_model }}
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
Expand Down Expand Up @@ -173,6 +213,42 @@ runs:
env:
EXPERIMENTAL_ALLOWED_DOMAINS: ${{ inputs.experimental_allowed_domains }}

- name: Install Security Skills
if: steps.prepare.outputs.contains_trigger == 'true' && steps.prepare.outputs.install_security_skills == 'true'
shell: bash
run: |
echo "Installing security skills from Factory-AI/skills..."
SKILLS_DIR="$HOME/.factory/skills"
mkdir -p "$SKILLS_DIR"

# Clone public skills repo (sparse checkout for efficiency)
TEMP_DIR=$(mktemp -d)
git clone --filter=blob:none --sparse \
"https://github.com/Factory-AI/skills.git" \
"$TEMP_DIR" 2>/dev/null || {
echo "Warning: Could not clone skills repo. Security skills will not be available."
exit 0
}

cd "$TEMP_DIR"
git sparse-checkout set \
security/threat-model-generation \
security/commit-security-scan \
security/vulnerability-validation \
security/security-patch-generation 2>/dev/null || true

# Copy skills to ~/.factory/skills/
for skill in threat-model-generation commit-security-scan vulnerability-validation security-patch-generation; do
if [ -d "security/$skill" ]; then
cp -r "security/$skill" "$SKILLS_DIR/"
echo " Installed skill: $skill"
fi
done

# Cleanup
rm -rf "$TEMP_DIR"
echo "Security skills installation complete"

- name: Run Droid Exec
id: droid
if: steps.prepare.outputs.contains_trigger == 'true'
Expand Down
3 changes: 1 addition & 2 deletions base-action/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ async function run() {
mcpTools: process.env.INPUT_MCP_TOOLS,
systemPrompt: process.env.INPUT_SYSTEM_PROMPT,
appendSystemPrompt: process.env.INPUT_APPEND_SYSTEM_PROMPT,
pathToDroidExecutable:
process.env.INPUT_PATH_TO_DROID_EXECUTABLE,
pathToDroidExecutable: process.env.INPUT_PATH_TO_DROID_EXECUTABLE,
showFullOutput: process.env.INPUT_SHOW_FULL_OUTPUT,
});
} catch (error) {
Expand Down
37 changes: 24 additions & 13 deletions base-action/src/run-droid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,12 @@ export async function runDroid(promptPath: string, options: DroidOptions) {
const cfg = JSON.parse(options.mcpTools);
const servers = cfg?.mcpServers || {};
const serverNames = Object.keys(servers);

if (serverNames.length > 0) {
console.log(`Registering ${serverNames.length} MCP servers: ${serverNames.join(", ")}`);

console.log(
`Registering ${serverNames.length} MCP servers: ${serverNames.join(", ")}`,
);

for (const [name, def] of Object.entries<any>(servers)) {
const cmd = [def.command, ...(def.args || [])]
.filter(Boolean)
Expand All @@ -143,12 +145,15 @@ export async function runDroid(promptPath: string, options: DroidOptions) {
.join(" ");

const addCmd = `droid mcp add ${name} "${cmd}" ${envFlags}`.trim();

try {
await execAsync(addCmd, { env: { ...process.env } });
console.log(` ✓ Registered MCP server: ${name}`);
} catch (e: any) {
console.error(` ✗ Failed to register MCP server ${name}:`, e.message);
console.error(
` ✗ Failed to register MCP server ${name}:`,
e.message,
);
throw e;
}
}
Expand Down Expand Up @@ -184,15 +189,19 @@ export async function runDroid(promptPath: string, options: DroidOptions) {
// Log custom arguments if any
if (options.droidArgs && options.droidArgs.trim() !== "") {
console.log(`Custom Droid arguments: ${options.droidArgs}`);

// Check for deprecated MCP tool naming
const enabledToolsMatch = options.droidArgs.match(/--enabled-tools\s+["\']?([^"\']+)["\']?/);
const enabledToolsMatch = options.droidArgs.match(
/--enabled-tools\s+["\']?([^"\']+)["\']?/,
);
if (enabledToolsMatch && enabledToolsMatch[1]) {
const tools = enabledToolsMatch[1].split(",").map(t => t.trim());
const oldStyleTools = tools.filter(t => t.startsWith("mcp__"));
const tools = enabledToolsMatch[1].split(",").map((t) => t.trim());
const oldStyleTools = tools.filter((t) => t.startsWith("mcp__"));

if (oldStyleTools.length > 0) {
console.warn(`Warning: Found ${oldStyleTools.length} tools with deprecated mcp__ prefix. Update to new pattern (e.g., github_comment___update_droid_comment)`);
console.warn(
`Warning: Found ${oldStyleTools.length} tools with deprecated mcp__ prefix. Update to new pattern (e.g., github_comment___update_droid_comment)`,
);
}
}
}
Expand Down Expand Up @@ -247,7 +256,10 @@ export async function runDroid(promptPath: string, options: DroidOptions) {
const parsed = JSON.parse(line);
if (!sessionId && typeof parsed === "object" && parsed !== null) {
const detectedSessionId = parsed.session_id;
if (typeof detectedSessionId === "string" && detectedSessionId.trim()) {
if (
typeof detectedSessionId === "string" &&
detectedSessionId.trim()
) {
sessionId = detectedSessionId;
console.log(`Detected Droid session: ${sessionId}`);
}
Expand All @@ -272,7 +284,6 @@ export async function runDroid(promptPath: string, options: DroidOptions) {
// In non-full-output mode, suppress non-JSON output
}
});

});

// Handle stdout errors
Expand Down
19 changes: 7 additions & 12 deletions base-action/test/parse-shell-args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,8 @@ describe("shell-quote parseShellArgs", () => {
});

test("should parse simple arguments", () => {
expect(parseShellArgs("--auto medium")).toEqual([
"--auto",
"medium",
]);
expect(parseShellArgs("-s session-123")).toEqual([
"-s",
"session-123",
]);
expect(parseShellArgs("--auto medium")).toEqual(["--auto", "medium"]);
expect(parseShellArgs("-s session-123")).toEqual(["-s", "session-123"]);
});

test("should handle double quotes", () => {
Expand All @@ -27,10 +21,11 @@ describe("shell-quote parseShellArgs", () => {
});

test("should handle single quotes", () => {
expect(parseShellArgs("--file '/tmp/prompt.md'"))
.toEqual(["--file", "/tmp/prompt.md"]);
expect(parseShellArgs("'arg with spaces'"))
.toEqual(["arg with spaces"]);
expect(parseShellArgs("--file '/tmp/prompt.md'")).toEqual([
"--file",
"/tmp/prompt.md",
]);
expect(parseShellArgs("'arg with spaces'")).toEqual(["arg with spaces"]);
});
test("should handle escaped characters", () => {
expect(parseShellArgs("arg\\ with\\ spaces")).toEqual(["arg with spaces"]);
Expand Down
Loading