-
-
Risk score and band on merge request 37: 2.38, LOW, route Hands-off
-
Merge request 37: the six-step soloship-pipeline chain, one note per step
-
Four routes: Hands-off, Supervised, Assisted, Blocked
-
Audit chain: hash-linked records, verified by soloship audit verify
-
SoloShip: risk-scored merge requests with GitLab Duo Agent Platform
-
MR 37: the six soloship-pipeline notes in order, score to audit
-
MR 36 auth helper: score 23.23, MEDIUM, route Supervised
-
MR 27 confirmed secret: score 100.0, CRITICAL, route Blocked
-
CI pipeline: test, pip-audit, secret detection, Semgrep, risk, verify, package
-
docs/EVIDENCE.md: calibration targets and measured label-set results
See it in 60 seconds. Watch the video, or open merge request 37. Nobody mentioned a bot. Opening the merge request started one custom GitLab Duo flow, and six notes appeared in order: score, review, config check, security triage, release decision, audit record. The score, 2.38, is the same number the engine prints on a laptop. GitLab Duo agent session pages may ask for a GitLab sign-in; the repository and the merge requests open without one.
Inspiration
When a team has a second reviewer, a typo fix, an auth change, and a leaked secret get different attention. A solo developer or a two-person team does not have that reviewer, so all three take the same road: merge. When code gets cheaper to produce, deciding what is safe to ship becomes the scarce skill. We wanted the missing reviewer. Not a chatbot's opinion, but a decision you can reproduce, explain, and audit.
What it does
SoloShip scores every merge request from 0 to 100 and picks one of four routes.
| Band | Score | Route | What happens |
|---|---|---|---|
| LOW | 0 to 8 | Hands-off | Can pass every gate |
| MEDIUM | 9 to 59 | Supervised | Automatic until production, then one person approves |
| HIGH | 60 to 100, or raised by a rule | Assisted | A person at every gate |
| CRITICAL | confirmed secret, score 100 | Blocked | The release is blocked |
Security scans stay on for every band.
The score comes from a deterministic Python engine, not from a model's opinion. GitLab Duo Agent Platform flows run the engine on the real diff, read the result, and post it on the merge request.
One trigger, six steps. The custom flow soloship-pipeline starts when a merge request is created. Six components run in order: risk scorer, reviewer, config guard, security triage, release summary, and auditor. Each one reads the previous note and quotes it. On merge request 37 the flow finished with score 2.38, LOW, Hands-off. It ran again on merge request 38, once when the draft was created and once when it was marked ready.
Built on GitLab Duo Agent Platform
Duo is how SoloShip works, not a label on it.
- Ten custom flows in the AI Catalog, each with its own service account: the
soloship-pipelineorchestrator and nine single-purpose flows (risk scorer, reviewer, config guard, security triage, release summary, auditor, planner, creator, monitor). - Real tools, real diffs. The flows call
get_merge_request,list_merge_request_diffs, andcreate_merge_request_note. They run in apython:3.12-slimenvironment with a setup script, so the engine executes inside the Duo runner. - Flows that do not guess. The reviewer must quote a changed line. Security triage reports a finding only when a scanner artifact contains it. When the diff cannot be read, the scorer says so and posts no score.
- Every decision is recorded. The auditor appends one line to a hash-chained JSONL log on the
audit-logbranch.soloship audit verifyaccepted 8 records. - CI around it. The pipeline runs tests, pip-audit, secret detection, Semgrep SAST, a container image build, and a staging health check. A
deploy_productionjob exists as a manual approval gate for the Supervised route. It is a gate, not a real deploy.
| Flow | Trigger | What it does | Session |
|---|---|---|---|
| soloship-pipeline | Merge request created | Runs all six steps in one session | 9297544 |
| risk-scorer | Mention | Runs the engine on the diff, posts score, band, route | 9296937 |
| reviewer | Mention | Quotes a changed file and a changed line | 9296955 |
| config-guard | Mention | Names changed paths, flags policy and sensitive ones | 9296964 |
| security-triage | Mention | Reports only what a scanner artifact contains | 9296973 |
| release-summary | Mention | Quotes the score and says whether production waits | 9296984 |
| auditor | Mention | Appends one record to the audit chain | 9296998 |
| planner | Mention | Posts the task split as an issue note | 9296328 |
| creator | Mention | Opens a draft merge request with a test of an existing helper | 9296600 |
| monitor | Mention | Posts a finding present in a successful SAST artifact | 9296365 |
The first six mention flows ran as a chain on merge request 36. Planner, creator, and monitor each ran on their own mention.
How the score works
The engine reads soloship/risk/weights.yaml, sums ten signals, and applies five hard rules that can change the published band.
| Hard rule | Effect |
|---|---|
| Confirmed secret | Score 100, band CRITICAL |
| Critical new SAST finding or critical dependency vulnerability | Band at least HIGH, score unchanged |
Policy file (.gitlab-ci.yml or the weights file) |
Band at least HIGH, score unchanged |
Sensitive path (auth/, security/, migrations/, Dockerfile, .gitlab-ci.yml) |
LOW is raised to MEDIUM |
| Failed test gate | Blocks the release |
That is why the published band and the number can differ, and why we publish both side by side:
| Merge request | Score | Band | Why |
|---|---|---|---|
| !3 docs change | 2.56 | LOW | Wording only |
| !36 auth helper | 23.23 | MEDIUM | Score is in 9 to 59, and the auth path is on the sensitive-path floor |
| !23 critical SAST fixture | 21.15 | HIGH | The security rule raises the band, the score stays 21.15 |
| !27 confirmed secret fixture | 100 | CRITICAL | The secret rule sets both |
Challenges
- The numeric band and the published band are different facts, and a single cutoff table hides the hard rules. We fixed this by publishing score and band together.
- Early flows could not read merge request diffs. We added the diff tool and made the empty-diff case post "no score produced" instead of crashing.
- Duo's default environment skips setup. We moved to a full environment with an image and a setup script so the engine runs inside the runner.
- A config guard prompt missed
waves/w17/soloship/risk/weights.yamlbecause it matched an exact path. We changed the prompt to match path endings and reran it. - The group MCP setting could not be enabled with our role.
Accomplishments
Measured results, not a user count or a time-saved claim.
| Measurement | Result |
|---|---|
| Frozen label sets, exact band | 19/30, 11/15, 13/20 |
| Within one band | 30/30, 15/15, 20/20 |
| High-risk merge requests labeled LOW | 0 on all three sets |
| GitLab risk-scorer notes matching the engine | 30 of 30 merge requests |
| Tests | 101 passed locally |
| Install from a warm wheel cache | 6.1885 seconds |
One soloship score |
0.1382 seconds |
| Duo flows with a finished session | 10 |
| Audit chain | 8 records verified |
The same assistant wrote all three label sets, so treat the accuracy numbers as a consistency check, not independent validation. Details are in docs/EVIDENCE.md.
Adopt it in three steps
python3 -m venv .venv- Install the repository with
.venv/bin/pip install .venv/bin/soloship score
A second folder on the same machine did this in 6.19 seconds from a warm wheel cache.
What we learned
A flow is only useful if it reads the diff and quotes it. A deterministic core makes a language-model flow trustworthy, because the model reports and routes while the number comes from code. And an audit trail changes how a decision feels: you can verify it later.
What's next
- Run the route's parent pipeline example on GitLab.
- Give Package its own Duo flow. Today it is a CI image job.
- Let Duo call the engine as an MCP tool (
score_change). The server is defined, but the group flag is off, and the AI Catalog path needs an approved HTTP endpoint. - Start the existing nine flows from triggers other than mention.
Honest limits
- Held-out exact band match is 11/15, short of our locked 12/15 target. The target was not changed.
- The nine single-purpose flows ran only when a person mentioned them. The orchestrator ran from the Merge request created and marked ready triggers.
- MCP was not enabled for this group, and no session called it.
deploy_productionis a manual approval gate, not a real deploy. The LOW-band "automatic" path is policy, not wired into CI.- Package has no Duo flow. Create adds a test and does not implement the issue.
- Software carbon intensity was not measured.
Everything above is reproducible from the public repository. Start with the video, then merge request 37.
Built With
- fastapi
- gitlab-ai-catalog
- gitlab-ci/cd
- gitlab-duo-agent-platform
- pytest
- python
- yaml