Skip to content

Commit 07a1377

Browse files
committed
Pinning: guard SHA metadata repairs
1 parent 478cffd commit 07a1377

2 files changed

Lines changed: 96 additions & 6 deletions

File tree

internal/pin/plan.go

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,6 @@ func planWorkflow(ctx context.Context, wr checks.WorkflowReport, opts PlanOption
130130
if rewriteRefs == nil {
131131
rewriteRefs = wr.ActionRefs
132132
}
133-
rewriteRefKeys := actionRefKeys(rewriteRefs)
134133
selfActionRefKeys := actionRefKeys(wr.SelfActionRefs)
135134
if opts.PartialScan && !opts.NoNarrow && opts.Tagger != nil {
136135
// ponytail: refuse any local-action repair in a partial scan; track
@@ -150,7 +149,7 @@ func planWorkflow(ctx context.Context, wr checks.WorkflowReport, opts PlanOption
150149

151150
if !wr.NeedsAttention() && !repinMoved {
152151
entries = verifiedEntries(inventory, wr.Path)
153-
rw := narrowVerifiedEntries(ctx, entries, opts, rewriteRefKeys)
152+
rw := narrowVerifiedEntries(ctx, entries, opts, rewriteRefs)
154153
wplans = append(wplans, WorkflowPlan{Path: wr.Path, Rewrites: rw, SelfActionFiles: wr.SelfActionFiles})
155154
return planResult{entries: entries, wplans: wplans}, nil
156155
}
@@ -184,7 +183,7 @@ func planWorkflow(ctx context.Context, wr checks.WorkflowReport, opts PlanOption
184183
}
185184

186185
if len(unrecordedRefs) == 0 {
187-
rw := narrowVerifiedEntries(ctx, entries, opts, rewriteRefKeys)
186+
rw := narrowVerifiedEntries(ctx, entries, opts, rewriteRefs)
188187
wplans = append(wplans, WorkflowPlan{Path: wr.Path, Rewrites: rw, SelfActionFiles: wr.SelfActionFiles})
189188
return planResult{entries: entries, wplans: wplans}, nil
190189
}
@@ -274,7 +273,7 @@ func planWorkflow(ctx context.Context, wr checks.WorkflowReport, opts PlanOption
274273

275274
// Record workflow plan if there are rewrites.
276275
// Also narrow any verified (already-recorded) entries that have imprecise refs.
277-
if verifiedRW := narrowVerifiedEntries(ctx, entries, opts, rewriteRefKeys); len(verifiedRW) > 0 {
276+
if verifiedRW := narrowVerifiedEntries(ctx, entries, opts, rewriteRefs); len(verifiedRW) > 0 {
278277
for k, v := range verifiedRW {
279278
rewrites[k] = v
280279
}
@@ -643,10 +642,11 @@ func verifiedEntries(inventory []checks.InventoryEntry, path string) []Entry {
643642
// narrowVerifiedEntries upgrades already-recorded direct deps to full semver
644643
// tags when possible, returning the workflow-YAML rewrites. Skipped for
645644
// --no-narrow, transitive deps, and refs the user kept imprecise (sticky v4).
646-
func narrowVerifiedEntries(ctx context.Context, entries []Entry, opts PlanOptions, rewriteRefKeys map[string]bool) map[string]string {
645+
func narrowVerifiedEntries(ctx context.Context, entries []Entry, opts PlanOptions, rewriteRefs []parserlock.ActionRef) map[string]string {
647646
if opts.NoNarrow || opts.Tagger == nil {
648647
return nil
649648
}
649+
rewriteRefKeys := actionRefKeys(rewriteRefs)
650650
rewrites := make(map[string]string)
651651
for i := range entries {
652652
e := &entries[i]
@@ -664,8 +664,15 @@ func narrowVerifiedEntries(ctx context.Context, entries []Entry, opts PlanOption
664664
if newRef == "" {
665665
continue
666666
}
667+
if parserlock.IsFullSha(newRef) || hasConflictingLockTarget(opts.Store, e.NWO, newRef, e.SHA) {
668+
continue
669+
}
667670
oldRef := e.Ref
668-
rewrites[e.NWO+"@"+oldRef] = e.NWO + "@" + newRef
671+
for _, ref := range rewriteRefs {
672+
if strings.EqualFold(ref.Owner+"/"+ref.Repo, e.NWO) && ref.Ref == oldRef {
673+
rewrites[ref.FullName()+"@"+oldRef] = ref.FullName() + "@" + newRef
674+
}
675+
}
669676
e.Ref = newRef
670677
e.AutoFixedRef = oldRef
671678
continue
@@ -716,6 +723,18 @@ func narrowVerifiedEntries(ctx context.Context, entries []Entry, opts PlanOption
716723
return rewrites
717724
}
718725

726+
func hasConflictingLockTarget(store *lockfile.State, nwo, ref, sha string) bool {
727+
if store == nil {
728+
return false
729+
}
730+
action, ok := store.File().Dependencies[nwo+"@"+ref]
731+
if !ok {
732+
return false
733+
}
734+
_, targetSHA, ok := strings.Cut(action.Commit, "-")
735+
return ok && !strings.EqualFold(targetSHA, sha)
736+
}
737+
719738
func actionRefKeys(refs []parserlock.ActionRef) map[string]bool {
720739
keys := make(map[string]bool, len(refs))
721740
for _, ref := range refs {

internal/pin/plan_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"testing"
66

77
"github.com/github/gh-actions-lock/internal/dep"
8+
"github.com/github/gh-actions-lock/internal/lockfile"
89
"github.com/github/gh-actions-lock/internal/pipeline/checks"
910

1011
parserlock "github.com/github/actions-lockfile/go/pkg/lockfile"
@@ -324,6 +325,76 @@ func TestNarrowVerifiedEntries_StickyPrecision(t *testing.T) {
324325
)
325326
})
326327

328+
t.Run("SHA metadata does not rewrite to itself", func(t *testing.T) {
329+
tagger, _ := newTagger(t)
330+
report := fastPathReport(sha)
331+
report.Inventory[0].Dep.Branch = sha
332+
report.ActionRefs = []parserlock.ActionRef{{
333+
Owner: "actions",
334+
Repo: "checkout",
335+
Ref: sha,
336+
}}
337+
338+
result, err := planWorkflow(context.Background(), report, PlanOptions{Tagger: tagger}, func(string) {})
339+
require.NoError(t, err)
340+
341+
require.Len(t, result.entries, 1)
342+
assert.Equal(t, sha, result.entries[0].Ref)
343+
assert.Empty(t, result.entries[0].AutoFixedRef)
344+
assert.Empty(t, result.wplans[0].Rewrites)
345+
})
346+
347+
t.Run("repair preserves source NWO spelling", func(t *testing.T) {
348+
tagger, _ := newTagger(t)
349+
report := fastPathReport(sha)
350+
report.Inventory[0].Dep.Tag = "v4.2.1"
351+
report.ActionRefs = []parserlock.ActionRef{{
352+
Owner: "Actions",
353+
Repo: "Checkout",
354+
Ref: sha,
355+
}}
356+
357+
result, err := planWorkflow(context.Background(), report, PlanOptions{Tagger: tagger}, func(string) {})
358+
require.NoError(t, err)
359+
360+
require.Len(t, result.entries, 1)
361+
assert.Equal(t, "v4.2.1", result.entries[0].Ref)
362+
assert.Equal(t,
363+
map[string]string{"Actions/Checkout@" + sha: "Actions/Checkout@v4.2.1"},
364+
result.wplans[0].Rewrites,
365+
)
366+
})
367+
368+
t.Run("repair declines conflicting symbolic target", func(t *testing.T) {
369+
tagger, _ := newTagger(t)
370+
store, err := lockfile.LoadState(t.TempDir(), fakeMeta{})
371+
require.NoError(t, err)
372+
target := dep.Dependency{
373+
NWO: "actions/checkout",
374+
Ref: "v4.2.1",
375+
SHA: "def4560000000000000000000000000000000000",
376+
HashAlgo: "sha1",
377+
}
378+
require.NoError(t, store.Set(context.Background(), "other.yml",
379+
[]dep.Dependency{target}, nil, map[string]bool{target.Key(): true}))
380+
381+
report := fastPathReport(sha)
382+
report.Inventory[0].Dep.Tag = target.Ref
383+
report.ActionRefs = []parserlock.ActionRef{{
384+
Owner: "actions",
385+
Repo: "checkout",
386+
Ref: sha,
387+
}}
388+
389+
result, err := planWorkflow(context.Background(), report, PlanOptions{Tagger: tagger, Store: store}, func(string) {})
390+
require.NoError(t, err)
391+
392+
require.Len(t, result.entries, 1)
393+
assert.Equal(t, sha, result.entries[0].Ref)
394+
assert.Empty(t, result.entries[0].AutoFixedRef)
395+
assert.Empty(t, result.wplans[0].Rewrites)
396+
})
397+
327398
t.Run("branch ref main is NOT narrowed", func(t *testing.T) {
328399
// main is not version-shaped, so narrowing must not touch it.
329400
// Non-version refs are intentional choices (e.g. vercel/next.js@canary).

0 commit comments

Comments
 (0)