Skip to content

Ambiguous ref resolution: pinact resolves @vX to branch when a tag of the same name exists, diverging from Actions runtime #1477

@amccabe-astronomer

Description

@amccabe-astronomer

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

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

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

  3. 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:221GetCommitSHA1 wrapper and cache

Investigation and draft produced collaboratively with Claude Code (AI assistant); content reviewed and verified against the source tree.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions