qwen-code/docs/users/features/channels/gitlab.md
OrbitZore ec9c36ef82
feat(channels): add GitLab polling channel adapter (#7862)
* feat(channels): add GitLab polling channel adapter

Poll GitLab todos via @gitbeaker/rest, dispatch notes through the
existing PollingChannelBase pipeline. Key design points:

- action_prompt_template config drives event filtering and metadata
  rendering (unconfigured actions are skipped)
- Per-repo cursor (repo[chatId].last_read) as notes window lower bound,
  global lastProcessedAt for todo-level dedup
- mark_done after successful processing; failure skips mark_done for
  retry on next poll
- Mention gating delegated to base GroupGate (adapter only sets
  isMentioned flag)
- First-contact body fallback for todos with no notes (e.g. mention in
  issue description)

* fix(channels/gitlab): persist cursor after each successful todo

Call saveCursor() immediately after advancing lastProcessedAt so that
progress is durable even if the process crashes mid-poll. Also removes
the local watermark variable in favor of direct assignment.

* fix(channels/gitlab): persist cursor on every advancement including skips

* fix(channels/gitlab): address review critical issues

- Remove non-functional proxyAgent (gitbeaker doesn't support it)
- Construct repo_url from host + path (API doesn't return web_url)
- Handle directly_addressed action (falls back to mentioned template)
- First-contact fetches target description instead of using todo.body
- Move todo.project dereference inside try block
- Filter confidential notes
- Update channel-registry.test.ts for gitlab entry

* fix(channels/gitlab): address review suggestions

- Warn on connect if action_prompt_template is not configured
- Guard todo.target.iid before use
- Skip paths now mark_done (best-effort) to clean GitLab UI
- Remove postErrorComment (avoids duplicate comments on retry)
- Fetch only first page of notes (desc, maxPages:1, perPage:100)
  instead of paginating entire note history
- Extract fetchRecentNotes for single-page windowed enumeration

* refactor(channels/gitlab): simplify to todo.body dispatch, add description mention support

- Remove notes API fetching; dispatch todo.body directly
- Detect description mentions via target_url anchor (#note_ absence)
- Always fetch target description for %description% metadata
- Remove per-repo cursor; dedup via cursor + mark_done only
- Cursor advances regardless of success/failure (no retry)
- Use zod for cursor validation
- Rename template vars to GitLab terminology:
  %project% %project_url% %target_type% %iid% %title% %description% %todo_id%
- Support %% escape for literal percent

* docs(channels): add GitLab adapter documentation

- New user guide: docs/users/features/channels/gitlab.md
- Update _meta.ts navigation
- Update developer adapter matrix and SDK list

* fix(channels/gitlab): use correct Issues.show(issueIid, { projectId }) signature

* chore: regenerate NOTICES.txt for new gitlab channel dependencies

* fix(channels/gitlab): address review suggestions

- Add todo.project null guard (item 2)
- Single-pass regex for %% escape + %var% substitution (item 4)
- sendThreadMessage throws directly on undefined threadId (item 5)
- Dedup fetchDescription with per-poll cache (item 6)
- Remove per-todo saveCursor; base class saves after pollOnce (item 7)
- Add undefined threadId test (item 8)
- Expand confidential notes limitation in docs (item 3)

* test(channels/gitlab): add mention tests, directly_addressed coverage, skip assertions, temp cleanup

- New mention.test.ts: 14 cases for testBotMention/stripBotMention/escapeRegex
- Add directly_addressed fallback test
- Skip tests now assert TodoLists.done + cursor advancement
- afterEach cleans up mkdtempSync temp dirs

* fix(channels/gitlab): address review round 4

- Non-mention actions (assigned, etc.) set forceMentioned=true to bypass GroupGate
- Merge dead note-filter tests into single 'skips todo authored by bot'
- Log fetchDescription errors to stderr instead of silent swallow
- Post error comment on issue/MR when handleInbound fails (best-effort)

* fix(channels/gitlab): always force isMentioned=true, remove regex re-derivation

The action_prompt_template config is already the event filter, and
GitLab has already decided the mention when creating the todo.
Re-deriving isMentioned via regex on todo.body causes permanent
message loss when the regex misses (description mention + fetch
failure, group mentions). Always set forceMentioned=true so
GroupGate never drops a todo that passed the template filter.

* fix(channels/gitlab): propagate fetchDescription errors for description mentions

For note mentions, description is metadata-only — fetch failure is
logged and swallowed. For description mentions, description IS the
message — fetch failure now propagates to the outer catch, which
posts the ⚠️ error comment so the user knows to re-mention.

* perf(channels/gitlab): clean up stale todos, skip unnecessary fetchDescription

- Mark stale todos (updated_at <= cursor) as done on each poll to
  prevent perpetual re-fetching of pre-existing pending todos
- Skip fetchDescription for note mentions when template does not
  contain %description%, saving one API call per todo
- Update docs: stale todo cleanup, error comment on failure

* docs(channels/gitlab): clarify requireMention is bypassed, template is the real filter

* Apply suggestions from code review

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>

* fix(channels/gitlab): use todo ID cursor instead of timestamp to eliminate equal-timestamp loss

Timestamp-based cursors (second granularity) could silently destroy
todos sharing the same updated_at as the cursor boundary. Switch to
monotonically increasing todo IDs which are unique and collision-free.

Add initialized flag to preserve first-start drain semantics: pre-existing
pending todos are marked done without dispatch on the first poll cycle.

* fix(channels/gitlab): harden first-poll drain, add ordering tests, fix lockfile

- Replace Math.max(...spread) with reduce to avoid RangeError on large
  backlogs (~100k+ todos). Move initialized=true after the drain work so
  any throw retries the drain instead of falling through to dispatch.
- Add unit tests: identical-timestamp delivery and id-order-when-updated_at-disagrees
  (kills M2 sort mutant).
- Align lockfile: file:../base → ^0.21.0 for channel-base dep.

* fix(channels/gitlab): include dot in mention lookahead for GitLab usernames

GitLab usernames may contain dots (e.g. bot.name). The lookahead
character class inherited from GitHub omitted '.', causing @bot.name
to match as @bot. Add '.' to the negated class.

* docs(channels/gitlab): align docs with ID cursor and drain semantics

- Add first-poll drain as step 2 in How It Works
- Clarify GroupGate always passes (isMentioned forced true)
- Document initialized flag in Known Limitations

* Apply suggestions from code review

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>

* fix(channels/gitlab): align package version and channel-base dependency to 0.21.1

Bump version from 0.21.0 to 0.21.1 to match other channel packages after
upstream merge. Pin @qwen-code/channel-base to exact 0.21.1 instead of
^0.21.0, matching the convention used by other published channels.

* fix(channels/gitlab): regenerate lockfile to match package.json versions

Manually add only gitlab-related lockfile entries (workspace, @gitbeaker
packages, transitive deps, channel-gitlab link) without unrelated npm
normalization churn.

* test(channels/gitlab): add regression tests for first-poll drain hardening

Two tests that kill the M1 (Math.max spread RangeError) and M2 (flag
ordering) mutants which survived the original 46-test suite:

- 150k todo drain verifies reduce() handles large backlogs without
  RangeError and without dispatching
- Drain throw verifies initialized stays false so the next poll retries
  the drain instead of falling through to dispatch

Test file duration: ~40ms → ~170ms.

* docs(channels/gitlab): clarify groupPolicy must be "open" and add runtime warning

The default groupPolicy "disabled" silently drops all mentions — todos are
marked done and cursor advances, but no dispatch occurs. Fix misleading docs
that said "GroupGate always passes" (only true at groupPolicy: "open") and
add a connect()-time warning when groupPolicy is not "open".

* fix(channels/gitlab): correct xcase integrity hash in lockfile

The manually added xcase entry had a typo in the sha512 hash (ys → ks),
causing npm ci EINTEGRITY failures in CI.

* fix(channels/gitlab): correct requester-utils integrity hash in lockfile

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(channels/gitlab): allow groupPolicy "allowlist" in warning and docs

The groupPolicy warning and docs incorrectly stated that groupPolicy
must be "open". In reality "allowlist" with the project listed also
works because isMentioned is forced true and GroupGate only requires
the group to be listed. Also fix the inaccurate "no error is logged"
claim — ChannelBase logs preflight rejected reason=group_disabled.

Fixes R5-🟡3 from PR #7862 review.

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-29 14:28:31 +00:00

8.7 KiB

GitLab

This guide covers setting up a Qwen Code channel that monitors GitLab todos and responds to mentions on issues and merge requests.

Prerequisites

  • A GitLab account (or a dedicated bot account)
  • A GitLab Personal Access Token with read_api and api scopes

Creating a Token

  1. Go to Preferences → Access Tokens
  2. Create a token with these scopes:
    • read_api — read todos and project data
    • api — post notes (comments) on issues/MRs
  3. Save the token securely as an environment variable

Configuration

Add the channel to ~/.qwen/settings.json:

{
  "channels": {
    "my-gitlab": {
      "type": "gitlab",
      "token": "$GITLAB_TOKEN",
      "pollInterval": 60000,
      "senderPolicy": "open",
      "sessionScope": "chat_thread",
      "cwd": "/path/to/your/project",
      "groupPolicy": "open",
      "action_prompt_template": {
        "mentioned": "Project: %project% | URL: %project_url% | Author: %author% | Type: %target_type% | IID: %iid% | Title: %title% | Description: %description% | TodoID: %todo_id%"
      }
    }
  }
}

Set the token as an environment variable:

export GITLAB_TOKEN="glpat-your_token_here"

Self-hosted GitLab

For self-hosted instances, set baseUrl:

{
  "baseUrl": "https://gitlab.example.com"
}

Configuration Options

Option Default Description
token (required) PAT with read_api + api scopes
pollInterval 60000 Poll interval in ms
baseUrl https://gitlab.com GitLab instance URL
action_prompt_template (required for processing) Maps GitLab action names to metadata templates
groupPolicy "disabled" Must be "open", or "allowlist" with the project listed
senderPolicy "allowlist" Who can trigger the bot

action_prompt_template

This field controls which todo actions are processed and how metadata is rendered. Only actions with a configured template are dispatched; all others are skipped and marked done.

{
  "action_prompt_template": {
    "mentioned": "Project: %project% | Author: %author% | Title: %title%"
  }
}

The directly_addressed action (comment starting with @bot) automatically falls back to the mentioned template if not explicitly configured.

Available Action Keys

Key Trigger
mentioned Someone @mentions the bot in a comment or description (not at the start)
directly_addressed A comment starts with @bot (falls back to mentioned template)
assigned Someone assigns the bot to an issue/MR
review_requested Someone requests the bot as a reviewer on an MR
approval_required An MR requires the bot's approval (approval rules)
marked Someone marks the bot's comment/issue/MR (star)
build_failed A CI/CD pipeline fails on the bot's branch/MR
unmergeable An MR the bot is involved with becomes unmergeable (conflicts)
merge_train_removed An MR is removed from the merge train

Only keys present in action_prompt_template are processed. Unconfigured actions are skipped and marked done silently.

Template Variables

Variable Value
%project% Project path (e.g., owner/repo)
%project_url% Full project URL
%author% Todo author username
%target_type% Issue or MergeRequest
%iid% Issue/MR internal ID
%title% Issue/MR title
%description% Issue/MR description body
%todo_id% GitLab todo ID
%% Literal % (escape)

Unknown variables are preserved as-is in the output.

Prompt Assembly

The template renders into envelope.metadata (structured context). The triggering text (todo.body or description) goes into envelope.text (primary prompt). The base class assembles the final prompt sent to the agent:

[alice] please fix this bug

Project: owner/repo | URL: https://gitlab.com/owner/repo | Author: alice | Type: Issue | IID: 42 | Title: Test Issue | Description: ... | TodoID: 100
  • Line 1: [sender] prefix + envelope.text (with @bot stripped)
  • Line 3: envelope.metadata (rendered template, sanitized)

You do not need a %body% variable — the comment/description text is always the primary prompt content, and the template provides supplementary context below it.

⚠️ Security

On a public project, setting senderPolicy: "open" allows any GitLab user who @mentions the bot to submit prompts that drive the agent in your cwd.

Always use senderPolicy: "allowlist" with explicit allowedUsers on public projects.

Mention Detection

The adapter always sets isMentioned = true on dispatched envelopes, because GitLab has already determined the mention when creating the todo. The action_prompt_template config is the real event filter — only actions with a configured template are processed. The @bot mention is stripped from the message text before dispatch via stripBotMention.

⚠️ groupPolicy Must Be "open" or "allowlist"

groupPolicy must be set to "open", or "allowlist" with the project explicitly listed, for todos to be processed. The default value "disabled" drops all mentions: todos are marked done and the cursor advances, but no dispatch occurs. A rejection is logged (preflight rejected reason=group_disabled) but the todo is still consumed. If your bot is not responding to mentions, check that groupPolicy is not "disabled".

How It Works

The adapter uses GitLab's Todos API as the message source:

  1. Poll GET /todos?state=pending for new todos
  2. First-poll drain: if the cursor has never been initialized (initialized: false), all pending todos are marked done without dispatch and the cursor advances to the max todo ID. This prevents a backlog flood on first start.
  3. Clean up stale todos: todos with id <= cursor are marked done (best-effort) to prevent them from being re-fetched on every poll
  4. Filter by id > cursor and configured action_prompt_template
  5. Detect mention type via target_url anchor:
    • #note_123 present → comment mention → text is todo.body (the comment)
    • No anchor → description mention → text is the issue/MR description
  6. Dispatch the envelope through handleInbound (requires groupPolicy: "open" or "allowlist" with the project listed)
  7. Advance cursor and mark todo done (best-effort)

The cursor (lastProcessedId) advances regardless of dispatch success or failure. Failed dispatches post a ⚠️ error comment on the issue/MR and are not retried — the user can re-mention the bot to trigger a new todo.

Known Limitations

  • First start skips existing pending todos. The cursor initializes to { lastProcessedId: 0, initialized: false } on first launch. On the first poll cycle, all pre-existing pending todos are marked done without dispatch (the initialized flag gates this one-time drain), preventing a backlog flood.
  • The bot does not read prior conversation history — only the triggering content is processed.
  • Confidential (internal) notes: If someone @mentions the bot in a confidential note, the todo body contains that internal text and the agent will process it. The bot's reply is always posted as a public note, potentially exposing internal discussion. GitLab's todo API does not expose note visibility, so the adapter cannot filter this. Avoid @mentioning the bot in confidential notes.
  • Requires read_api + api PAT scopes. Group-level or project-level tokens work if they have these scopes.
  • Todos for Epics, Designs, and Alerts are skipped (only Issues and MRs are processed).

Starting the Channel

qwen channel start my-gitlab