From 5ddba6baf57a7ab18fd929242c8a75f4f202d6d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 23:53:58 +0000 Subject: [PATCH] release-tag: name the desired state, because Dependabot cannot see an untagged repo Falsified live an hour after #21 merged: guest-room has run the exact Dependabot config #21 fans out -- github-actions ecosystem, weekly, grouped -- since June, and its freshly-refreshed group PR (guest-room#61) bumps checkout/codeql/scorecard pins across three workflow files while leaving deps.yml's osv-scan pin untouched at 8b7d8a8, three template moves behind. Same engine, same config. The difference is that those actions have tags: Dependabot resolves 'latest' for a SHA-pinned uses: via the referenced repo's tags, and this repo had none, so the one pin the whole loop exists to move was invisible to the applier. release-tag.yml mints a patch tag on every push to main touching the lanes, templates, or tools (explicit vX.Y.Z via dispatch for minor/major). It holds contents:write on THIS repo only -- nothing org-wide, no broker entry, no new standing grant. Tags are never moved; minting an existing tag fails. Also corrects the head-tracking claim #21 wrote into the template header and README: Dependabot converges callers onto the latest TAG, not the default-branch head. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_017RMKHpGXzw9Zkv49fzVssg --- .github/dependabot.yml | 14 ++-- .github/workflows/release-tag.yml | 102 ++++++++++++++++++++++++++++++ README.md | 10 ++- templates/dependabot.yml | 14 ++-- 4 files changed, 129 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/release-tag.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5f1ef78..0376284 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -25,11 +25,15 @@ # byte-identical deps.yml, and every OTHER action pin (checkout, # setup-node, setup-deno) going stale the same way. # -# Dependabot converges callers onto the head of each action's default branch, -# which is what the template pin IS at bump time. The census stays on as the -# independent verifier that convergence actually happened — Dependabot proposes, -# a human merges, the Monday census proves. Once the fleet reads current, flip -# caller-pins' `fail-on-lag` to true and the loop is closed: plan must be empty. +# Dependabot converges callers onto each referenced repo's LATEST TAG — not its +# default-branch head. An untagged repo is invisible to it: guest-room ran this +# exact ecosystem weekly since June and its group PR bumped checkout/codeql pins +# while leaving the osv-scan pin three template moves behind, because +# ci-workflows had no tags. release-tag.yml now mints one per lane change, +# which is the piece of state that makes this config able to act on the lane +# pin at all. The census stays on as the independent verifier — Dependabot +# proposes, a human merges, the Monday census proves. Once the fleet reads +# current, flip caller-pins' `fail-on-lag` to true: plan must be empty. # # ADOPTING THIS FILE # ------------------ diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml new file mode 100644 index 0000000..0c18833 --- /dev/null +++ b/.github/workflows/release-tag.yml @@ -0,0 +1,102 @@ +name: release-tag +# Mint a version tag on every merge that changes what the fleet consumes. +# +# WHY TAGS EXIST AT ALL — Dependabot cannot see an untagged repo. +# ---------------------------------------------------------------- +# The fleet's convergence loop (ci-workflows#10, #21) is: template = desired +# state, caller-pins census = plan, Dependabot = apply. Dependabot resolves +# "latest" for a SHA-pinned `uses:` via the referenced repo's TAGS — an +# untagged repo offers it nothing to resolve against, so the pin is invisible +# to it. That is not a guess: guest-room has run Dependabot weekly on the +# github-actions ecosystem since June, and its refreshed group PR (#61, +# 2026-08-03) bumps checkout/codeql/scorecard pins across three files while +# leaving deps.yml's osv-scan pin untouched at 8b7d8a8 — three template moves +# behind. Same engine, same config; the only difference is that those actions +# have tags and this repo had none. +# +# So the tag is not ceremony. It is the piece of state that makes the managed +# applier able to act, which keeps the whole loop free of bespoke actors and +# broker credentials. This workflow holds contents:write on THIS repo only — +# nothing org-wide, nothing minted, no new standing grant. +# +# WHAT GETS TAGGED +# ---------------- +# Every push to main touching the reusable lanes, the templates they are +# copied from, or the tools they embed. Patch-increment by default (v0.1.0 → +# v0.1.1); a deliberate minor/major is a workflow_dispatch with the `tag` +# input. Tags are lightweight and immutable-by-convention: a tag once pushed +# is never moved — Dependabot and the callers' `# vX.Y.Z` provenance comments +# both assume a tag names one commit forever. +# +# Callers do not reference tags directly — they stay SHA-pinned (org policy). +# Dependabot rewrites both the SHA and the trailing `# vX.Y.Z` comment, which +# is exactly the shape check_template_pins.py requires. +on: + push: + branches: [main] + paths: + - ".github/workflows/osv-scan.yml" + - ".github/workflows/env-check-drift.yml" + - ".github/workflows/env-record.yml" + - "templates/**" + - "tools/**" + workflow_dispatch: + inputs: + tag: + description: >- + Explicit tag to mint (e.g. v1.0.0) for a deliberate minor/major. + Blank = next patch after the latest v* tag (v0.1.0 if none exist). + required: false + default: "" + type: string + +permissions: + contents: write + +concurrency: + # Serialize: two merges racing to compute "next patch" must not mint the + # same tag twice. cancel-in-progress stays false — every merge gets a tag. + group: release-tag + cancel-in-progress: false + +jobs: + tag: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + # Tags need history to enumerate; persist-credentials stays ON here, + # deliberately unlike every other lane in this repo — the push below + # is the entire job. + fetch-depth: 0 + + - name: Compute and push the tag + env: + EXPLICIT: ${{ inputs.tag || '' }} + run: | + set -euo pipefail + git fetch --tags --quiet + if [ -n "${EXPLICIT}" ]; then + case "${EXPLICIT}" in + v[0-9]*.[0-9]*.[0-9]*) tag="${EXPLICIT}" ;; + *) echo "::error::explicit tag '${EXPLICIT}' is not vX.Y.Z"; exit 1 ;; + esac + else + latest="$(git tag -l 'v[0-9]*' --sort=-v:refname | head -1)" + if [ -z "${latest}" ]; then + tag="v0.1.0" + else + base="${latest#v}" + major="${base%%.*}"; rest="${base#*.}" + minor="${rest%%.*}"; patch="${rest#*.}" + tag="v${major}.${minor}.$((patch + 1))" + fi + fi + if git rev-parse -q --verify "refs/tags/${tag}" >/dev/null; then + echo "::error::${tag} already exists — tags are never moved. Use a dispatch with an explicit higher tag." + exit 1 + fi + git tag "${tag}" "${GITHUB_SHA}" + git push origin "refs/tags/${tag}" + echo "FDS-RELEASE-TAG {\"tag\":\"${tag}\",\"sha\":\"${GITHUB_SHA}\"}" + echo "### ${tag} → ${GITHUB_SHA}" >> "$GITHUB_STEP_SUMMARY" diff --git a/README.md b/README.md index 6c6322c..24b711c 100644 --- a/README.md +++ b/README.md @@ -199,12 +199,20 @@ The same shape, with one step that exists because skipping it is what [#10](https://github.com/bounded-systems/ci-workflows/issues/10) is about: 1. Edit `.github/workflows/osv-scan.yml` -2. Merge, and re-pin `templates/deps.yml` to the merge commit +2. Merge, and re-pin `templates/deps.yml` to the merge commit — `release-tag.yml` + mints the version tag on that push automatically 3. **Let the callers converge, then verify.** Dependabot (`templates/dependabot.yml`, vendored per repo as `.github/dependabot.yml`) opens each caller's re-pin PR on its weekly cycle; a human merges. The Monday `caller-pins.yml` census proves the fleet actually converged. +The tag in step 2 is load-bearing, not ceremony: **Dependabot resolves a SHA-pinned +`uses:` via the referenced repo's tags, so an untagged repo is invisible to it.** +Measured, not assumed — guest-room ran this exact Dependabot config weekly since June, +and its refreshed group PR (guest-room#61) bumped `checkout`/`codeql`/`scorecard` pins +across three files while leaving `deps.yml`'s osv-scan pin untouched at `8b7d8a8`, +three template moves behind. Same engine, same config; the only difference was tags. + Step 3 matters because `uses: …@` resolves the reusable workflow *at that commit*: a caller's pin decides which scanner that repo actually runs, a lane improvement that stops at step 2 is merged and deployed to nobody, and — unlike the canonical-script diff --git a/templates/dependabot.yml b/templates/dependabot.yml index 5f1ef78..0376284 100644 --- a/templates/dependabot.yml +++ b/templates/dependabot.yml @@ -25,11 +25,15 @@ # byte-identical deps.yml, and every OTHER action pin (checkout, # setup-node, setup-deno) going stale the same way. # -# Dependabot converges callers onto the head of each action's default branch, -# which is what the template pin IS at bump time. The census stays on as the -# independent verifier that convergence actually happened — Dependabot proposes, -# a human merges, the Monday census proves. Once the fleet reads current, flip -# caller-pins' `fail-on-lag` to true and the loop is closed: plan must be empty. +# Dependabot converges callers onto each referenced repo's LATEST TAG — not its +# default-branch head. An untagged repo is invisible to it: guest-room ran this +# exact ecosystem weekly since June and its group PR bumped checkout/codeql pins +# while leaving the osv-scan pin three template moves behind, because +# ci-workflows had no tags. release-tag.yml now mints one per lane change, +# which is the piece of state that makes this config able to act on the lane +# pin at all. The census stays on as the independent verifier — Dependabot +# proposes, a human merges, the Monday census proves. Once the fleet reads +# current, flip caller-pins' `fail-on-lag` to true: plan must be empty. # # ADOPTING THIS FILE # ------------------