Inspiration

Code generation is getting cheap. Deciding whether a merge request is safe to ship is not. On a small team with no security department, that decision usually lives in a chat thread: someone glances at the scan results, someone says "looks fine", and the release goes out. Months later nobody can say why it was considered safe. We wanted the decision to be made inside GitLab, by agents that do the checking, with one person approving the outcome, and with a record that anyone can verify instead of trust.

What it does

Release Ledger is a set of GitLab Duo Agent Platform flows plus a release gate in CI. It is built for the Supervised level: agents run the multi-step workflow on their own, and a human approves only the outcome.

  • Marking a merge request ready triggers the release-readiness flow. A deterministic tool step pulls the vulnerability list, then a supervisor sends out four sub-agents:
    • a security reviewer that re-checks every critical and high finding against the code on the branch being released (not just the scanner's status);
    • a pipeline checker that reads the MR's latest pipeline and quotes the failure cause;
    • a config checker that compares the CI config, the release guard and the agent config with the default branch and runs the CI linter, so a "small refactor" that weakens the release guard is caught as drift;
    • a checklist writer that assembles the report.
  • The flow stops at a human approval gate. One person chooses Approve, Modify (the agents rewrite the report from feedback) or Reject.
  • On approve of a READY report, an agent commits the approved report to the MR branch together with the exact head SHA that was approved.
  • Merging starts the release jobs. The gate refuses to release unless there is exactly one approval record, it says READY, every stage verdict passes (Security CLEAR, Pipeline PASS, Config OK), and what was merged is exactly what was approved. Then CI builds the package, a CycloneDX SBOM and SHA256SUMS, and writes ledger.json.
  • Every release gets three links: the SBOM, the checksums and the ledger. ci/verify_ledger.py re-derives every claim in the ledger from git and from the downloaded files (11 checks). Change one byte of the SBOM and it fails.
  • When the answer is NOT READY, the flow opens a "Release blockers" issue with one task per blocker. Mentioning the blocker-fixer flow there makes it read the failing log and open a draft fix MR, in source code only, never in tests or CI.
  • A separate pipeline-doctor flow is triggered by failed pipelines and posts the cause on the merge request.

The nine stages: plan (blocker issue), create (draft fix MR), verify (pipeline check), package (package + SBOM), secure (finding re-check), release (gated release job), configure (config drift + CI lint), monitor (pipeline doctor), govern (human approval + verifiable ledger).

How we built it

  • Three flows written in the v1 flow YAML (in flows/), created in the AI Catalog and enabled on the project with three triggers: merge request marked ready, pipeline events (filtered to failed pipelines not started by a flow), and mention.
  • Components used: a deterministic tool step, a supervisor with sub-agents, a human approval gate, and single-turn tool agents for posting and committing.
  • .gitlab/duo/agent-config.yml and a real .gitlab-ci.yml with unit tests, GitLab SAST, dependency scanning, secret detection, packaging, SBOM generation and the release gate.
  • The release gate (ci/release_notes.py) and the verifier (ci/verify_ledger.py) are plain Python with no dependencies, and share one parser for the approval record.
  • The sample application is a small Python package (a version calculator from conventional commits) that the flows actually release. Releases v0.1.0, v0.2.0 and v0.3.0 were all shipped through the flow, and the pipeline history is public.

Challenges we ran into

  • The gate actually refused a release once: the approved report was written with whole-line bold, and the parser could not read a verdict. The pipeline doctor found the same cause independently, and the fix went through its own guard merge request before the release was retried.
  • GitLab delivered the same pipeline event twice; the pipeline doctor now picks its target deterministically and checks existing notes before posting.
  • The vulnerability tools do not expose "no longer detected", so the security reviewer reads the files on the release ref and treats anything it cannot confirm as still present.
  • Reject cancels the whole session, so nothing that must happen after a rejection can hang off the reject route.

Accomplishments that we're proud of

  • All nine lifecycle stages have real, linkable evidence in the project: merge requests, issues, pipelines and three releases.
  • The human approves once, at the outcome, and the approval is bound to an exact commit: pushing after approval makes the release gate refuse.
  • Anyone can check a release with one command, without trusting the release page.

What we learned

Agents are good at gathering and explaining evidence; the decision to release should still be bound to something a machine can check. Keeping the gate deterministic and the agents advisory made every failure visible instead of silent.

What's next

  • Let a Maintainer-level agent resolve vulnerabilities that are verified fixed.
  • Sign the ledger and the SBOM.
  • Package the flows so any project can enable them in a few clicks.

Built With

  • cyclonedx
  • dependency-scanning
  • gitlab-ci
  • gitlab-duo-agent-platform
  • gitlab-releases
  • gitlab-sast
  • python
  • secret-detection
Share this project:

Updates

Submission history