From fd5424bd0fc1a218104cad3da2510e2f627c4347 Mon Sep 17 00:00:00 2001 From: fOuttaMyPaint <154358121+TMHSDigital@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:37:56 -0400 Subject: [PATCH] fix(scaffold): derive meta-repo action pins from VERSION 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 --- .github/workflows/validate.yml | 22 +++++++++++----- VERSION | 2 +- scaffold/create-tool.py | 36 +++++++++++++++++++++++++++ scaffold/templates/drift-check.yml.j2 | 2 +- scaffold/templates/release.yml.j2 | 7 ++++-- standards/ci-cd.md | 4 ++- tests/test_release_doc_sync.py | 18 +++++++++----- 7 files changed, 74 insertions(+), 17 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index eb95824..c17b0e1 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -153,17 +153,27 @@ jobs: - name: Scaffold regression checks for DTD#41 patterns run: | + # Meta action pins are DERIVED from the meta VERSION at generation + # time, so assert against the derived train rather than a hardcoded + # literal (which would itself go stale on the next meta release). + VERSION=$(cat VERSION) + IFS='.' read -r META_MAJOR META_MINOR META_PATCH <<< "$VERSION" + # validate-counts job present grep -q 'validate-counts' /tmp/scaffold-test/ci-test-plugin/.github/workflows/validate.yml \ || { echo "::error::validate.yml missing validate-counts job"; exit 1; } - # drift-check pinned to @v1.9 - grep -q 'drift-check@v1.9' /tmp/scaffold-test/ci-test-plugin/.github/workflows/drift-check.yml \ - || { echo "::error::drift-check.yml not pinned to @v1.9"; exit 1; } + # drift-check pinned to @vMAJOR.MINOR (derived from meta VERSION) + grep -q "drift-check@v${META_MAJOR}.${META_MINOR}" /tmp/scaffold-test/ci-test-plugin/.github/workflows/drift-check.yml \ + || { echo "::error::drift-check.yml not pinned to derived @v${META_MAJOR}.${META_MINOR}"; exit 1; } + + # release.yml consumes release-doc-sync@vMAJOR (derived from meta VERSION) + grep -q "release-doc-sync@v${META_MAJOR}" /tmp/scaffold-test/ci-test-plugin/.github/workflows/release.yml \ + || { echo "::error::release.yml does not consume derived release-doc-sync@v${META_MAJOR}"; exit 1; } - # release.yml consumes release-doc-sync@v1 - grep -q 'release-doc-sync@v1' /tmp/scaffold-test/ci-test-plugin/.github/workflows/release.yml \ - || { echo "::error::release.yml does not consume release-doc-sync@v1"; exit 1; } + # release.yml pins release-doc-sync meta-repo-ref to the full meta release tag + grep -q "meta-repo-ref: v${META_MAJOR}.${META_MINOR}.${META_PATCH}" /tmp/scaffold-test/ci-test-plugin/.github/workflows/release.yml \ + || { echo "::error::release.yml meta-repo-ref not pinned to derived v${META_MAJOR}.${META_MINOR}.${META_PATCH}"; exit 1; } # release.yml has the initial-release version-handling branch grep -q 'Initial release' /tmp/scaffold-test/ci-test-plugin/.github/workflows/release.yml \ diff --git a/VERSION b/VERSION index 15b989e..41c11ff 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.0 +1.16.1 diff --git a/scaffold/create-tool.py b/scaffold/create-tool.py index 6a6d300..a339d5a 100644 --- a/scaffold/create-tool.py +++ b/scaffold/create-tool.py @@ -21,6 +21,7 @@ TEMPLATES_DIR = Path(__file__).parent / "templates" STANDARDS_VERSION_FILE = Path(__file__).parent.parent / "STANDARDS_VERSION" +VERSION_FILE = Path(__file__).parent.parent / "VERSION" LICENSE_FILES = { "cc-by-nc-nd-4.0": "CC-BY-NC-ND-4.0", @@ -69,6 +70,36 @@ def read_standards_version() -> str: return raw +def read_meta_version() -> tuple[int, int, int]: + """Read the meta-repo VERSION at generation time, split into (major, minor, patch). + + Workflow action pins in generated repos are DERIVED from this so a repo + scaffolded after any future meta release is born current instead of stale. + Hardcoding today's number in the templates only moves staleness forward one + release; deriving from the live VERSION removes it. If VERSION is missing or + malformed, fail loudly rather than emit a wrong pin. + """ + try: + raw = VERSION_FILE.read_text(encoding="utf-8").strip() + except FileNotFoundError: + print( + f"Error: VERSION file not found at {VERSION_FILE}. " + "The scaffold must run from a working copy of Developer-Tools-Directory." + ) + sys.exit(1) + except OSError as e: + print(f"Error: could not read {VERSION_FILE}: {e}") + sys.exit(1) + m = re.fullmatch(r"(\d+)\.(\d+)\.(\d+)", raw) + if not m: + print( + f"Error: VERSION contents '{raw}' are not a valid X.Y.Z semver string. " + "Refusing to derive workflow action pins from a malformed value." + ) + sys.exit(1) + return int(m.group(1)), int(m.group(2)), int(m.group(3)) + + def parse_args(): parser = argparse.ArgumentParser( description="Scaffold a new TMHSDigital developer tool repository", @@ -152,6 +183,7 @@ def main(): rule_names = [f"rule-{i + 1}" for i in range(args.rules)] standards_version = read_standards_version() + meta_major, meta_minor, meta_patch = read_meta_version() ctx = { "name": args.name, @@ -170,6 +202,10 @@ def main(): "repo_owner": "TMHSDigital", "repo_name": slug, "standards_version": standards_version, + "meta_major": meta_major, + "meta_minor": meta_minor, + "meta_patch": meta_patch, + "meta_version": f"{meta_major}.{meta_minor}.{meta_patch}", } print(f"\nScaffolding '{args.name}' ({slug}) into {output_dir}\n") diff --git a/scaffold/templates/drift-check.yml.j2 b/scaffold/templates/drift-check.yml.j2 index 4e858fc..4d01c11 100644 --- a/scaffold/templates/drift-check.yml.j2 +++ b/scaffold/templates/drift-check.yml.j2 @@ -15,7 +15,7 @@ jobs: contents: read steps: - uses: actions/checkout@v6 - - uses: TMHSDigital/Developer-Tools-Directory/.github/actions/drift-check@v1.9 + - uses: TMHSDigital/Developer-Tools-Directory/.github/actions/drift-check@v{{ meta_major }}.{{ meta_minor }} with: mode: self format: gh-summary diff --git a/scaffold/templates/release.yml.j2 b/scaffold/templates/release.yml.j2 index 80d607a..793e366 100644 --- a/scaffold/templates/release.yml.j2 +++ b/scaffold/templates/release.yml.j2 @@ -133,13 +133,16 @@ jobs: with open(readme, 'w') as f: f.write(content) " -{% raw %} - name: Sync release docs if: steps.check.outputs.skip == 'false' - uses: TMHSDigital/Developer-Tools-Directory/.github/actions/release-doc-sync@v1 + uses: TMHSDigital/Developer-Tools-Directory/.github/actions/release-doc-sync@v{{ meta_major }} with: +{% raw %} plugin-version: ${{ steps.new.outputs.version }} previous-version: ${{ steps.current.outputs.version }} +{% endraw %} + meta-repo-ref: v{{ meta_major }}.{{ meta_minor }}.{{ meta_patch }} +{% raw %} - name: Commit version bump if: steps.check.outputs.skip == 'false' diff --git a/standards/ci-cd.md b/standards/ci-cd.md index 5cef946..e415a71 100644 --- a/standards/ci-cd.md +++ b/standards/ci-cd.md @@ -64,12 +64,14 @@ Runs the ecosystem drift checker against the repo's own agent files to detect ve **Required configuration:** ```yaml -- uses: TMHSDigital/Developer-Tools-Directory/.github/actions/drift-check@v1.9 +- uses: TMHSDigital/Developer-Tools-Directory/.github/actions/drift-check@v. with: mode: self format: gh-summary ``` +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`) by the same rule. See [`release-doc-sync.md`](release-doc-sync.md) for that action's pinning convention. + `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. ### 4. `stale.yml` diff --git a/tests/test_release_doc_sync.py b/tests/test_release_doc_sync.py index 583aafd..2be0f89 100644 --- a/tests/test_release_doc_sync.py +++ b/tests/test_release_doc_sync.py @@ -25,6 +25,7 @@ import json import os +import re import subprocess import sys from pathlib import Path @@ -673,12 +674,17 @@ def test_outputs_present(self, action_doc): ): assert key in outputs, f"missing output: {key}" - def test_meta_repo_ref_default_is_v1_0(self, action_doc): - """The pinning convention from DTD#5 is that tool repos consume - @v1.0 (matching drift-check@v1.7's pattern of major-floating tags). - Defending the default keeps tool-repo PRs from accidentally - consuming @main.""" - assert action_doc["inputs"]["meta-repo-ref"]["default"] == "v1.0" + def test_meta_repo_ref_default_is_floating_major(self, action_doc): + """DTD#14: the meta-repo-ref default must be a floating MAJOR tag + (v1, v2, ...), never a hardcoded MINOR (v1.0) or PATCH. The major + train is auto-maintained by release.yml on every release, so a + consumer that does not override the input tracks the latest patch + instead of pinning to whatever minor was current when the action + last shipped. Defending the default keeps tool-repo PRs from + accidentally consuming @main or a stale minor.""" + assert re.fullmatch( + r"v\d+", action_doc["inputs"]["meta-repo-ref"]["default"] + ), "meta-repo-ref default must be a floating major tag (v1, v2, ...)" def test_steps_follow_drift_check_pattern(self, action_doc): """Composite must check out the meta-repo at the pinned ref into a