Skip to content

Make release tags immutable; pin consumer examples to @v1.2 - #17

Merged
feng-shiplight merged 4 commits into
mainfrom
fix-tag-resolution
Aug 12, 2026
Merged

Make release tags immutable; pin consumer examples to @v1.2#17
feng-shiplight merged 4 commits into
mainfrom
fix-tag-resolution

Conversation

@feng-shiplight

@feng-shiplight feng-shiplight commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Makes ci-triage release tags immutable, then uses them.

The ruleset

A release-tags repository ruleset now covers refs/tags/v* with deletion, update and non_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:

$ git push --force origin bc24593:refs/tags/v1.2
remote: - Cannot force-push to this tag
 ! [remote rejected] bc24593 -> v1.2 (push declined due to repository rule violations)

$ git push --delete origin v1.2
remote: - Cannot delete this tag
 ! [remote rejected] v1.2 (push declined due to repository rule violations)

$ git ls-remote origin 'refs/tags/v1.2^{}'
5f0d8356d4ef22859c30cee6a627b930672e89c0	refs/tags/v1.2^{}

Both rejected; the tag still resolves to the release commit. Now was the safe moment to run that test — nothing consumes v1.2 yet.

Consumer examples go back to @v1.2

The 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.2 again. 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 in triage.yml stay 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.1 was moved from 61ec8ca to c3b9a4d before 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 -ttag), not the commit — pasting it into uses: 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

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>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. SHA pinning — all four consumer-facing uses: examples are now pinned to 5f0d8356d4ef22859c30cee6a627b930672e89c0 (the v1.2 commit on main) rather than to the mutable @v1.2 tag. The upload-report SHA is unified to the same commit (PR body notes git diff bc24593 5f0d835 -- upload-report is empty, so two SHAs for the same code was noise).

  2. ^{} dereference — the old git ls-remote … refs/tags/v1.2 would return 48658c6f…, the SHA of the annotated tag object, not the underlying commit. GitHub's uses: needs the commit SHA. Adding ^{} dereferences correctly. The explanation added to the README is technically accurate.

  3. 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.2

That 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>
@feng-shiplight feng-shiplight changed the title Pin README examples to the v1.2 commit; fix tag resolution Make release tags immutable; pin consumer examples to @v1.2 Aug 12, 2026
claude[bot]
claude Bot previously requested changes Aug 12, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.2

A 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.1 twelve commits behind. Keep it.
  • Noting that v1.1 was moved and that the ruleset must not gain bypass actors is honest and useful operational context.
  • Removing the bc24593 / 5f0d835 split for upload-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.yml are 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.

feng-shiplight and others added 2 commits August 11, 2026 19:36
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>
@feng-shiplight

Copy link
Copy Markdown
Contributor Author

Pushed 8041f6d and 641348a.

[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] ls-remote recipe — restored. Including the ^{} dereference, so the annotated-tag correction is published rather than discarded. Consumers who want the policy-independent guarantee have the mechanism in the quick-start.

[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 @v1.x.y, backed by tag protection, because a readable ref that says which release you are on is worth more to them than a 40-character SHA. Your analysis of why they differ is correct and is now reflected in the text, so a consumer choosing between them sees the real trade-off rather than an assertion of equivalence — but the recommended default stays the tag.

Two corrections to the framing in your finding, for the record:

  • The asymmetry with triage.yml is not advice-versus-practice. The internal pins cannot use a tag: they name the previous commit, which is not a tagged release, so no tag exists to point at. They are SHA-pinned by necessity, not by a stricter standard.
  • The ruleset carries no bypass actors, so the escalation you describe is not "an org admin moves the tag" — it is delete the ruleset, move the tag, recreate the ruleset. Still possible, and the README now says so, but it is an auditable settings change rather than a push.

Separately, unrelated to your findings: 641348a strips release-specific detail that had accumulated in the versioning section — which SHA v1.1 moved between, how far behind the pins were, the force-push verification, which tags the pattern froze. That is commit-message and release-note material; a README should carry the rule, not the history that produced it. The resolved SHA is gone from the ls-remote output for the same reason: the command teaches the mechanism, printing one release's answer just dates the file.

claude[bot]
claude Bot previously requested changes Aug 12, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.2

For 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.2

The 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_env patterns are unchanged.
  • The internal SHA pins in triage.yml are untouched.
  • No hardcoded repo names, secret names, URLs, or runner labels were added.
  • The actions/checkout and actions/download-artifact references in the publish-incidents example remain SHA-pinned.
  • The three-step release process instructions are clearer than the previous guidance.

@feng-shiplight
feng-shiplight dismissed stale reviews from claude[bot] and claude[bot] August 12, 2026 02:42

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.

@feng-shiplight
feng-shiplight merged commit 6497981 into main Aug 12, 2026
1 check passed

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.y is 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.2

The prior form was:

uses: ShiplightAI/ci-triage/upload-report@bc2459322269c0a38a5e1c642845604480233005 # v1.2

Suggested 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.2

The 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-remote fix 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... and actions/download-artifact@d3f86a1... are correctly pinned; no regression there.
  • Internal triage.yml pins 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 new github.event.* interpolation, no changes to extra_env handling.

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.

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