Skip to content

Commit 8a644d1

Browse files
committed
Pipeline: retain symbolic roots for SHA uses
1 parent bbab407 commit 8a644d1

3 files changed

Lines changed: 72 additions & 4 deletions

File tree

cmd/gh-actions-lock/command_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,64 @@ jobs:
610610
assert.Equal(t, []string{"example/action@main"}, transitiveDep.RequiredBy)
611611
}
612612

613+
func TestCheckCommand_BareSHAKeepsSymbolicLockRootDirect(t *testing.T) {
614+
reg := &httpmock.Registry{}
615+
defer reg.Verify(t)
616+
617+
sha := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
618+
reg.Register(
619+
httpmock.GraphQLForRepo("example", "action"),
620+
httpmock.JSONResponse(map[string]any{
621+
"data": map[string]any{
622+
"a0": testRepoResponse("example/action", sha, nodeActionYAML),
623+
"a1": testRepoResponse("example/action", sha, nodeActionYAML),
624+
},
625+
}),
626+
)
627+
reg.Register(
628+
httpmock.REST("GET", "repos/example/action/branches"),
629+
httpmock.JSONResponse([]any{}),
630+
)
631+
reg.Register(
632+
httpmock.REST("GET", "repos/example/action/branches"),
633+
httpmock.JSONResponse([]any{}),
634+
)
635+
reg.Register(
636+
httpmock.REST("GET", "repos/example/action/tags"),
637+
httpmock.JSONResponse([]any{
638+
map[string]any{"name": "v1", "commit": map[string]any{"sha": sha}},
639+
}),
640+
)
641+
642+
workflowPath := writeTempWorkflow(t, `
643+
name: ci
644+
on: push
645+
jobs:
646+
test:
647+
runs-on: ubuntu-latest
648+
steps:
649+
- uses: example/action@`+sha+`
650+
`,
651+
"example/action@v1=sha1-"+sha,
652+
)
653+
654+
stdout, _, err := runCommandWithHTTP(t, reg,
655+
"--no-narrow", "--json=workflows", workflowPath,
656+
)
657+
require.NoError(t, err)
658+
659+
var payload struct {
660+
Workflows []struct {
661+
Dependencies []format.Dependency `json:"dependencies"`
662+
} `json:"workflows"`
663+
}
664+
require.NoError(t, json.Unmarshal([]byte(stdout), &payload))
665+
require.Len(t, payload.Workflows, 1)
666+
require.Len(t, payload.Workflows[0].Dependencies, 1)
667+
assert.True(t, payload.Workflows[0].Dependencies[0].Direct)
668+
assert.Contains(t, readTempLockfilePins(t), " - 'example/action@v1'")
669+
}
670+
613671
func TestCheckCommand_JSONDefaultFieldsExcludesDependencies(t *testing.T) {
614672
reg := &httpmock.Registry{}
615673
defer reg.Verify(t)

internal/pipeline/diagnose.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"strings"
1010

11+
parserlock "github.com/github/actions-lockfile/go/pkg/lockfile"
1112
"github.com/github/gh-actions-lock/internal/dep"
1213
"github.com/github/gh-actions-lock/internal/ghapi"
1314
"github.com/github/gh-actions-lock/internal/lockfile"
@@ -61,6 +62,9 @@ func diagnoseOneParsed(ctx context.Context, pw checks.ParsedWorkflow, r *resolve
6162
directRefs := make(map[ghapi.NWORef]bool, len(pw.Refs))
6263
for _, ref := range pw.Refs {
6364
directRefs[ghapi.ForNWORef(ref.Owner, ref.Repo, ref.Ref)] = true
65+
if parserlock.IsFullSha(ref.Ref) {
66+
directRefs[ghapi.ForNWORef(ref.Owner, ref.Repo, strings.ToLower(ref.Ref))] = true
67+
}
6468
}
6569

6670
// Resolve live state: hits cache when ParseAll's caller pre-warmed the
@@ -117,11 +121,10 @@ func diagnoseOneParsed(ctx context.Context, pw checks.ParsedWorkflow, r *resolve
117121
}
118122

119123
for _, dep := range pw.RecordedDeps {
120-
owner, repo := dep.OwnerRepo()
121124
wr.Inventory = append(wr.Inventory, checks.InventoryEntry{
122125
Dep: dep,
123126
File: pw.Path,
124-
Direct: directRefs[ghapi.ForNWORef(owner, repo, dep.Ref)],
127+
Direct: isDirectDependency(dep, directRefs),
125128
})
126129
}
127130
parentMap := mergeParentMaps(pw.RecordedParents, resolvedParents)

internal/pipeline/finding_enrich.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package pipeline
22

33
import (
4+
"strings"
5+
46
"github.com/github/gh-actions-lock/internal/dep"
57
"github.com/github/gh-actions-lock/internal/ghapi"
68
"github.com/github/gh-actions-lock/internal/pipeline/checks"
@@ -18,8 +20,7 @@ func attachParent(f *checks.Finding, depByKey map[string]dep.Dependency, directR
1820
if f.Dependency == nil {
1921
return
2022
}
21-
owner, repo := f.Dependency.OwnerRepo()
22-
if directRefs[ghapi.ForNWORef(owner, repo, f.Dependency.Ref)] {
23+
if isDirectDependency(*f.Dependency, directRefs) {
2324
return
2425
}
2526
// Prefer the dep snapshot from the workflow's RecordedDeps (it has the
@@ -34,6 +35,12 @@ func attachParent(f *checks.Finding, depByKey map[string]dep.Dependency, directR
3435
}
3536
}
3637

38+
func isDirectDependency(d dep.Dependency, directRefs map[ghapi.NWORef]bool) bool {
39+
owner, repo := d.OwnerRepo()
40+
return directRefs[ghapi.ForNWORef(owner, repo, d.Ref)] ||
41+
d.SHA != "" && directRefs[ghapi.ForNWORef(owner, repo, strings.ToLower(d.SHA))]
42+
}
43+
3744
// isTransitivePin reports whether the finding refers to a dep reached via
3845
// composite expansion (i.e. has parents in the parent map).
3946
func isTransitivePin(f checks.Finding, depByKey map[string]dep.Dependency, parentMap map[string][]string) bool {

0 commit comments

Comments
 (0)