adopt · forge profile · gitlab enterprise
Assay on GitLab Enterprise
Assay’s briefs (the units of work), registers (the append-only logs), lifecycle, board, and statusgen are forge-agnostic: git and markdown, unchanged. What is GitHub-shaped today is the identity, permission, and CI layer, and the desk tools that speak the forge API. This is the GitLab profile of that layer: self-managed EE or gitlab.com, mapped control for control onto GitLab’s mechanisms. The governing requirement is a single line: the profile must be at least as secure as the existing GitHub controls, even where the mechanism is completely different. “Weaker but disclosed” is non-conforming.
This is the published design, not a shipped feature. Assay does not claim GitLab support as available until a live pilot has round-tripped one brief todo→done on a real GitLab group with the parity table below walked against live settings and recorded as Evidence. Until that conformance gate is met, treat this page as the profile an evaluating team can read and plan against, not as an install you can run today.
The tier ladder: Premium floor, Ultimate for risk-classed work
Parity is not uniform across GitLab editions, so the profile states a floor and a ceiling rather than a single “supported” checkbox:
- Premium is the floor
Protected branches, approval rules, required pipelines, and push-rule secret checks reach parity on the protected lines an adopter configures.
- Ultimate is required for public or risk-classed work
Custom roles, Secret Detection, external status checks, and pipeline execution policy are the mechanisms that close the last gaps in the parity table, and those are Ultimate-only.
- GitLab Free / CE is non-conforming
It cannot meet the parity requirement and is declared non-conforming for this profile. An adopter on Free/CE is outside the profile, not on a lesser tier of it.
Identity: service accounts, one per role
GitLab has no GitHub-Apps analog: no manifest flow, no per-resource permission matrix, no JWT-minted installation tokens. The role fleet maps instead to service accounts: seatless bot users owned by the top-level group, one per role, each holding its own personal access token. Attribution separation holds exactly as it does on GitHub: notes, approvals, and commits carry the service-account identity, which the author’s own token cannot produce. It carries the same limit: separation of attribution, not proof of diligence.
| Assay role | GitLab identity | Mechanism |
|---|---|---|
| reviewer | service account | Developer; MR notes + approvals; at Ultimate, a custom role without push |
| worker | service account | Developer; branches + Draft: MRs |
| verifier | service account | Developer (commits Evidence); excluded from approval eligibility by approval rules |
| desk | service account | Developer; coordination via MRs |
| issue-loop | service account | Reporter |
| intake-loop | service account | Reporter |
| board-writer | service account | Developer + allowed-to-push entry on protected main, the ruleset-bypass analog |
| promote | usually no identity at all | collapses into a human-merged MR against the ci-config project; see CI isolation below |
| auditor | service account | Reporter, and no more; at Ultimate a custom role carrying read abilities only. Holds no write anywhere in the group: the hardening checker’s identity |
The auditor is the one identity defined by what it cannot do. The repository-hardening checker reads a project’s live settings against a checklist and applies nothing, because every setting it reads is an owner’s act; giving it a desk role’s token would hand a read-only tool a credential that can push, comment and approve. So it is its own seatless account at Reporter (read code, read project metadata, write nothing), and the settings GitLab shows only to a Maintainer or Owner come back could-not-check rather than being bought with a membership that could change them. A human re-runs those rows. Same rule, same accepted price, as the GitHub profile.
Provisioning, by hand
Provisioning is one service account per role plus the protected-branch and approval-rule settings that give each the right reach. A create-fleet-gitlab.sh helper stands the fleet up once it lands; until then the table above is the by-hand checklist:
- Create each seatless account
Under the top-level group, one per role.
- Mint its token
See custody below.
- Set the two protected-
mainlistsTo grant board-writer alone the push and humans alone the merge.
Security parity: the per-control table
Parity is assessed control by control, and verified per deployment: the live pilot walks this table against the real group settings and records the result as Evidence. Where GitLab’s mechanism differs from GitHub’s, the verdict says whether it reaches parity, at which tier, and in one case where it is stronger.
| GitHub control | GitLab mechanism | Verdict |
|---|---|---|
| Per-resource App permissions | Premium: role + token-scope narrowing + protected branches; Ultimate: custom roles | parity at Ultimate; Premium parity on protected lines only → risk-classed work requires Ultimate |
| Ruleset bypass = single board-writer | Protected-branch allowed-to-push list = exactly board-writer | parity |
| No self-approval | Approval rules: prevent-author + prevent-committers | parity |
| Required CI checks before merge | “Pipelines must succeed” + required approvals; Ultimate: external status checks | parity |
workflows permission guarding CI | Locked ci-config project + external CI config path; Ultimate: pipeline execution policy pins it | stronger: the CI definition lives outside the writable repo |
| Human-gated workflow promotion | Human-merged MR into the ci-config project | parity, fewer moving parts |
| Short-lived minted tokens | Rotate-on-mint + short expiry policy | parity; the single-valid-credential property is stronger, TTL shape differs |
| Secret push protection | Push-rule secret checks (Premium) + Secret Detection (Ultimate); the house leak sweep runs in CI regardless | parity |
| Immutable release integrity | Protected tags + audit events + sha256 pins in .assay-versions | parity via the pin discipline |
| Merge is always the human’s | Allowed-to-merge = humans only | parity |
CI isolation: the ci-config project
GitLab has no workflows permission class: .gitlab-ci.yml is an ordinary file. It does not need one. A project’s CI configuration can point at a file in a different project (the external CI config path, available on all tiers), and Ultimate groups can enforce an injected pipeline by policy. The profile uses one locked ci-config project per group: humans-only Maintainer, protected main, approval rules on. Every fleet project sets its CI config path to that project, and the bot identities are simply never members of it.
Workflow promotion then collapses to an ordinary human-merged MR into the ci-config project: structural isolation rather than procedural. This is the one place the GitLab profile is stronger than the GitHub controls it must match: the CI definition lives outside the repository the fleet can write to, so no desk identity can alter what runs against its own change.
Token custody: rotate-on-mint
Personal access tokens are long-lived, and naive handling would be a custody downgrade, which the parity requirement forbids. The profile closes the gap on four fronts:
- Rotate-on-mint. Every token mint calls the rotation API, which returns a fresh token and atomically invalidates the old one. At most one valid credential per role exists at any moment, and any captured token dies at the next mint. Roles are single-window by convention; parallel actors get per-actor service accounts, never shared tokens.
- Expiry backstop. The group or instance token-lifetime policy is set short (7 days recommended), so an idle fleet leaves no live credential.
- File custody unchanged.
0600token files, path-only printing, never in an environment variable or on the command line. - Audit events (Premium and up) are reviewed for rotation and use anomalies.