delivery-gates
August 10, 2026 · View on GitHub
Delivery controls that actually block, rather than conventions people agree to and forget.
Four deterministic gates, plus one LLM reviewer that deliberately cannot block:
| Gate | What it does | Use it |
|---|---|---|
| ADR Guard | Fails a pull request that changes watched code without recording an architecture decision | chohan-sarmad-ali/delivery-gates@v1 (Marketplace) |
| Release blockers | Fails while a ship checklist still has unchecked items marked as blockers | chohan-sarmad-ali/delivery-gates/release-blockers@v1 |
| Link check | Fails when a relative link in any markdown file points at something that isn't there | chohan-sarmad-ali/delivery-gates/link-check@v1 |
| ADR numbers | Fails when two decision records share a number in the same directory — the silent duplicate a merge never flags | chohan-sarmad-ali/delivery-gates/adr-numbers@v1 |
| Code review (workflow, not an action) | An LLM reviews every pull request — and by design can only comment, never block | code-review.yml, policy in ADR-0002 |
The split is the point: blocking is reserved for checks whose failure reason can be stated exactly. An LLM's judgement gets a voice, not a veto.
This repository is governed by its own gates — every pull request here runs all four, and the ADR guard has been proven to fail on a real violation, not only in tests.
ADR Guard
Most teams adopt architecture decision records, then discover six months later that nobody updated one. This asks the question on every pull request.
What a failure looks like
Error: This pull request changes 3 watched file(s) without recording a decision.
- services/engine/pricing.py
- services/engine/api.py
- app/checkout/route.ts
And in the job summary:
ADR guard failed
Either add an ADR under a path in
adr-paths, or waive it explicitly by adding a line to the pull request description:ADR-Exempt: <why this needs no ADR>
Wire it in
# .github/workflows/adr-guard.yml
name: adr-guard
on: pull_request
jobs:
adr-guard:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # required — the guard diffs against the merge base
- uses: chohan-sarmad-ali/delivery-gates@v1
with:
watch-paths: 'src/**,app/**,services/**'
adr-paths: 'docs/adr/**'
One commit. No dependencies — it runs on the Python already present on the runner.
Inputs
| Input | Default | What it does |
|---|---|---|
watch-paths | src/**,app/**,lib/**,services/** | Code significant enough that changing it should be a recorded decision |
adr-paths | docs/adr/**,docs/decisions/** | Where decisions live. Touching one satisfies the gate |
ignore-paths | tests, docs, lockfiles | Never require a decision |
waiver-marker | ADR-Exempt: | Prefix that waives the gate, followed by a reason |
base-ref | the PR base | Branch to diff against |
Globs support **, * and ?. * does not cross a /, so src/*.py matches
src/app.py but not src/deep/app.py.
Release blockers
Parses a ship checklist and fails while any item marked as a blocker — BLOCKER in the
item or its heading, or the ⛔ character — is still unchecked. Ordinary unchecked items are
ignored: a checklist you cannot merge against until every box is ticked stops being written
honestly.
- uses: actions/checkout@v4
- uses: chohan-sarmad-ali/delivery-gates/release-blockers@v1
with:
checklist-path: 'docs/SHIP-CHECKLIST.md'
Full example with the reasoning inline: examples/release-blockers.yml.
Link check
Fails when a relative link in a markdown file points at something that isn't there, and
reports file:line -> target. External URLs are skipped deliberately — they rot for
reasons unconnected to your repository, and a check that goes red for someone else's
downtime is one people learn to disbelieve (ADR-0003).
- uses: actions/checkout@v4
- uses: chohan-sarmad-ali/delivery-gates/link-check@v1
ADR numbers
Two branches each take "the next number", the filenames differ, nothing conflicts, and git
merges both — the duplicate surfaces months later when "ADR-7" turns out to be ambiguous.
This converts the silent variant into a loud one. Numbers compare as integers (007 and
0007 collide); collisions are scoped per directory, so category-numbered subdirectories
stay clean (ADR-0004, with the six-year history
in adr/madr#28).
- uses: actions/checkout@v4
- uses: chohan-sarmad-ali/delivery-gates/adr-numbers@v1
Design decisions that apply to every gate
Deliberately hard to trip. Tests, documentation and lockfiles are ignored by default, and an ADR anywhere in the diff satisfies the guard. That looks like weakness and isn't: a gate that fires on a dependency bump gets switched off within a fortnight, and a switched-off gate protects nothing. The failure mode worth optimising against is being ignored, not being evaded.
Waivers are first-class, and they demand a reason. ADR-Exempt: with nothing after it
does not waive — that isn't an exception, it's the gate being quietly disabled. With a
reason, the run passes and the reason is written into the job summary where it can be read
later. A gate with no waiver procedure doesn't get respected. It gets deleted.
Fail open when the environment is wrong. A missing merge base, an absent checklist, an unset credential — each produces a warning and a green check rather than a failure. A check that blocks for the wrong reason teaches people to bypass it for the right ones.
Every decision above is recorded, with its accepted costs, in docs/adr/.
Tests
python -m unittest discover -s tests -v
72 tests, standard library only, no install step. Five drive the ADR guard against a real git repository rather than mocking the diff.
Licence
MIT.
Maintained by Sarmad Ali Chohan — more on making delivery discipline executable, including the case studies behind these gates, on my profile. LinkedIn.