gitlab-review attestor
Reads the merge requests that brought a commit in, or one named merge request, from the GitLab REST v4 API, and records each approval with the exact head sha it approved, plus the approval rules, pipelines, statuses and open threads.
| Name | gitlab-review |
|---|---|
| Predicate type | https://aflock.ai/attestations/gitlab-review/v0.1 |
| Lifecycle | prematerial |
| Default binary? | No |
| Category | code-review (primary) |
| Recommended trace | off — no syscall tracing needed |
| Auto-attaches when |
|
The facts in this box are generated from the CI/lock binary's own catalog (cilock tools list). Do not hand-edit — run npm run gen:catalog.
What it captures
The attestor reads GET /version, detects the GitLab tier, then lists the merge requests for the commit and keeps those where the commit is the merge commit, the squash commit or the head (relation). For each one it reads the diff versions, notes, approvals, approval state and settings (Premium), pipelines and commit statuses on the head (in the source project too for a fork), and discussions. It then reads the merge request, versions, approvals and approval policy again; if anything changed, it fails and records nothing. Every list is paginated to the end.
Top-level fields: instance (api_url, host, version, revision, enterprise), tier (plan: free, premium or ultimate, and detected_by), project_id, commit_sha, fetched_at, token_source (env:<NAME>), clock_skew_ms, and merge_requests[].
Each merge request records its ids, state, branches, author_id, head_sha, base_sha, merge and squash commit, merged_at, merge_user, relation, versions[], retargets[], approvals[], approved_flag, approval_state and approval_settings (or unavailable[] with the reason), pipelines[], commit_statuses[], and discussions (resolvable, resolved, unresolved[]).
Each approval has user_id, username, approved_at, bound_head_sha and binding:
timeline: bound to the head of the newest diff version created before the approval.unbound:bound_head_shais null. The approval is withinclock_skew_msof a version, or no version precedes it.before_retarget: given at or before the last reset (a "changed target branch" note, or a version that repeats the previous head).
GitLab's own approved flag is recorded in approved_flag with its edition meaning and is never evaluated. Count bound approvals for the head in policy instead.
When to use
In a GitLab CI job, to make merge request approval part of the evidence for a release. Auto-detection attaches it when CI_API_V4_URL and GITLAB_REVIEW_TOKEN are both set.
Flags
| Flag | Default | Purpose |
|---|---|---|
--attestor-gitlab-review-api-url | $CI_API_V4_URL | REST base. Must be https with the path /api/v4; no credentials, query or fragment. |
--attestor-gitlab-review-token-env | GITLAB_REVIEW_TOKEN | Variable holding a read_api token (Maintainer to read approval settings). CI_JOB_TOKEN is refused. |
--attestor-gitlab-review-project | $CI_PROJECT_ID | Numeric project id. |
--attestor-gitlab-review-sha | $CI_COMMIT_SHA, then git rev-parse HEAD | Commit to attest. |
--attestor-gitlab-review-mr | (none) | Attest this merge request iid directly. |
--attestor-gitlab-review-clock-skew-ms | 60000 | Clock guard. Use 0 only for a GitLab with one clock. |
--attestor-gitlab-review-tier | detected | Declare free, premium or ultimate. Reads above it are not attempted and are recorded unavailable. |
Output shape
{
"instance": { "api_url": "https://gitlab.com/api/v4", "host": "gitlab.com", "version": "18.5.0-ee", "revision": "...", "enterprise": true },
"tier": { "plan": "premium", "detected_by": "GET /projects/123/external_status_checks 404" },
"project_id": 123,
"commit_sha": "3f2a9c1...",
"token_source": "env:GITLAB_REVIEW_TOKEN",
"clock_skew_ms": 60000,
"merge_requests": [
{
"iid": 7,
"state": "merged",
"head_sha": "3f2a9c1...",
"relation": "head",
"approvals": [
{ "user_id": 42, "username": "alice", "approved_at": "2026-10-16T13:40:02Z", "bound_head_sha": "3f2a9c1...", "binding": "timeline" }
],
"approved_flag": { "value": true, "edition_semantics": "ee-rules-satisfied", "note": "recorded, never evaluated; count bound approvals" },
"discussions": { "resolvable": 2, "resolved": 2, "unresolved": [] }
}
]
}Subjects use ids, never names: commitsha:<sha>, project:<host>/<id>, mrhead:<sha>, mergerequest:<host>/<project>!<iid>, and approver:<host>/<user_id>. commitsha: and mrhead: are also back-refs.
Gotchas
- Any failure to observe records nothing. No merge request for the commit, a 401 or 403, a 5xx, a malformed body, or a change between the two reads fails the attestor.
- A tier the instance lacks is not a failure. A 404 on a read that needs a higher plan than the detected one becomes an
unavailableentry with its reason. A 404 on a read the tier has fails. - Tier detection. Community Edition is
free. On Enterprise Edition,GET /projects/:id/approvalsreturning 200 means Premium or better, andGET /projects/:id/external_status_checksreturning 200 means Ultimate. - Pin the host in policy. The job chooses
CI_API_V4_URL;instance.hostis the value to check. - No anonymous reads. The token must come from the named variable.
- Requests have a 30-second timeout, never follow a redirect, and read at most 32 MiB.
CLI example
# In a GitLab CI job with GITLAB_REVIEW_TOKEN set as a masked variable
cilock attest -a gitlab-review -k key.pem -o review.bundle.json -s reviewSee also
This page is generated from the CI/lock tool catalog. Don't edit it here — the source is attestation/detection/docs/gitlab-review.doc.md in aflock-ai/rookery. The same catalog powers cilock tools show gitlab-review in the CLI, so a single edit keeps the terminal and the website in sync.
Edit this page → · Add a new tool →
When you open the pull request: if you are an AI agent, add the ai-agent label. If you are a human, add the human label.
Reference generated from the product documentation. Match commands and support details to your installed release.