Skip to content

Commit 3e8d3d7

Browse files
committed
Transfers: recognize redirected refs after partial resolution
1 parent f14d43f commit 3e8d3d7

2 files changed

Lines changed: 36 additions & 12 deletions

File tree

internal/pin/plan.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
parserlock "github.com/github/actions-lockfile/go/pkg/lockfile"
1010
"github.com/github/gh-actions-lock/internal/dep"
11+
"github.com/github/gh-actions-lock/internal/ghapi"
1112
"github.com/github/gh-actions-lock/internal/lockfile"
1213
"github.com/github/gh-actions-lock/internal/pinpool"
1314
"github.com/github/gh-actions-lock/internal/pipeline/checks"
@@ -356,20 +357,24 @@ func addTransferredRepositoryRewrites(deps []dep.Dependency, directTracker lockf
356357
// resolve. On a partial failure deps holds the refs that did resolve, so only
357358
// the genuine misses (attempted and not in deps) are marked Unresolved.
358359
func unresolvedEntries(wr checks.WorkflowReport, unrecordedRefs []parserlock.ActionRef, deps []dep.Dependency, resolveErr error) []Entry {
359-
resolved := make(map[string]bool, len(deps))
360+
resolved := make(map[ghapi.NWORef]bool, len(deps))
360361
for _, d := range deps {
361-
resolved[strings.ToLower(d.NWO+"@"+d.Ref)] = true
362+
owner, repo := d.OwnerRepo()
363+
resolved[ghapi.ForNWORef(owner, repo, d.Ref)] = true
364+
for _, ref := range d.OriginalRefs {
365+
resolved[ghapi.ForNWORef(ref.Owner, ref.Repo, ref.Ref)] = true
366+
}
362367
}
363-
attempted := make(map[string]bool, len(unrecordedRefs))
368+
attempted := make(map[ghapi.NWORef]bool, len(unrecordedRefs))
364369
for _, ref := range unrecordedRefs {
365-
attempted[strings.ToLower(ref.Owner+"/"+ref.Repo+"@"+ref.Ref)] = true
370+
attempted[ghapi.ForNWORef(ref.Owner, ref.Repo, ref.Ref)] = true
366371
}
367372
var out []Entry
368373
for _, f := range wr.Findings {
369374
if f.ActionRef == nil {
370375
continue
371376
}
372-
key := strings.ToLower(f.ActionRef.Owner + "/" + f.ActionRef.Repo + "@" + f.ActionRef.Ref)
377+
key := ghapi.ForNWORef(f.ActionRef.Owner, f.ActionRef.Repo, f.ActionRef.Ref)
373378
if !attempted[key] || resolved[key] {
374379
continue
375380
}

internal/pin/plan_test.go

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -150,14 +150,14 @@ func TestPlanWorkflow_PartialResolutionFailure(t *testing.T) {
150150

151151
goodSHA := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
152152

153-
// Both refs fold into one batched query: a0=good/action resolves,
153+
// Both refs fold into one batched query: a0=old/action redirects and resolves,
154154
// a1=bad/private is null (repo not found).
155155
reg.Register(
156-
httpmock.GraphQLForRepo("good", "action"),
156+
httpmock.GraphQLForRepo("old", "action"),
157157
httpmock.JSONResponse(map[string]any{
158158
"data": map[string]any{
159159
"a0": map[string]any{
160-
"nameWithOwner": "good/action",
160+
"nameWithOwner": "new/action",
161161
"object": map[string]any{
162162
"oid": goodSHA,
163163
"file": map[string]any{"object": map[string]any{"text": "name: Good\nruns:\n using: node20\n"}},
@@ -176,7 +176,7 @@ func TestPlanWorkflow_PartialResolutionFailure(t *testing.T) {
176176
Path: ".github/workflows/test.yml",
177177
Findings: []checks.Finding{
178178
{
179-
ActionRef: &parserlock.ActionRef{Owner: "good", Repo: "action", Ref: "v1"},
179+
ActionRef: &parserlock.ActionRef{Owner: "old", Repo: "action", Ref: "v1"},
180180
Category: "unpinned",
181181
Severity: checks.SeverityWarning,
182182
Confidence: checks.ConfidenceHigh,
@@ -189,7 +189,7 @@ func TestPlanWorkflow_PartialResolutionFailure(t *testing.T) {
189189
},
190190
},
191191
ActionRefs: []parserlock.ActionRef{
192-
{Owner: "good", Repo: "action", Ref: "v1"},
192+
{Owner: "old", Repo: "action", Ref: "v1"},
193193
{Owner: "bad", Repo: "private", Ref: "main"},
194194
},
195195
}
@@ -219,12 +219,31 @@ func TestPlanWorkflow_PartialResolutionFailure(t *testing.T) {
219219
assert.Equal(t, "main", unresolved[0].Ref)
220220
assert.Contains(t, unresolved[0].Reason, "not found")
221221

222-
// good/action must be pinned (not poisoned by the bad ref).
222+
// The redirected action must be pinned under its canonical NWO, not
223+
// misclassified as unresolved because the sibling failed.
223224
require.Len(t, pinned, 1, "expected exactly one pinned entry")
224-
assert.Equal(t, "good/action", pinned[0].NWO)
225+
assert.Equal(t, "new/action", pinned[0].NWO)
225226
assert.Equal(t, goodSHA, pinned[0].SHA)
226227
}
227228

229+
func TestUnresolvedEntriesPreservesRefCase(t *testing.T) {
230+
resolvedRef := parserlock.ActionRef{Owner: "old", Repo: "action", Ref: "Release"}
231+
failedRef := parserlock.ActionRef{Owner: "old", Repo: "action", Ref: "release"}
232+
wr := checks.WorkflowReport{Findings: []checks.Finding{
233+
{ActionRef: &resolvedRef, Category: checks.NotPinned},
234+
{ActionRef: &failedRef, Category: checks.NotPinned},
235+
}}
236+
237+
got := unresolvedEntries(wr, []parserlock.ActionRef{resolvedRef, failedRef}, []dep.Dependency{{
238+
NWO: "new/action",
239+
Ref: resolvedRef.Ref,
240+
OriginalRefs: []parserlock.ActionRef{resolvedRef},
241+
}}, assert.AnError)
242+
243+
require.Len(t, got, 1)
244+
assert.Equal(t, failedRef.Ref, got[0].Ref)
245+
}
246+
228247
// TestPlanWorkflow_AllResolutionsFail verifies that when ALL refs in a
229248
// workflow fail resolution, every finding is marked Unresolved and no
230249
// reachability is attempted.

0 commit comments

Comments
 (0)