Skip to content

Add --resolve-refs flag#7

Merged
TooFastTooCurious merged 1 commit intomainfrom
resolve-refs
Apr 15, 2026
Merged

Add --resolve-refs flag#7
TooFastTooCurious merged 1 commit intomainfrom
resolve-refs

Conversation

@TooFastTooCurious
Copy link
Copy Markdown
Contributor

Summary

Closes #5.

New opt-in flag --resolve-refs that calls the GitHub commits API for each tag- or branch-pinned action reference and records the current commit SHA in ActionRef.ResolvedSHA alongside the original ref. Turns a mutable-tag BOM into a stable evidentiary record, which matters for audit, compliance, or forensic use cases where "what actually ran" needs to be recoverable months later.

Ben proposed this in #5 and agreed on covering both tags and branches (someone pinning to main has still opted into stability, they just won't have used this flag). Name changed from --resolve-tags to --resolve-refs to match the broader coverage.

The problem

Today:
   workflow:  actions/checkout@v4
        │
        ▼
   BOM records: "actions/checkout@v4"
        │
        │  (six months later, maintainer force-pushes v4
        │   to a different commit)
        ▼
   BOM no longer represents what actually ran.
With --resolve-refs:
   workflow:  actions/checkout@v4
        │
        ▼
   GitHub API: /repos/actions/checkout/commits/v4
        │
        ▼
   BOM records: "actions/checkout@v4"
                 + resolved_sha: "34e11487..."
        │
        │  (six months later, v4 moves)
        ▼
   BOM still points at the immutable SHA that was
   resolved at generation time.

The original ref: "v4" is preserved so the contributor's stated intent stays visible in the BOM. resolved_sha is additive metadata, not a replacement.

What gets resolved

Ref type Behavior Reasoning
Tag (v4) Resolved Tags are mutable.
Branch (main) Resolved Branches are even more mutable. Someone pinning to a branch opted out of stability, but a BOM record of where the branch pointed at gen time is still useful for forensics.
SHA Skipped Already immutable. No work to do.
Docker Skipped Not a Git ref.
Local action (./foo) Skipped Not a Git ref.

Dedup is keyed on owner/repo@ref so two actions referencing the same tag in different subdirectories collapse into a single API call.

Rate limiting

Each unique tag or branch ref costs one authenticated API call (5000/hour with token, 60/hour anonymous). Same pattern as --verify-shas:

  • --resolve-refs --offline fails fast at flag validation
  • --resolve-refs with no token emits a startup rate-limit warning via the collector so --fail-on-warnings catches it
  • Mid-run 403/429 emits a single rate-limit warning and suppresses remaining resolutions

Network or 5xx errors emit a ref-resolve category warning for that specific ref without populating resolved_sha. 404 (ref not found) does the same.

Interaction with --verify-shas

Orthogonal. --verify-shas walks SHA-pinned refs and checks reachability. --resolve-refs walks tag and branch refs and populates resolved_sha. When both flags are set, ref resolution runs first (gives the fuller BOM), then SHA verification runs over the genuinely SHA-pinned refs.

No conflict, no double-counting, no new semantics.

Output

ActionRef.ResolvedSHA was already a field on the struct (placeholder, never populated). The JSON formatter surfaces it automatically when set:

{
  "uses": "actions/checkout@v4",
  "ref": "v4",
  "ref_type": "tag",
  "resolved_sha": "34e114876b0b11c390a56381ad16ebd13914f8d5"
}

No output-format code needed to change. CycloneDX and SPDX formatters could surface the resolved SHA as a property or version in a follow-up.

Files changed

pkg/resolver/resolve_refs.go        new  (RefResolver, GitHubRefResolver, ResolveABOMRefs)
pkg/resolver/resolve_refs_test.go   new  (mockRefResolver + 8 test cases)
pkg/warnings/warnings.go            +1   (new CategoryRefResolve constant)
cmd/root.go                         +2   (new --resolve-refs flag)
cmd/scan.go                         +19  (flag validation, wiring, ordering)
cmd/check.go                        +16  (mirror of scan wiring)
README.md                           +29  (new section, feature bullet, flag table row)

Test plan

Automated (all passing):

  • go test ./...
  • go vet ./...
  • go build ./...

Unit tests cover:

  • Tag resolves, populates ResolvedSHA
  • Branch resolves, populates ResolvedSHA
  • SHA ref is skipped (no API call)
  • Docker and local action types skipped
  • Dedup across subdirectory variants hits the API once
  • Ref not found (404) emits ref-resolve warning, leaves ResolvedSHA empty
  • Transport error emits ref-resolve warning
  • Mid-run rate limit emits one warning, suppresses remaining resolutions
  • Nil collector does not panic

Manual smoke:

  • abom --help lists --resolve-refs
  • abom scan . --resolve-refs --offline errors with a clear "remove --offline" message
  • Anonymous --resolve-refs emits the rate-limit startup warning

Live integration against api.github.com was not included for the same reason as the verify-shas PR (flaky under anonymous rate limits in the harness).

Resolve tag and branch refs to the commit SHA they currently point to at
BOM-generation time, stored in ActionRef.ResolvedSHA alongside the
original ref. Turns a mutable-tag BOM into a stable evidentiary record.

Covers tag and branch refs. SHA-pinned, Docker, and local refs are
skipped. The original ref is preserved so contributor intent stays
visible.

Shares the GitHub API auth and rate-limit pattern with --verify-shas.
When both flags are set, resolution runs first.
@TooFastTooCurious TooFastTooCurious merged commit 10f0aec into main Apr 15, 2026
2 checks passed
@TooFastTooCurious TooFastTooCurious deleted the resolve-refs branch April 15, 2026 23:06
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.

Feature: Optionally resolve tags to hash when building an ABOM

1 participant