delivery-gates

August 10, 2026 · View on GitHub

tests adr-guard GitHub Marketplace

Delivery controls that actually block, rather than conventions people agree to and forget.

Four deterministic gates, plus one LLM reviewer that deliberately cannot block:

GateWhat it doesUse it
ADR GuardFails a pull request that changes watched code without recording an architecture decisionchohan-sarmad-ali/delivery-gates@v1 (Marketplace)
Release blockersFails while a ship checklist still has unchecked items marked as blockerschohan-sarmad-ali/delivery-gates/release-blockers@v1
Link checkFails when a relative link in any markdown file points at something that isn't therechohan-sarmad-ali/delivery-gates/link-check@v1
ADR numbersFails when two decision records share a number in the same directory — the silent duplicate a merge never flagschohan-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 blockcode-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

InputDefaultWhat it does
watch-pathssrc/**,app/**,lib/**,services/**Code significant enough that changing it should be a recorded decision
adr-pathsdocs/adr/**,docs/decisions/**Where decisions live. Touching one satisfies the gate
ignore-pathstests, docs, lockfilesNever require a decision
waiver-markerADR-Exempt:Prefix that waives the gate, followed by a reason
base-refthe PR baseBranch 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.


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.