# gitlab-review attestor

Source: https://www.testifysec.com/docs/cilock/attestors/gitlab-review/

The cilock gitlab-review attestor snapshots the GitLab merge request review state of a commit (approvals bound to the exact sha they approved, approval rules by tier, head pipelines and statuses, unresolved threads) and signs it as in-toto evidence.

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 | - preenv\_set: CI\_API\_V4\_URL<br>- preenv\_set: GITLAB\_REVIEW\_TOKEN |

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_sha` is null. The approval is within `clock_skew_ms` of 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

 

```json
{
  "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 `unavailable` entry with its reason. A 404 on a read the tier has fails.
 - **Tier detection.** Community Edition is `free`. On Enterprise Edition, `GET /projects/:id/approvals` returning 200 means Premium or better, and `GET /projects/:id/external_status_checks` returning 200 means Ultimate.
 - **Pin the host in policy.** The job chooses `CI_API_V4_URL`; `instance.host` is 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

 

```bash
# 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 review
```

 

## See also

 

- [`gitlab`](https://www.testifysec.com/docs/cilock/attestors/gitlab)
 - [Catalog row](https://www.testifysec.com/docs/cilock/reference/attestor-catalog)

 

---

**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`](https://github.com/aflock-ai/rookery/edit/main/attestation/detection/docs/gitlab-review.doc.md) in [aflock-ai/rookery](https://github.com/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 →](https://github.com/aflock-ai/rookery/edit/main/attestation/detection/docs/gitlab-review.doc.md) ·  [Add a new tool →](https://github.com/aflock-ai/rookery/new/main/attestation/detection/docs)

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.
