Skip to content

Commit 42455b1

Browse files
ci(staleness): tolerate in-window reusable pins + wire deliberate bump path (#397)
## Problem `standards` hosts reusable workflows (`governance-reusable.yml`, `hypatia-scan-reusable.yml`, `scorecard-reusable.yml`, …). Consumer repos pin them by full SHA, and `scripts/check-workflow-staleness.sh` (run in every consumer via the governance reusable) **failed the consumer's CI whenever its pin ≠ standards HEAD**. Because HEAD moves often, every standards commit turned every consumer PR red until someone manually re-bumped the pins. Observed live in a stapeln session: HEAD moved `5a93d9d` → `d72fe5a` → `4ddc926` in ~30 min, each move forcing a manual 3-file pin bump. The gate was a treadmill, not a guard. The intent to automate this exists as the Hypatia rule `sha_bump_propagation.ex`, but it was never wired — so the gate just nagged. ## Decision — recency window (Option B), + wire the deliberate-bump path Chosen mechanism: **a recency-window tolerant gate**, mirroring the estate's own staleness idiom (Hypatia **HYP-S006 `registry-staleness`**, which tolerates drift for `stale_after_days: 14` before escalating rather than failing on first mismatch). Full rationale incl. rejected alternatives in [`ADR-003`](docs/decisions/ADR-003-workflow-pin-staleness-window.adoc). A pinned SHA now **passes** when it is a genuine **ancestor of HEAD within the window**: - ≤ `STALENESS_WINDOW_COMMITS` commits behind HEAD (default **50**), **OR** - ≤ `STALENESS_WINDOW_DAYS` days old (default **14**). A pin still **fails** when it is **out of window** (pins can't rot forever; known-bad historical pins stay blocked) or **not a published standards commit** (forged/fork/divergent SHAs rejected — supply-chain integrity preserved). A pin **ahead** of the gate's HEAD view (a race during a HEAD move) is treated as fresh. In-window-but-behind pins emit a non-failing `::notice` nudge. This satisfies the core requirement — *a consumer PR no longer fails purely because standards moved* — which the auto-PR-only option (A) does not (the in-flight PR stays red until the bump merges). The mutable `@vN` tag option (C) was rejected as a supply-chain regression vs full-SHA pinning. ### Wiring `sha_bump_propagation` (the deliberate bump) A *wanted* bump now lands via **`scripts/propagate-workflow-pins.sh`** — an audit-first helper that rewrites a consumer's standards pins to a target SHA and **stages them on a branch, never committing/pushing** (mirrors `propagate-gitignore-67-68.sh`). This is the concrete standards-side action the Hypatia `sha_bump_propagation` rule routes to (gitbot-fleet runs it and opens a draft PR). The gate no longer *forces* the bump. ## Changes - **`scripts/check-workflow-staleness.sh`** — exact-HEAD → recency-window ancestry check; non-failing nudge for in-window pins; forged/out-of-window still fail. - **`scripts/propagate-workflow-pins.sh`** *(new)* — audit-first deliberate-bump helper (`--fix` stages a branch; never commits/pushes). - **`.github/workflows/governance-reusable.yml`** — clone standards with `--filter=tree:0` (full commit graph, minimal data) so the gate can do ancestry/age math. - **`docs/decisions/ADR-003-…adoc`** *(new)* — the decision + rejected alternatives + supply-chain trade-off. - **Tests** *(new/rewritten)* — `check-workflow-staleness-test.sh` (12 hermetic cases) + `propagate-workflow-pins-test.sh` (11 cases). - **`Justfile`** — `staleness-check` / `staleness-propagate` / `staleness-test` recipes. ## Verification (against real `standards` history + a stapeln-shaped consumer) | Scenario | Result | |---|---| | stapeln pinned **3 commits behind HEAD** (the treadmill) | ✅ **PASS** (relaxed gate + nudge) | | Out-of-window pin (tight window, real 19d/49-behind ancestor) | ✅ **FAIL** (window has teeth) | | Forged SHA | ✅ **FAIL** (supply-chain integrity) | | Gate run against `standards` itself | ✅ **PASS** (no self-red) | ``` 12/12 check-workflow-staleness tests passed 11/11 propagate-workflow-pins tests passed ``` ## Guardrails - No SPDX/licence header was added, removed, or swept in any modified file. New files carry correct SPDX from birth (MPL-2.0 for the sole-owner shell scripts, matching `propagate-gitignore-67-68.sh`; CC-BY-SA-4.0 for the ADR, matching ADR-002). - The propagation helper never commits or pushes (no unattended mutations). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01BKyi8Bht8xdXyajpkaTdYP --- _Generated by [Claude Code](https://claude.ai/code/session_01BKyi8Bht8xdXyajpkaTdYP)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4ddc926 commit 42455b1

10 files changed

Lines changed: 803 additions & 105 deletions

.github/workflows/governance-reusable.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ jobs:
2929
3030
- name: Clone standards repository
3131
run: |
32-
git clone --depth 1 https://github.com/hyperpolymath/standards.git "$HOME/standards"
32+
# Treeless partial clone: full commit graph (needed by the staleness
33+
# gate's ancestry / commits-behind / age math) without paying for
34+
# every tree+blob in history. Falls back to a deep clone if the
35+
# server rejects the partial-clone filter.
36+
git clone --filter=tree:0 https://github.com/hyperpolymath/standards.git "$HOME/standards" \
37+
|| git clone --depth 200 https://github.com/hyperpolymath/standards.git "$HOME/standards"
3338
3439
- name: Run staleness check
3540
run: |

0-AI-MANIFEST.a2ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ content-addressed `source_hash`. Prose explanation: `REGISTRY.adoc`.
2626
| Protocols | `*-protocol/` (registry stream `protocol`) |
2727
| Readiness grades | `adoption-readiness-grades/`, `foundations-readiness-grades/`, `component-readiness-grades/`, `toolchain-readiness-grades/` |
2828
| Enforcement / CI | `.github/workflows/`, `hooks/` |
29-
| Drift / staleness automation | `REGISTRY.adoc`, `hypatia-rules/` (HYP-S006) |
29+
| Drift / staleness automation | `REGISTRY.adoc`, `hypatia-rules/` (HYP-S006); reusable-pin freshness: `docs/decisions/ADR-003-workflow-pin-staleness-window.adoc`, `scripts/check-workflow-staleness.sh`, `scripts/propagate-workflow-pins.sh` |
3030
| Derived architecture map | `TOPOLOGY.md` (generated — do not edit) |
3131
| Current project state | `.machine_readable/6a2/STATE.a2ml` |
3232

Justfile

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,23 @@ topology: registry
2424
registry-check:
2525
@bash scripts/build-registry.sh --check
2626

27+
# Run the workflow-staleness gate against a repo (default: this one)
28+
staleness-check path=".":
29+
@bash scripts/check-workflow-staleness.sh "{{path}}"
30+
31+
# Audit (or --fix) consumer reusable-workflow SHA pins toward a target.
32+
# Read-only by default; pass extra args after the path, e.g.
33+
# just staleness-propagate ../stapeln --fix --to <sha>
34+
staleness-propagate path="." *args="":
35+
@bash scripts/propagate-workflow-pins.sh {{args}} "{{path}}"
36+
37+
# Regression tests for the staleness gate + the pin-propagation helper
38+
staleness-test:
39+
@echo "=== check-workflow-staleness ==="
40+
@bash scripts/tests/check-workflow-staleness-test.sh
41+
@echo "=== propagate-workflow-pins ==="
42+
@bash scripts/tests/propagate-workflow-pins-test.sh
43+
2744
# Aggregate compliance gate: registry drift (hard dep) + RSR self-audit (informational)
2845
validate: registry-check
2946
@echo "=== validate: RSR compliance gate ==="

README.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Every other "what is this repo" doc is now a thin pointer back to these two
5454
| link:.github/workflows/[.github/workflows/] + link:hooks/[hooks/] + <<Enforcement>>
5555

5656
| The **drift / staleness automation** — how this repo keeps itself honest
57-
| link:REGISTRY.adoc[REGISTRY.adoc] + link:hypatia-rules/[hypatia-rules/] (HYP-S006)
57+
| link:REGISTRY.adoc[REGISTRY.adoc] + link:hypatia-rules/[hypatia-rules/] (HYP-S006); reusable-workflow pin freshness: link:docs/decisions/ADR-003-workflow-pin-staleness-window.adoc[ADR-003] (`scripts/check-workflow-staleness.sh` gate + `scripts/propagate-workflow-pins.sh` deliberate-bump)
5858

5959
| The **architecture map** (derived, never frozen)
6060
| link:TOPOLOGY.md[TOPOLOGY.md] (generated from STATE + registry)

docs/FUTURE-WORK.adoc

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,34 @@ grades ingested. Five upstream dependencies.
4848
**When to revive:** Once the full 5-layer innervation stack is running
4949
end-to-end on at least 10 pilot repos.
5050

51+
=== 16. Cross-repo reusable-workflow pin propagation (auto-bump)
52+
53+
**What:** The cross-repo half of the workflow-pin staleness story. The
54+
standards-side is done (ADR-003: the gate tolerates a recency window, and
55+
`scripts/propagate-workflow-pins.sh` performs an audit-first bump). What
56+
remains is the *fleet* wiring so a deliberate bump fans out automatically:
57+
58+
* Activate the Hypatia rule `lib/rules/sha_bump_propagation.ex`
59+
(hyperpolymath/hypatia) so a "consumer pin is behind standards HEAD"
60+
finding is routed (`:review`) rather than inert.
61+
* Wire gitbot-fleet to run `propagate-workflow-pins.sh --fix` in the flagged
62+
consumer and open a DRAFT bump PR (mirrors the `.git-private-farm`
63+
dispatch pattern in `instant-sync.yml`; gated on a PAT, graceful-skip
64+
without it).
65+
* Optional standards-side scheduled workflow (`propagate-workflow-pins.yml`)
66+
that, on a cadence, audits the fleet and dispatches the above — sketch
67+
only; do not ship the cross-repo dispatch without the PAT + owner sign-off
68+
(estate "no unattended mutations" guardrail).
69+
70+
**Why parked:** The actionable pieces live in *other* repos (hypatia,
71+
gitbot-fleet) — out of scope for a standards PR. No technical precursor; it
72+
needs cross-repo PRs + a dispatch PAT, not a VeriSimDB-style dependency.
73+
74+
**When to revive:** Whenever the deliberate-bump cadence is wanted. The gate
75+
no longer forces it (consumers are green within the window), so this is a
76+
convenience/freshness improvement, not a treadmill fix. Track via issues in
77+
hypatia + gitbot-fleet referencing ADR-003.
78+
5179
== Out-of-scope (not parked — closed)
5280

5381
- Per-repo dashboards (PanLL handles this organically via manifests)
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// SPDX-License-Identifier: CC-BY-SA-4.0
2+
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath)
3+
= ADR-003: Workflow reusable-pin staleness — recency window, not exact-HEAD
4+
:adr-status: Accepted
5+
:adr-date: 2026-06-21
6+
:adr-author: Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
7+
:toc:
8+
:toc-placement: preamble
9+
10+
== Status
11+
12+
*Accepted* 2026-06-21. Supersedes the exact-HEAD comparison previously baked
13+
into `scripts/check-workflow-staleness.sh`.
14+
15+
== Context
16+
17+
`standards` hosts the estate's reusable workflows
18+
(`governance-reusable.yml`, `hypatia-scan-reusable.yml`,
19+
`scorecard-reusable.yml`, …). Consumer repos (stapeln, k9-svc, meta-a2ml, …)
20+
call them pinned by full commit SHA, e.g.:
21+
22+
[source,yaml]
23+
----
24+
uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@<sha>
25+
----
26+
27+
`scripts/check-workflow-staleness.sh` — run in every consumer via the
28+
governance reusable — *failed the consumer's CI whenever its pin != standards
29+
HEAD*. Because standards HEAD moves often (sometimes several times an hour),
30+
this turned every standards commit into a fleet-wide red:
31+
32+
* Observed live in a stapeln session: standards HEAD moved
33+
`5a93d9d` → `d72fe5a` → `4ddc926` in ~30 minutes. Each move forced a manual
34+
bump of the same three pins (`governance` / `hypatia` / `scorecard`) in the
35+
consumer, just to get back to green.
36+
* The gate was supposed to *guard* against genuinely rotten pins (the
37+
pre-cache-fix Hypatia reusable; the SARIF-publishing Scorecard). Instead it
38+
*nagged* on every HEAD move and produced no safety — the consumer's pin was
39+
fine; standards had simply moved.
40+
41+
The intent to automate the bump already exists as the Hypatia rule
42+
`lib/rules/sha_bump_propagation.ex`, but the propagation was never wired, so
43+
the gate just nagged.
44+
45+
== Decision
46+
47+
Replace exact-HEAD equality with a *recency window*, mirroring the estate's
48+
existing staleness idiom — Hypatia *HYP-S006 `registry-staleness`*, which
49+
tolerates drift for `stale_after_days: 14` before it escalates rather than
50+
failing on the first mismatch.
51+
52+
A pinned SHA now *passes* `check-workflow-staleness.sh` when it is a genuine
53+
ancestor of standards HEAD that is *within the window*:
54+
55+
* at most `STALENESS_WINDOW_COMMITS` commits behind HEAD (default *50*), *OR*
56+
* at most `STALENESS_WINDOW_DAYS` days old (default *14*, matching HYP-S006
57+
`stale_after_days`).
58+
59+
The union ("commits OR days") covers both failure shapes: a high-velocity
60+
burst (many commits, tiny wall-clock age) is saved by the days axis; a slow
61+
repo (few commits over a long span) is saved by the commits axis.
62+
63+
A pin still *fails* when it is:
64+
65+
* *out of the window* — older than both axes (so pins cannot rot forever, and
66+
the known-bad historical pins stay blocked); or
67+
* *not a published standards commit* — a forged, fork, or divergent SHA that
68+
is not an ancestor of HEAD (supply-chain integrity is preserved — the pin
69+
must still be a real standards mainline commit).
70+
71+
A pin that is *ahead* of the gate's HEAD view (a race during a HEAD move) is
72+
treated as fresh.
73+
74+
In-window-but-behind pins emit a non-failing `::notice` nudge pointing at the
75+
deliberate-bump path, so the gate informs without blocking.
76+
77+
=== Wiring the deliberate bump (the `sha_bump_propagation` half)
78+
79+
Relaxing the gate removes the *forced* treadmill bump. A *wanted* bump (pick
80+
up a fix early, or a scheduled cadence) lands via
81+
`scripts/propagate-workflow-pins.sh` — an audit-first helper that rewrites a
82+
consumer's standards pins to a target SHA and stages them on a branch, *never*
83+
committing or pushing (mirroring `scripts/propagate-gitignore-67-68.sh` and
84+
the estate "no unattended mutations" guardrail). This is the concrete
85+
standards-side action the Hypatia `sha_bump_propagation` rule routes to:
86+
the rule surfaces a `:review` finding "consumer pin is behind HEAD"; gitbot-fleet
87+
runs this script and opens a DRAFT PR. The gate decides nothing about *when*
88+
to bump; it only blocks pins that have fallen out of the window.
89+
90+
=== Mechanics
91+
92+
The gate needs the standards commit graph (ancestry, commits-behind, commit
93+
age), so `governance-reusable.yml` now clones standards with
94+
`git clone --filter=tree:0` (a treeless partial clone: full commit history,
95+
minimal data) instead of `--depth 1`, with a `--depth 200` fallback.
96+
97+
== Alternatives considered
98+
99+
[cols="1,4,2",options="header"]
100+
|===
101+
| Option | Description | Verdict
102+
103+
| *A — auto-propagation only*
104+
| A scheduled bot opens pin-bump PRs across all consumers on every HEAD move
105+
(wire `sha_bump_propagation` + gitbot-fleet), leaving the gate exact-HEAD.
106+
| *Rejected as the sole fix.* It does not stop the in-flight consumer PR going
107+
red — the red persists until the bump PR merges — so it fails the core
108+
requirement ("a consumer PR no longer fails purely because standards
109+
moved"). Also risks PR spam (one bump PR per consumer per HEAD move).
110+
Adopted as the *complementary* deliberate-bump path, not the gate.
111+
112+
| *B — recency window* (chosen)
113+
| Tolerate any in-window ancestor pin at the gate.
114+
| *Chosen.* Directly satisfies the requirement, self-contained in standards,
115+
fully testable, and idiomatic (HYP-S006 window). Keeps supply-chain teeth
116+
(ancestor-only + bounded window).
117+
118+
| *C — released `@vN` tag*
119+
| Move the reusables behind a moving tag (`@v1`) the consumers pin, advanced
120+
deliberately on release.
121+
| *Rejected.* Pinning a *mutable* tag is a supply-chain regression versus
122+
full-SHA pinning — a tag can be force-moved, breaking the "the SHA is the
123+
version" property the estate relies on (see `DEPENDABOT-POLICY.adoc`). It
124+
also needs a release/retag process and a one-off rewrite of every consumer.
125+
Heavier and weaker than B.
126+
|===
127+
128+
== Consequences
129+
130+
* *Positive*: consumer CI no longer reds purely because standards moved;
131+
the treadmill is gone. The known-bad historical pins and forged SHAs remain
132+
blocked. The deliberate-bump path is now wired and audit-first.
133+
* *Trade-off*: a consumer may run a reusable up to ~50 commits / 14 days behind
134+
HEAD. This is the intended supply-chain posture: pins are still full SHAs of
135+
real standards commits, just allowed a bounded lag. Tighten per-repo via
136+
`STALENESS_WINDOW_COMMITS` / `STALENESS_WINDOW_DAYS` where a tighter SLA is
137+
warranted (e.g. set both low to require near-HEAD).
138+
* *Security-relevant reusables*: if a *security fix* lands in a reusable and
139+
must reach consumers faster than the window, drive it through the
140+
propagation path (open bump PRs immediately) rather than narrowing the gate
141+
globally.
142+
143+
== Verification
144+
145+
`scripts/tests/check-workflow-staleness-test.sh` (hermetic fixture history)
146+
covers fresh / ahead / in-window-by-commits / in-window-by-days /
147+
out-of-window / forged, plus the retired-enforcer and SARIF structural rules.
148+
`scripts/tests/propagate-workflow-pins-test.sh` covers audit/fix/idempotence
149+
and branch staging. End-to-end against real standards history, a
150+
stapeln-shaped consumer pinned 3 commits behind HEAD passes the relaxed gate
151+
(with a nudge), while an out-of-window pin and a forged SHA both still fail.
152+
153+
== Cross-refs
154+
155+
* `scripts/check-workflow-staleness.sh` — the windowed gate.
156+
* `scripts/propagate-workflow-pins.sh` — the audit-first deliberate-bump helper.
157+
* `hypatia-rules/registry-staleness.a2ml` (HYP-S006) — the window idiom this
158+
mirrors.
159+
* `hyperpolymath/hypatia` `lib/rules/sha_bump_propagation.ex` — the rule this
160+
wires.
161+
* `docs/DEPENDABOT-POLICY.adoc` — the "the SHA is the version" posture that
162+
rules out mutable-tag pinning (option C).
163+
* `scripts/propagate-gitignore-67-68.sh` — the audit-first propagation pattern
164+
this helper follows.

0 commit comments

Comments
 (0)