Skip to content

Commit d9f1ef7

Browse files
fix(scaffold): derive meta-repo action pins from VERSION (#72)
Workflow action refs in generated tool repos were hardcoded literals (drift-check@v1.9, release-doc-sync@v1), so every newly scaffolded repo was born pinned to a stale meta-repo train. Hand-correcting one repo (Blender @v1.9 -> @v1.15) treated the symptom; the source kept minting stale refs. Derive all three pins from the live meta VERSION at generation time: - drift-check -> @v{MAJOR}.{MINOR} (MAJOR.MINOR train) - release-doc-sync -> @v{MAJOR} (MAJOR train) - meta-repo-ref -> v{MAJOR}.{MINOR}.{PATCH} (full current release tag) A repo scaffolded after any future meta release is now born current instead of stale. The validate.yml scaffold regression checks and the ci-cd.md drift-check pin example are likewise derive-from-current rather than hardcoded. Also fixes a stale test that asserted the release-doc-sync default was v1.0 (it is v1, the floating-major train, since #43). Scope: newly generated repos only; existing-repo ref bumps remain the periodic standards re-stamp's job. Signed-off-by: fOuttaMyPaint <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 65a7729 commit d9f1ef7

7 files changed

Lines changed: 74 additions & 17 deletions

File tree

.github/workflows/validate.yml

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -153,17 +153,27 @@ jobs:
153153
154154
- name: Scaffold regression checks for DTD#41 patterns
155155
run: |
156+
# Meta action pins are DERIVED from the meta VERSION at generation
157+
# time, so assert against the derived train rather than a hardcoded
158+
# literal (which would itself go stale on the next meta release).
159+
VERSION=$(cat VERSION)
160+
IFS='.' read -r META_MAJOR META_MINOR META_PATCH <<< "$VERSION"
161+
156162
# validate-counts job present
157163
grep -q 'validate-counts' /tmp/scaffold-test/ci-test-plugin/.github/workflows/validate.yml \
158164
|| { echo "::error::validate.yml missing validate-counts job"; exit 1; }
159165
160-
# drift-check pinned to @v1.9
161-
grep -q 'drift-check@v1.9' /tmp/scaffold-test/ci-test-plugin/.github/workflows/drift-check.yml \
162-
|| { echo "::error::drift-check.yml not pinned to @v1.9"; exit 1; }
166+
# drift-check pinned to @vMAJOR.MINOR (derived from meta VERSION)
167+
grep -q "drift-check@v${META_MAJOR}.${META_MINOR}" /tmp/scaffold-test/ci-test-plugin/.github/workflows/drift-check.yml \
168+
|| { echo "::error::drift-check.yml not pinned to derived @v${META_MAJOR}.${META_MINOR}"; exit 1; }
169+
170+
# release.yml consumes release-doc-sync@vMAJOR (derived from meta VERSION)
171+
grep -q "release-doc-sync@v${META_MAJOR}" /tmp/scaffold-test/ci-test-plugin/.github/workflows/release.yml \
172+
|| { echo "::error::release.yml does not consume derived release-doc-sync@v${META_MAJOR}"; exit 1; }
163173
164-
# release.yml consumes release-doc-sync@v1
165-
grep -q 'release-doc-sync@v1' /tmp/scaffold-test/ci-test-plugin/.github/workflows/release.yml \
166-
|| { echo "::error::release.yml does not consume release-doc-sync@v1"; exit 1; }
174+
# release.yml pins release-doc-sync meta-repo-ref to the full meta release tag
175+
grep -q "meta-repo-ref: v${META_MAJOR}.${META_MINOR}.${META_PATCH}" /tmp/scaffold-test/ci-test-plugin/.github/workflows/release.yml \
176+
|| { echo "::error::release.yml meta-repo-ref not pinned to derived v${META_MAJOR}.${META_MINOR}.${META_PATCH}"; exit 1; }
167177
168178
# release.yml has the initial-release version-handling branch
169179
grep -q 'Initial release' /tmp/scaffold-test/ci-test-plugin/.github/workflows/release.yml \

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.16.0
1+
1.16.1

scaffold/create-tool.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
TEMPLATES_DIR = Path(__file__).parent / "templates"
2323
STANDARDS_VERSION_FILE = Path(__file__).parent.parent / "STANDARDS_VERSION"
24+
VERSION_FILE = Path(__file__).parent.parent / "VERSION"
2425

2526
LICENSE_FILES = {
2627
"cc-by-nc-nd-4.0": "CC-BY-NC-ND-4.0",
@@ -69,6 +70,36 @@ def read_standards_version() -> str:
6970
return raw
7071

7172

73+
def read_meta_version() -> tuple[int, int, int]:
74+
"""Read the meta-repo VERSION at generation time, split into (major, minor, patch).
75+
76+
Workflow action pins in generated repos are DERIVED from this so a repo
77+
scaffolded after any future meta release is born current instead of stale.
78+
Hardcoding today's number in the templates only moves staleness forward one
79+
release; deriving from the live VERSION removes it. If VERSION is missing or
80+
malformed, fail loudly rather than emit a wrong pin.
81+
"""
82+
try:
83+
raw = VERSION_FILE.read_text(encoding="utf-8").strip()
84+
except FileNotFoundError:
85+
print(
86+
f"Error: VERSION file not found at {VERSION_FILE}. "
87+
"The scaffold must run from a working copy of Developer-Tools-Directory."
88+
)
89+
sys.exit(1)
90+
except OSError as e:
91+
print(f"Error: could not read {VERSION_FILE}: {e}")
92+
sys.exit(1)
93+
m = re.fullmatch(r"(\d+)\.(\d+)\.(\d+)", raw)
94+
if not m:
95+
print(
96+
f"Error: VERSION contents '{raw}' are not a valid X.Y.Z semver string. "
97+
"Refusing to derive workflow action pins from a malformed value."
98+
)
99+
sys.exit(1)
100+
return int(m.group(1)), int(m.group(2)), int(m.group(3))
101+
102+
72103
def parse_args():
73104
parser = argparse.ArgumentParser(
74105
description="Scaffold a new TMHSDigital developer tool repository",
@@ -152,6 +183,7 @@ def main():
152183
rule_names = [f"rule-{i + 1}" for i in range(args.rules)]
153184

154185
standards_version = read_standards_version()
186+
meta_major, meta_minor, meta_patch = read_meta_version()
155187

156188
ctx = {
157189
"name": args.name,
@@ -170,6 +202,10 @@ def main():
170202
"repo_owner": "TMHSDigital",
171203
"repo_name": slug,
172204
"standards_version": standards_version,
205+
"meta_major": meta_major,
206+
"meta_minor": meta_minor,
207+
"meta_patch": meta_patch,
208+
"meta_version": f"{meta_major}.{meta_minor}.{meta_patch}",
173209
}
174210

175211
print(f"\nScaffolding '{args.name}' ({slug}) into {output_dir}\n")

scaffold/templates/drift-check.yml.j2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
contents: read
1616
steps:
1717
- uses: actions/checkout@v6
18-
- uses: TMHSDigital/Developer-Tools-Directory/.github/actions/drift-check@v1.9
18+
- uses: TMHSDigital/Developer-Tools-Directory/.github/actions/drift-check@v{{ meta_major }}.{{ meta_minor }}
1919
with:
2020
mode: self
2121
format: gh-summary

scaffold/templates/release.yml.j2

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,16 @@ jobs:
133133
with open(readme, 'w') as f:
134134
f.write(content)
135135
"
136-
{% raw %}
137136
- name: Sync release docs
138137
if: steps.check.outputs.skip == 'false'
139-
uses: TMHSDigital/Developer-Tools-Directory/.github/actions/release-doc-sync@v1
138+
uses: TMHSDigital/Developer-Tools-Directory/.github/actions/release-doc-sync@v{{ meta_major }}
140139
with:
140+
{% raw %}
141141
plugin-version: ${{ steps.new.outputs.version }}
142142
previous-version: ${{ steps.current.outputs.version }}
143+
{% endraw %}
144+
meta-repo-ref: v{{ meta_major }}.{{ meta_minor }}.{{ meta_patch }}
145+
{% raw %}
143146

144147
- name: Commit version bump
145148
if: steps.check.outputs.skip == 'false'

standards/ci-cd.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,14 @@ Runs the ecosystem drift checker against the repo's own agent files to detect ve
6464
**Required configuration:**
6565

6666
```yaml
67-
- uses: TMHSDigital/Developer-Tools-Directory/.github/actions/drift-check@v1.9
67+
- uses: TMHSDigital/Developer-Tools-Directory/.github/actions/drift-check@v<MAJOR>.<MINOR>
6868
with:
6969
mode: self
7070
format: gh-summary
7171
```
7272
73+
Pin the action to the meta-repo's current `MAJOR.MINOR` floating tag, never `@main` and never an older hardcoded minor. The scaffold derives this pin from the meta-repo `VERSION` at generation time, so newly created repos are born current; bumping the pin in existing repos is the periodic standards re-stamp's job. The companion `release-doc-sync` action pins to the `MAJOR` train (`@v<MAJOR>`) by the same rule. See [`release-doc-sync.md`](release-doc-sync.md) for that action's pinning convention.
74+
7375
`mode: self` checks only the calling repo's checkout; no cross-repo token is needed. Findings at `info` severity are advisory. Findings at `error` or `warn` severity indicate real drift that should be addressed.
7476

7577
### 4. `stale.yml`

tests/test_release_doc_sync.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import json
2727
import os
28+
import re
2829
import subprocess
2930
import sys
3031
from pathlib import Path
@@ -673,12 +674,17 @@ def test_outputs_present(self, action_doc):
673674
):
674675
assert key in outputs, f"missing output: {key}"
675676

676-
def test_meta_repo_ref_default_is_v1_0(self, action_doc):
677-
"""The pinning convention from DTD#5 is that tool repos consume
678-
@v1.0 (matching drift-check@v1.7's pattern of major-floating tags).
679-
Defending the default keeps tool-repo PRs from accidentally
680-
consuming @main."""
681-
assert action_doc["inputs"]["meta-repo-ref"]["default"] == "v1.0"
677+
def test_meta_repo_ref_default_is_floating_major(self, action_doc):
678+
"""DTD#14: the meta-repo-ref default must be a floating MAJOR tag
679+
(v1, v2, ...), never a hardcoded MINOR (v1.0) or PATCH. The major
680+
train is auto-maintained by release.yml on every release, so a
681+
consumer that does not override the input tracks the latest patch
682+
instead of pinning to whatever minor was current when the action
683+
last shipped. Defending the default keeps tool-repo PRs from
684+
accidentally consuming @main or a stale minor."""
685+
assert re.fullmatch(
686+
r"v\d+", action_doc["inputs"]["meta-repo-ref"]["default"]
687+
), "meta-repo-ref default must be a floating major tag (v1, v2, ...)"
682688

683689
def test_steps_follow_drift_check_pattern(self, action_doc):
684690
"""Composite must check out the meta-repo at the pinned ref into a

0 commit comments

Comments
 (0)