Inspiration

Code review is mostly automated now. The decision that comes after it is not. On small teams someone opens the pipeline, scrolls the security tab, checks the changelog, asks in chat whether an incident is open and then clicks deploy. On a busy day half of that list gets skipped. I wanted that checklist to run itself without handing the final say to a model that might be wrong.

What it does

When a merge request pipeline finishes, a GitLab Duo Agent Platform flow starts from a pipeline trigger. Three agents run in a line.

  • The investigator has read-only tools. It finds the MR, the newest pipeline, an evidence file the pipeline produced, failed jobs, open incident issues, earlier verdicts and the release policy in the repo.
  • The judge has no tools. It applies five written rules and writes a verdict note where every line links to a job, a file or an issue.
  • The publisher posts the note, sets one gonogo::go, gonogo::hold or gonogo::no-go label and opens one issue per blocker on a NO-GO after checking for duplicates. Tool options pin all other merge request fields to null, so it can only add or remove labels.

The evidence file comes from plain, unit-tested Python, not from the model. It holds nine checks (tests, coverage, SAST, secret detection, environment config ranges, changelog, sensitive files, change size, deleted tests) and a floor verdict. The model may make the verdict stricter but never looser.

After a merge, the pipeline packages the site into the generic package registry, writes release notes and stops at a manual production job. A person starts it. The job runs gonogo gate, which re-reads the evidence floor and reads the labels of the one merge request that merged the deployed commit, matched by its merge or squash commit, never the union of every merge request that contains it. The label alone is not enough, because any developer can add it by hand. The gate also reads the merge request notes with the CI job token and needs the latest verdict note from the GoNoGo service account to be GO for the merged head commit, and it needs that same commit-bound GO for every merge request since the last release tag. It fails closed. If the labels or notes cannot be read, the label is missing, or there is no trusted GO verdict that names the merged head commit, the deploy is refused. Then a smoke check polls the live GitLab Pages site until it serves the new version, and the pipeline cuts a GitLab Release.

A second flow handles the time after release. When someone files an issue labelled incident, it lists the merge requests in the last production deployment, ranks likely suspects by file overlap and posts a rollback plan. It never rolls back by itself.

How I built it

  • Two custom flows in flow registry v1 YAML (.gitlab/duo/flows/) with coding_environment: none, because they only call the GitLab API. Triggers on Pipeline events with a filter on object_attributes.status in success or failed and object_attributes.source equal to merge_request_event, plus Assign reviewer, Mention and Work item created.
  • A custom Duo Chat agent, GoNoGo explainer, that tells a developer the smallest set of changes that would turn a verdict into GO.
  • A Python package with no third-party runtime dependencies for evidence, gate, release notes, smoke checks and config checks, with 129 unit tests at 95 percent line coverage.
  • A pipeline with test, SAST, secret detection, evidence, package, deploy, monitor and release stages.
  • tools/validate_flows.py checks each flow against the JSON schema GitLab itself uses for flow YAML, plus graph, prompt variable and tool name checks. It found a real problem early. max_cycles is in the flow registry docs but the GitLab schema rejects it, so I removed it before the first upload.

DevSecOps stages covered

Plan (blocker issues), create (merge requests), verify (tests, coverage, preview smoke check), package (generic package registry), secure (SAST, secret detection, evidence checks), release (GitLab Release with generated notes), configure (environment config validation, production environment), monitor (production smoke check, a scheduled health check job that is written but has not run yet, incident triage flow), govern (policy as code, evidence archived per pipeline, verdict trail on every MR, deploy gate).

Challenges I ran into

The first version of the pipeline trigger fired on every pipeline status change, so two early HOLD notes appeared on !1 while the evidence job was still running. I narrowed the trigger to status success or failed and source merge_request_event, which limits it to finished merge request pipelines. The first incident was also filed before any production deployment existed, so triage had nothing to compare with. I closed it with a note and refiled it as #8, and triage then found deployment 1627480743 and ranked !1 as the suspect.

The project shows the GO verdict on !1 and its merge as 2fb4f8b. A later run that started after incident #2 was filed relabelled the merged MR HOLD. Because no token was set, the gate of that day skipped the label check and deployed anyway. Both behaviours are now fixed. Merged merge requests are skipped, and the gate fails closed. A clean cycle then proved the fix. GoNoGo put HOLD on its own fix MR !3 because SAST found a medium SSRF risk in the new label read, gave GO after a pinned HTTPS fix, and after the merge the deploy job logged MR carries gonogo::go. without any token, passed its smoke check and cut release v0.1.26.

On 19 October GoNoGo held a second fix of its own. MR !4 made the gate read only the merge request that merged the deployed commit. GoNoGo said HOLD because the new lookup had no merged state check and its fast-forward fallback had no squash guard, two real gaps. After the fix it said GO. The first deploy attempt hit a network error on the runner while reading the label and the gate refused it, and the retry logged MR carries gonogo::go. and cut release v0.1.32. Then MR !5 was merged before its verdict arrived. Its deploy job logged MR does not carry gonogo::go (labels none), deploy refused. The flow run on the merged MR stopped in the investigator and changed nothing, though its judge still wrote a HOLD it could not attach, so the flow now routes a SKIP from the investigator straight to the end.

Later on 19 October GoNoGo held MR !6 because two diff notes loosened the gate's trust path, and gave GO after two fix commits. Then MR !8 closed a gap found in review, where a hand-added gonogo::go label used to be enough. It was tested live. MR !8 and the README-only MR !9 were both merged with the label added by hand and no verdict. The deploy of !8 was refused on its HOLD evidence floor, so only !9 tested the new check. The deploy job of !9 printed Evidence floor is GO. and MR carries gonogo::go. and still refused with No GO verdict from ai-gonogo-gitlab-ai-hackathon on !9, deploy refused. MR !10 then took the normal path. GoNoGo said GO at 07:21 UTC, the gate logged GO verdict from ai-gonogo-gitlab-ai-hackathon on !10 is newer than the last commit., the smoke check passed and the pipeline cut release v0.1.56. Release v0.1.56 also carried !8 and !9, which never got a verdict, because the gate checks only the merge request that merged the deployed commit. That gap is now closed. MR !11 binds each GO verdict to the head commit of its merge request, and MR !12 and MR !13 make the gate require a commit-bound GO for every merge request since the last release tag. GoNoGo held these gate changes ten times with the evidence floor at GO, seven times on !12 and three on !13, each time on a real gap, before it said GO. The first deploy after !12 was refused because the evidence floor was NO-GO, after a unit test failed only on main, and the same job also showed that the deploy image has no git to read the release history. !13 fixed the test and moved the release range into the evidence file, which GoNoGo had approved too early on !12. In job 17296415580 the gate logged Release range since v0.1.59 has a GO verdict for every merge request. and still refused, because the two changes together are 549 lines, above the 400 line review limit, so the release is held. The pipeline has no approval path for a held release yet, so production stays at v0.1.59 until the owner adds a protected, approver-gated deploy job. Since then MR !14 also makes every accepted GO verdict name its commit and drops the old timestamp rule.

Accomplishments that I'm proud of

The demo's risky merge request deletes the test that would have caught its own bug, and every remaining test passes. GoNoGo still said NO-GO on the live run, because the config range check and the changelog check are plain code that the change did not touch. The verdict is on MR !2, and the safe MR !1 got GO and was merged.

What I learned

A pipeline trigger is noisier than it looks. The GoNoGo flow fired on every status change at first and posted HOLD notes before the evidence job had finished, so a filter on the finished merge request pipeline matters more than the flow prompt. I also learned that incident triage needs a deployment to compare with, because an incident filed before the first deploy has nothing to rank.

What's next for GoNoGo

Add a deterministic first step that ends the flow on a merged or closed merge request without any model call. Read deployment frequency and change failure rate from the Deployments API so the judge can weigh a risky change against the team's real track record, and let teams keep their policy in a shared group project so every repo uses the same rules.

Path and autonomy

Path A, Start Fresh. Supervised. Agents run the whole review on their own and a person approves the outcome by starting the production deploy.

Links

Built by Michael Lip, github.com/theluckystrike, Warsaw, Poland.

Built With

  • ai-agents
  • devops
  • gitlab
  • gitlab-ci
  • gitlab-duo
  • python
  • release-management
Share this project:

Updates

Submission history