Skip to content

Instantly share code, notes, and snippets.

@factory-ben
Created October 3, 2025 15:41
Show Gist options
  • Select an option

  • Save factory-ben/8ac1e71557a015efc34e8e9c46e55f7c to your computer and use it in GitHub Desktop.

Select an option

Save factory-ben/8ac1e71557a015efc34e8e9c46e55f7c to your computer and use it in GitHub Desktop.
Setting up droid exec code review on GitLab

Droid Code Review for GitLab

Follow these steps to run the Factory Droid automated code review inside GitLab merge requests.

  1. Create CI/CD variables In your project’s Settings → CI/CD → Variables, add:
  • FACTORY_API_KEY (masked, protects access to Factory).
  • GITLAB_TOKEN (masked, personal access token with the api scope for posting discussions).
  1. Ensure full git history Leave the variable GIT_DEPTH at 0 (the pipeline sets this automatically) so Droid can inspect the entire diff.

  2. Save the pipeline file Add the provided .gitlab-ci.yml to the repository root (or include it from your existing pipeline).

  3. Provide suitable runners Run the job on a Linux x86_64 runner with outbound internet access so it can install the Droid CLI and call the GitLab API.

  4. Open a merge request Pipelines triggered by merge request events will execute the Droid review. Findings appear as MR discussions; if none are found, Droid posts a “No issues” summary note once per MR.

stages:
- review
variables:
GIT_DEPTH: "0"
GIT_STRATEGY: fetch
droid_code_review:
stage: review
image: alpine:3.20
resource_group: "droid-review-$CI_MERGE_REQUEST_IID"
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
before_script:
- set -euo pipefail
- apk add --no-cache bash curl git jq python3
- curl -fsSL https://app.factory.ai/cli | sh
- export PATH="$HOME/.local/bin:$PATH"
- droid --version
- git config user.name "Droid Agent"
- git config user.email "droidagent@factory.ai"
- git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
script:
- set -euo pipefail
- git diff "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME...$CI_COMMIT_SHA" > diff.txt
- curl --fail --silent --show-error \
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
"$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes" \
-o existing_notes.json
- curl --fail --silent --show-error \
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
"$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/discussions?per_
page=100" \
-o discussions.json
- curl --fail --silent --show-error \
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
"$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/changes" \
| jq '[.changes[] | {filename: .new_path, patch: .diff}]' > files.json
- printf '%s' "${CI_MERGE_REQUEST_DESCRIPTION:-}" > mr_description.txt
- cat > prompt.txt <<'EOF'
You are an automated code review system for GitLab merge requests. Review the MR diff and report
actionable problems only.
Available inputs in the current directory:
- diff.txt: git diff between the target branch and this commit.
- files.json: array of {filename, patch} entries with diff hunks (use positions from patches).
- existing_notes.json: high-level notes already posted on the MR (avoid duplicates).
- discussions.json: inline discussion threads (use your replies here when relevant).
- mr_description.txt: merge request description for context.
Task: Produce comments.json with up to 10 issues. Each entry must be either:
1. New inline note:
{
"path": "path/to/file.js",
"line": 42,
"body": "Actionable feedback",
"position_type": "text"
}
• "line" is the new line number in the diff.
• Only comment on lines changed in this MR.
2. Reply to an existing discussion:
{
"discussion_id": "thread-id",
"note_id": 123456,
"body": "Follow-up message"
}
- discussion_id and note_id must come from discussions.json.
Focus on critical correctness issues such as:
- Dead/unreachable code
- Broken control flow or missing returns
- Async/await misuse
- Mutable React state / reducer bugs
- useEffect dependency mistakes
- Incorrect operators (== vs === etc.)
- Off-by-one errors and incorrect indexing
- Overflow/underflow risks
- Regex catastrophic backtracking
- Missing base cases in recursion
- Dangerous type coercion
- Null/undefined dereferences
- Resource leaks
- SQL/XSS injection or security risks
- Concurrency and race conditions
- Missing error handling in critical paths
Comment requirements:
- State the exact problem and why it matters.
- Provide a concrete fix (include code suggestion blocks when helpful).
- No style nitpicks or subjective opinions.
- Skip items already resolved in existing_notes.json or discussions.json.
If no issues exist, output [].
EOF
- droid exec --auto high --model claude-sonnet-4-5-20250929 -f prompt.txt
- if [ ! -f comments.json ]; then
echo "comments.json was not generated"; exit 1;
fi
- python3 - <<'PY'
import json, os, sys, urllib.request
api = os.environ["CI_API_V4_URL"]
project = os.environ["CI_PROJECT_ID"]
mr_iid = os.environ["CI_MERGE_REQUEST_IID"]
token = os.environ["GITLAB_TOKEN"]
base_sha = os.environ.get("CI_MERGE_REQUEST_DIFF_BASE_SHA") or os.environ.get("CI_COMMIT_BEFORE_SHA")
or ""
head_sha = os.environ.get("CI_COMMIT_SHA")
start_sha = base_sha
headers = {"PRIVATE-TOKEN": token, "Content-Type": "application/json"}
with open("comments.json", "r", encoding="utf-8") as f:
comments = json.load(f)
if not isinstance(comments, list):
print("comments.json is not a list; skipping submission")
sys.exit(0)
def request(method, endpoint, payload):
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
url=f"{api}/projects/{project}{endpoint}",
data=data,
headers=headers,
method=method
)
with urllib.request.urlopen(req) as resp:
resp.read()
new_notes = [c for c in comments if isinstance(c, dict) and "path" in c and "line" in c and "body" in
c]
replies = [c for c in comments if isinstance(c, dict) and "discussion_id" in c and "note_id" in c and
"body" in c]
for comment in new_notes:
position = {
"position_type": comment.get("position_type", "text"),
"base_sha": base_sha,
"start_sha": start_sha,
"head_sha": head_sha,
"new_path": comment["path"],
"new_line": comment["line"],
}
payload = {"body": comment["body"], "position": position}
request("POST", f"/merge_requests/{mr_iid}/discussions", payload)
for reply in replies:
payload = {"body": reply["body"], "in_reply_to_id": reply["note_id"]}
request("POST", f"/merge_requests/{mr_iid}/discussions/{reply['discussion_id']}/notes", payload)
if not new_notes and not replies:
with open("existing_notes.json", "r", encoding="utf-8") as f:
existing = json.load(f)
has_no_issues = any(
isinstance(note, dict)
and isinstance(note.get("author"), dict)
and "[bot]" in note["author"].get("username", "")
and note.get("body") and ("no issues found" in note["body"].lower() or "✅" in note["body"])
for note in existing
)
if not has_no_issues:
payload = {"body": "✅ No issues found in the current changes."}
request("POST", f"/merge_requests/{mr_iid}/notes", payload)
PY
artifacts:
when: on_failure
paths:
- diff.txt
- files.json
- existing_notes.json
- discussions.json
- prompt.txt
- mr_description.txt
- comments.json
- "$HOME/.factory/logs/"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment