Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.16.0
1.16.1
36 changes: 36 additions & 0 deletions scaffold/create-tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand All @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion scaffold/templates/drift-check.yml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 5 additions & 2 deletions scaffold/templates/release.yml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
4 changes: 3 additions & 1 deletion standards/ci-cd.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<MAJOR>.<MINOR>
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<MAJOR>`) 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`
Expand Down
18 changes: 12 additions & 6 deletions tests/test_release_doc_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import json
import os
import re
import subprocess
import sys
from pathlib import Path
Expand Down Expand Up @@ -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
Expand Down