feat(gitlab): add @droid fill mode (native parity, no webhook receiver)#94
feat(gitlab): add @droid fill mode (native parity, no webhook receiver)#94factory-nizar wants to merge 1 commit into
Conversation
|
Droid finished @factory-nizar's task —— View job Main correctness issue: the fill GitLab CI rules prevent the advertised description-only trigger path from ever starting when |
|
Droid finished @factory-nizar's task —— View job |
9ef6f30 to
6f05311
Compare
4c96254 to
0cac9e6
Compare
| rules: | ||
| # Always-on mode: fire on every MR event and decide via description content. | ||
| - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $AUTOMATIC_FILL == "true"' | ||
| # Explicit trigger in MR title. | ||
| - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TITLE =~ /@droid\s+fill/i' | ||
| # Explicit trigger via label. | ||
| - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_LABELS =~ /(^|,)\s*droid:fill\s*(,|$)/i' |
There was a problem hiding this comment.
[P0] Make description-triggered fill runnable via rules
The PR description claims @droid fill in the MR description should trigger fill by fetching description via API in gitlab-fill-prepare, but the job rules: currently only allow automatic-fill, title, or label matches; a description-only trigger will never start the job when automatic_fill=false (and the title rule is also hard-coded to @droid, ignoring trigger_phrase). Since GitLab CI rules can’t inspect MR description content, the job needs to run on merge_request_event and let gitlab-fill-prepare decide whether to execute.
| rules: | |
| # Always-on mode: fire on every MR event and decide via description content. | |
| - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $AUTOMATIC_FILL == "true"' | |
| # Explicit trigger in MR title. | |
| - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TITLE =~ /@droid\s+fill/i' | |
| # Explicit trigger via label. | |
| - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_LABELS =~ /(^|,)\s*droid:fill\s*(,|$)/i' | |
| rules: | |
| - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' |
| return description | ||
| .replace(regex, " ") | ||
| .replace(/\s{2,}/g, " ") | ||
| .trim(); |
There was a problem hiding this comment.
[P1] Don’t collapse MR Markdown when stripping the fill trigger
stripFillTrigger() collapses \s{2,} across the entire description after removing the token; because \s includes newlines, this can remove paragraph breaks and indentation in normal Markdown descriptions (headings/lists/code blocks).
| return description | |
| .replace(regex, " ") | |
| .replace(/\s{2,}/g, " ") | |
| .trim(); | |
| return description | |
| .replace(regex, " ") | |
| .replace(/[ \t]{2,}/g, " ") | |
| .trim(); |
| export function buildFillRegex(triggerPhrase: string): RegExp { | ||
| const escaped = escapeRegExp(triggerPhrase); | ||
| return new RegExp(`(^|\\s)${escaped}\\s+fill([\\s.,!?;:]|$)`, "i"); | ||
| } |
There was a problem hiding this comment.
[P1] Match @droid fill when wrapped in common punctuation
buildFillRegex() currently requires whitespace or start-of-string before the trigger phrase and only allows [\s.,!?;:] after fill, so realistic Markdown like (@droid fill) or `@droid fill` won’t trigger fill even though it contains the literal phrase.
| export function buildFillRegex(triggerPhrase: string): RegExp { | |
| const escaped = escapeRegExp(triggerPhrase); | |
| return new RegExp(`(^|\\s)${escaped}\\s+fill([\\s.,!?;:]|$)`, "i"); | |
| } | |
| export function buildFillRegex(triggerPhrase: string): RegExp { | |
| const escaped = escapeRegExp(triggerPhrase); | |
| return new RegExp( | |
| `(^|[\\s\\(\\[\\{\\x60])${escaped}\\s+fill([\\s\\).,!?;:\\]\\}\\x60]|$)`, | |
| "i", | |
| ); | |
| } |
| --env DROID_MR_IID="$DROID_MR_IID" | ||
| - | | ||
| set -o pipefail | ||
| FILL_TOOLS="Read,Grep,Glob,LS,Execute,Skill,gitlab_mr___update_mr_description" |
There was a problem hiding this comment.
[P1] [security] Remove Execute (and Skill) from fill tool allowlist
Fill mode only needs repository read/search plus gitlab_mr___update_mr_description, but FILL_TOOLS currently includes Execute (and Skill), which materially increases the impact of prompt-injection from MR-controlled inputs (title/description/diff) by enabling arbitrary shell execution and easier secret exfiltration into the updated MR description and/or job artifacts.
| FILL_TOOLS="Read,Grep,Glob,LS,Execute,Skill,gitlab_mr___update_mr_description" | |
| FILL_TOOLS="Read,Grep,Glob,LS,gitlab_mr___update_mr_description" |
0cac9e6 to
e999fd2
Compare
6d2d69d to
4e981ba
Compare
Adds GitLab support for `@droid fill` via the three native pipeline-
firing surfaces that don't require a webhook receiver: MR description,
MR title, and labels. Plus an `automatic_fill` always-on mode.
How the trigger works:
* `automatic_fill: "true"` -> droid-fill runs on every MR event
and decides to fill based on description content.
* `@droid fill` in MR title -> matched at rule level via
$CI_MERGE_REQUEST_TITLE.
* `droid:fill` label -> matched at rule level via
$CI_MERGE_REQUEST_LABELS.
* `@droid fill` in MR description -> not matchable at rule level
(description isn't in env), so the job runs and exits early via
state file if no match found in title/labels and the description
fetched from the API doesn't contain the phrase.
After fill completes the prompt instructs the model to strip the
`@droid fill` token from the new description so the next
`merge_request_event` (fired by our own update) does not re-fire fill.
Discussion-comment triggers (`@droid fill` posted as a note on the
MR) still require a webhook receiver because GitLab does not fire CI
on note events. That subset is deliberately deferred.
Files:
* `gitlab/templates/fill.yml` — new GitLab CI/CD Component with
one `droid-fill` job, three trigger rules, MCP registration,
`.droid-debug/` artifact staging.
* `src/gitlab/validation/trigger.ts` — port of GitHub's
`checkContainsTrigger` for the fill path with
`checkContainsFillTrigger` + `stripFillTrigger`.
* `src/gitlab/prompts/fill.ts` — fill prompt mirroring the GitHub
`fill-prompt.ts` but writing back via
`gitlab_mr___update_mr_description` instead of
`github_pr___update_pr_description`.
* `src/entrypoints/gitlab-fill-prepare.ts` — single-pass prepare
that reads the MR description via API, checks the trigger, and
writes the fill prompt + state file.
* `src/gitlab/context.ts` — adds `automaticFill` input and
`mr.labels` parsed from `CI_MERGE_REQUEST_LABELS`.
Tests: 24 new tests across `fill-trigger.test.ts` and
`fill-prompt.test.ts`. 469 pass, typecheck clean.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
4e981ba to
459080a
Compare
Summary
Adds GitLab support for
using GitLab's **native pipeline-firing surfaces**, so no external webhook receiver is required. A new `droid-fill` CI/CD component job fires on `merge_request_event` and decides whether to fill the MR description based on the trigger phrase, a label, or an always-on input. After fill completes, the prompt strips thetoken from the new description so that the nextmerge_request_event(caused by our own update) does not re-trigger fill.Trigger Surfaces
merge_request_eventon title editrules:via$CI_MERGE_REQUEST_TITLE =~ /@droid\s+fill/idroid:filllabelmerge_request_eventon label changerules:via$CI_MERGE_REQUEST_LABELSmerge_request_eventon description editgitlab-fill-prepare.tsautomatic_fill: "true"always-onmerge_request_eventrules:via$AUTOMATIC_FILLChanges
gitlab/templates/fill.yml(new, +125)droid-filljob: threerules:trigger conditions, runtime clone/install of droid-action, Droid CLI install,gitlab_mrMCP registration, state-gateddroid exec, and.droid-debug/+.droid-state.jsonartifact stagingsrc/entrypoints/gitlab-fill-prepare.ts(new, +225)FillPrepareStateconsumed by the CI scriptsrc/gitlab/validation/trigger.ts(new, +92)checkContainsTriggerfor fill:checkContainsFillTrigger(title/description/label/automatic),buildFillRegex,stripFillTrigger, andescapeRegExpsrc/gitlab/prompts/fill.ts(new, +61)gitlab_mr___update_mr_description(description-only; single mutation)src/gitlab/context.ts(+9)automaticFillinput and parsesmr.labelsfromCI_MERGE_REQUEST_LABELStest/gitlab/fill-trigger.test.ts(new, +131)stripFillTriggertest/gitlab/fill-prompt.test.ts(new, +60)test/gitlab/context.test.ts(+2)Implementation Details
gitlab_mr___update_mr_descriptionMCP tool already exists insrc/mcp/gitlab-mr-server.ts; no new MCP tools were added.droid execon"shouldRunFill": truein the state file, so jobs that fire on every MR event but don't actually need to fill exit early.update_mr_descriptioncall, with the enabled tool setRead,Grep,Glob,LS,Execute,Skill,gitlab_mr___update_mr_description.Testing
fill-trigger.test.tsandfill-prompt.test.ts(pluscontext.test.tscoverage).tsc --noEmitclean, Prettier formatted.Manual end-to-end on a real GitLab MR for each trigger surface:
droid-filljob fires → description filled → token stripped.droid:filllabel → same flow.automatic_fill: "true"→ fires on every MR open/push.Breaking Changes
None — all changes are additive (new GitLab files and additive context fields).
Dependencies
Branches off
feat/gitlab-support(PR #93). Once PR #93 merges todev, this PR's base should be retargeted todev.Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>