Skip to content

Tag narrowing, dependabot contract, and integration hardening - #48

Merged
nodeselector merged 13 commits into
mainfrom
nodeselector/add-no-narrow-flag
Jun 14, 2026
Merged

Tag narrowing, dependabot contract, and integration hardening#48
nodeselector merged 13 commits into
mainfrom
nodeselector/add-no-narrow-flag

Conversation

@nodeselector

Copy link
Copy Markdown
Collaborator

What

Adds tag narrowing (splat → semver precision) with sticky lock behavior, establishes the dependabot-core integration contract with golden-captured JSON bodies, and fixes several correctness issues in --no-onboard and --no-fix paths.

Why

Three motivations converged on this branch:

  1. Tag narrowing: actions/checkout@v4 should resolve to v4.2.0 (the most precise semver tag pointing at the same commit). This reduces blast radius — v4 can move to any 4.x, but v4.2.0 is immutable. Users who explicitly chose a splat ref keep it (sticky precision).

  2. Dependabot contract: dependabot-core consumes check --json output to enforce lockfile pins during dependency updates. The CLI's exit codes, finding categories, severity levels, and JSON shape ARE the integration contract. Without a shared test corpus, the two sides drift silently.

  3. Correctness fixes: --no-fix wasn't showing the SSO authorization URL. --no-onboard on fresh repos exited 1 (blocking dependabot runs that can't bootstrap). Transient 403s during re-resolution silently dropped existing pins.

How

Tag narrowing (--no-narrow flag)

  • Default: narrow splat refs (v4v4.2.0) to most precise semver tag at the same commit
  • --no-narrow: skip narrowing entirely
  • Sticky precision: if the lockfile already has v4 (imprecise), re-pin keeps v4 — respects the user's prior choice
  • Semver nudge: when a non-semver splat is used, prints a hint suggesting semver tags

Dependabot contract (dependabot scenario category)

  • 9 scenarios in catalog.yml covering the exact --json body shape dependabot-core parses
  • Golden capture mode: ruby test/integration/run.rb --golden-update dependabot runs each scenario through the real binary and writes the literal JSON stdout into expect.golden_json
  • On every subsequent run, the harness validates actual output matches the golden body exactly
  • Scenarios include: clean repin, onboarding skip, new-action refusal, corrupt lockfile, multi-workflow, version-ref suppression, transient-403 retention, impostor/forgery stubs

Exit code semantics (--no-onboard)

  • OnboardingRequired findings downgraded to severity: info and IsValid() = true
  • Exit 0 when only onboarding-required findings remain (non-blocking skip)
  • Mixed runs with real blockers still exit 1
  • Findings stay in findings[] at exit 0 for consumer observability

Pin retention on transient failures

  • retainImpostorPinsretainUnresolvablePins: broadened to also retain entries where resolution status is Unresolved (not just impostor commits)
  • Covers 403/SSO failures during re-resolution — existing pin preserved verbatim

Bug fixes

  • SSO authorization URL now surfaces in --no-fix mode
  • --no-onboard summary names each refused workflow/action (was just a count)

Risk

Aspect Assessment
Blast radius Users who relied on exit 1 from --no-onboard on fresh repos — this is the dependabot consumer, who requested the change
Reversibility 🟢 All changes are additive or behavioral adjustments; revert is clean
Rollback plan Revert PR; no migrations or state changes
Dependencies dependabot-core consumer (separate repo) vendors catalog.yml — they're already tracking this branch

Testing

  • All Go tests pass (go test ./... — 16 packages)
  • Integration harness: 7/9 dependabot scenarios green, 2 skipped (impostor/forgery need stub infra)
  • Golden capture validates exact JSON body match on every run
  • Existing scenario categories (onboarding, SSO, API errors, narrowing) pass
  • retainUnresolvablePins has dedicated unit tests for impostor, no-op, and unresolved cases

Commit guide

  1. feat: add --no-narrow flag and respect locked ref precision — Core narrowing implementation
  2. narrowing: nudge non-semver refs, global sticky precision — UX polish
  3. rename mutable-ref to version-ref — Terminology alignment
  4. port contract-essential pieces from PR #36 — Bring forward shared catalog infra
  5. scenarios: add narrowing, onboarding, and lockfile recovery cases — Test matrix expansion
  6. check: surface SSO authorization URL in --no-fix mode — Bug fix
  7. pin: retain existing pins when re-resolution fails — 403/transient fix
  8. onboard: name refused workflows in --no-onboard summary — UX improvement
  9. scenarios: add dbot_transient_403_drops_pin contract scenario — Contract coverage
  10. onboard: exit 0 when only onboarding-required findings remain — Exit code semantics
  11. scenarios: add dbot_impostor_blocks and dbot_forgery_blocks stubs — Stub placeholders
  12. harness: golden capture mode for dependabot contract scenarios — Golden capture + IsValid fix

Follow-ups

  • Impostor/forgery integration test stub infrastructure (multi-phase HTTP mocking)
  • Golden capture for additional scenario categories beyond dependabot
  • lockfile_template field in scenarios for self-contained vendoring

nodeselector and others added 12 commits June 12, 2026 10:53
Add --no-narrow flag to preserve mutable version refs (e.g. v4) in the
lock comment instead of narrowing them to full patch tags (v4.2.1).

Once a dep is locked with an imprecise tag, subsequent re-pins (e.g. on
ref-moved) respect that choice by checking the existing lockfile for
mutable refs before narrowing. This makes the precision sticky without
requiring --no-narrow on every run.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Narrowing now checks whether a ref is a full semver tag (v4.2.1) rather
than just whether it's a mutable semver. This broadens the nudge to
cover any non-semver ref (v4, main, arbitrary tags) — pushing users
toward precise refs where each tag resolves to exactly one commit.

Sticky precision is computed globally across all workflows (not per-WF)
to avoid creating duplicate dep entries at different ref granularities
for the same NWO. Once a dep is locked imprecisely anywhere, all
workflows consistently use that ref.

Terminal output: TermWarn nudge listing non-semver deps with guidance.
JSON output: info-severity mutable-ref findings injected per workflow.
Both suppressed when --no-narrow is set (user explicitly opted out).
Drop the 'mutable' framing — any git ref can be rewritten today, so
the term is misleading. VersionRef better describes what we're checking:
is the ref a full semver tag (v4.2.1) or not.

Renames: MutableRef to VersionRef, mutable-ref to version-ref,
prevMutableNWO to prevImpreciseNWO, IsMutable to IsFull.
Bring in the infrastructure Dependabot needs to shell out to
gh-actions-pin check --no-onboard --no-narrow --no-interactive:

- --no-onboard / --no-interactive persistent flags on root command
- onboard gate: rewrites not-pinned → onboarding-required for new
  entries when --no-onboard is set, so relock never silently adds deps
- corrupt lockfile recovery with interactive confirm / CI fail-fast
- branch/tag preservation in lockfile Set() for branchless read-path
- stale inventory pruning so Plan() skips entries the pipeline dropped
- impostor pin retention and commit helpers (pin/commit.go)
- OnboardingRequired in terminal error + alerted category lists

All existing tests pass; new test files for lock recovery, impostor
retention, and lockfile state included.
22 new scenarios across 3 categories:
- narrowing (12): default v4→full, minor→patch, branch skip, --no-narrow
  keeps major/minor, sticky imprecise re-pin, nudge visibility, JSON
- onboarding (8): --no-onboard new/tracked/mixed, JSON findings,
  terminal caution, flag composition with --no-narrow/--no-interactive
- lockfile (2): corrupt + --no-fix fails, corrupt + --no-interactive fails
The --no-fix early return path exits before the SSO URL display block,
so users never see the actionable authorization link when running
read-only. Move the SSO URL surface before the early return so it
shows regardless of fix mode.

Closes #45
Rename retainImpostorPins → retainUnresolvablePins and broaden the
filter to also retain Unresolved entries (not just Investigate +
ImpostorCommit). When a 403/SSO or transient error prevents
re-resolution, the existing on-disk pin is preserved instead of
being silently dropped.

New test verifies the Unresolved retention path round-trips through
a lockfile write.

Closes #46
Change gateNoOnboard to return refused entry labels (e.g.
"actions/checkout@v4 in .github/workflows/ci.yml") instead of
just a count. renderPinSummary now lists each refused entry so
users know exactly what was skipped.

Also removes the stale 'live' tag assertion from catalog_test.go
(live scenarios were removed earlier).

Closes #44
SSO 403 on a previously-pinned action — exercises the not-pinned/error
finding path that the dependabot-core dropped-pin guard keys on.
Uses the default SSO 403 stub with a pinned_checkout lockfile template.

Ref #47
When --no-onboard skips new workflows and no other blocking findings
exist, exit 0 instead of 1. Onboarding-required findings are
downgraded to info severity (non-blocking) but remain in the JSON
output so machine consumers can still observe the skip.

This aligns with the dependabot-core consumer contract: dependabot
never bootstraps lockfiles (that's the onboarding flow's job), so
an incremental run that only sees new/un-onboarded workflows should
not block.

Mixed runs (onboarding-required + impostor/not-pinned/unresolved)
still exit 1 because the blocking findings survive.

Closes #43
Skipped scenarios for the dependabot contract category covering
impostor-commit and lockfile-forgery findings. Both require
multi-phase HTTP stub infra (GraphQL + REST) that doesn't exist
yet — marked with skip + needs_stub.

Category strings confirmed: impostor-commit, lockfile-forgery.
Severity: error. Shape matches existing dbot scenarios.
Add --golden-update [category] to the integration harness. Runs each
scenario in the category, captures the literal --json stdout body,
and writes it back into catalog.yml under expect.golden_json. This
makes the binary the source of truth for the contract — when you
change a category, severity, or exit mapping, re-run golden-update
and the scenario bodies change in the same PR.

Also:
- OnboardingRequired now returns IsValid()=true, fixing valid:false
  in JSON output at exit 0 for --no-onboard runs.
- golden_json assertion validates the full JSON body on every run.
- Catalog Go structs gain JQCheck, GoldenJSON, and Skip fields.
- Fixed greater_than/gt jq assertion mismatch.
- Updated stale dbot scenario assertions caught by golden capture:
  dbot_not_pinned, dbot_multi_workflow, dbot_transient_403_drops_pin.
GitHub Advanced Security started work on behalf of nodeselector June 14, 2026 18:15 View session
GitHub Advanced Security finished work on behalf of nodeselector June 14, 2026 18:16
@nodeselector
nodeselector marked this pull request as ready for review June 14, 2026 18:33
Copilot AI review requested due to automatic review settings June 14, 2026 18:33

Copilot AI 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.

⚠️ Not ready to approve

There are correctness issues in new behavior/tests (notably lockfile recovery path selection under GH_ACTIONS_PIN_WORKFLOWS_DIR, sticky narrowing semantics, and scenario exit expectations) that can break intended contract behavior.

Pull request overview

This PR expands gh-actions-pin check to support Dependabot’s relock contract by hardening exit-code semantics, adding tag-narrowing behavior (with opt-out + “sticky precision”), and strengthening integration tests via a scenario catalog with golden-captured JSON bodies.

Changes:

  • Add tag narrowing (major/minor → full patch tag at same commit) with --no-narrow, plus “version-ref” informational nudges and JSON surfacing.
  • Introduce --no-onboard gating (refuse onboarding new workflows/actions) and adjust validity semantics so onboarding-required-only runs are non-blocking (exit 0, valid: true).
  • Add/extend integration scenario infrastructure (jq assertions, golden JSON capture/update flow, richer harness UI) and lockfile corruption recovery behavior.
File summaries
File Description
test/scenarios/catalog.yml Adds new scenario categories (narrowing/onboarding/dependabot), new scenarios, and golden JSON contract fixtures.
test/scenarios/catalog.go Extends scenario schema to support skip, jq checks, and embedded golden JSON bodies.
test/scenarios/catalog_test.go Updates tag-based catalog test expectations after removing “live” tagging.
test/integration/run.rb Enhances fixture generation and assertion hydration (jq checks, golden JSON diff), adds lockfile templates.
test/integration/harness.rb Adds golden capture mode, interactive review tooling, PTY prompt automation, diff caching/paging.
internal/pipeline/checks/finding.go Adjusts workflow attention/validity logic to treat version-ref and onboarding-required as non-blocking.
internal/pipeline/checks/category.go Updates onboarding-required definition and adds the new version-ref category.
internal/pipeline/checks/category_test.go Freezes/validates new category string and inconclusive semantics.
internal/pin/retain_impostor_test.go Adds unit tests ensuring unresolved/impostor pins are retained during co-located re-pins.
internal/pin/plan.go Implements tag narrowing + sticky precision logic and prunes stale inventory entries.
internal/pin/commit.go Retains existing pins when a dependency becomes unresolvable during re-resolution.
internal/lockfile/state.go Surfaces corrupt-lockfile errors, preserves branch/tag for unchanged pins, and tightens parsing behavior.
internal/lockfile/state_test.go Adds regression and golden-format tests for lockfile stability, corruption surfacing, and GC behavior.
go.mod Adds new indirect dependencies required by updated runtime/test behavior.
go.sum Updates module checksums for new/updated dependencies.
cmd/gh-actions-pin/root.go Introduces persistent flags and lockfile corruption recovery wiring for root invocations.
cmd/gh-actions-pin/pin_summary.go Adds onboarding refusal summary details and version-ref nudge output.
cmd/gh-actions-pin/onboard_gate.go Implements report rewriting + ref dropping for --no-onboard.
cmd/gh-actions-pin/lockrecovery.go Adds interactive corrupt-lockfile delete-and-recreate policy (TTY/CI aware).
cmd/gh-actions-pin/lockrecovery_test.go Tests lockfile recovery policy behaviors (interactive/non-interactive/read-only).
cmd/gh-actions-pin/format/terminal.go Ensures onboarding-required is surfaced in terminal error summaries/alert logic.
cmd/gh-actions-pin/check.go Wires --no-narrow, --no-onboard, version-ref injection, and read-only SSO URL surfacing.
.gitignore Ignores dist/ output directory.

Copilot's findings

  • Files reviewed: 21/23 changed files
  • Comments generated: 7

Note

Your feedback helps us improve the quality of this feature.
Please use 👍 or 👎 to tell us whether this assessment is correct.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/pin/plan.go
Comment thread test/scenarios/catalog.yml
Comment thread test/scenarios/catalog.yml Outdated
Comment thread test/scenarios/catalog.yml Outdated
Comment thread test/scenarios/catalog.yml Outdated
Comment thread test/scenarios/catalog.yml Outdated
Comment thread cmd/gh-actions-pin/root.go
- plan.go: only track imprecise semver refs (v4, v3.1) in the sticky-
  precision set, not branch refs like main. Branch refs are a different
  kind of reference entirely and shouldn't disable narrowing for semver
  refs of the same NWO.

- catalog.yml: fix 4 onboarding scenario exit codes (1→0). Under the
  Option 2 semantics (#43), onboarding-required findings are info-level
  and non-blocking — runs with only onboarding-required findings exit 0.
  Also fix fresh_no_narrow_nudge_suppressed description to match intent.

- root.go: thread workflowsDir through corrupt lockfile recovery path.
  Previously the recovery always pointed at .github/workflows/actions.lock
  even when GH_ACTIONS_PIN_WORKFLOWS_DIR overrode the load path.
GitHub Advanced Security started work on behalf of nodeselector June 14, 2026 18:49 View session
GitHub Advanced Security finished work on behalf of nodeselector June 14, 2026 18:50
@nodeselector
nodeselector merged commit bb543bd into main Jun 14, 2026
8 checks passed
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.

2 participants