Skip to content

Commit fd11d1f

Browse files
ci: add launcher-standard prose↔a2ml lock-step gate (#172)
## Summary `launcher/README.adoc` §Sync requirement declares that the machine-readable form (`launcher/launcher-standard.a2ml`) and the prose form (`docs/UX-standards/launcher-standard.adoc`) MUST be edited in the same PR, and calls drift between them a *"standards-compliance bug"* — but nothing enforced the rule. The recently-fixed cross-doc contradictions in [#170](#170) were exactly the drift this rule was meant to prevent. This adds `.github/workflows/launcher-standard-lockstep.yml` — a hard gate that runs on every PR touching either file. If only one is in the diff, it fails with a message naming the missing partner and pointing back at §Sync requirement. ## How it works \`\`\`yaml on: pull_request: paths: - 'launcher/launcher-standard.a2ml' - 'docs/UX-standards/launcher-standard.adoc' \`\`\` GitHub's `paths:` filter ensures the workflow only triggers when one of the two files is touched (cheap — no wasted runs for unrelated PRs). Inside the job, a 30-line bash check diffs `pull_request.base.sha` against `pull_request.head.sha`, computes which of the two files are *touched* vs *missing*, and fails iff the touched set is non-empty but incomplete. ## Dry-run verification Logic dry-run locally against six synthetic diffs: | Diff | Expected | Result | |------|----------|--------| | only a2ml touched | FAIL — drift | FAIL ✓ | | only adoc touched | FAIL — drift | FAIL ✓ | | both touched | PASS | PASS ✓ | | both touched + unrelated | PASS | PASS ✓ | | neither touched | skip (no trigger) | workflow doesn't run ✓ | | a2ml + unrelated (drift) | FAIL — drift | FAIL ✓ | ## Pattern conformance Modelled on the existing `makefile-blocker.yml` gate: - Same SPDX header. - Same `actions/checkout@de0fac2…` pin (verified live against `github.com/actions/checkout` — corresponds to v4 commit `de0fac2e4500dabe0009e67214ff5f5447ce83dd` "Fix tag handling" #2356). - Same `concurrency` block that cancels superseded runs (safe — this is a read-only check, no mutations). - Same `permissions: contents: read`. ## Scope notes - **Initial group is the 2-file pair** the README literally calls out. Once [#171](#171) lands and introduces `launcher/resolve-desktop-tools.sh`, a follow-up should extend the group to include the resolver (which mirrors `[resolution]` in the a2ml and currently relies on reviewer discipline). - **Hard fail, not warning.** The README uses "MUST" / "is a bug" language; the gate matches that strictness. Genuinely one-sided edits (e.g. a prose-only typo fix with no a2ml equivalent) need a no-op whitespace touch to the other file — the error message documents this escape. - **Not added to required-checks list.** Branch-protection config lives outside the repo. Owner can promote this to a required check via the UI or `.github/settings.yml` once they've seen it pass green on a few PRs. ## Coordination - Fully independent of #170 and #171 — touches a different file (`.github/workflows/launcher-standard-lockstep.yml`). No conflicts in any merge order. ## Test plan - [x] Dry-run against 6 synthetic diff scenarios — all expected outcomes - [x] actions/checkout SHA pin verified live - [x] YAML syntactically valid (workflow loads — visible in Actions tab once branch is pushed) - [ ] First live trigger will happen when PR #170 or #171 next pushes and touches a group file — at that point this gate's run will appear on those PRs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4bc34e6 commit fd11d1f

1 file changed

Lines changed: 98 additions & 0 deletions

File tree

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
name: launcher-standard lock-step
3+
4+
# Enforces the sync requirement declared in launcher/README.adoc §Sync
5+
# requirement: the machine-readable form (launcher-standard.a2ml) and the
6+
# prose form (docs/UX-standards/launcher-standard.adoc) MUST be edited in
7+
# the same PR. Drift between the two is a standards-compliance bug.
8+
#
9+
# How it works: the workflow triggers only when one of the two files in
10+
# the lock-step group is touched. Inside the job we then verify that the
11+
# other file is *also* in the PR diff. If not, fail with an actionable
12+
# message naming the missing file.
13+
14+
on:
15+
pull_request:
16+
paths:
17+
- 'launcher/launcher-standard.a2ml'
18+
- 'docs/UX-standards/launcher-standard.adoc'
19+
20+
concurrency:
21+
group: ${{ github.workflow }}-${{ github.ref }}
22+
cancel-in-progress: true
23+
24+
permissions:
25+
contents: read
26+
27+
jobs:
28+
lockstep:
29+
name: Verify launcher-standard lock-step
30+
runs-on: ubuntu-latest
31+
steps:
32+
- name: Checkout (full history for base/head diff)
33+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4
34+
with:
35+
fetch-depth: 0
36+
37+
- name: Verify both files changed
38+
env:
39+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
40+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
41+
run: |
42+
set -euo pipefail
43+
44+
# The lock-step group. Editing any of these MUST be accompanied
45+
# by edits to the others in the same PR.
46+
declare -a GROUP=(
47+
"launcher/launcher-standard.a2ml"
48+
"docs/UX-standards/launcher-standard.adoc"
49+
)
50+
51+
mapfile -t CHANGED < <(git diff --name-only "${BASE_SHA}" "${HEAD_SHA}")
52+
53+
declare -a TOUCHED=()
54+
declare -a MISSING=()
55+
for f in "${GROUP[@]}"; do
56+
found=0
57+
for c in "${CHANGED[@]}"; do
58+
if [[ "$c" == "$f" ]]; then
59+
found=1
60+
break
61+
fi
62+
done
63+
if [[ $found -eq 1 ]]; then
64+
TOUCHED+=("$f")
65+
else
66+
MISSING+=("$f")
67+
fi
68+
done
69+
70+
# If the workflow ran, paths: filter matched at least one group
71+
# member — so TOUCHED is non-empty. The check is whether MISSING
72+
# is also non-empty (partial-touch = drift).
73+
if [[ ${#MISSING[@]} -gt 0 ]]; then
74+
echo "::error::launcher-standard lock-step violation"
75+
echo ""
76+
echo "Per launcher/README.adoc §Sync requirement, the machine-readable"
77+
echo "form (a2ml) and the prose form (adoc) MUST be edited in the"
78+
echo "same PR. Drift between them is a standards-compliance bug."
79+
echo ""
80+
echo "Touched in this PR:"
81+
for f in "${TOUCHED[@]}"; do
82+
echo " + ${f}"
83+
done
84+
echo ""
85+
echo "Required but NOT touched:"
86+
for f in "${MISSING[@]}"; do
87+
echo " - ${f}"
88+
done
89+
echo ""
90+
echo "Fix: add the matching edit to the file(s) above. If your"
91+
echo "change is genuinely one-sided (e.g. a prose-only typo fix"
92+
echo "with no a2ml equivalent), add a no-op whitespace touch to"
93+
echo "the other file with a commit message explaining why."
94+
exit 1
95+
fi
96+
97+
echo "::notice::launcher-standard lock-step: OK — both files touched"
98+
printf ' + %s\n' "${TOUCHED[@]}"

0 commit comments

Comments
 (0)