Skip to content

feat(gitlab): add @droid fill mode (native parity, no webhook receiver)#94

Draft
factory-nizar wants to merge 1 commit into
feat/gitlab-supportfrom
feat/gitlab-fill
Draft

feat(gitlab): add @droid fill mode (native parity, no webhook receiver)#94
factory-nizar wants to merge 1 commit into
feat/gitlab-supportfrom
feat/gitlab-fill

Conversation

@factory-nizar
Copy link
Copy Markdown
Contributor

@factory-nizar factory-nizar commented Jun 2, 2026

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 the token from the new description so that the next merge_request_event (caused by our own update) does not re-trigger fill.

Discussion-comment triggers (`` typed as a note on the MR) are not supported here — GitLab does not fire CI on note events, so that surface would require a webhook receiver and is deliberately deferred.

Trigger Surfaces

Trigger surface How it fires Matched at
`` in MR title merge_request_event on title edit CI rules: via $CI_MERGE_REQUEST_TITLE =~ /@droid\s+fill/i
droid:fill label merge_request_event on label change CI rules: via $CI_MERGE_REQUEST_LABELS
`` in MR description merge_request_event on description edit Job runs, fetches description via API, matches in gitlab-fill-prepare.ts
automatic_fill: "true" always-on every merge_request_event CI rules: via $AUTOMATIC_FILL

Changes

File Change
gitlab/templates/fill.yml (new, +125) GitLab CI/CD component with a single droid-fill job: three rules: trigger conditions, runtime clone/install of droid-action, Droid CLI install, gitlab_mr MCP registration, state-gated droid exec, and .droid-debug/ + .droid-state.json artifact staging
src/entrypoints/gitlab-fill-prepare.ts (new, +225) Single-pass prepare entrypoint: sets up the GitLab token and Droid settings, fetches the MR (description is not exposed as an env var) via the API, runs the trigger check, and writes the fill prompt plus a FillPrepareState consumed by the CI script
src/gitlab/validation/trigger.ts (new, +92) Port of the GitHub checkContainsTrigger for fill: checkContainsFillTrigger (title/description/label/automatic), buildFillRegex, stripFillTrigger, and escapeRegExp
src/gitlab/prompts/fill.ts (new, +61) Fill prompt that writes the description back via gitlab_mr___update_mr_description (description-only; single mutation)
src/gitlab/context.ts (+9) Adds the automaticFill input and parses mr.labels from CI_MERGE_REQUEST_LABELS
test/gitlab/fill-trigger.test.ts (new, +131) Tests for trigger detection and stripFillTrigger
test/gitlab/fill-prompt.test.ts (new, +60) Tests for the fill prompt generation
test/gitlab/context.test.ts (+2) Coverage for the new context fields

Implementation Details

  • The gitlab_mr___update_mr_description MCP tool already exists in src/mcp/gitlab-mr-server.ts; no new MCP tools were added.
  • The CI script gates droid exec on "shouldRunFill": true in the state file, so jobs that fire on every MR event but don't actually need to fill exit early.
  • The fill prompt is constrained to description-only output and a single update_mr_description call, with the enabled tool set Read,Grep,Glob,LS,Execute,Skill,gitlab_mr___update_mr_description.

Testing

  • New tests added across fill-trigger.test.ts and fill-prompt.test.ts (plus context.test.ts coverage).
  • Author-reported: 469 pass total (was 445), tsc --noEmit clean, Prettier formatted.
    Manual end-to-end on a real GitLab MR for each trigger surface:
  1. Title — edit MR title to include `` → droid-fill job fires → description filled → token stripped.
  2. Label — add droid:fill label → same flow.
  3. Description — write `` in the description → API fetch matches → fill runs.
  4. Automatic-onautomatic_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 to dev, this PR's base should be retargeted to dev.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

@factory-droid
Copy link
Copy Markdown
Contributor

factory-droid Bot commented Jun 2, 2026

Droid finished @factory-nizar's task —— View job


Security Review

Main correctness issue: the fill GitLab CI rules prevent the advertised description-only trigger path from ever starting when automatic_fill=false. Also, tighten stripFillTrigger/buildFillRegex to avoid Markdown mangling and common punctuation false-negatives, and reduce the fill tool allowlist to shrink prompt-injection blast radius.

@factory-droid
Copy link
Copy Markdown
Contributor

factory-droid Bot commented Jun 2, 2026

Droid finished @factory-nizar's task —— View job


@factory-nizar factory-nizar marked this pull request as draft June 2, 2026 17:41
@factory-nizar factory-nizar force-pushed the feat/gitlab-support branch from 9ef6f30 to 6f05311 Compare June 2, 2026 17:44
Comment thread templates/fill.yml
Comment on lines +46 to +52
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'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Suggested change
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"'

Comment on lines +88 to +91
return description
.replace(regex, " ")
.replace(/\s{2,}/g, " ")
.trim();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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).

Suggested change
return description
.replace(regex, " ")
.replace(/\s{2,}/g, " ")
.trim();
return description
.replace(regex, " ")
.replace(/[ \t]{2,}/g, " ")
.trim();

Comment on lines +35 to +38
export function buildFillRegex(triggerPhrase: string): RegExp {
const escaped = escapeRegExp(triggerPhrase);
return new RegExp(`(^|\\s)${escaped}\\s+fill([\\s.,!?;:]|$)`, "i");
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Suggested change
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",
);
}

Comment thread templates/fill.yml
--env DROID_MR_IID="$DROID_MR_IID"
- |
set -o pipefail
FILL_TOOLS="Read,Grep,Glob,LS,Execute,Skill,gitlab_mr___update_mr_description"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Suggested change
FILL_TOOLS="Read,Grep,Glob,LS,Execute,Skill,gitlab_mr___update_mr_description"
FILL_TOOLS="Read,Grep,Glob,LS,gitlab_mr___update_mr_description"

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant