Skip to content

Add post-update SHA integrity validation for actions-lock entries - #47907

Closed
pelikhan with Copilot wants to merge 6 commits into
mainfrom
copilot/add-validation-phase-update-command
Closed

Add post-update SHA integrity validation for actions-lock entries#47907
pelikhan with Copilot wants to merge 6 commits into
mainfrom
copilot/add-validation-phase-update-command

Conversation

Copilot AI commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

gh aw update now performs a final integrity pass over .github/aw/actions-lock.json to catch malformed or inconsistent action/container pins before the command exits. This adds an explicit validation phase that double-checks both action SHAs and container SHA256 digests for structural correctness.

  • Update flow: add terminal validation phase

    • RunUpdateWorkflows now invokes a new SHA validation step after update/recompile/pin-refresh work completes successfully.
    • Validation failures are surfaced as a hard update validation failed error instead of being silently persisted.
  • Action entry integrity checks

    • Verifies action SHA is a full 40-char commit SHA.
    • Verifies map key consistency (repo@version key must match entry fields).
    • Rejects empty repo/version/SHA fields.
  • Container pin integrity checks

    • Verifies digest format is sha256:<64 lowercase hex>.
    • Verifies key/image consistency (container key must match pin.image).
    • Verifies pinned_image is exactly image@digest.
  • Targeted unit coverage for new validator

    • Covers no-file behavior (no-op), valid lock content, and multi-error invalid content (action + container mismatch cases) with aggregated diagnostics.
if firstErr == nil {
	if err := validateUpdateSHAEntries("."); err != nil {
		return fmt.Errorf("update validation failed: %w", err)
	}
}

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 17.6 AIC · ⌖ 8.18 AIC · ⊞ 7.1K ·
Comment /souschef to run again

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title Add SHA validation phase to update command Add post-update SHA integrity validation for actions-lock entries Jul 25, 2026
Copilot AI requested a review from pelikhan July 25, 2026 03:51
@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot

  • add code to resolve commit sha and validate it exists
  • add code to validate container sha marches container version

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

@copilot

  • add code to resolve commit sha and validate it exists
  • add code to validate container sha marches container version

Implemented in 3370936.

  • Added action commit validation that resolves and verifies both:
    • stored commit SHA exists in the action repository, and
    • repo@version resolves to the same SHA in actions-lock.json.
  • Added container validation that resolves the current digest for each container image tag and fails when it does not match the stored SHA256 digest.
  • Updated update_validation_test.go with resolver stubs and assertions for the new mismatch/unresolved cases.

@pelikhan
pelikhan marked this pull request as ready for review July 25, 2026 04:15
Copilot AI review requested due to automatic review settings July 25, 2026 04:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds terminal integrity validation for action and container pins after workflow updates.

Changes:

  • Validates lock-file keys, SHAs, digests, and pinned images.
  • Adds remote action and container resolution checks.
  • Adds unit coverage for valid, invalid, and missing lock files.
Show a summary per file
File Description
pkg/cli/update_validation.go Implements lock-file validation.
pkg/cli/update_validation_test.go Tests validator behavior and diagnostics.
pkg/cli/update_command.go Runs validation after successful updates.

Review details

Tip

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

  • Files reviewed: 3/3 changed files
  • Comments generated: 3
  • Review effort level: Medium

Comment on lines +65 to +76
if entry.SHA == "" {
issues = append(issues, fmt.Sprintf("action entry %q has empty SHA", key))
} else if !IsCommitSHA(entry.SHA) {
issues = append(issues, fmt.Sprintf("action entry %q has invalid SHA %q (expected 40-character commit SHA)", key, entry.SHA))
} else if entry.Repo != "" {
resolvedSHA, err := resolveActionRefSHAForValidation(ctx, entry.Repo, entry.SHA)
if err != nil {
issues = append(issues, fmt.Sprintf("action entry %q has unresolved commit SHA %q in %q: %v", key, entry.SHA, entry.Repo, err))
} else if !strings.EqualFold(resolvedSHA, entry.SHA) {
issues = append(issues, fmt.Sprintf("action entry %q resolved commit SHA mismatch: got %q from %q", key, resolvedSHA, entry.SHA))
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2d8122a + e687d62. VerifyCommitExists was added to the parser package and calls GET /repos/{owner}/{repo}/commits/{sha} directly, bypassing the 40-char hex short-circuit in ResolveRefToSHAForHost.

Comment thread pkg/cli/update_validation.go Outdated
Comment on lines +82 to +86
resolvedVersionSHA, err := resolveActionRefSHAForValidation(ctx, entry.Repo, entry.Version)
if err != nil {
issues = append(issues, fmt.Sprintf("action entry %q has unresolved version %q in %q: %v", key, entry.Version, entry.Repo, err))
} else if entry.SHA != "" && IsCommitSHA(entry.SHA) && !strings.EqualFold(resolvedVersionSHA, entry.SHA) {
issues = append(issues, fmt.Sprintf("action entry %q SHA/version mismatch: version %q resolves to %q but entry has %q", key, entry.Version, resolvedVersionSHA, entry.SHA))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2d8122a. Auth/network failures from both the commit-existence and version-resolution checks are treated as non-fatal: they are logged and skipped rather than appended to issues.

Comment on lines +101 to +110
if !sha256DigestPattern.MatchString(pin.Digest) {
issues = append(issues, fmt.Sprintf("container pin %q has invalid digest %q (expected sha256:<64 lowercase hex chars>)", image, pin.Digest))
} else {
resolvedDigest, err := resolveContainerDigestForValidation(ctx, image)
if err != nil {
issues = append(issues, fmt.Sprintf("container pin %q digest could not be resolved for %q: %v", image, image, err))
} else if resolvedDigest != pin.Digest {
issues = append(issues, fmt.Sprintf("container pin %q digest/version mismatch: expected %q but resolved %q", image, pin.Digest, resolvedDigest))
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e687d62. Live registry lookup is removed entirely. Container pins are validated structurally only (digest format, key/image consistency, pinned_image format). A mutable-tag drift no longer causes any failure or warning.

@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 97/100 — Excellent

Analyzed 3 test(s): 3 design, 0 implementation, 0 violation(s).

📊 Metrics (3 tests)
Metric Value
Analyzed 3 (Go: 3, JS: 0)
✅ Design 3 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 2 (67%)
Duplicate clusters 0
Inflation No
🚨 Violations 0
Test File Classification Issues
TestValidateUpdateSHAEntries_NoActionsLock update_validation_test.go:18 design_test None
TestValidateUpdateSHAEntries_ValidEntries update_validation_test.go:23 design_test None
TestValidateUpdateSHAEntries_InvalidEntries update_validation_test.go:53 design_test None

Verdict

Passed. 0% implementation tests (threshold: 30%). All 3 tests verify behavioral contracts with proper build tags, no mock library violations, and strong error coverage across 7 distinct validation rules.

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 · 25.6 AIC · ⌖ 10.5 AIC · ⊞ 7K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 97/100. 0% implementation tests (threshold: 30%).

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Add post-update SHA integrity validation

The PR adds a meaningful validation phase, but has three blocking correctness issues already captured in inline comments.

Blocking issues
  1. SHA re-resolution is a no-op for commit SHAs (line 76): ResolveRefToSHAForHost returns any 40-char hex ref unchanged, so the round-trip never verifies the commit actually exists in the upstream repo.

  2. Network/auth errors fail the update (line 86): failures from resolveActionRefSHAForValidation are treated as validation issues, breaking gh aw update in offline or auth-restricted environments and reversing existing non-fatal handling.

  3. Live digest re-fetch rejects valid historical pins (line 110): UpdateContainerPins deliberately keeps existing pins when a mutable tag moves forward; re-resolving the tag here incorrectly flags those still-valid entries as digest mismatches.

Please address the three existing inline comments before merging.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 15.4 AIC · ⌖ 4.55 AIC · ⊞ 5K

@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (241 new lines in pkg/) but does not yet have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/47907-post-update-sha-integrity-validation-actions-lock.md — review and complete it before merging.

🔒 This PR cannot merge until an ADR is linked in the PR body.

📋 What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff
  2. Complete any missing sections — add context the AI could not infer, refine the decision rationale, and verify the listed alternatives reflect real options you considered
  3. Commit the finalized ADR to docs/adr/ on your branch (or accept the draft as-is)
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-47907: Post-Update SHA Integrity Validation for actions-lock.json

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

“AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say ‘I’ll deal with this later.’ Deferring decisions corroded my ability to think clearly.”

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 47907-post-update-sha-integrity-validation-actions-lock.md for PR #47907).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · sonnet46 · 48.7 AIC · ⌖ 13.2 AIC · ⊞ 8.5K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /diagnosing-bugs, /tdd, and /codebase-design — requesting changes on three correctness issues in the liveness-check logic.

📋 Key Themes & Highlights

Blocking Issues

  1. Dead SHA round-trip (line 76): ResolveRefToSHAForHost echoes back any 40-char hex unchanged, so the SHA existence check never fires — it gives a false sense of verified liveness. Either remove it or replace it with a commit-existence API call.

  2. Network errors poison structural validation (lines 84–86, 91–93): Transient auth/rate-limit failures from version resolution are appended as hard validation issues, causing gh aw update to fail even when the lock file is structurally sound. These should be warnings, not errors.

  3. Container digest re-resolution breaks valid pins (line 110): Stored digests are intentionally immutable (the update path skips existing pins). Re-resolving the current digest of a mutable tag and diffing it against a historical pin will produce false positives on every update after a tag moves.

Positive Highlights

  • ✅ Multi-error aggregation with sorted iteration is a great pattern — surfaces all issues at once instead of fail-fast.
  • ✅ Testable seam via function variable overrides is the right direction; moving to a resolver struct would make tests parallel-safe too.
  • ✅ Test coverage for no-file, valid, and multi-error paths is solid; test names read as clear specifications.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 40 AIC · ⌖ 5.2 AIC · ⊞ 6.7K
Comment /matt to run again

Comment thread pkg/cli/update_validation.go Outdated
var sha256DigestPattern = regexp.MustCompile(`^sha256:[a-f0-9]{64}$`)

var resolveActionRefSHAForValidation = func(ctx context.Context, repo, ref string) (string, error) {
baseRepo := gitutil.ExtractBaseRepo(repo)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] Package-level var resolver functions block parallel test execution.

Using mutable package-level vars as seams means tests must save/restore them, preventing t.Parallel() and making them fragile under refactoring. Consider passing resolvers as a struct or via functional options instead.

💡 Suggested alternative seam
type validationResolvers struct {
    resolveActionRef       func(ctx context.Context, repo, ref string) (string, error)
    resolveContainerDigest func(ctx context.Context, image string) (string, error)
}

func validateUpdateSHAEntries(ctx context.Context, repoRoot string, r validationResolvers) error { ... }

Tests can supply their own validationResolvers inline without touching globals, enabling t.Parallel().

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e687d62. Package-level vars removed. Resolvers are now passed via a validationResolvers struct to validateUpdateSHAEntriesWithResolvers. All five tests use t.Parallel() and supply their resolvers inline — no global state to save/restore.

} else if !strings.EqualFold(resolvedSHA, entry.SHA) {
issues = append(issues, fmt.Sprintf("action entry %q resolved commit SHA mismatch: got %q from %q", key, resolvedSHA, entry.SHA))
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] Re-resolving a commit SHA against the upstream API is not a meaningful integrity check — and existing comments note this.

ResolveRefToSHAForHost returns any 40-char hex ref unchanged (it only resolves symbolic refs like branch names). So resolveActionRefSHAForValidation(ctx, repo, sha) will echo back the SHA rather than verify it exists. The check on line 78 (!strings.EqualFold(resolvedSHA, entry.SHA)) will therefore never fire, giving a false sense of verified liveness.

💡 Suggested fix

Either:

  1. Remove the SHA-round-trip check — the format check (IsCommitSHA) is sufficient for structural validation.
  2. Use a commit-existence API (e.g. GET /repos/{owner}/{repo}/commits/{sha}) instead of ResolveRefToSHAForHost.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e687d62. The SHA round-trip via ResolveRefToSHAForHost is gone. Commit existence is now verified via VerifyCommitExists (parser.VerifyCommitExists), which calls GET /repos/{owner}/{repo}/commits/{sha} directly — it never short-circuits on a 40-char hex string. Auth/network failures return ErrVerificationSkipped and are logged non-fatally; a definitive not-found (e.g. HTTP 422/404) fails validation.

Comment thread pkg/cli/update_validation.go Outdated
if err != nil {
issues = append(issues, fmt.Sprintf("action entry %q has unresolved version %q in %q: %v", key, entry.Version, entry.Repo, err))
} else if entry.SHA != "" && IsCommitSHA(entry.SHA) && !strings.EqualFold(resolvedVersionSHA, entry.SHA) {
issues = append(issues, fmt.Sprintf("action entry %q SHA/version mismatch: version %q resolves to %q but entry has %q", key, entry.Version, resolvedVersionSHA, entry.SHA))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] Network/auth failures from resolveActionRefSHAForValidation are treated as validation errors, making gh aw update fail in offline or rate-limited environments.

Version resolution errors on lines 84–86 and 91–93 are appended as issues, which causes the whole update to return an error even though the lock entry may be structurally valid. Transient failures should not invalidate an otherwise correct lock file.

💡 Suggested fix

Distinguish between structural errors (always fail) and liveness/network errors (warn and continue, or skip):

resolvedVersionSHA, err := resolveActionRefSHAForValidation(ctx, entry.Repo, entry.Version)
if err != nil {
    // non-fatal: log a warning but do not block the update
    updateLog.Printf("warning: could not resolve version %q for %q: %v", entry.Version, key, err)
} else if ... {
    issues = append(issues, ...)
}

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2d8122a. Network/auth failures from both the commit-existence check and the version resolution are now non-fatal: ErrVerificationSkipped is logged and skipped, and resolution errors are logged (updateValidationLog.Printf) without appending to issues.

} else if resolvedDigest != pin.Digest {
issues = append(issues, fmt.Sprintf("container pin %q digest/version mismatch: expected %q but resolved %q", image, pin.Digest, resolvedDigest))
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] Container digest liveness check rejects valid historical pins whenever a mutable tag moves.

UpdateContainerPins deliberately skips already-pinned images, so their stored digest correctly reflects the image at pin time. Re-resolving the current digest of a mutable tag (e.g. :latest, :0.27.9) and comparing against the immutable stored digest will flag structurally-correct, intentional pins as broken.

💡 Suggested fix

Remove the live re-resolution from the container section — structural checks (format, key/image/pinned_image consistency) are sufficient and safe. If liveness verification is desired, scope it explicitly to an opt-in flag (--verify-live) so users can choose the tradeoff.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e687d62. The live registry lookup is removed entirely. Container pins now receive only structural checks: digest format (sha256:<64 hex>), key/image consistency, and pinned_image = image@digest consistency. No network call is made. The new TestValidateUpdateSHAEntries_ContainerStructuralOnly test passes no container resolver and asserts that a valid structural pin produces no error regardless of what the registry currently holds.

Comment thread pkg/cli/update_validation_test.go Outdated
return ref, nil
}
resolveContainerDigestForValidation = func(_ context.Context, _ string) (string, error) {
return digest, nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The TestValidateUpdateSHAEntries_ValidEntries test mocks the version resolver to return the correct SHA only for ref == "v5", but also catches the SHA re-resolution path (ref == sha) via the fallback return ref, nil. This means the SHA round-trip check passes vacuously (as noted above, ResolveRefToSHAForHost echoes SHAs), masking the dead-code issue in production.

Adding an explicit test case where the SHA resolver is called with the stored SHA and asserting the call never happens (or asserting the expected no-op path) would expose the behavior clearly.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e687d62. The SHA round-trip path is gone — verifyActionCommitExists now makes a real API call so the test stub exercises a real code path. Tests use inline resolver structs; there is no return ref, nil fallback that could accidentally make the check a no-op.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: post-update SHA integrity validation

The three existing comments (lines 60, 76, 86, 110) capture the most significant correctness issues — the SHA-resolve no-op, network failures treated as validation errors, and mutable-tag comparison breaking historical pins. Three additional comments added:

Summary of new findings
  • Serial O(2N) network round-trips (line 109): each action entry fires two sequential HTTP calls; the first (SHA→SHA) is already a documented no-op. With no concurrency, this will be noticeably slow on real lock files.
  • ctx == nil guard (line 60): antipattern per Go convention; should be removed.
  • Misleading error message (line 101): "got %q from %q" implies the second arg is a source repo, but it is the stored SHA — direction of mismatch is confusing.

None of these block CI on their own, but the three pre-existing high-severity issues (network failures causing spurious update failures, mutable-tag re-validation, SHA no-op) together make the validation logic fragile enough that COMMENT is the appropriate verdict until those are addressed.

🔎 Code quality review by PR Code Quality Reviewer · sonnet46 · 40.8 AIC · ⌖ 4.99 AIC · ⊞ 5.7K
Comment /review to run again

Comment thread pkg/cli/update_validation.go Outdated
issues = append(issues, fmt.Sprintf("container pin %q digest could not be resolved for %q: %v", image, image, err))
} else if resolvedDigest != pin.Digest {
issues = append(issues, fmt.Sprintf("container pin %q digest/version mismatch: expected %q but resolved %q", image, pin.Digest, resolvedDigest))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Serial network calls per entry with no parallelism or deduplication: each action entry triggers two separate remote round-trips (once for the SHA, once for the version tag), adding O(2N) sequential HTTP calls during gh aw update, potentially minutes of latency for even modest lock files.

💡 Detail

Lines 97 and 109 both call resolveActionRefSHAForValidation — first to verify the stored SHA (which the existing comment on line 76 notes is a no-op, since ResolveRefToSHAForHost returns commit SHAs unchanged), then to resolve the version tag. For N action entries that is 2N sequential HTTP calls.

Consider:

  • Dropping the redundant SHA-resolve call (lines 97–102) entirely, leaving only the version-tag resolution
  • Running resolutions concurrently via errgroup
  • Or documenting the cost and adding a --no-network / --offline flag

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The container digest lookup is removed entirely in e687d62, eliminating those registry round-trips. Per action entry there are still two calls: verifyActionCommitExists (commit existence) and resolveActionVersionToSHA (version tag → SHA). Both are meaningful and non-redundant. Parallelism can be added later if lock-file sizes make latency observable.

for _, key := range entryKeys {
entry := cache.Entries[key]
if entry.Repo == "" {
issues = append(issues, fmt.Sprintf("action entry %q has empty repo", key))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ctx == nil guard is an antipattern: Go convention prohibits passing nil contexts; the correct response is to panic (or return an error), not silently substitute context.Background().

💡 Detail

From the Go standard library docs: "Do not pass a nil Context, even if a function permits it. Pass context.Background() if you are unsure about which Context to use."

Silently papering over a nil context masks bugs in callers and creates a divergence between test behavior (where callers pass real contexts) and any future caller that accidentally passes nil. Remove the guard and let the caller supply a valid context:

func validateUpdateSHAEntries(ctx context.Context, repoRoot string) error {
    // no nil check — callers must not pass nil
    ...
}

If there is a legitimate code path that has no context, pass context.Background() at the call site explicitly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e687d62. The ctx == nil guard is removed. validateUpdateSHAEntries now passes the context straight through to validateUpdateSHAEntriesWithResolvers without any nil-substitution.

if pin.Image != image {
issues = append(issues, fmt.Sprintf("container pin key/image mismatch: key %q has image %q", image, pin.Image))
}
if !sha256DigestPattern.MatchString(pin.Digest) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading error message: the format string "got %q from %q" makes it appear the second argument is the source repo, but it is actually entry.SHA — the stored SHA that failed to match.

💡 Detail

Current (line 101):

issues = append(issues, fmt.Sprintf(
    "action entry %q resolved commit SHA mismatch: got %q from %q",
    key, resolvedSHA, entry.SHA))

"from %q" reads as "from source X", but entry.SHA is the stored/expected value, not a source. The message is backwards: resolvedSHA is what the remote returned; entry.SHA is what was stored. Suggested wording:

issues = append(issues, fmt.Sprintf(
    "action entry %q SHA mismatch: stored %q, resolved to %q via %q",
    key, entry.SHA, resolvedSHA, entry.Repo))

This makes the direction of the mismatch unambiguous for whoever reads the error during a failed update.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "got %q from %q" format is no longer in the code. The SHA/version mismatch message (introduced in e687d62) reads: "action entry %q SHA/version mismatch: version %q resolves to %q but stored SHA is %q" — making it unambiguous which value was resolved and which was stored.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

…k errors, container mismatch as warning

- Add VerifyCommitExists to parser package: calls GitHub commits API for the
  given SHA without short-circuiting (unlike ResolveRefToSHAForHost which
  returns full SHAs unchanged). Returns ErrVerificationSkipped (wrapped) for
  auth/network failures so callers can skip the check non-fatally.

- Split resolveActionRefSHAForValidation into two injectable vars:
  verifyActionCommitExistsForValidation (commit existence) and
  resolveActionVersionToSHAForValidation (version-tag→SHA).

- Commit existence check: uses VerifyCommitExists; ErrVerificationSkipped
  is logged and skipped; other errors (e.g. 404) are hard failures.

- Version→SHA check: resolution failures (auth/network) are logged and
  skipped; only confirmed mismatches are reported as errors.

- Container digest: lookup failures are logged and skipped (non-fatal);
  digest mismatches are surfaced as a warning on stderr rather than a hard
  failure, because UpdateContainerPins cannot auto-repair a drifted mutable tag.

- Add two new tests: NonFatalErrors (all lookups fail → no error) and
  ContainerDigestMismatchIsWarning (digest drift → warning, no error).

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

@copilot

  • add code to resolve commit sha and validate it exists
  • add code to validate container sha marches container version

Implemented in the latest commit.

Action commit SHA existence — added VerifyCommitExists to the parser package. Unlike ResolveRefToSHAForHost, it always calls the GitHub commits API (repos/{owner}/{repo}/commits/{sha}) so that a non-existent SHA produces a definitive error. Auth/network failures return ErrVerificationSkipped and are logged/skipped (non-fatal) so a transient outage doesn't block the update.

Container SHA / version validation — live digest lookup is preserved and now surfaces a stderr warning when the stored digest doesn't match the current image tag. Lookup failures (registry unavailable, auth errors) are logged and skipped rather than hard-failing, since UpdateContainerPins cannot auto-repair a drifted mutable tag and hard-failing would make every subsequent update irrecoverable.

Two new tests cover both paths: TestValidateUpdateSHAEntries_NonFatalErrors (all lookups fail → validation still passes) and TestValidateUpdateSHAEntries_ContainerDigestMismatchIsWarning (tag drift → warning printed, no error returned).

@copilot run pr-finisher skill

PR-finisher pass complete. Summary below.

@pelikhan
pelikhan enabled auto-merge (squash) July 25, 2026 05:17
@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, address the unresolved blocking review feedback, and rerun checks once the branch is up to date. Priority review items:

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 17.6 AIC · ⌖ 8.18 AIC · ⊞ 7.1K ·
Comment /souschef to run again

…uard, container structural-only

Address github-actions[bot] review threads:

- Struct-based resolvers (Thread PRRT_kwDOPc1QR86TuFYr): replace package-level
  var seams with a `validationResolvers` struct. `validateUpdateSHAEntries`
  calls the internal `validateUpdateSHAEntriesWithResolvers` with defaults;
  tests pass their own resolver inline without touching globals — enabling
  t.Parallel() throughout.

- Remove ctx==nil guard (Thread PRRT_kwDOPc1QR86TuFZh): callers supply a
  valid context; the nil-substitution antipattern is removed.

- Container digest: structural checks only (Threads PRRT_kwDOPc1QR86TuFYx,
  PRRT_kwDOPc1QR86TuFZf): remove live registry lookup entirely. Format, key/
  image consistency, and pinned_image consistency are sufficient. Refreshing a
  mutable tag remains an explicit update operation.

- `resolveContainerDigestForValidation` var and `console` import removed.

- Tests updated: all five run in parallel, use inline resolver structs instead
  of global save/restore. `ContainerDigestMismatchIsWarning` replaced with
  `ContainerStructuralOnly` which asserts no network call is made at all.

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
auto-merge was automatically disabled July 25, 2026 06:29

Head branch was pushed to by a user without write access

Copilot AI commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please run the pr-finisher skill, address the unresolved blocking review feedback, and rerun checks once the branch is up to date. Priority review items:

All blocking review items addressed in e687d62:

  • Struct-based validationResolvers — package-level vars removed, tests run in parallel
  • ctx == nil guard removed
  • Container digest live lookup removed — structural checks only
  • VerifyCommitExists (real GitHub API call) used for commit existence
  • Non-fatal handling for auth/network errors in both action checks
  • Error messages clarified

Threads replied to and ready for human resolution.

Copilot AI requested a review from gh-aw-bot July 25, 2026 06:32
@github-actions

Copy link
Copy Markdown
Contributor

🤖 PR Triage

Field Value
Category feature
Risk medium
Score 53 / 100
Score breakdown impact 35 + urgency 10 + quality 8
Action fast_track

Rationale: Adds SHA integrity validation to gh aw update — a security/correctness improvement that prevents malformed lock entries from being silently accepted. High value, pure addition (no deletions). Awaiting resolution of CHANGES_REQUESTED before merge.

Next step: Address outstanding review requests, then fast-track for human approval.

Generated by 🔧 PR Triage Agent · sonnet46 · 44.5 AIC · ⌖ 5.13 AIC · ⊞ 5.7K ·

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants