From 340bbc22c10634c82b598dbad61749913915d7ff Mon Sep 17 00:00:00 2001 From: Feng Qian Date: Tue, 11 Aug 2026 19:24:57 -0700 Subject: [PATCH 1/4] Pin README examples to the v1.2 commit; fix tag resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- README.md | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index e09075b..fd0bbbf 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ on: jobs: triage: - uses: ShiplightAI/ci-triage/.github/workflows/triage.yml@v1.2 + uses: ShiplightAI/ci-triage/.github/workflows/triage.yml@5f0d8356d4ef22859c30cee6a627b930672e89c0 # v1.2 permissions: contents: write pull-requests: write @@ -62,20 +62,16 @@ jobs: PLAYWRIGHT_PROXY_PASSWORD=${{ secrets.PLAYWRIGHT_PROXY_PASSWORD }} ``` -A reusable-workflow ref is the one thing this repo cannot pin by SHA for you: -the correct SHA is the release commit itself, which does not exist while the -file is being written. **For an immutable pin, resolve the tag once and paste -the SHA**, since a tag can be moved after you adopt it: +Every `uses:` in this README is pinned to the `v1.2` commit rather than to +`@v1.2`, because **a tag can be moved after you adopt it** — this repo has done +it. To adopt a later release, resolve its tag once and paste the commit: ```console -$ 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.3^{}' ``` -```yaml - uses: ShiplightAI/ci-triage/.github/workflows/triage.yml@ # v1.2 -``` - -Every other example below is already SHA-pinned. +The `^{}` matters: releases here are annotated tags, so without it `ls-remote` +returns the tag object rather than the commit, and `uses:` needs the commit. The consumer's test workflows must upload their `shiplight-report/` so triage can read the failure evidence. Use the **`upload-report` helper action** — it bakes in @@ -86,7 +82,7 @@ the right path and excludes the heavy Playwright traces (`*.zip`) and videos ```yaml - name: Upload test report (for triage) if: ${{ !cancelled() }} - uses: ShiplightAI/ci-triage/upload-report@bc2459322269c0a38a5e1c642845604480233005 # v1.2 + uses: ShiplightAI/ci-triage/upload-report@5f0d8356d4ef22859c30cee6a627b930672e89c0 # v1.2 # matrix/sharded jobs: give each shard a unique name # with: # name: test-report-shard-${{ matrix.shardIndex }} @@ -124,7 +120,7 @@ The test workflow's report upload takes the prefix directly, since the `upload-report` action's path is a literal: ```yaml -- uses: ShiplightAI/ci-triage/upload-report@bc2459322269c0a38a5e1c642845604480233005 # v1.2 +- uses: ShiplightAI/ci-triage/upload-report@5f0d8356d4ef22859c30cee6a627b930672e89c0 # v1.2 with: report-dir: yaml-examples/shiplight-report ``` @@ -194,7 +190,7 @@ Add a sibling job to the caller workflow: ```yaml jobs: triage: - uses: ShiplightAI/ci-triage/.github/workflows/triage.yml@v1.2 + uses: ShiplightAI/ci-triage/.github/workflows/triage.yml@5f0d8356d4ef22859c30cee6a627b930672e89c0 # v1.2 permissions: contents: write pull-requests: write @@ -274,12 +270,14 @@ permissions mode, live credentials). Treat this repo as high-trust: - Consumers should pin by full commit SHA with the release tag as a trailing comment, never `@main`. Tags in this repo have moved before, so a bare - `@v1.x.y` is mutable in practice and not a supply-chain pin. The only ref the - examples above cannot pre-resolve is the reusable workflow itself; resolve - that one with `git ls-remote` as shown in the quick-start. -- Cut the release tag immediately after merging the pin bump. Between those two - moments the documented tag does not resolve, so a consumer copying the - quick-start gets an unresolved-ref failure. + `@v1.x.y` is mutable in practice and not a supply-chain pin. +- Cutting a release is three steps, in order. Merge the feature. Merge a second + PR bumping the internal pins to the feature's merge commit — they name the + *previous* commit because a commit cannot pin itself, and the composite-action + code is identical across the two, so the `# v1.x` comment stays honest. Tag + that second merge commit, then update this README's examples to the tag's + commit. Skipping the last step is what left `# v1.1` pointing twelve commits + behind the tag it named. - The internal `uses: ShiplightAI/ci-triage[/setup]@ # v1.x` references in `triage.yml` are pinned by SHA and are immutable. Bump both the SHA and its release-tag comment when cutting a new version so the whole pipeline runs at From b4ebb4924306eec32090e35eba742af00e3cab8b Mon Sep 17 00:00:00 2001 From: Feng Qian Date: Tue, 11 Aug 2026 19:31:13 -0700 Subject: [PATCH 2/4] Make release tags immutable; document tag pinning for consumers 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) --- README.md | 44 +++++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index fd0bbbf..cda7159 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ on: jobs: triage: - uses: ShiplightAI/ci-triage/.github/workflows/triage.yml@5f0d8356d4ef22859c30cee6a627b930672e89c0 # v1.2 + uses: ShiplightAI/ci-triage/.github/workflows/triage.yml@v1.2 permissions: contents: write pull-requests: write @@ -62,16 +62,11 @@ jobs: PLAYWRIGHT_PROXY_PASSWORD=${{ secrets.PLAYWRIGHT_PROXY_PASSWORD }} ``` -Every `uses:` in this README is pinned to the `v1.2` commit rather than to -`@v1.2`, because **a tag can be moved after you adopt it** — this repo has done -it. To adopt a later release, resolve its tag once and paste the commit: - -```console -$ git ls-remote https://github.com/ShiplightAI/ci-triage 'refs/tags/v1.3^{}' -``` - -The `^{}` matters: releases here are annotated tags, so without it `ls-remote` -returns the tag object rather than the commit, and `uses:` needs the commit. +Pin to a release tag (`@v1.2`), never `@main`. **Release tags here are +immutable**: a repository ruleset on `refs/tags/v*` blocks deletion, updates and +force-pushes with no bypass actors, so a tag you adopt cannot be repointed at +different code afterwards. A commit SHA works too and is equivalent, but the tag +is readable and tells you which release you are on. The consumer's test workflows must upload their `shiplight-report/` so triage can read the failure evidence. Use the **`upload-report` helper action** — it bakes in @@ -82,7 +77,7 @@ the right path and excludes the heavy Playwright traces (`*.zip`) and videos ```yaml - name: Upload test report (for triage) if: ${{ !cancelled() }} - uses: ShiplightAI/ci-triage/upload-report@5f0d8356d4ef22859c30cee6a627b930672e89c0 # v1.2 + uses: ShiplightAI/ci-triage/upload-report@v1.2 # matrix/sharded jobs: give each shard a unique name # with: # name: test-report-shard-${{ matrix.shardIndex }} @@ -120,7 +115,7 @@ The test workflow's report upload takes the prefix directly, since the `upload-report` action's path is a literal: ```yaml -- uses: ShiplightAI/ci-triage/upload-report@5f0d8356d4ef22859c30cee6a627b930672e89c0 # v1.2 +- uses: ShiplightAI/ci-triage/upload-report@v1.2 with: report-dir: yaml-examples/shiplight-report ``` @@ -190,7 +185,7 @@ Add a sibling job to the caller workflow: ```yaml jobs: triage: - uses: ShiplightAI/ci-triage/.github/workflows/triage.yml@5f0d8356d4ef22859c30cee6a627b930672e89c0 # v1.2 + uses: ShiplightAI/ci-triage/.github/workflows/triage.yml@v1.2 permissions: contents: write pull-requests: write @@ -268,16 +263,19 @@ the repository root, and is empty when the target never resolved to a real file. These scripts run in a **privileged** job (`contents: write`, agent in bypassed- permissions mode, live credentials). Treat this repo as high-trust: -- Consumers should pin by full commit SHA with the release tag as a trailing - comment, never `@main`. Tags in this repo have moved before, so a bare - `@v1.x.y` is mutable in practice and not a supply-chain pin. +- Consumers pin to a release tag (`@v1.x.y`), never `@main`. Those tags are + immutable: the `release-tags` ruleset covers `refs/tags/v*` with `deletion`, + `update` and `non_fast_forward`, and **no bypass actors**, so not even an org + admin can repoint a published release. Verified by attempting both a + force-move and a delete of `v1.2`; the server rejected each. +- That protection is the load-bearing part. `v1.1` was moved from `61ec8ca` to + `c3b9a4d` before the ruleset existed, which is how the internal pins ended up + naming a commit twelve behind the tag they claimed. Do not relax the ruleset + or add bypass actors to it. - Cutting a release is three steps, in order. Merge the feature. Merge a second - PR bumping the internal pins to the feature's merge commit — they name the - *previous* commit because a commit cannot pin itself, and the composite-action - code is identical across the two, so the `# v1.x` comment stays honest. Tag - that second merge commit, then update this README's examples to the tag's - commit. Skipping the last step is what left `# v1.1` pointing twelve commits - behind the tag it named. + PR bumping the internal pins to the feature's merge commit. Tag that second + merge commit — the tag is permanent from that moment, so get the commit right + before pushing it. - The internal `uses: ShiplightAI/ci-triage[/setup]@ # v1.x` references in `triage.yml` are pinned by SHA and are immutable. Bump both the SHA and its release-tag comment when cutting a new version so the whole pipeline runs at From 8041f6dab845bed1dc9572ed7e51fd4f9c06a366 Mon Sep 17 00:00:00 2001 From: Feng Qian Date: Tue, 11 Aug 2026 19:36:26 -0700 Subject: [PATCH 3/4] Correct the tag-vs-SHA claim; keep the SHA recipe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- README.md | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index cda7159..c02524b 100644 --- a/README.md +++ b/README.md @@ -62,11 +62,24 @@ jobs: PLAYWRIGHT_PROXY_PASSWORD=${{ secrets.PLAYWRIGHT_PROXY_PASSWORD }} ``` -Pin to a release tag (`@v1.2`), never `@main`. **Release tags here are -immutable**: a repository ruleset on `refs/tags/v*` blocks deletion, updates and -force-pushes with no bypass actors, so a tag you adopt cannot be repointed at -different code afterwards. A commit SHA works too and is equivalent, but the tag -is readable and tells you which release you are on. +Pin to a release tag (`@v1.2`), never `@main`. Release tags here are protected: +a repository ruleset on `refs/tags/v*` blocks deletion, updates and +force-pushes, with no bypass actors, so a tag you adopt cannot be repointed at +different code. + +That is a *policy* guarantee, not a cryptographic one — a ruleset can be changed +by someone with admin rights on this repo, whereas a commit SHA cannot be +changed by anyone. The tag is the recommended pin because it is readable and +says which release you are on. If you want the guarantee that does not depend on +this repo's settings staying correct, resolve the tag once and pin the commit: + +```console +$ git ls-remote https://github.com/ShiplightAI/ci-triage 'refs/tags/v1.2^{}' +5f0d8356d4ef22859c30cee6a627b930672e89c0 refs/tags/v1.2^{} +``` + +The `^{}` matters: releases here are annotated tags, so without it you get the +tag object rather than the commit, and `uses:` needs the commit. The consumer's test workflows must upload their `shiplight-report/` so triage can read the failure evidence. Use the **`upload-report` helper action** — it bakes in @@ -263,15 +276,20 @@ the repository root, and is empty when the target never resolved to a real file. These scripts run in a **privileged** job (`contents: write`, agent in bypassed- permissions mode, live credentials). Treat this repo as high-trust: -- Consumers pin to a release tag (`@v1.x.y`), never `@main`. Those tags are - immutable: the `release-tags` ruleset covers `refs/tags/v*` with `deletion`, - `update` and `non_fast_forward`, and **no bypass actors**, so not even an org - admin can repoint a published release. Verified by attempting both a +- Consumers pin to a release tag (`@v1.x.y`), never `@main`. The `release-tags` + ruleset 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 — including an admin's. Verified by attempting both a force-move and a delete of `v1.2`; the server rejected each. -- That protection is the load-bearing part. `v1.1` was moved from `61ec8ca` to - `c3b9a4d` before the ruleset existed, which is how the internal pins ended up - naming a commit twelve behind the tag they claimed. Do not relax the ruleset - or add bypass actors to it. +- 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; a + commit SHA has no such dependency. Consumers who want the stronger form should + pin the commit, and the quick-start shows how to resolve it. +- The ruleset is load-bearing. `v1.1` was moved from `61ec8ca` to `c3b9a4d` + before it existed, which is how the internal pins ended up naming a commit + twelve behind the tag they claimed. Do not relax it or add bypass actors. + Note it also freezes `v1` and `v1.1`, so the stale `v1` alias can no longer be + advanced without narrowing the pattern first. - Cutting a release is three steps, in order. Merge the feature. Merge a second PR bumping the internal pins to the feature's merge commit. Tag that second merge commit — the tag is permanent from that moment, so get the commit right From 641348a55b6c3ccce63c67e74cee89254c41c985 Mon Sep 17 00:00:00 2001 From: Feng Qian Date: Tue, 11 Aug 2026 19:37:53 -0700 Subject: [PATCH 4/4] Drop release-specific detail from the README 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) --- README.md | 50 +++++++++++++++++--------------------------------- 1 file changed, 17 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index c02524b..76d4a1e 100644 --- a/README.md +++ b/README.md @@ -62,24 +62,17 @@ jobs: PLAYWRIGHT_PROXY_PASSWORD=${{ secrets.PLAYWRIGHT_PROXY_PASSWORD }} ``` -Pin to a release tag (`@v1.2`), never `@main`. Release tags here are protected: -a repository ruleset on `refs/tags/v*` blocks deletion, updates and -force-pushes, with no bypass actors, so a tag you adopt cannot be repointed at -different code. - -That is a *policy* guarantee, not a cryptographic one — a ruleset can be changed -by someone with admin rights on this repo, whereas a commit SHA cannot be -changed by anyone. The tag is the recommended pin because it is readable and -says which release you are on. If you want the guarantee that does not depend on -this repo's settings staying correct, resolve the tag once and pin the commit: +Pin to a release tag, never `@main`. Release tags are protected against +deletion, updates and force-pushes, so a tag you adopt keeps pointing at the +code you adopted. To pin a commit instead — a guarantee that does not depend on +this repo's settings — resolve the tag first: ```console $ git ls-remote https://github.com/ShiplightAI/ci-triage 'refs/tags/v1.2^{}' -5f0d8356d4ef22859c30cee6a627b930672e89c0 refs/tags/v1.2^{} ``` -The `^{}` matters: releases here are annotated tags, so without it you get the -tag object rather than the commit, and `uses:` needs the commit. +The `^{}` matters: releases are annotated tags, so without it you get the tag +object rather than the commit, and `uses:` needs the commit. The consumer's test workflows must upload their `shiplight-report/` so triage can read the failure evidence. Use the **`upload-report` helper action** — it bakes in @@ -277,27 +270,18 @@ These scripts run in a **privileged** job (`contents: write`, agent in bypassed- permissions mode, live credentials). Treat this repo as high-trust: - Consumers pin to a release tag (`@v1.x.y`), never `@main`. The `release-tags` - ruleset 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 — including an admin's. Verified by attempting both a - force-move and a delete of `v1.2`; the server rejected each. -- 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; a - commit SHA has no such dependency. Consumers who want the stronger form should - pin the commit, and the quick-start shows how to resolve it. -- The ruleset is load-bearing. `v1.1` was moved from `61ec8ca` to `c3b9a4d` - before it existed, which is how the internal pins ended up naming a commit - twelve behind the tag they claimed. Do not relax it or add bypass actors. - Note it also freezes `v1` and `v1.1`, so the stale `v1` alias can no longer be - advanced without narrowing the pattern first. + ruleset protects `refs/tags/v*` with no bypass actors, so no push can repoint + a published release. That is a policy guarantee rather than a cryptographic + one — anyone who can edit repository settings can undo it — so do not relax + the ruleset, and pin a commit instead if you need a guarantee independent of + it. The pattern covers every `v*` tag, including any moving major-version + alias. - Cutting a release is three steps, in order. Merge the feature. Merge a second PR bumping the internal pins to the feature's merge commit. Tag that second - merge commit — the tag is permanent from that moment, so get the commit right - before pushing it. + merge commit — tags cannot be moved afterwards, so confirm the commit first. - The internal `uses: ShiplightAI/ci-triage[/setup]@ # v1.x` references in - `triage.yml` are pinned by SHA and are immutable. Bump both the SHA and its - release-tag comment when cutting a new version so the whole pipeline runs at - one ref. They necessarily name the *previous* commit — a commit cannot pin - itself — so the release tag is cut on the commit that bumps them, and the - composite-action code at the pinned SHA is identical to the tagged release. + `triage.yml` are pinned by SHA. Bump both the SHA and its release-tag comment + when cutting a new version so the whole pipeline runs at one ref. They name + the *previous* commit, since a commit cannot pin itself; the composite-action + code is unchanged between it and the release. - Restrict who can push/tag here more tightly than the consumer repos.