Skip to content

Commit c91224a

Browse files
dirvineHermes Agent
andauthored
ci: add ADR governance compliance (#104)
* ci: add ADR governance compliance * fix: compare ADR governance against first parent --------- Co-authored-by: Hermes Agent <hermes@saorsa.io>
1 parent 4d04484 commit c91224a

7 files changed

Lines changed: 342 additions & 0 deletions

File tree

.adr-kit.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# ADR kit configuration for repository-level decision governance.
2+
adr_directory: docs/adr
3+
filename_pattern: "ADR-[0-9]{4}-[a-z0-9-]+\.md"
4+
statuses:
5+
- Proposed
6+
- Accepted
7+
- Superseded
8+
- Deprecated
9+
- Rejected
10+
immutable_statuses:
11+
- Accepted
12+
required_sections:
13+
- Context
14+
- Decision
15+
- Consequences
16+
- Validation
17+
human_acceptance_required: true
18+
ai_may_accept: false
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: ADR Governance
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'docs/adr/**'
7+
- '.adr-kit.yaml'
8+
- 'scripts/adr-governance.py'
9+
- '.github/workflows/adr-governance.yml'
10+
push:
11+
branches:
12+
- main
13+
- master
14+
paths:
15+
- 'docs/adr/**'
16+
- '.adr-kit.yaml'
17+
- 'scripts/adr-governance.py'
18+
- '.github/workflows/adr-governance.yml'
19+
20+
jobs:
21+
adr-governance:
22+
name: Validate ADRs and immutable Accepted status
23+
runs-on: ubuntu-latest
24+
steps:
25+
- uses: actions/checkout@v4
26+
with:
27+
fetch-depth: 0
28+
- name: Run ADR governance checks
29+
run: python3 scripts/adr-governance.py
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# ADR-0001: Adopt Architecture Decision Records
2+
3+
- **Status:** Accepted
4+
- **Date:** 2026-06-01
5+
- **Decision owners:** Engineering team
6+
- **Reviewers:** Engineering team
7+
- **Supersedes:** none
8+
- **Superseded by:** none
9+
- **Related:** Portfolio ADR governance rollout
10+
11+
## Context
12+
13+
This repository participates in a wider Autonomi/Saorsa engineering portfolio where architectural decisions affect protocols, storage behaviour, cryptography, APIs, operations, and long-term maintenance. AI-assisted coding makes it easier to generate large changes quickly, but it also increases the risk that design intent, trade-offs, and constraints are lost.
14+
15+
## Decision Drivers
16+
17+
- Preserve the reasoning behind architectural choices.
18+
- Make trade-offs visible during code review.
19+
- Give humans and AI coding tools a reliable source of architectural context.
20+
- Prevent silent drift from accepted engineering decisions.
21+
- Support cross-repository consistency across Autonomi and Saorsa projects.
22+
23+
## Considered Options
24+
25+
1. Keep architecture reasoning only in PR descriptions and issues.
26+
2. Maintain informal design notes without lifecycle governance.
27+
3. Adopt version-controlled Architecture Decision Records with CI governance.
28+
29+
## Decision
30+
31+
We will maintain Architecture Decision Records in `docs/adr/` using the repository template. New decisions start as `Proposed`; once reviewed and agreed, they may be marked `Accepted`. Accepted ADRs are immutable: if the decision changes, a new superseding ADR must be created rather than editing the accepted record.
32+
33+
## Consequences
34+
35+
### Positive
36+
37+
- Architectural intent becomes searchable and reviewable.
38+
- AI coding agents have explicit project constraints to inspect before changing code.
39+
- Reviews can check decision quality, not just implementation mechanics.
40+
- Supersession creates an audit trail instead of rewriting history.
41+
42+
### Negative / Trade-offs
43+
44+
- Design work becomes more explicit and may slow rushed changes.
45+
- Engineers must keep ADRs aligned with meaningful architectural changes.
46+
47+
### Neutral / Operational
48+
49+
- CI enforces ADR format and immutable Accepted status.
50+
- Reviewers should reject architectural PRs with weak, missing, or AI-generated-without-debate ADRs.
51+
52+
## Validation
53+
54+
The ADR governance CI job must pass on every PR. Reviewers should verify that architectural changes include appropriate ADR coverage and that accepted ADRs are not modified in-place.

docs/adr/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Architecture Decision Records
2+
3+
This directory contains Architecture Decision Records (ADRs) for this repository.
4+
5+
## Rules
6+
7+
1. Use `ADR-NNNN-short-title.md` names with four-digit numbers.
8+
2. New ADRs start as `Proposed`.
9+
3. `Accepted` ADRs are immutable. If the decision changes, create a new ADR and mark the old ADR as superseded by reference, not by editing its accepted content.
10+
4. Architectural PRs must add or update an ADR before merge.
11+
5. Reviews must check ADR correctness, evidence, trade-offs, and compliance — not just presence.
12+
13+
## Template
14+
15+
Use [`TEMPLATE.md`](./TEMPLATE.md).
16+
17+
## Tooling
18+
19+
See [`TOOLING.md`](./TOOLING.md) for `adrs`, `adr-kit`, and AI harness setup.

docs/adr/TEMPLATE.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# ADR-NNNN: <Decision Title>
2+
3+
- **Status:** Proposed
4+
- **Date:** YYYY-MM-DD
5+
- **Decision owners:** <names/handles>
6+
- **Reviewers:** <names/handles>
7+
- **Supersedes:** <ADR-NNNN or none>
8+
- **Superseded by:** <ADR-NNNN or none>
9+
- **Related:** <issues/PRs/specs>
10+
11+
## Context
12+
13+
What problem, constraint, or architectural tension forced this decision?
14+
15+
## Decision Drivers
16+
17+
- <driver 1>
18+
- <driver 2>
19+
- <driver 3>
20+
21+
## Considered Options
22+
23+
1. <option A>
24+
2. <option B>
25+
3. <option C>
26+
27+
## Decision
28+
29+
We will <state the chosen option clearly>.
30+
31+
## Consequences
32+
33+
### Positive
34+
35+
- <benefit>
36+
37+
### Negative / Trade-offs
38+
39+
- <cost or risk>
40+
41+
### Neutral / Operational
42+
43+
- <ongoing implication>
44+
45+
## Validation
46+
47+
How will we know this decision remains correct? Include tests, metrics, audit checks, or review triggers.
48+
49+
## Notes for AI-assisted work
50+
51+
AI tools may help draft this ADR, but **must not mark it Accepted without human review**. Accepted ADRs are immutable: create a new superseding ADR rather than editing an Accepted ADR.

docs/adr/TOOLING.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# ADR Tooling and AI Harness Setup
2+
3+
We use ADRs as engineering memory, not paperwork. They capture *why* a decision was made, what alternatives were rejected, and what consequences we accept.
4+
5+
## Install `adrs`
6+
7+
`adrs` is the preferred local CLI for creating, searching, and checking ADRs.
8+
9+
```bash
10+
cargo install adrs
11+
# or, if the repo has a Rust toolchain wrapper, use that wrapper's cargo equivalent.
12+
```
13+
14+
Useful commands:
15+
16+
```bash
17+
adrs list
18+
adrs search "post-quantum"
19+
adrs doctor
20+
```
21+
22+
## Install `adr-kit`
23+
24+
`adr-kit` is used for agent-aware ADR analysis and policy/lint generation where available.
25+
26+
Recommended isolated install:
27+
28+
```bash
29+
uv tool install adr-kit
30+
# fallback
31+
uvx adr-kit --help
32+
```
33+
34+
If the published package name differs on your machine, install from the project source used by the team and keep it isolated with `uv tool` or `pipx` rather than a global Python environment.
35+
36+
## AI harness guidance: pi, Codex, Claude Code, OpenCode
37+
38+
Add this project instruction to every AI coding harness profile (`AGENTS.md`, `CLAUDE.md`, Codex/OpenCode project rules, pi harness prompts, etc.):
39+
40+
```text
41+
Before changing architecture, protocols, storage formats, crypto, network behaviour, public APIs, data models, or operational invariants, inspect docs/adr/.
42+
If the change creates or changes an architectural decision, draft or update a Proposed ADR using docs/adr/TEMPLATE.md.
43+
Never edit an Accepted ADR. Create a superseding ADR instead.
44+
Never mark an ADR Accepted autonomously; that requires human engineering review and debate.
45+
During review, check ADR correctness, rejected alternatives, evidence, consequences, and immutable-Accepted compliance.
46+
```
47+
48+
## Review standard
49+
50+
Do **not** "vibe code" ADRs. A useful ADR must show clear thinking: context, options, trade-offs, consequences, and validation. AI can help prepare a draft, but humans must debate and own the decision.

scripts/adr-governance.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#!/usr/bin/env python3
2+
"""Repository-local ADR governance checks.
3+
4+
Enforces:
5+
- ADR files live under docs/adr/ and use ADR-NNNN-short-title.md.
6+
- Required sections exist.
7+
- Status is present and from the allowed lifecycle.
8+
- Accepted ADRs are immutable after acceptance. If a decision changes, create a
9+
new ADR and supersede by reference rather than editing the Accepted ADR.
10+
"""
11+
from __future__ import annotations
12+
13+
import os
14+
import re
15+
import subprocess
16+
import sys
17+
from pathlib import Path
18+
19+
ADR_DIR = Path("docs/adr")
20+
ALLOWED_STATUSES = {"Proposed", "Accepted", "Superseded", "Deprecated", "Rejected"}
21+
REQUIRED_SECTIONS = ["Context", "Decision", "Consequences", "Validation"]
22+
FILENAME_RE = re.compile(r"^ADR-\d{4}-[a-z0-9][a-z0-9-]*\.md$")
23+
STATUS_RE = re.compile(r"(?im)^\s*(?:[-*]\s*)?.*?Status.*?:\s*(.+?)\s*$")
24+
25+
26+
def run(cmd: list[str]) -> str:
27+
return subprocess.check_output(cmd, text=True, stderr=subprocess.DEVNULL).strip()
28+
29+
30+
def status_of(text: str) -> str | None:
31+
m = STATUS_RE.search(text)
32+
return m.group(1).strip().strip("*").strip() if m else None
33+
34+
35+
def base_ref() -> str | None:
36+
ref = os.environ.get("GITHUB_BASE_REF")
37+
if ref:
38+
return f"origin/{ref}"
39+
# On push, compare against first parent where available.
40+
try:
41+
return run(["git", "rev-parse", "HEAD^1"])
42+
except Exception:
43+
return None
44+
45+
46+
def changed_files_against_base(base: str) -> list[str]:
47+
try:
48+
return run(["git", "diff", "--name-only", f"{base}...HEAD"]).splitlines()
49+
except Exception:
50+
try:
51+
return run(["git", "diff", "--name-only", f"{base}", "HEAD"]).splitlines()
52+
except Exception:
53+
return []
54+
55+
56+
def file_at(ref: str, path: str) -> str | None:
57+
try:
58+
return run(["git", "show", f"{ref}:{path}"])
59+
except Exception:
60+
return None
61+
62+
63+
def main() -> int:
64+
errors: list[str] = []
65+
if not ADR_DIR.exists():
66+
print("No docs/adr directory; nothing to validate.")
67+
return 0
68+
69+
adr_files = sorted(p for p in ADR_DIR.glob("ADR-*.md") if p.is_file())
70+
base = base_ref()
71+
changed = changed_files_against_base(base) if base else []
72+
changed_adr_paths = {Path(name) for name in changed if name.startswith("docs/adr/ADR-") and name.endswith(".md")}
73+
74+
# Grandfather legacy ADRs when first installing governance. Enforce full
75+
# structure on ADRs touched by this PR, while still checking duplicate
76+
# numbers across the full directory.
77+
files_to_validate = sorted((Path(p) for p in changed_adr_paths if Path(p).exists()), key=str) if base else adr_files
78+
79+
seen_numbers: dict[str, Path] = {}
80+
for path in adr_files:
81+
number = path.name.split("-", 2)[1] if "-" in path.name else path.name
82+
if number in seen_numbers:
83+
errors.append(f"{path}: duplicate ADR number also used by {seen_numbers[number]}")
84+
seen_numbers[number] = path
85+
86+
for path in files_to_validate:
87+
if not FILENAME_RE.match(path.name):
88+
errors.append(f"{path}: filename must match ADR-NNNN-short-title.md")
89+
text = path.read_text(encoding="utf-8")
90+
st = status_of(text)
91+
if not st:
92+
errors.append(f"{path}: missing Status")
93+
elif st not in ALLOWED_STATUSES:
94+
errors.append(f"{path}: invalid Status '{st}' (allowed: {', '.join(sorted(ALLOWED_STATUSES))})")
95+
for section in REQUIRED_SECTIONS:
96+
if not re.search(rf"(?im)^##\s+{re.escape(section)}\b", text):
97+
errors.append(f"{path}: missing required section '## {section}'")
98+
99+
if base:
100+
for name in changed:
101+
if not (name.startswith("docs/adr/ADR-") and name.endswith(".md")):
102+
continue
103+
old = file_at(base, name)
104+
if old is None:
105+
continue
106+
old_status = status_of(old)
107+
if old_status == "Accepted":
108+
errors.append(
109+
f"{name}: Accepted ADRs are immutable. Create a new superseding ADR instead of editing this file."
110+
)
111+
112+
if errors:
113+
print("ADR governance failed:")
114+
for e in errors:
115+
print(f"- {e}")
116+
return 1
117+
print(f"ADR governance passed ({len(adr_files)} ADR file(s) checked).")
118+
return 0
119+
120+
if __name__ == "__main__":
121+
raise SystemExit(main())

0 commit comments

Comments
 (0)