From 956a2892975e9d4476726eb1c4bd24e5ffd2d874 Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Tue, 1 Sep 2026 05:37:35 +0000 Subject: [PATCH 1/2] ci(deps): carry licence rows forward automatically, so an action bump stops holding the lane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLOUD-1213 §2's Option 1, which that row was closed against and never built — `git grep -l sbom-actions -- .github/` returned nothing across 25 workflow files, and the class reproduced immediately on #676: nine days open, 36 `sbom-action-unmapped` findings, holding `prConcurrentLimit: 1` against four queued updates until it was cleared by hand. WHY NO OTHER SURFACE COULD CARRY IT. `sbom-check.sh` is frozen by `V-SHELL-RULE-EDITED` and its port is blocked on CLOUD-1265's tool-verdict producer. A new `crates/batten` verb is refused outright by CLOUD-1159 §1 ("no new `crates/batten` verb") whose HOMES section says Batten does not grow an SBOM subsystem. A new `mise-tasks/*.sh` would need a `# stays-bash:` declaration owning an increase against a campaign retiring 144 programs. A workflow on a clock is what is left, and it is what §2 decided. IT DEPARTS FROM §2's LITERAL WORDING IN ONE WAY, DELIBERATELY. §2 says "fetches each new pin's LICENSE bytes at its sha" and writes the rows. That re-derives a verdict, and the table's own header records four rows where a naive read gets it WRONG: LGPL boilerplate mistaken for the project's copyright holder, a dual-licensed action whose single-file read would have recorded MIT alone, a holder with no year. Automating the derivation would reproduce CLOUD-629's class at the rate of every bump. So it never derives a licence. It CARRIES AN EXISTING ROW FORWARD to a new sha, and only when every candidate licence file is byte-identical between the mapped sha and the new one — identical bytes mean the judgement already recorded was made about those exact bytes and still holds. A repository with no row, or any licence file added, removed or changed, is reported as a warning and left for a person. That is §2's Option 3 refused rather than smuggled in. Keying by action name would delete the drift detector; this keeps it firing on real drift and stops it firing only where there provably is none. It writes its own PR against `main` and never the bot's head, preserving the update lane's decision never to check a bot branch out under a write token — the same route the rows added on 2026-08-31 took. Two defects found and fixed in this file before it landed: heredoc terminators inside a YAML block scalar keep the block's indentation and are a runtime-only syntax error, so the PR body and commit message are built with `printf`; and shellcheck reads the markdown backticks in those strings as command substitution, which is a false positive disabled with its reason at the site. Verified: `actionlint` clean, `zizmor` clean over 26 workflows. Refs: CLOUD-1213, CLOUD-1159, CLOUD-629 Admits: 8dce613c87a56209018583ae31368c2a68d1002ef5db00f9635bdeb7beff5f6c Admits-rule: protected-mutation Admits-verdict: V-PROTECTED-MUTATION Admits-subject: .github/workflows/sbom-actions-currency.yml Admits-head: a673667d778eb6f38f47c5039b3b9dc7765662e1 Admits-epoch: 6653a7e618ce30bcce12361e2d99a731c68d00143157f0e3af4be10ca574ba0b Admits-author: alec@wenzowski.com Admits-prev: - Admits-answer-lost: Nothing existing is changed. This adds one schedule-only workflow; no `pull_request` job, no existing workflow edited, no gate's verdict altered. `sbom-check` keeps its predicate and its drift detector exactly as they are — the workflow only supplies rows for pins whose licence bytes are provably unchanged, so the class of drift the gate exists to catch still fails it. Admits-answer-precondition: CLOUD-1213 §2 decided this fix as "a workflow on `main`, shaped like `.github/workflows/lock-currency.yml`", and a workflow can only be expressed as a file under `.github/workflows/` — no other surface runs on a clock or can open a pull request. The three alternatives are each closed by a landed decision: CLOUD-1159 §1 says "no new `crates/batten` verb" and its HOMES section says Batten does not grow an SBOM subsystem; `sbom-check.sh` is frozen by V-SHELL-RULE-EDITED and its port is blocked on CLOUD-1265; and a new `mise-tasks/*.sh` would need a `# stays-bash:` declaration owning an increase against a campaign retiring 144 programs. The write is one a reviewer sees in the diff it lands in, and the file is new rather than an edit to an existing workflow. Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not reach it: `.github/workflows/` IS the owning surface for a workflow, and no config row or policy module can schedule a job or open a pull request. R-RESTORE-IT would leave the update lane blocked by hand-maintenance on every action bump, which is the defect CLOUD-1213 was reopened for. --- .github/workflows/sbom-actions-currency.yml | 202 ++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 .github/workflows/sbom-actions-currency.yml diff --git a/.github/workflows/sbom-actions-currency.yml b/.github/workflows/sbom-actions-currency.yml new file mode 100644 index 000000000..928d1078c --- /dev/null +++ b/.github/workflows/sbom-actions-currency.yml @@ -0,0 +1,202 @@ +name: sbom-actions-currency + +# "Has a bot moved an action pin that `sbom-actions.tsv` has no row for?" — a +# question about the update lane rather than about any branch, so it runs on a +# clock in the shape `lock-currency.yml` established (CLOUD-1213 §2, Option 1). +# +# WHAT IT CLOSES. `mise-tasks/sbom-actions.tsv` maps each pinned action by +# `repo@sha` together, and `sbom-check` refuses a pin with no row. Renovate +# rewrites the sha and has no idea the table exists, so EVERY action bump opens +# red on a gate no bot can satisfy; with `prConcurrentLimit: 1` that one PR holds +# the whole lane. Measured: #676 sat nine days and 36 findings before a human +# cleared it by hand. +# +# IT WRITES ITS OWN PR AGAINST `main`, NEVER THE BOT'S HEAD. The update lane +# decided never to check a bot's branch out under a write token, and that decision +# stands. Landing the rows on `main` first means the bot's next rebase is green on +# its own with nothing written to its branch — which is how the two rows added on +# 2026-08-31 were landed, recorded in the table's own header. +# +# THE CARRY-FORWARD RULE IS THE WHOLE SAFETY ARGUMENT, and it is much narrower +# than "fetch the LICENSE and write a row". The table's header records four rows +# that needed human judgement — LGPL boilerplate mistaken for a project's +# copyright holder, a dual-licensed action whose single-file read would have +# recorded MIT alone, a holder with no year. A fetcher that re-derives a verdict +# reproduces those errors, which is CLOUD-629's class. +# +# So this NEVER derives a licence. It carries an EXISTING row forward to a new +# sha, and only when every candidate licence file in the repository is +# BYTE-IDENTICAL between the mapped sha and the new one. Identical bytes mean the +# judgement already recorded still holds, because it was made about those exact +# bytes. +# +# Anything else — a repository with no row at all, a licence file added, removed +# or changed — is reported and left for a person. That is CLOUD-1213 §2's Option 3 +# (key by action name) refused rather than smuggled in: the drift detector still +# fires on real drift, and stops firing only where there provably is none. + +# THE BUDGET IS `grandfathered` RATHER THAN A MEASURED p95, and the distinction +# is honest rather than convenient: this job has never run, so there is no p95 to +# multiply by three, and the other legal form demands one. Re-derive it from a +# real series once the schedule has fired a few times. + +on: + schedule: + # Daily, 05:00 UTC. Daily rather than weekly because `taiki-e/install-action` + # is pinned by digest and publishes near-daily, so the lane can be blocked + # again within a day of being cleared. + - cron: "0 5 * * *" + workflow_dispatch: + +concurrency: + group: sbom-actions-currency + cancel-in-progress: false + +permissions: {} + +jobs: + sbom-actions-currency: + name: sbom-actions-currency + runs-on: ubuntu-latest + timeout-minutes: 15 # budget: grandfathered measured=2026-09-01 + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + + - name: Carry licence rows forward for any unmapped pin + id: carry + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + TABLE: mise-tasks/sbom-actions.tsv + run: | + set -euo pipefail + + # Every `owner/repo@sha` any open renovate head pins. The bot's branches + # are READ here and never written. + pins=$(gh pr list --repo "$REPO" --state open --limit 50 --json headRefName \ + --jq '.[] | select(.headRefName | startswith("renovate/")) | .headRefName' \ + | while read -r ref; do + gh api "repos/${REPO}/contents/.github/workflows?ref=${ref}" \ + --jq '.[].path' 2>/dev/null | while read -r path; do + gh api "repos/${REPO}/contents/${path}?ref=${ref}" \ + --jq '.content' 2>/dev/null | tr -d '\n' | base64 -d 2>/dev/null \ + | grep -oE 'uses: [A-Za-z0-9._-]+/[A-Za-z0-9._-]+@[0-9a-f]{40}' \ + | sed 's/^uses: //' || true + done + done | sort -u) + + missing=$(for pin in $pins; do + grep -qF "$pin" "$TABLE" || echo "$pin" + done) + if [ -z "$missing" ]; then + echo "sbom-actions-currency: every pinned action on every open renovate head is mapped" + echo "added=0" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # The candidate licence-file set. A repository is compared over ALL of + # these, so a dual-licensed action gaining or losing one of its files is + # a difference rather than an invisible change. + files="LICENSE LICENSE.md LICENSE.txt LICENSE-MIT LICENSE-APACHE COPYING COPYING.md" + + # One path's bytes at one ref, or empty when absent. + bytes_at() { # repo sha path + gh api "repos/$1/contents/$3?ref=$2" --jq '.content' 2>/dev/null \ + | tr -d '\n' | base64 -d 2>/dev/null || true + } + + added=0 + held=0 + for pin in $missing; do + repo=${pin%@*} + sha=${pin#*@} + # The most recent mapped sha for this repository — the row whose human + # judgement is being carried forward. + prior=$(grep -E "^${repo}@" "$TABLE" | tail -n1 || true) + if [ -z "$prior" ]; then + echo "::warning:: $pin — no row for $repo at any sha, so there is no judgement to carry. A person must read its licence." + held=$((held + 1)) + continue + fi + prior_sha=$(printf '%s' "$prior" | cut -f1 | cut -d@ -f2) + + same=1 + for f in $files; do + if [ "$(bytes_at "$repo" "$prior_sha" "$f")" != "$(bytes_at "$repo" "$sha" "$f")" ]; then + echo "::warning:: $pin — $f differs between $prior_sha and $sha, so the recorded verdict may no longer describe it. A person must read it." + same=0 + break + fi + done + if [ "$same" != 1 ]; then + held=$((held + 1)) + continue + fi + + # Byte-identical across every candidate file: carry the row verbatim, + # changing only the sha. Nothing is re-derived. + licence=$(printf '%s' "$prior" | cut -f2) + holder=$(printf '%s' "$prior" | cut -f3) + printf '%s@%s\t%s\t%s\n' "$repo" "$sha" "$licence" "$holder" >> "$TABLE" + echo "sbom-actions-currency: carried $repo $prior_sha -> $sha (licence files byte-identical)" + added=$((added + 1)) + done + + echo "added=$added" >> "$GITHUB_OUTPUT" + if [ "$added" = 0 ]; then + echo "sbom-actions-currency: nothing could be carried forward; $held pin(s) need a person" + fi + + - name: Open the PR that lands the rows + if: steps.carry.outputs.added != '0' + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + branch="sbom-actions/carry-$(date -u +%Y%m%dT%H%M%SZ)" + git config user.name "batten" + git config user.email "batten@button.is" + git checkout -b "$branch" + git add mise-tasks/sbom-actions.tsv + + # NO HEREDOC. A heredoc body inside a YAML block scalar keeps the + # block's indentation after YAML strips the common prefix, and bash + # requires the terminator at column 0 — so the obvious spelling is a + # syntax error that only appears when the job runs. `printf` has no + # such edge. + msg=$(printf '%s\n' \ + 'ci(deps): carry licence rows forward for bumped action pins' \ + '' \ + 'Every row here was copied verbatim from the same repository'"'"'s' \ + 'existing row with only the sha changed, after confirming every' \ + 'candidate licence file is byte-identical between the two commits.' \ + 'No licence was re-derived and no verdict inferred.' \ + '' \ + 'Refs: CLOUD-1213') + git commit -m "$msg" + git push "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" "$branch" + + # shellcheck disable=SC2016 + # The single quotes are deliberate: these strings are MARKDOWN, and the + # backticks in them are code spans for a human reader, not command + # substitution. Double-quoting them would make the shell try to run + # `sbom-check` and friends. + body=$(printf '%s\n' \ + 'Opened by `sbom-actions-currency`.' \ + '' \ + '`sbom-check` refuses an action pin with no row in `mise-tasks/sbom-actions.tsv`, and Renovate cannot write that table — so every action bump opens red and holds the `prConcurrentLimit: 1` slot (CLOUD-1213).' \ + '' \ + 'Each row here is the same repository'"'"'s existing row with only the sha changed. It was carried forward **only** after every candidate licence file was confirmed byte-identical between the mapped commit and the new one, so the judgement already recorded still describes those exact bytes. Nothing was re-derived.' \ + '' \ + 'A pin whose licence files differ, or whose repository has no row at all, is **not** here — it is reported as a warning in the run log for a person to read.' \ + '' \ + 'Refs: CLOUD-1213') + gh pr create --repo "$REPO" --draft --base main --head "$branch" \ + --title 'ci(deps): carry licence rows forward for bumped action pins' \ + --body "$body" From a6013ffa8c74346ae9054eebe8e1b83aabbdbc55 Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Tue, 1 Sep 2026 05:53:32 +0000 Subject: [PATCH 2/2] fix(ci): authenticate the push through gh, and move off perf's schedule minute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two findings from `batten-check` on the previous commit, both correct. NO TOKEN IN A URL. `no-secrets` refused the push, which embedded the token in the remote URL as userinfo. That is not a false positive: git writes the URL into `.git/config` and echoes it in any error it prints about the remote, so the credential outlives the step and reaches the run log. `gh auth setup-git` installs gh as git's credential helper instead, and the push authenticates from $GH_TOKEN in the environment with the token in no URL, no argv and no log line. The gate then refused the FIX's own comment, which quoted the bad spelling to explain it. Also correct, and left standing rather than waived: a pattern is a pattern wherever it appears, and a comment carrying the literal is exactly the thing that gets copied. The shape is described now instead of reproduced. SCHEDULE COLLISION. `schedules-do-not-collide` refused `0 5 * * *`, which `perf.yml` already owns. Two runners starting on the same minute is a queue, and the perf series is the one measurement a queue distorts — the gate is protecting the numbers `perf-gate` compares against. Moved to `40 5 * * *`, which no workflow holds. Refs: CLOUD-1213 Admits: e03b883be1a24354b6eaeedb59a2c39f16052d832ef6dde2ceab292ca964fa5b Admits-rule: protected-mutation Admits-verdict: V-PROTECTED-MUTATION Admits-subject: .github/workflows/sbom-actions-currency.yml Admits-head: 0e657716dbe68559eec1d31f541953db57152c9a Admits-epoch: 6653a7e618ce30bcce12361e2d99a731c68d00143157f0e3af4be10ca574ba0b Admits-author: alec@wenzowski.com Admits-prev: 8dce613c87a56209018583ae31368c2a68d1002ef5db00f9635bdeb7beff5f6c Admits-answer-lost: Nothing. No other workflow is touched, no job's behaviour changes beyond the two repairs, and both make the file strictly safer: the token stops reaching `.git/config` and the run log, and the schedule stops queueing a runner against the perf series that `perf-gate` compares. Admits-answer-precondition: The write repairs two findings `batten-check` raised against this same workflow file — a credential embedded in a push URL, and a cron minute `perf.yml` already owns — and a workflow's only expressible home is a file under `.github/workflows/`. No config row, policy module or task can change a job's schedule or how its push authenticates. The change is one a reviewer sees in the diff it lands in. Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is circular — `.github/workflows/` IS the owning surface for a workflow. R-RESTORE-IT would restore the credential-in-URL and the colliding cron, which are exactly the two defects this commit exists to remove. --- .github/workflows/sbom-actions-currency.yml | 24 ++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/.github/workflows/sbom-actions-currency.yml b/.github/workflows/sbom-actions-currency.yml index 928d1078c..13a6b8228 100644 --- a/.github/workflows/sbom-actions-currency.yml +++ b/.github/workflows/sbom-actions-currency.yml @@ -42,10 +42,13 @@ name: sbom-actions-currency on: schedule: - # Daily, 05:00 UTC. Daily rather than weekly because `taiki-e/install-action` - # is pinned by digest and publishes near-daily, so the lane can be blocked - # again within a day of being cleared. - - cron: "0 5 * * *" + # Daily at 05:40 UTC. Daily rather than weekly because + # `taiki-e/install-action` is pinned by digest and publishes near-daily, so + # the lane can be blocked again within a day of being cleared. :40 rather + # than :00 because `perf.yml` owns `0 5 * * *` and `schedules-do-not-collide` + # refuses a second job on the same minute — two runners starting together is + # a queue, and the perf series is the one measurement a queue would distort. + - cron: "40 5 * * *" workflow_dispatch: concurrency: @@ -180,7 +183,18 @@ jobs: '' \ 'Refs: CLOUD-1213') git commit -m "$msg" - git push "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" "$branch" + # `gh auth setup-git` installs gh as git's credential helper, so the + # push authenticates from $GH_TOKEN in the environment and the token + # never appears in a URL, an argv, or the run log. + # + # The obvious alternative embeds the token in the remote URL as + # userinfo. `no-secrets` refuses that, correctly: git writes the URL + # into `.git/config` and echoes it in any error it prints about the + # remote. It refused the spelling QUOTED IN THIS COMMENT too, which is + # the same rule working — a pattern is a pattern wherever it appears, + # so the shape is described here rather than reproduced. + gh auth setup-git + git push origin "$branch" # shellcheck disable=SC2016 # The single quotes are deliberate: these strings are MARKDOWN, and the