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.

What this page is

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 tododone 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:

The GitLab tier ladder: Premium is the floor, Ultimate for risk-classed work, Free is outside the profile Three rungs. At the top, Ultimate: required for public or risk-classed work, because custom roles, Secret Detection and external status checks are Ultimate-only. In the middle, Premium: the floor, with parity on the protected lines an adopter configures. A bracket spans these two and reads the profile. At the bottom, hatched and below a dashed line, Free / CE: non-conforming, outside the profile. Ultimate required for public or risk-classed work Premium the floor: parity on protected lines Free / CE non-conforming: outside the profile custom roles, Secret Detection, external status checks the profile outside it
Two rungs are inside the profile and one is not. Free / CE is not a lesser tier of the profile; it is hatched because it is outside it.
  1. Premium is the floor

    Protected branches, approval rules, required pipelines, and push-rule secret checks reach parity on the protected lines an adopter configures.

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

  3. 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 roleGitLab identityMechanism
reviewerservice accountDeveloper; MR notes + approvals; at Ultimate, a custom role without push
workerservice accountDeveloper; branches + Draft: MRs
verifierservice accountDeveloper (commits Evidence); excluded from approval eligibility by approval rules
deskservice accountDeveloper; coordination via MRs
issue-loopservice accountReporter
intake-loopservice accountReporter
board-writerservice accountDeveloper + allowed-to-push entry on protected main, the ruleset-bypass analog
promoteusually no identity at allcollapses into a human-merged MR against the ci-config project; see CI isolation below
auditorservice accountReporter, 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:

  1. Create each seatless account

    Under the top-level group, one per role.

  2. Mint its token

    See custody below.

  3. Set the two protected-main lists

    To 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 controlGitLab mechanismVerdict
Per-resource App permissionsPremium: role + token-scope narrowing + protected branches; Ultimate: custom rolesparity at Ultimate; Premium parity on protected lines only → risk-classed work requires Ultimate
Ruleset bypass = single board-writerProtected-branch allowed-to-push list = exactly board-writerparity
No self-approvalApproval rules: prevent-author + prevent-committersparity
Required CI checks before merge“Pipelines must succeed” + required approvals; Ultimate: external status checksparity
workflows permission guarding CILocked ci-config project + external CI config path; Ultimate: pipeline execution policy pins itstronger: the CI definition lives outside the writable repo
Human-gated workflow promotionHuman-merged MR into the ci-config projectparity, fewer moving parts
Short-lived minted tokensRotate-on-mint + short expiry policyparity; the single-valid-credential property is stronger, TTL shape differs
Secret push protectionPush-rule secret checks (Premium) + Secret Detection (Ultimate); the house leak sweep runs in CI regardlessparity
Immutable release integrityProtected tags + audit events + sha256 pins in .assay-versionsparity via the pin discipline
Merge is always the human’sAllowed-to-merge = humans onlyparity

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. 0600 token 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.