pinact version
v3.9.0 — observed via suzuki-shunsuke/pinact-action@cf51507d80d4d6522a07348e3d58790290eaf0b6 with verify: true and min_age: 7.
Environment
- OS: platform-independent (server-side resolution).
- CPU: any.
Overview
When a target action repository has both a branch and a tag with the same name (e.g. v5), pinact pins workflows to the branch SHA, but the GitHub Actions runtime resolves the same @v5 reference to the tag. The pinned SHA therefore diverges from what the runtime actually executes.
Root cause: pkg/controller/run/parse_line.go resolves refs via repositoriesService.GetCommitSHA1(..., action.Version, \"\"), which hits the REST endpoint /repos/{owner}/{repo}/commits/{ref}. That endpoint follows git's native precedence — branch before tag when ambiguous. The Actions runner implements the opposite precedence (tag before branch).
Why this matters beyond correctness
-
Pin diverges from runtime. A reader of @A # v5 assumes the runtime executes A when it sees @v5. Where branch and tag diverge, the pin is the branch tip, not the tag — so the comment misrepresents what Actions will run.
-
Verification hits the same ambiguity. pinact verify and any reviewer running gh api repos/.../commits/v5 query the same endpoint and get the same branch-first answer. The natural "is this pin stale?" check confirms the misrepresentation rather than catching it. Only an explicit tag-namespace query (git/matching-refs/tags/v5) surfaces the divergence.
-
Defense-in-depth against supply-chain drift. Where a maintainer keeps both a trusted tag v5 = A and a mutable branch v5 = B, pinact binds downstream users to B. The pin is immutable (a SHA), but the decision about which SHA was made against a mutable ref. Force-pushes to the v5 branch silently affect new pins and --update runs.
This is a correctness bug whose fix also closes a defense-in-depth gap — not a critical vulnerability. Aligning pinact with the Actions runtime's tag-first precedence collapses all three cases.
How to reproduce
Two public actions exhibit this today (discovered while running pinact-action across a workflow repo):
| Repo |
Branch vX SHA |
Tag vX SHA |
Relationship |
peter-evans/slash-command-dispatch |
0683e68c |
9bdcd791 (= v5.0.2) |
Diverged — 6 commits removed from branch |
thollander/actions-comment-pull-request |
65f9e5c9 |
24bffb9b (= v3.0.1) |
Branch 13 commits behind tag |
Minimal workflow
# .github/workflows/example.yml
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: peter-evans/slash-command-dispatch@v5
Commands
Branch-first resolution (what pinact sees today):
$ gh api repos/peter-evans/slash-command-dispatch/commits/v5 --jq .sha
0683e68c... # branch SHA
Tag namespace (what Actions runtime resolves to):
$ gh api repos/peter-evans/slash-command-dispatch/git/matching-refs/tags/v5 --jq '.[0].object.sha'
9bdcd791... # tag SHA (= v5.0.2)
Expected Behaviour
When a ref matches both a tag and a branch, pinact pins to the tag SHA — matching the Actions runtime's tag-first resolution.
Actual Behaviour
pinact pins to the branch SHA. For peter-evans/slash-command-dispatch@v5, pinact run writes @0683e68c... # v5, but @v5 at runtime executes 9bdcd791... — different code.
Important Factoids
min_age is not involved. The bug lives in pinCurrentVersion, which doesn't consult the cooldown.
- Not a go-github bug.
/commits/{ref}'s branch-first precedence is documented GitHub REST API behavior. The fix belongs in pinact's caller.
- Six call sites in
pkg/controller/run/parse_line.go pass a semver-like ref to GetCommitSHA1 (lines 222, 233, 292, 312, 354, 440). All are affected because they treat the ref as a tag intent.
- GHES users hit the same bug —
ClientResolver.GetRepositoriesService routes both hosts through the same GetCommitSHA1 wrapper.
Proposed fix
Prefix the ref with tags/ when calling GetCommitSHA1, forcing tag-namespace resolution. On a 404, fall back to the bare ref so that actions that ship a v1 branch but no v1 tag continue to resolve. The fallback also preserves behavior for refs deliberately aimed at a branch.
A PR is ready along these lines, but I'm happy to redirect the approach if you prefer a different shape — for example, consulting the already-populated ListTags cache rather than a second REST call. Will open the PR once this issue has a number to reference.
References
pkg/controller/run/parse_line.go:222,233,292,312,354,440 — affected call sites
pkg/github/service.go:221 — GetCommitSHA1 wrapper and cache
Investigation and draft produced collaboratively with Claude Code (AI assistant); content reviewed and verified against the source tree.
pinact version
v3.9.0— observed viasuzuki-shunsuke/pinact-action@cf51507d80d4d6522a07348e3d58790290eaf0b6withverify: trueandmin_age: 7.Environment
Overview
When a target action repository has both a branch and a tag with the same name (e.g.
v5), pinact pins workflows to the branch SHA, but the GitHub Actions runtime resolves the same@v5reference to the tag. The pinned SHA therefore diverges from what the runtime actually executes.Root cause:
pkg/controller/run/parse_line.goresolves refs viarepositoriesService.GetCommitSHA1(..., action.Version, \"\"), which hits the REST endpoint/repos/{owner}/{repo}/commits/{ref}. That endpoint follows git's native precedence — branch before tag when ambiguous. The Actions runner implements the opposite precedence (tag before branch).Why this matters beyond correctness
Pin diverges from runtime. A reader of
@A # v5assumes the runtime executesAwhen it sees@v5. Where branch and tag diverge, the pin is the branch tip, not the tag — so the comment misrepresents what Actions will run.Verification hits the same ambiguity.
pinact verifyand any reviewer runninggh api repos/.../commits/v5query the same endpoint and get the same branch-first answer. The natural "is this pin stale?" check confirms the misrepresentation rather than catching it. Only an explicit tag-namespace query (git/matching-refs/tags/v5) surfaces the divergence.Defense-in-depth against supply-chain drift. Where a maintainer keeps both a trusted tag
v5 = Aand a mutable branchv5 = B, pinact binds downstream users toB. The pin is immutable (a SHA), but the decision about which SHA was made against a mutable ref. Force-pushes to thev5branch silently affect new pins and--updateruns.This is a correctness bug whose fix also closes a defense-in-depth gap — not a critical vulnerability. Aligning pinact with the Actions runtime's tag-first precedence collapses all three cases.
How to reproduce
Two public actions exhibit this today (discovered while running
pinact-actionacross a workflow repo):vXSHAvXSHApeter-evans/slash-command-dispatch0683e68c9bdcd791(= v5.0.2)thollander/actions-comment-pull-request65f9e5c924bffb9b(= v3.0.1)Minimal workflow
Commands
Branch-first resolution (what pinact sees today):
Tag namespace (what Actions runtime resolves to):
Expected Behaviour
When a ref matches both a tag and a branch, pinact pins to the tag SHA — matching the Actions runtime's tag-first resolution.
Actual Behaviour
pinact pins to the branch SHA. For
peter-evans/slash-command-dispatch@v5,pinact runwrites@0683e68c... # v5, but@v5at runtime executes9bdcd791...— different code.Important Factoids
min_ageis not involved. The bug lives inpinCurrentVersion, which doesn't consult the cooldown./commits/{ref}'s branch-first precedence is documented GitHub REST API behavior. The fix belongs in pinact's caller.pkg/controller/run/parse_line.gopass a semver-like ref toGetCommitSHA1(lines 222, 233, 292, 312, 354, 440). All are affected because they treat the ref as a tag intent.ClientResolver.GetRepositoriesServiceroutes both hosts through the sameGetCommitSHA1wrapper.Proposed fix
Prefix the ref with
tags/when callingGetCommitSHA1, forcing tag-namespace resolution. On a 404, fall back to the bare ref so that actions that ship av1branch but nov1tag continue to resolve. The fallback also preserves behavior for refs deliberately aimed at a branch.A PR is ready along these lines, but I'm happy to redirect the approach if you prefer a different shape — for example, consulting the already-populated
ListTagscache rather than a second REST call. Will open the PR once this issue has a number to reference.References
pkg/controller/run/parse_line.go:222,233,292,312,354,440— affected call sitespkg/github/service.go:221—GetCommitSHA1wrapper and cacheInvestigation and draft produced collaboratively with Claude Code (AI assistant); content reviewed and verified against the source tree.