Skip to content

ci(deps): carry licence rows forward automatically, so an action bump stops holding the lane - #803

Merged
wenzowski merged 2 commits into
mainfrom
claude/cloud-1213-sbom-actions
Sep 1, 2026
Merged

ci(deps): carry licence rows forward automatically, so an action bump stops holding the lane#803
wenzowski merged 2 commits into
mainfrom
claude/cloud-1213-sbom-actions

Conversation

@wenzowski

Copy link
Copy Markdown
Contributor

What this is

CLOUD-1213 §2's Option 1, which that row was closed against on 2026-09-01 and never built. git grep -l sbom-actions -- .github/ returned nothing across 25 workflow files, and the class reproduced immediately: #676 sat nine days open with 36 sbom-action-unmapped findings, holding prConcurrentLimit: 1 against four queued updates, until it was cleared by hand in 77fbad3b.

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.

Why a workflow, and not any of the three other shapes

Each was closed by a landed decision, not by preference:

  • sbom-check.sh is frozen by V-SHELL-RULE-EDITED; its port is blocked on CLOUD-1265's tool-verdict producer.
  • A crates/batten verb is refused by CLOUD-1159 §1 outright — "no new crates/batten verb" — whose HOMES section adds that Batten does not grow an SBOM subsystem (CLOUD-455).
  • 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 remains, and it is what §2 decided, shaped like lock-currency.yml.

The one place it departs from §2, 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 once per 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, whereas this keeps it firing on real drift and stops it firing only where there provably is none.

It opens its own PR against main and never writes the bot's head, preserving the update lane's decision never to check a bot branch out under a write token.

Two defects caught before landing

  • Heredoc terminators inside a YAML block scalar keep the block's indentation after YAML strips the common prefix, so the obvious spelling is a runtime-only syntax error. The commit message and PR body are built with printf instead, with the reason at the site.
  • shellcheck reads the markdown backticks in those strings as command substitution (SC2016) — a false positive, disabled with its reason rather than by double-quoting, which would have made the shell try to run sbom-check.

actionlint clean; zizmor clean over 26 workflows; timeout-check green (41 jobs, every timeout justified).

Note for review

The budget is grandfathered rather than a measured p95 because the job has never run and the other legal form demands a number I cannot honestly produce yet. Re-derive it once the schedule has fired a few times.

Closes CLOUD-1213

@linear-code

linear-code Bot commented Sep 1, 2026

Copy link
Copy Markdown
CLOUD-1213 `sbom-check`'s licence table needs a human on every action pin move, and only a person can regenerate it

Why

Found while fixing CLOUD-1207 and deliberately not repaired there, because
the refusal is the design working rather than a defect.

sbom-check's second half: mise-tasks/sbom-actions.tsv maps each pinned
action's licence by repo AND sha together, and its own header is explicit
that this is the point — "a table row whose sha is stale is exactly the drift…*
A renovate bump that does not record the new commit's license fails the gate
*rather than silently degrading the document." Regenerating the table is
mise run sbom plus a commit, which the update lane cannot do, having decided
for good reason never to check a bot's head out under a write token.
mise-action-floor states the same shape as an accepted cost in prose:
"such a PR then sits open and red until somebody closes it."

What #771 already settled, so this row is smaller than when it was filed

The original body carried two concerns. The second — that the lane burned CI
indefinitely while waiting for that person, and said nothing about it — is
fixed: the freeze arm drafts a red head, halts the rebases, and comments once
naming the failing checks. The lane's own header now states the bound rather
than claiming a scope its gates refuse.

So what remains is only the first: should this class need a person at all?
Waiting is now free, which is what makes this a design question rather than a
bill.

A correction to the measurement above (2026-08-31)

This row said *"Measured on #676: 34 × *sbom-action-unmapped, every pin in the
tree." That is wrong and the argument does not need it. sbom-check.sh:358-371
loops per pin and matches repo@sha as one key, so only a moved pin is
unmapped. #676 moves exactly two — jdx/mise-action v4.2.4→v4.2.5 and
linear/linear-release-action v0.15.1→v0.16.0. The cost of the human step is
two table rows per bump, not a full regeneration, which makes option 2 cheaper
than this row assumed and is why the decision below can be made at all.

The options

  1. Teach something in the lane to regenerate the table. Needs a write to the
    bot's head — the property the lander was built to avoid — or a follow-up
    commit from a trusted lane.
  2. Accept it. A licence is a fact about a commit, and recording it deliberately
    is defensible; the cost is now bounded to one frozen PR and one comment.
  3. Key the table by action name rather than sha. Rejected — it deletes the
    drift detector that makes a committed licence table defensible at all.

Refinement — Ready

Refinement gate: Definition of Ready & Done. This body carries only specializations.

  • **Source of truth (§1). **mise-tasks/sbom-check.sh's unmapped loop and
    mise-tasks/sbom-actions.tsv's header.
  • Computable predicate (§2). mise run sbom-check exits 0 on a head whose
    action pins moved, with no hand edit between the bump and the check. Option 1
    is the decision (2026-08-31): a workflow on main, shaped like
    .github/workflows/lock-currency.yml, reads the open renovate/* PR's pins,
    fetches each new pin's LICENSE bytes at its sha, and opens its own PR
    against main adding the rows. Option 2 is declined — it leaves the lane's
    throughput bounded by whoever notices a freeze. Option 3 stays rejected.
  • Effect (§3). No command-surface change.
  • Output & exit (§5). Pointer-only, unchanged — the gate already emits
    path:line and a count, never a licence or a holder.
  • **Commit / bump (§6). **ci → no bump.
  • **Test obligation (§7). **mise-tasks/sbom-check.sh is frozen by
    V-SHELL-RULE-EDITED, so any change reaching it takes the port-and-retire
    shape: the predicate as a policy/*.rego module plus a
    crates/batten/tests/*.rs tier, with one conserves arm per deleted path.
  • Blockers (§8). None.
{
  "source_of_truth": "mise-tasks/sbom-actions.tsv",
  "gate": { "task": "sbom-check", "exits": [0, 1] },
  "commit_type": "ci",
  "blockers": [],
  "tests": [
    {
      "file": "tests/sbom-check.bats",
      "mutation": "drop the per-pin unmapped loop, so a moved action pin reports mapped"
    }
  ]
}

REOPENED 2026-09-01. Closed against #788 with §2's Option 1 never built — git grep -l sbom-actions -- .github/ returns nothing across 25 workflow files — and the class reproduced immediately on #676, which was still holding the prConcurrentLimit: 1 slot four hours later and had to be cleared by hand (77fbad3b). The freeze half of this row IS done and stays credited; what is reopened is Option 1. See the comment thread for the measurement and for a correction to this body's own pin arithmetic.

Acceptance

  • A github-actions bump reaches green on sbom-check with no hand edit
    between the bump and the check.
  • The instance in hand is cleared in the same change: the two rows ci(deps): update actions #676 needs
    are on main, renovate-frozen is off it, and the prConcurrentLimit: 1
    slot is free for the 18 updates queued behind it in Dependency Dashboard #494.

Review in Linear

@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Free

Run ID: d15edc2f-1f35-456e-aa07-1fba6acb5acb

📥 Commits

Reviewing files that changed from the base of the PR and between c52854e and a6013ff.

📒 Files selected for processing (1)
  • .github/workflows/sbom-actions-currency.yml

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Added the sbom-actions-currency workflow. It runs on a daily schedule or manual trigger. It checks open Renovate branches for unmapped action SHAs. It carries licence rows forward only when candidate licence files match byte-for-byte. It warns when no prior row exists or licence files changed. When updates exist, it creates a branch, commits the rows, pushes the branch, and opens a draft pull request against main.

Merge Risk: ⚪ Minimal · up to a6013

This PR adds a localized scheduled workflow for carrying licence rows forward when action licence files are unchanged; no actionable merge-blocking risk remains after normal checks and review.


Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

… stops holding the lane

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: a673667
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.
…le minute

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: 0e65771
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.
@wenzowski
wenzowski marked this pull request as ready for review September 1, 2026 06:19
@wenzowski
wenzowski force-pushed the claude/cloud-1213-sbom-actions branch from 7d60898 to a6013ff Compare September 1, 2026 06:19
@sonarqubecloud

sonarqubecloud Bot commented Sep 1, 2026

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@wenzowski
wenzowski merged commit a6013ff into main Sep 1, 2026
6 of 7 checks passed
@wenzowski
wenzowski deleted the claude/cloud-1213-sbom-actions branch September 1, 2026 06:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant