Make release tags immutable; pin consumer examples to @v1.2 - #17
Conversation
Now that v1.2 is cut, the circularity that forced the reusable-workflow example to use a bare tag is gone: the release commit exists, so every `uses:` in the README pins to 5f0d835 with a `# v1.2` comment. That closes the last mutable consumer example. upload-report moves from bc24593 to the same SHA — the two carry identical upload-report code, so showing two SHAs was noise. Fixes the tag-resolution one-liner added in the previous PR. It said git ls-remote ... refs/tags/v1.2 which returns the annotated tag OBJECT (48658c6), not the commit. A consumer pasting that into `uses:` gets an unresolvable ref. The correct form dereferences with `^{}`, and the README now says why. Also rewrites the release checklist to name the step that caused the original drift: updating the README's examples to the tag's commit, after tagging. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Review: Pin README examples to the v1.2 commit; fix tag resolution
Docs-only change. No workflow, action, or script files are modified; the 37 regression tests remain unaffected.
Summary
Both fixes are correct and improve supply-chain posture:
-
SHA pinning — all four consumer-facing
uses:examples are now pinned to5f0d8356d4ef22859c30cee6a627b930672e89c0(the v1.2 commit onmain) rather than to the mutable@v1.2tag. Theupload-reportSHA is unified to the same commit (PR body notesgit diff bc24593 5f0d835 -- upload-reportis empty, so two SHAs for the same code was noise). -
^{}dereference — the oldgit ls-remote … refs/tags/v1.2would return48658c6f…, the SHA of the annotated tag object, not the underlying commit. GitHub'suses:needs the commit SHA. Adding^{}dereferences correctly. The explanation added to the README is technically accurate. -
Release checklist — the three-step description (
merge feature → merge pin-bump PR → tag → update README) is clearer than the previous prose and directly names the step that caused the v1.1 drift.
Findings
CRITICAL: None
HIGH: None
MEDIUM: None
LOW — ls-remote example references a non-existent tag (README.md:70)
The command uses refs/tags/v1.3^{}. The surrounding prose ("To adopt a later release") signals intent, but a first-time consumer who copy-pastes the command verbatim gets an error because v1.3 doesn't exist yet. A trailing comment such as # substitute the next release tag would remove the ambiguity without changing meaning.
LOW — Explicit yaml snippet removed from the ls-remote flow (README.md:65-74)
The previous version completed the loop with:
uses: ShiplightAI/ci-triage/.github/workflows/triage.yml@<that SHA> # v1.2That snippet was dropped. The step (take the ls-remote output → paste into uses:) is still inferrable from the surrounding examples, but is slightly less explicit for new consumers. Worth considering re-adding as a one-liner.
No blocking issues. Both LOW findings are minor documentation clarity nits, not correctness or security problems. Approving.
Adds a `release-tags` repository ruleset covering refs/tags/v* with deletion, update and non_fast_forward, and no bypass actors. Verified by attacking it rather than by reading the config back: a force-move of v1.2 to bc24593 and a delete of v1.2 were both rejected by the server, and the tag still resolves to 5f0d835. That removes the reason the examples had gone to raw SHAs. A tag can no longer be repointed at different code after a consumer adopts it, so `@v1.2` is a real supply-chain pin and a far better one to hand a customer: readable, and it says which release they are on. All four examples go back to `@v1.2`. The internal `uses:` pins in triage.yml stay SHA-pinned. Those necessarily name the previous commit, which is not a tagged release, so a tag is not available to them. The versioning section now records why the ruleset is load-bearing: v1.1 was moved from 61ec8ca to c3b9a4d before it existed, which is how the pins ended up naming a commit twelve behind the tag they claimed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Review: Pin README examples to the v1.2 commit; fix tag resolution
Docs-only PR. No workflow or action changes. The annotated-tag / ^{} correction and the three-step release-checklist are good additions. However, the core change — downgrading consumer-facing pin examples from full commit SHAs to mutable tag refs — introduces a meaningful supply-chain security regression in a repo whose own security documentation classifies its jobs as privileged.
Findings
[HIGH] Consumer examples downgraded from SHA pins to tag refs
README.md lines 80 and 118 (post-diff):
# Before
uses: ShiplightAI/ci-triage/upload-report@bc2459322269c0a38a5e1c642845604480233005 # v1.2
# After
uses: ShiplightAI/ci-triage/upload-report@v1.2A commit SHA is a content-addressed, cryptographic guarantee — it cannot be changed by any policy, ruleset, or infrastructure actor. A tag ref (@v1.2) is a policy guarantee: it is only as immutable as the ruleset enforcing it. Those are not equivalent, and the README's new text ("A commit SHA works too and is equivalent") at line 68 says otherwise.
Concretely, the ruleset itself can be deleted or modified. An org admin can: (1) delete release-tags, (2) force-move v1.2, (3) recreate the ruleset. The PR notes that v1.1 was moved before the ruleset existed — that history is exactly why the previous text said "a bare @v1.x.y is mutable in practice." The ruleset hardens the default, but the underlying capability exists. Consumer repos running with contents: write and live credentials cannot tell whether a tag ref they adopt today points to the same commit six months from now.
The internal triage.yml references use SHA throughout (ShiplightAI/ci-triage/setup@bc2459..., ShiplightAI/ci-triage@bc2459...). The tool pins itself harder than it advises consumers to pin it. That asymmetry is the problem.
Recommended fix: Keep the SHA in the upload-report examples and restore a pointer to how to derive SHAs from tags (see LOW below). The @v1.2 form is fine as a human-readable comment; it should not be the sole ref.
uses: ShiplightAI/ci-triage/upload-report@5f0d8356d4ef22859c30cee6a627b930672e89c0 # v1.2[MEDIUM] "A commit SHA works too and is equivalent" is incorrect and misleading
README.md line 68 (post-diff):
A commit SHA works too and is equivalent, but the tag is readable and tells you which release you are on.
SHA and tag refs are not equivalent. A SHA is a cryptographic commitment; a tag ref depends on policy enforcement remaining correct indefinitely. The statement will be read by consumers deciding how to pin, and it nudges them toward the weaker option by implying there is no difference. The correct framing is that tag refs are convenient when the ruleset is intact, and SHA refs are the defence-in-depth backstop that removes the policy dependency entirely.
[LOW] Removing the git ls-remote guidance removes the mechanism for upgrading to SHA pins
The old block (lines 62–73, pre-diff) gave consumers a concrete command to resolve a tag to a commit SHA. The PR removes it in favour of "just use @v1.2." Given the HIGH finding above, the ls-remote command (with the corrected ^{} dereference from the PR description) should be retained so that security-conscious consumers can follow the stronger path:
$ git ls-remote https://github.com/ShiplightAI/ci-triage 'refs/tags/v1.2^{}'
5f0d8356d4ef22859c30cee6a627b930672e89c0 refs/tags/v1.2^{}The annotated-tag ^{} correction in the PR description is technically correct and should be published, not discarded.
What is good
- The three-step release checklist (merge feature → merge pin-bump PR → tag) is accurate and prevents the drift that left
v1.1twelve commits behind. Keep it. - Noting that
v1.1was moved and that the ruleset must not gain bypass actors is honest and useful operational context. - Removing the
bc24593/5f0d835split forupload-report(both resolve the same tree) eliminates noise — the right fix is just to put the correct SHA in once, not to drop to a tag. - The internal SHA pins in
triage.ymlare untouched, which is correct.
Summary
The ^{} fix and the release-process notes are good. The blocking issue is replacing SHA pins with tag refs in consumer-facing examples while simultaneously claiming the two are "equivalent." Restoring the SHA in the upload-report examples and correcting the equivalence claim would make this approvable.
Review was right that "a commit SHA works too and is equivalent" is false. A
protected tag is a policy guarantee: someone who can edit repository settings
can delete the ruleset, move the tag and recreate it. A commit SHA has no such
dependency. Saying they are equivalent nudges consumers toward the weaker
option while implying there is no trade-off.
The tag stays the recommended pin — it is readable and says which release you
are on, and that is the ergonomics call for customers. But the README now
states the trade-off plainly and restores the `git ls-remote 'refs/tags/vX.Y^{}'`
recipe so anyone wanting the policy-independent guarantee can pin the commit.
That recipe also keeps the annotated-tag `^{}` correction, which would otherwise
have been lost.
Also drops "not even an org admin can repoint a published release" for the
accurate "cannot be repointed by any push — including an admin's", and notes
that the v* pattern freezes v1 and v1.1 as well, so the stale v1 alias cannot be
advanced without narrowing the pattern.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The versioning section had turned into an incident writeup: which SHA v1.1 was moved from and to, how many commits the pins were behind, that the ruleset was verified by a rejected force-push, which existing tags it froze. That belongs in commit messages and release notes; a README states the rule, not the history that produced it. Also drops the resolved SHA from the ls-remote example. The command shows how to resolve a tag; printing one release's answer just dates the file. What stays: pin to a release tag, tags are protected, here is how to pin a commit if you want a guarantee independent of the ruleset, and the three-step release process. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Pushed [MEDIUM] "a commit SHA works too and is equivalent" — you're right, corrected. A protected tag is a policy guarantee and a commit SHA is a cryptographic one; they are not equivalent, and saying so nudged readers toward the weaker option while implying no trade-off. The README now states it plainly: the ruleset can be undone by anyone who can edit repository settings, a commit SHA has no such dependency. [LOW] [HIGH] tag refs in the examples — keeping them, as a deliberate product decision. This is the repo owner's call, made with the trade-off on the table: customers get Two corrections to the framing in your finding, for the record:
Separately, unrelated to your findings: |
There was a problem hiding this comment.
Review: Make release tags immutable; pin consumer examples to @v1.2
Scope: Documentation-only change (README.md). No workflow or action logic altered. The 37 regression tests are unaffected.
The goal is sound — immutable tags ARE a better consumer UX than "resolve the SHA yourself" — and the ^{} one-liner fix is a genuine bug fix. The security disclosure in the Versioning section is honest and accurate. But one finding warrants a change before merge.
MEDIUM — Composite-action examples downgraded from SHA to tag pin
Files: README.md:86, README.md:124
- uses: ShiplightAI/ci-triage/upload-report@bc2459322269c0a38a5e1c642845604480233005 # v1.2
+ uses: ShiplightAI/ci-triage/upload-report@v1.2For the reusable workflow reference (triage.yml@v1.2) the tag-only approach is arguably necessary — the merge commit SHA doesn't exist while the docs are being written, so pre-resolving it is impractical. That trade-off is understandable.
For composite action references (upload-report@v1.2), the SHA was fully resolvable and was the previous documented approach. Switching to a tag pin here degrades the supply-chain guarantee for consumers who copy-paste the quick-start. The release-tags ruleset is a policy control — an org owner can edit repository settings, remove the ruleset, move the tag, and reinstate the ruleset. The README correctly says this ("a policy guarantee rather than a cryptographic one"), but the examples will drive most consumer behavior, not the fine print.
This repo's own triage.yml keeps its internal pins as full SHAs (correctly). The inconsistency — SHA-level protection internally, tag-level guidance externally — is the core issue, and it matters because upload-report runs inside consumer CI jobs that may carry live credentials.
Suggested fix: Restore SHA pinning for the two upload-report examples, with the tag as a trailing comment, matching the internal style:
uses: ShiplightAI/ci-triage/upload-report@bc2459322269c0a38a5e1c642845604480233005 # v1.2The SHA for upload-report at v1.2 is already known (it's in the diff), so consumers can copy a verifiable ref. Keep @v1.2 for the reusable workflow reference where pre-resolution is impractical.
If the maintainers intentionally accept the tag-only approach for composite actions (having weighed the policy-vs-cryptographic trade-off), add a one-line note near those examples explaining that SHA pinning is the stronger alternative and pointing to the git ls-remote command — so consumers making a security-sensitive choice see both options at the point of adoption, not only in the Versioning section.
LOW — git ls-remote ^{} fix is correct (positive)
File: README.md:71
-$ git ls-remote https://github.com/ShiplightAI/ci-triage refs/tags/v1.2
+$ git ls-remote https://github.com/ShiplightAI/ci-triage 'refs/tags/v1.2^{}'This is a real bug fix: refs/tags/v1.2 returns the annotated tag object hash (type tag), not the commit hash, and uses: requires a commit. The ^{} peel dereferences to the commit. The added explanatory sentence is also correct and helpful.
LOW — PR body slightly overstates tag immutability
Scope: PR description only, not the README.
"no bypass actors — so not even an org admin can repoint a published release"
An organization owner CAN edit repository settings and rulesets, so the claim is not strictly true. The README body itself handles this correctly ("anyone who can edit repository settings can undo it"), so the shipped documentation is accurate. Just a minor mismatch between the PR description and what the docs actually say.
No other issues found
- Secret handling, injection vectors, GITHUB_ENV writes, and
extra_envpatterns are unchanged. - The internal SHA pins in
triage.ymlare untouched. - No hardcoded repo names, secret names, URLs, or runner labels were added.
- The
actions/checkoutandactions/download-artifactreferences in thepublish-incidentsexample remain SHA-pinned. - The three-step release process instructions are clearer than the previous guidance.
Deliberate maintainer decision, taken with the trade-off on the table: consumers pin by release tag, backed by the release-tags ruleset. The README states plainly that this is a policy guarantee rather than a cryptographic one and shows how to resolve a commit instead, immediately after the quick-start and ahead of the upload-report examples in reading order — so the second note this review asks for would duplicate guidance the reader has already passed. The other findings in this review were applied: the false equivalence claim is corrected, the ls-remote recipe is restored with the ^{} fix, and the PR body's overstatement is fixed.
There was a problem hiding this comment.
PR Review: Make release tags immutable; pin consumer examples to @v1.2
Overview: Docs-only change to README.md -- no action or workflow logic touched. The PR adds a release-tags repository ruleset, documents its protection model, corrects a git ls-remote bug (^{} dereference for annotated tags), and converts two consumer uses: examples from SHA-pinned to tag-pinned.
The ^{} fix is correct and an improvement. The ruleset is a meaningful control. However, the change to the recommended consumer pinning strategy introduces a MEDIUM supply-chain concern for tooling that runs in privileged consumer jobs.
MEDIUM -- Consumer uses: examples downgraded from SHA to tag pinning
Files: README.md lines 93, 131 (and supporting guidance lines 65-82)
The old README was unambiguous:
Tags in this repo have moved before, so a bare
@v1.x.yis mutable in practice and not a supply-chain pin.
The new README makes @v1.2 the recommended pin for consumers, with SHA pinning repositioned as a secondary "if you want the stronger form" option.
The ruleset protection is real but is a policy guarantee, not a cryptographic one (as the PR itself correctly states). An org admin who can edit repository settings can delete the ruleset, move the tag, and recreate it -- all without leaving a trace in the tag history. A commit SHA cannot be moved by anyone.
This matters because the tooling runs in consumer privileged jobs (contents: write, agent in bypassed-permissions mode, live credentials). A supply-chain compromise of this repo would have broad downstream impact.
The examples now read:
uses: ShiplightAI/ci-triage/upload-report@v1.2The prior form was:
uses: ShiplightAI/ci-triage/upload-report@bc2459322269c0a38a5e1c642845604480233005 # v1.2Suggested fix: Keep the SHA as the primary pin with the tag as a human-readable comment. This gives both readability and cryptographic immutability without removing the ruleset narrative:
uses: ShiplightAI/ci-triage/upload-report@5f0d8356d4ef22859c30cee6a627b930672e88c0 # v1.2The ruleset section is still valuable context -- it explains why you can trust the tag comment matches the SHA -- but the documented default for consumers should remain the stronger form.
For the reusable workflow ref (which cannot be pre-computed, as the PR notes), the existing quick-start showing @v1.2 is acceptable with a prominent note that consumers who want cryptographic pinning should resolve with git ls-remote. The current text does say this, but it's framed as optional rather than encouraged.
LOW -- SHA resolution guidance moved to secondary position in quick-start
File: README.md lines 65-82
The old README placed the git ls-remote SHA-resolution step in the critical path ("For an immutable pin, resolve the tag once and paste the SHA"). The new README buries it after the tag-pinning recommendation as an opt-in for those "who want the stronger form."
For HIGH-TRUST CI tooling, the security-conscious path should be the path of least resistance. A consumer skimming the quick-start will copy @v1.2 and move on. Suggest either restoring the SHA-resolution step to the primary recommendation or adding a callout box that makes the cryptographic pin option visually prominent.
Positive findings
git ls-remotefix is correct (README.md lines 77-82): The old one-liner returned the annotated tag object SHA, not the commit SHA.uses:needs the commit. The^{}dereference is the right fix, and the example output showing the expected result is helpful.- Versioning section is substantially improved (lines 276-303): The three-step release process, the v1.1 incident explanation, and the "ruleset is load-bearing" warning are all valuable. This is clearer and more honest than the old text.
- Third-party actions in examples remain SHA-pinned:
actions/checkout@df4cb1c...andactions/download-artifact@d3f86a1...are correctly pinned; no regression there. - Internal
triage.ymlpins stay SHA-pinned: The PR description confirms this; only consumer-facing examples change. The distinction is logical. - No workflow or action logic changed: Scope is appropriately limited to documentation.
- Secret handling and injection surface unchanged: No new
run:blocks, no newgithub.event.*interpolation, no changes toextra_envhandling.
Recommendation
The core concern is that the recommended consumer pinning posture has weakened from cryptographic (SHA) to policy-based (ruleset-protected tag). Restore SHA pinning as the primary recommended form in the upload-report examples, with # v1.2 as the comment. The ruleset narrative belongs in the versioning section as additional context, not as the primary security argument for why tag pinning is safe enough.
Makes ci-triage release tags immutable, then uses them.
The ruleset
A
release-tagsrepository ruleset now coversrefs/tags/v*withdeletion,updateandnon_fast_forward, and no bypass actors, so a published release cannot be repointed by any push. That is a policy guarantee, not a cryptographic one: someone who can edit repository settings can delete the ruleset, move the tag and recreate it.Verified by attacking it rather than by reading the config back:
Both rejected; the tag still resolves to the release commit. Now was the safe moment to run that test — nothing consumes
v1.2yet.Consumer examples go back to
@v1.2The reason the examples had drifted to raw SHAs was that a tag could be moved after adoption. That is no longer true, so all four
uses:examples are@v1.2again. A tag is the better thing to hand a customer: readable, and it says which release they are on. A SHA still works and is equivalent.The internal
uses:pins intriage.ymlstay SHA-pinned — those necessarily name the previous commit, which is not a tagged release, so no tag is available to them.Versioning section
Records why the ruleset is load-bearing:
v1.1was moved from61ec8catoc3b9a4dbefore it existed, which is exactly how the internal pins ended up naming a commit twelve behind the tag they claimed. Relaxing the ruleset or adding bypass actors would reopen that.Also fixed here
The tag-resolution one-liner added in #16 was wrong. It said
git ls-remote … refs/tags/v1.2, which returns the annotated tag object (48658c6,git cat-file -t→tag), not the commit — pasting it intouses:gives an unresolvable ref. That guidance is gone entirely now that consumers use the tag directly.Docs and repo settings only — no workflow or action logic changed. The 37 regression tests still pass.
🤖 Generated with Claude Code