Inspiration
Most of the teams I build for are one to five people: startups, agencies and internal tools teams, many in Nigeria and across Africa, shipping to production every week. None of them has a DevOps, security or release person. For them, the work after the commit is not slow, it is skipped. Nobody reads the scan results. Releases go out without notes. The first alert for an outage is a customer message.
I wanted to see how much of that skipped work a crew of GitLab Duo agents could take on, without handing the keys to production to a model. That decided the autonomy level: the agents do the preparation, a human makes the one decision that matters.
What it does
Shipwright is a GitLab CI/CD pipeline plus four GitLab Duo Agent Platform custom flows that take a small Python web service (SOP Desk, a FastAPI app that answers staff questions from policy documents) from "merge request opened" to "approved for production and watched". Across the nine DevSecOps stages:
| Stage | What Shipwright does |
|---|---|
| Plan | Work-item triage flow (trigger: work item created) checks for duplicates, sets type and size labels, and posts acceptance criteria, a test plan and a production-risk note. Proven: session 9649639 on work item #7 (labels type::bug, size::S, acceptance criteria, test plan). |
| Package | CI builds a Docker image and pushes it to the GitLab Container Registry. |
| Secure | GitLab SAST and Secret Detection templates run in their own secure stage; the Gatekeeper's security sub-agent explains which findings are real for this code. Scanners and the policy job start immediately (needs: []), so a failing test can never hide a leaked secret. Proven on MR !6: Secret Detection job 17354628762 found 2 critical leaks, and the Gatekeeper treated them as real. |
| Monitor | Staging is smoke-checked on every main pipeline (health + a known question with a known answer; a scheduled production check is designed but not switched on, to avoid a failing hourly job). When a pipeline fails, the Pipeline Medic / Incident Responder flow reads the logs, classifies the failure, and either opens a small fix MR (lint, missing dependency), explains a failing test on the MR, or, for production or a leaked secret, opens an incident issue with a runbook and prepares a revert MR. It never merges the revert. Proven: bad commit de9f09ca failed staging:smoke and produced incident #11 plus revert MR !4, which a human merged. On MR !6 the Medic diagnosed the failing tests on the MR and opened issue #15 for the leaked (fake) keys; on a flaky test it recommended the exact fix a human then applied (70db9482). |
The human gate: deploy:production is when: manual, allow_failure: false, on the protected production environment. Everything up to it is prepared by the pipeline and the agents; a person reads the evidence pack and clicks once.
Carbon-aware: before anything heavy runs, a diffscope job looks at which files changed and writes a decision the later jobs read. A docs-only change skips the test run, the container build and the staging smoke. A carbon job at the end estimates Software Carbon Intensity for the pipeline, SCI = (E × I + M) per pipeline run, using job durations from the GitLab API and documented assumptions in crew/carbon.py, and reports the estimated grams saved by skipped jobs. Measured on GitLab: a docs-only pipeline (2960157596) was estimated at 0.369 g CO2e, saving 0.337 g against full runs of about 0.47 to 0.48 g (pipelines 2960095009 and 2960150460). These are estimates from job durations and documented assumptions, not metered energy.
How we built it
- App: SOP Desk, FastAPI + Uvicorn, with a small hand-written Okapi BM25 retriever over Markdown SOP files. No paid AI calls in the app, so tests are deterministic and free.
- Crew toolkit (
crew/): plain Python modules the CI jobs run and the agents can read:changelog(conventional commits to release notes),triage(rule-based first pass over failed CI logs: test failure, coverage gate, lint, dependency, config, flaky infra, secret leak, smoke failure),diffscope,carbon,policyandsmoke, behind one CLI (python -m crew ...). - Pipeline (
.gitlab-ci.yml): 11 stages, scope → lint → test → secure → package → govern → release → staging → production → monitor → report. Uses GitLab's SAST and Secret Detection templates, Docker-in-Docker for the image, dotenv artifacts to pass decisions between jobs,release-clifor releases, and a scheduledmonitor:smokejob against production that is defined but not switched on (no production URL, to avoid a failing hourly job).- Agents (.gitlab/duo/flows/): four custom flows in the flow registry v1 YAML format, ambient environment, using GitLab's built-in tools (merge request diffs and notes, job logs, security findings, issues, branches, commits). The Gatekeeper uses the supervisor/sub-agent pattern; the Pipeline Medic uses a router with a tiny no-tool "route" agent that turns the diagnosis into one word the router can match. - Tests: 57 unit tests across the app, the crew helpers and the flow YAML (checked against the documented custom-flow restrictions), 97–98% coverage, ruff clean.
- Proof it runs: over twenty main and merge request pipelines on the GitLab project (green runs, a failed lint MR, a failed staging smoke, a revert, a docs-only skip, a blocked secret MR), more than 25 flow sessions, all three Gatekeeper verdicts, and
deploy:productionwaiting on the protected environment for a human, which is the design. I used AI coding assistance to scaffold and iterate; I reviewed, ran and can explain every part.
Challenges we ran into
Flows cannot start flows. GitLab triggers ignore bot actions, so one agent cannot hand off to the next by mentioning it. I designed the crew around real lifecycle events instead (work item created, MR created, pipeline passed, pipeline failed), so each flow is started by something a human or the pipeline actually did.
An over-strict reviewer. On MR !5 the Gatekeeper's reviewer found a new, real but rare CommonMark edge case on each re-run, and the prompt turned every bug into NEEDS CHANGES. I fixed each case with a test, and also set a severity bar: high/medium findings, untested behaviour or a failed pipeline mean NEEDS CHANGES; low-severity edge cases are listed as follow-ups for the human. (The final READY came with zero findings.)
No structured output in custom flows. To route the Pipeline Medic between "incident", "fix" and "ignore", I added a small agent with no tools whose only job is to reply with one routing word.
The first pipeline failed in its very first job: the
diffscopejob imported the whole CLI, which pulled inhttpx, an app dependency the lightweight job did not install. Fix: importhttpxonly inside the smoke check. The next pipeline went green.Free tier, no cloud deploy. I kept the budget at zero, so staging is an ephemeral container inside CI rather than a hosted service, and the production job records the human approval and calls a deploy hook only if one is configured.
Accomplishments that we're proud of
One coherent loop that touches all nine DevSecOps stages, where every stage leaves something a person can see: an MR note, a release, an evidence pack, an incident issue.
A real human gate: the agents prepare everything and are explicitly told never to merge, approve or deploy.
Deterministic helpers under the agents: the agents reason, but versioning, policy checks, scoping and carbon maths are plain tested Python.
Carbon accounting built into every pipeline, not added afterwards.
What we learned
How the GitLab Duo Agent Platform's flows, triggers, tools and service accounts fit together, and where their limits are (no flow-to-flow chaining, ambient environment only, tool scopes).
That "supervised" is a design decision, not a weaker version of "hands-off": for a small team, an agent that prepares a one-click decision is easier to trust than one that ships alone.
Splitting the work so that models handle judgement (is this finding real, what broke, how to explain it) and code handles anything that must be exact.
What's next for Shipwright
A runbook library: every incident runbook the responder writes saved to the repo so the next incident starts from the last one.
Measured before/after numbers on a real small-team project: minutes of manual post-commit work per release, and stages that used to be skipped.
Package the crew so another team can add it to its own project with one include and the four flows.
Better carbon inputs: runner-specific power figures and regional grid intensity instead of a global placeholder.
Log in or sign up for Devpost to join the conversation.