ci: track the spec's HEAD — the app conforms to the living spec

Design change, at the maintainer's direction: the gate now checks out
feedpak-spec at HEAD instead of a pinned SHA. .feedpak-spec-ref, its
40-char validation step, and the pin-bump machinery are gone.

Rationale: it is vital that the app conforms to the spec — the current
spec, not a snapshot. The pin bought determinism at the cost of a
maintenance loop (bump PRs, a PAT, weekly latency) and a window where the
gate verified against a stale spec. Tracking HEAD makes the dev flow fully
self-serve with zero upkeep: gated PR -> FEP -> spec merge -> re-run
checks -> green. Nothing to bump.

The trade-off is accepted with eyes open, and the docs state it: the
normal FEP is additive and can only loosen the gate, so it cannot redden
anyone's PR. Only a breaking spec change (rare, deliberate, MAJOR per the
spec's compatibility policy) turns PRs red repo-wide — which is the
correct org-wide signal that the app is out of conformance. The CI job
logs the spec SHA each run verified against, so any red run is
reproducible.

Failure messages now also say why it matters beyond the one PR (also at
the maintainer's direction): non-conformance that lands shows up as red CI
on every teammate's PR until it is resolved, and only its author can clear
it — the FEP route keeps everyone else unblocked. Tone softened throughout
(the exceptions-file header now explains rather than shouts).

Signed-off-by: topkoa <topkoa@gmail.com>
This commit is contained in:
topkoa
2026-07-13 01:07:12 -04:00
parent 203f82b6fe
commit c485f02211
8 changed files with 781 additions and 780 deletions
-9
View File
@@ -1,9 +0,0 @@
# Pinned commit of github.com/got-feedback/feedpak-spec that this repo is
# verified against by the `feedpak-spec` CI job (tools/check_spec_conformance.py).
#
# Pinned by SHA rather than tracking the spec's default branch on purpose: a
# change over there must never turn CI red on an unrelated PR here. When the
# spec moves, bump this SHA in its own PR — if that PR is red, the spec changed
# in a way core does not satisfy, which is exactly the signal we want, delivered
# as a reviewable PR instead of a surprise.
15e13e02062842d6660897623b41e04fd99ece58
+17 -17
View File
@@ -1,17 +1,17 @@
## What ## What
<!-- What does this PR do, and why? Link the issue it addresses. --> <!-- What does this PR do, and why? Link the issue it addresses. -->
## feedpak surface ## feedpak surface
<!-- The feedpak spec is sacrosanct: the spec defines the format, this app implements it. <!-- The feedpak spec is sacrosanct: the spec defines the format, this app implements it.
Delete this section ONLY if your change doesn't touch how the app reads or writes packs. --> Delete this section ONLY if your change doesn't touch how the app reads or writes packs. -->
- [ ] This PR does **not** change how the app reads/writes feedpaks (manifest keys, pack files, folder layout) - [ ] This PR does **not** change how the app reads/writes feedpaks (manifest keys, pack files, folder layout)
- [ ] …or it does, and the spec change landed first via the [FEP process](https://github.com/got-feedback/feedpak-spec/blob/main/CONTRIBUTING.md) — FEP / spec PR: `got-feedback/feedpak-spec#___` — and `.feedpak-spec-ref` is bumped to the merged SHA in this PR - [ ] …or it does, and the spec change landed first via the [FEP process](https://github.com/got-feedback/feedpak-spec/blob/main/CONTRIBUTING.md) — FEP / spec PR: `got-feedback/feedpak-spec#___` (once it merges, re-run this PR's checks and the gate goes green)
## Checklist ## Checklist
- [ ] `CHANGELOG.md` `[Unreleased]` updated (user-visible changes) - [ ] `CHANGELOG.md` `[Unreleased]` updated (user-visible changes)
- [ ] Tests added/updated for new behaviour - [ ] Tests added/updated for new behaviour
- [ ] Commits are DCO signed off (`git commit -s`) - [ ] Commits are DCO signed off (`git commit -s`)
+17 -21
View File
@@ -133,8 +133,16 @@ jobs:
# the only way a new key lands; core ingests the spec's example packs; packs # the only way a new key lands; core ingests the spec's example packs; packs
# committed here pass the spec's reference validator. Motivated by # committed here pass the spec's reference validator. Motivated by
# #933, where a manifest key (`original_audio`) shipped in core without ever # #933, where a manifest key (`original_audio`) shipped in core without ever
# reaching the spec. Pinned by SHA in .feedpak-spec-ref so a change over # reaching the spec.
# there can't redden an unrelated PR here. #
# The gate checks against the spec repo's HEAD, deliberately: the app must
# conform to the LIVING spec, always. The dev flow is self-serve — a gated
# PR opens a FEP, the spec PR merges, re-running this job goes green; no
# pin file to bump, nothing to maintain. Accepted trade-off: a BREAKING
# spec change (rare, deliberate, MAJOR per the spec's compatibility policy)
# reddens every PR here until core conforms — which is the correct
# org-wide signal that the app is out of conformance. The normal FEP is
# additive and can never redden this job.
name: feedpak-spec name: feedpak-spec
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -152,31 +160,19 @@ jobs:
python-version: '3.12' python-version: '3.12'
cache: 'pip' cache: 'pip'
- name: Read the pinned spec commit - name: Check out feedpak-spec at HEAD
id: spec
run: |
sha=$(grep -vE '^[[:space:]]*(#|$)' .feedpak-spec-ref | head -n1 | tr -d '[:space:]')
if [ -z "$sha" ]; then
echo "::error file=.feedpak-spec-ref::no commit SHA found in .feedpak-spec-ref"
exit 1
fi
# actions/checkout resolves branches and tags in `ref` too, so a
# non-SHA here would silently un-pin the spec — the one thing this
# file exists to prevent. Demand a full 40-char SHA.
if ! printf '%s' "$sha" | grep -qE '^[0-9a-fA-F]{40}$'; then
echo "::error file=.feedpak-spec-ref::expected a full 40-character commit SHA, got '$sha' — a branch or tag name would defeat the pin"
exit 1
fi
echo "sha=$sha" >> "$GITHUB_OUTPUT"
- name: Check out feedpak-spec at the pinned commit
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
repository: got-feedback/feedpak-spec repository: got-feedback/feedpak-spec
ref: ${{ steps.spec.outputs.sha }} ref: main
path: .feedpak-spec path: .feedpak-spec
persist-credentials: false persist-credentials: false
- name: Record the spec commit this run verified against
# HEAD-tracking means CI results can differ across time on the same
# commit. Log the exact spec SHA so a red run is reproducible.
run: git -C .feedpak-spec rev-parse HEAD
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
+4 -3
View File
@@ -20,11 +20,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
list falls behind the codebase); (2) **allowlist-closed**`feedpak-spec-exceptions.yml` never grows; list falls behind the codebase); (2) **allowlist-closed**`feedpak-spec-exceptions.yml` never grows;
(3) **forward** — core's `load_song()` ingests every example pack the spec ships; (3) **forward** — core's `load_song()` ingests every example pack the spec ships;
(4) **reverse** — every pack committed here passes the spec's own `tools/validate.py` (7/7 pass today). (4) **reverse** — every pack committed here passes the spec's own `tools/validate.py` (7/7 pass today).
The spec is pinned by SHA in `.feedpak-spec-ref` so a change over there can't redden an unrelated PR The gate verifies against the spec repo's **HEAD** — the app must conform to the living spec, and the
here; bump it in its own PR, and a red result is the signal that core doesn't satisfy the new spec. flow is self-serve: a gated PR opens a FEP, the spec PR merges, re-running checks goes green. Nothing to
pin, nothing to bump. Each run logs the spec SHA it verified against so results are reproducible.
**There is no in-repo escape hatch, by design.** A blocked PR has exactly one route: land the key in the **There is no in-repo escape hatch, by design.** A blocked PR has exactly one route: land the key in the
spec via the [FEP process](https://github.com/got-feedback/feedpak-spec/blob/main/CONTRIBUTING.md), then spec via the [FEP process](https://github.com/got-feedback/feedpak-spec/blob/main/CONTRIBUTING.md), then
bump `.feedpak-spec-ref` to the merged SHA in the same PR. `feedpak-spec-exceptions.yml` is a **closed re-run the PR's checks — the gate verifies against the spec's HEAD, so it goes green once the key is real. `feedpak-spec-exceptions.yml` is a **closed
grandfather list** for keys that predate the gate, not a bypass: a fourth check (**allowlist-closed**) grandfather list** for keys that predate the gate, not a bypass: a fourth check (**allowlist-closed**)
diffs it against the base branch and fails any PR that *adds* an entry, so it may only shrink. diffs it against the base branch and fails any PR that *adds* an entry, so it may only shrink.
`original_audio` is grandfathered there against #933 so the gate lands green and starts blocking the `original_audio` is grandfathered there against #933 so the gate lands green and starts blocking the
+703 -702
View File
File diff suppressed because it is too large Load Diff
+20 -12
View File
@@ -66,12 +66,14 @@ Land the key in the spec through the **feedpak Enhancement Proposal (FEP)** proc
3. **Land one PR there** that updates the normative spec (`spec/feedpak-v1.md`), the relevant JSON 3. **Land one PR there** that updates the normative spec (`spec/feedpak-v1.md`), the relevant JSON
Schema(s), an example in `examples/` that exercises it, and the changelog — *together*. A PR touching Schema(s), an example in `examples/` that exercises it, and the changelog — *together*. A PR touching
only one of those is incomplete. only one of those is incomplete.
4. **Back here**, bump `.feedpak-spec-ref` to that merged SHA, in the same PR as your code. The gate goes 4. **Back here**, just re-run your PR's checks. The gate verifies against the spec's HEAD, so the moment
green, because the key is now genuinely part of the format. your key is genuinely part of the format, your PR goes green — nothing to bump, nothing to maintain.
That is deliberately the only route. There is **no in-repo escape hatch** — no experimental prefix, no That's deliberately the only route — no experimental prefix, no self-serve allowlist — and it's usually a
self-serve allowlist. If your PR is blocked, the answer is a FEP, not a workaround. The person merging has quick one for additive keys. The reason it's worth the round-trip: the gate checks the whole repo against
to stop and decide whether the change is worth taking through the format process, which is the whole point. the living spec, so if non-conformance ever lands, it shows up as red CI on *every* teammate's open PR, and
only the person who introduced it can clear it. Going through the FEP keeps your change clean and keeps
everyone else unblocked.
The spec's own governance says the same thing: The spec's own governance says the same thing:
@@ -90,15 +92,21 @@ somewhere drift accumulates.
Deleting an entry does not, by itself, get you past the gate: layer 1 still fails while core reads the key. Deleting an entry does not, by itself, get you past the gate: layer 1 still fails while core reads the key.
The entry goes when the **code** goes. The entry goes when the **code** goes.
## Pinning ## Tracking the spec's HEAD
`.feedpak-spec-ref` holds the SHA of the `feedpak-spec` commit this repo is verified against. Pinned rather The gate checks out `feedpak-spec` at **HEAD**, on purpose: the app must conform to the *living* spec, and
than tracking the spec's default branch on purpose — a change over there must never turn CI red on an nobody should have to maintain a pin. The dev flow is fully self-serve — gated PR → FEP → spec merge →
unrelated PR here. re-run checks → green.
When the spec moves, bump the SHA in its own PR. If that PR is red, the spec changed in a way core doesn't Two properties to know about:
satisfy — exactly the signal we want, delivered as a reviewable PR rather than a surprise on someone else's
branch. - **The normal FEP is additive** (a new optional key), which only ever makes the gate *looser* — it cannot
redden anyone's PR. Only a **breaking** spec change (removing/renaming a key the app uses, tightening the
validator against committed packs) turns PRs red repo-wide — and per the spec's compatibility policy that
is a rare, deliberate MAJOR event, exactly when an org-wide "the app is out of conformance" signal is the
right outcome. The CI job logs the exact spec SHA each run verified against, so a red run is reproducible.
- **CI results can change over time on the same commit** — that is inherent to tracking a living contract,
and it is the point: green means "conformant *now*", not "conformant when written".
## Limitations ## Limitations
+11 -11
View File
@@ -1,13 +1,12 @@
# CLOSED grandfather list — manifest keys core reads or writes that predate the # CLOSED grandfather list — manifest keys core reads or writes that predate the
# spec-conformance gate and that the feedpak spec does not define. # spec-conformance gate and that the feedpak spec does not define.
# #
# ┌─────────────────────────────────────────────────────────────────────────┐ # Please don't add entries here — CI will flag any PR that grows this list, so
# │ THIS IS NOT AN ESCAPE HATCH. You cannot add to it. │ # it can only shrink over time. That's by design, not distrust: the moment the
# │ CI fails any PR that adds an entry here. The list may only SHRINK. │ # app touches a key the spec doesn't define, every teammate's PR starts failing
# └─────────────────────────────────────────────────────────────────────────┘ # the conformance gate too, and whoever added the key is the only person who
# # can fix it. The FEP process below avoids putting anyone in that spot. The
# There is deliberately no in-repo way to merge a manifest key the spec doesn't # feedpak spec's own governance is explicit:
# define. The feedpak spec's own governance is explicit:
# #
# "This repository defines the format only. Applications that read or write # "This repository defines the format only. Applications that read or write
# feedpak ... track this spec as a dependency; they do not drive it. # feedpak ... track this spec as a dependency; they do not drive it.
@@ -21,11 +20,12 @@
# on-disk shape, backward compatibility, and the version bump implied. # on-disk shape, backward compatibility, and the version bump implied.
# 2. Land one PR there updating the normative spec, the JSON Schemas, an # 2. Land one PR there updating the normative spec, the JSON Schemas, an
# example that exercises it, and the changelog — together. # example that exercises it, and the changelog — together.
# 3. Back here, bump `.feedpak-spec-ref` to that merged SHA in the same PR that # 3. Back here, re-run this PR's checks. The gate verifies against the spec's
# adds your code. The gate then goes green, because the key is now declared. # HEAD, so once your key is in the spec, the gate goes green.
# #
# That is the only route. If your PR is blocked by this gate, the answer is a # That's the supported route — and usually a quick one for additive keys. If
# FEP, not an entry in this file. # your PR is blocked by this gate, a FEP will get you unblocked properly; an
# entry here won't (CI rejects it).
# #
# Entries below exist ONLY because they predate the gate. Each is debt with a # Entries below exist ONLY because they predate the gate. Each is debt with a
# tracking issue, and each disappears when its issue is fixed. The gate also # tracking issue, and each disappears when its issue is fixed. The gate also
+9 -5
View File
@@ -87,10 +87,14 @@ EXCEPTIONS_FILE = REPO / "feedpak-spec-exceptions.yml"
# the spec's own governance says "a change is not part of the format until it # the spec's own governance says "a change is not part of the format until it
# lands here", and the FEP process is how it lands. # lands here", and the FEP process is how it lands.
FEP = ( FEP = (
"A new manifest key must go through the feedpak Enhancement Proposal process " "New manifest keys go through the feedpak Enhancement Proposal process "
"(https://github.com/got-feedback/feedpak-spec/blob/main/CONTRIBUTING.md): land a PR on " "(https://github.com/got-feedback/feedpak-spec/blob/main/CONTRIBUTING.md): land a PR on "
"feedpak-spec that updates the normative spec, the JSON Schemas, an example, and the " "feedpak-spec that updates the normative spec, the JSON Schemas, an example, and the "
"changelog together — then bump .feedpak-spec-ref to the merged SHA in this PR." "changelog together — then re-run this PR's checks; the gate verifies against the "
"spec's HEAD, so once your key is in the spec, this PR goes green. It matters beyond "
"this PR: the whole repo is checked against the living spec, so non-conformance that "
"slips in shows up as red CI on every teammate's PR until it's resolved — sorting it "
"out here keeps everyone else unblocked."
) )
@@ -254,8 +258,8 @@ def check_allowlist_closed(baseline: Path | None, bootstrap: bool) -> bool:
for key in added: for key in added:
_fail( _fail(
f"{EXCEPTIONS_FILE.name}: this PR ADDS an exception for '{key}'. The allowlist is " f"{EXCEPTIONS_FILE.name}: this PR adds an exception for '{key}', and the allowlist "
f"closed — it grandfathers keys that predate this gate and may only shrink. {FEP}" f"can't take new entries — it only grandfathers keys that predate the gate. {FEP}"
) )
if removed: if removed:
print(f" allowlist shrank (debt paid down): {', '.join(removed)}") print(f" allowlist shrank (debt paid down): {', '.join(removed)}")
@@ -458,7 +462,7 @@ def main() -> int:
"--spec", "--spec",
required=True, required=True,
type=Path, type=Path,
help="path to a feedpak-spec checkout (CI pins the SHA in .feedpak-spec-ref)", help="path to a feedpak-spec checkout (CI checks out the spec repo's HEAD)",
) )
ap.add_argument( ap.add_argument(
"--baseline-exceptions", "--baseline-exceptions",