Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@ on:
push:
branches:
- main
permissions:
id-token: write # for setup-goproxy
contents: read

jobs:
go-test:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
env:
GOPROXY: https://goproxy.githubapp.com/mod,https://proxy.golang.org,direct
GOPRIVATE: ''
GONOPROXY: ''
GONOSUMDB: github.com/github/*
steps:
- name: OIDC Setup for goproxy
uses: github/setup-goproxy@5e60e1074d42316dfe2949ebf9a92bf77b24645b # v1.1.0
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ require (
gopkg.in/yaml.v3 v3.0.1
)

require github.com/github/actions-lockfile/go v0.0.0-20260607192413-049be4ee6d5a
require github.com/github/actions-lockfile/go v0.0.1

require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/github/actions-lockfile/go v0.0.0-20260607192413-049be4ee6d5a h1:VLnFgRUbu5gvGp7GAlZeuhxgABlY5Nf7+0wLSxKV+P0=
github.com/github/actions-lockfile/go v0.0.0-20260607192413-049be4ee6d5a/go.mod h1:kp8pDNXwrr3fC+6Mgmh/ZODa6AsIEC+bmf1CLQ/7DEs=
github.com/github/actions-lockfile/go v0.0.1 h1:Xi9KmIq1DUnfZOlp8SJyEU2kgk8Y7g8ghtNg1xFiv6s=
github.com/github/actions-lockfile/go v0.0.1/go.mod h1:kp8pDNXwrr3fC+6Mgmh/ZODa6AsIEC+bmf1CLQ/7DEs=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/henvic/httpretty v0.0.6 h1:JdzGzKZBajBfnvlMALXXMVQWxWMF/ofTy8C3/OSUTxs=
Expand Down
34 changes: 19 additions & 15 deletions internal/lockfile/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,12 @@ func LoadState(repoRoot string, meta MetadataResolver) (*State, error) {
// pins this binary cannot interpret. Other parse failures
// (corrupt YAML, unknown fields) are treated as empty so a
// recoverable lockfile can be rewritten.
//
// The standalone parser emits a tool-agnostic hint ("upgrade the
// tool that reads this lockfile"), so name the concrete command
// for this binary here.
if errors.Is(err, parserlock.ErrFutureVersion) {
return nil, fmt.Errorf("reading %s: %w", parserlock.Path, err)
return nil, fmt.Errorf("reading %s: %w; run `gh extension upgrade gh-actions-pin` to update", parserlock.Path, err)
}
// Corrupt or unrecognized lockfile — treat as empty and overwrite.
file = parserlock.File{Version: parserlock.Version}
Expand All @@ -76,8 +80,8 @@ func LoadState(repoRoot string, meta MetadataResolver) (*State, error) {
if file.Version == "" {
file.Version = parserlock.Version
}
if file.Actions == nil {
file.Actions = map[string]parserlock.Action{}
if file.Dependencies == nil {
file.Dependencies = map[string]parserlock.Action{}
}
if file.Workflows == nil {
file.Workflows = map[string][]string{}
Expand All @@ -91,22 +95,22 @@ func LoadState(repoRoot string, meta MetadataResolver) (*State, error) {
}
// Normalize on-disk entries to the canonical (lowercased) pin form so any
// legacy mixed-case keys are rewritten on the next Save.
normalizedActions := make(map[string]parserlock.Action, len(file.Actions))
for pinKey, action := range file.Actions {
normalizedDependencies := make(map[string]parserlock.Action, len(file.Dependencies))
for pinKey, action := range file.Dependencies {
pin, ok := parserlock.ParsePin(pinKey)
Comment on lines 96 to 100
if !ok {
normalizedActions[pinKey] = action
normalizedDependencies[pinKey] = action
continue
}
canon := pin.String()
normalizedActions[canon] = action
normalizedDependencies[canon] = action
if action.OwnerID != 0 && action.RepoID != 0 {
// idCache is keyed by lowercase owner/repo to match lookupIDs and the
// canonical pin reader; mixed-case keys here would silently miss.
s.idCache[strings.ToLower(pin.Owner+"/"+pin.Repo)] = [2]int64{action.OwnerID, action.RepoID}
}
}
s.file.Actions = normalizedActions
s.file.Dependencies = normalizedDependencies
for wfKey, deps := range s.file.Workflows {
changed := false
normalized := make([]string, len(deps))
Expand Down Expand Up @@ -175,8 +179,8 @@ func (s *State) Get(workflowKey string) ([]dep.Dependency, error) {
func (s *State) AllDeps() []dep.Dependency {
s.mu.Lock()
defer s.mu.Unlock()
out := make([]dep.Dependency, 0, len(s.file.Actions))
for raw, action := range s.file.Actions {
out := make([]dep.Dependency, 0, len(s.file.Dependencies))
for raw, action := range s.file.Dependencies {
pin, ok := parserlock.ParsePin(raw)
if !ok {
continue
Expand Down Expand Up @@ -309,7 +313,7 @@ func (s *State) Set(ctx context.Context, workflowKey string, deps []dep.Dependen
}
sort.Strings(uses)
}
s.file.Actions[pinKey] = parserlock.Action{
s.file.Dependencies[pinKey] = parserlock.Action{
Tag: d.Tag,
Branch: d.Branch,
Commit: pin.Algo + "-" + pin.Hex,
Expand Down Expand Up @@ -339,7 +343,7 @@ func (s *State) Save() error {
return
}
used[key] = true
if a, ok := s.file.Actions[key]; ok {
if a, ok := s.file.Dependencies[key]; ok {
for _, child := range a.Uses {
walk(child)
}
Expand All @@ -350,15 +354,15 @@ func (s *State) Save() error {
walk(dep)
}
}
for key := range s.file.Actions {
for key := range s.file.Dependencies {
if !used[key] {
delete(s.file.Actions, key)
delete(s.file.Dependencies, key)
}
}

full := filepath.Join(s.repoRoot, parserlock.Path)

if len(s.file.Actions) == 0 && len(s.file.Workflows) == 0 {
if len(s.file.Dependencies) == 0 && len(s.file.Workflows) == 0 {
if err := os.Remove(full); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
Expand Down
12 changes: 6 additions & 6 deletions internal/lockfile/state_marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
// the file round-trips identically regardless of YAML scalar-resolution
// quirks: pin keys carry colons, tags can look like floats ("1.0"), refs
// can collide with YAML 1.1 booleans ("y", "no", "on", "off"). Schema
// field names (version, actions, workflows, tag, branch, …) stay
// field names (version, dependencies, workflows, tag, branch, …) stay
// unquoted because they're hardcoded and trivially safe.
func marshalDeterministic(file parserlock.File) ([]byte, error) {
root := &yaml.Node{Kind: yaml.MappingNode}
Expand Down Expand Up @@ -47,15 +47,15 @@ func marshalDeterministic(file parserlock.File) ([]byte, error) {
)
}

if len(file.Actions) > 0 {
keys := make([]string, 0, len(file.Actions))
for k := range file.Actions {
if len(file.Dependencies) > 0 {
keys := make([]string, 0, len(file.Dependencies))
for k := range file.Dependencies {
Comment on lines +50 to +52
keys = append(keys, k)
}
sort.Strings(keys)
depsNode := &yaml.Node{Kind: yaml.MappingNode}
for _, k := range keys {
a := file.Actions[k]
a := file.Dependencies[k]
entry := &yaml.Node{Kind: yaml.MappingNode}
if a.Tag != "" {
addQuotedField(entry, "tag", a.Tag)
Expand Down Expand Up @@ -95,7 +95,7 @@ func marshalDeterministic(file parserlock.File) ([]byte, error) {
return yaml.Marshal(doc)
}

// plainScalar is for hardcoded schema field names (version, actions, …).
// plainScalar is for hardcoded schema field names (version, dependencies, …).
func plainScalar(name string) *yaml.Node {
return &yaml.Node{Kind: yaml.ScalarNode, Value: name}
}
Expand Down
30 changes: 15 additions & 15 deletions internal/lockfile/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@ func TestState_PersistsTagAndBranch(t *testing.T) {
t.Fatalf("reopening store: %v", err)
}
checkoutKey := "actions/checkout@v4.2.1:sha1-abc123abc123abc123abc123abc123abc123abc1"
a, ok := store2.file.Actions[checkoutKey]
a, ok := store2.file.Dependencies[checkoutKey]
if !ok {
t.Fatalf("expected %s in reloaded lockfile, keys=%v", checkoutKey, actionKeys(store2.file.Actions))
t.Fatalf("expected %s in reloaded lockfile, keys=%v", checkoutKey, actionKeys(store2.file.Dependencies))
}
if a.Tag != "v4.2.1" {
t.Errorf("expected Tag=v4.2.1, got %q", a.Tag)
Expand All @@ -103,9 +103,9 @@ func TestState_PersistsTagAndBranch(t *testing.T) {
t.Errorf("expected Branch=main, got %q", a.Branch)
}
branchOnlyKey := "internal/branch-only@main:sha1-def456def456def456def456def456def456def4"
b, ok := store2.file.Actions[branchOnlyKey]
b, ok := store2.file.Dependencies[branchOnlyKey]
if !ok {
t.Fatalf("expected %s, keys=%v", branchOnlyKey, actionKeys(store2.file.Actions))
t.Fatalf("expected %s, keys=%v", branchOnlyKey, actionKeys(store2.file.Dependencies))
}
if b.Tag != "" {
t.Errorf("expected empty Tag, got %q", b.Tag)
Expand Down Expand Up @@ -152,7 +152,7 @@ func TestState_SetRejectsEmptyBranch(t *testing.T) {
// TestState_DiamondTransitiveDepEmittedCorrectly verifies that when two direct
// actions share a transitive dependency (diamond pattern: A→C, B→C), the
// lockfile correctly records `uses: [C]` on both A and B, and the shared dep
// C appears in the actions section exactly once.
// C appears in the dependencies section exactly once.
func TestState_DiamondTransitiveDepEmittedCorrectly(t *testing.T) {
dir := t.TempDir()
if err := os.MkdirAll(filepath.Join(dir, ".github", "workflows"), 0o755); err != nil {
Expand Down Expand Up @@ -202,23 +202,23 @@ func TestState_DiamondTransitiveDepEmittedCorrectly(t *testing.T) {
if err != nil {
t.Fatalf("reopening store: %v", err)
}
aAction, ok := store2.file.Actions[aPin]
aAction, ok := store2.file.Dependencies[aPin]
if !ok {
t.Fatalf("expected %s in lockfile, keys=%v", aPin, actionKeys(store2.file.Actions))
t.Fatalf("expected %s in lockfile, keys=%v", aPin, actionKeys(store2.file.Dependencies))
}
if len(aAction.Uses) != 1 || aAction.Uses[0] != sharedPin {
t.Errorf("expected A.Uses = [%s], got %v", sharedPin, aAction.Uses)
}
bAction, ok := store2.file.Actions[bPin]
bAction, ok := store2.file.Dependencies[bPin]
if !ok {
t.Fatalf("expected %s in lockfile, keys=%v", bPin, actionKeys(store2.file.Actions))
t.Fatalf("expected %s in lockfile, keys=%v", bPin, actionKeys(store2.file.Dependencies))
}
if len(bAction.Uses) != 1 || bAction.Uses[0] != sharedPin {
t.Errorf("expected B.Uses = [%s], got %v", sharedPin, bAction.Uses)
}
// Shared dep exists exactly once.
if _, ok := store2.file.Actions[sharedPin]; !ok {
t.Errorf("expected shared dep %s in actions, keys=%v", sharedPin, actionKeys(store2.file.Actions))
if _, ok := store2.file.Dependencies[sharedPin]; !ok {
t.Errorf("expected shared dep %s in dependencies, keys=%v", sharedPin, actionKeys(store2.file.Dependencies))
}
// Workflows section only lists direct refs.
wfDeps := store2.file.Workflows[".github/workflows/ci.yml"]
Expand Down Expand Up @@ -277,11 +277,11 @@ func TestState_SaveGCHandlesCyclicUses(t *testing.T) {
aPin := "owner/a@v1:sha1-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
bPin := "owner/b@v1:sha1-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"

if _, ok := store2.file.Actions[aPin]; !ok {
t.Errorf("expected %s to survive GC, keys=%v", aPin, actionKeys(store2.file.Actions))
if _, ok := store2.file.Dependencies[aPin]; !ok {
t.Errorf("expected %s to survive GC, keys=%v", aPin, actionKeys(store2.file.Dependencies))
}
if _, ok := store2.file.Actions[bPin]; !ok {
t.Errorf("expected %s to survive GC (reachable via cyclic uses:), keys=%v", bPin, actionKeys(store2.file.Actions))
if _, ok := store2.file.Dependencies[bPin]; !ok {
t.Errorf("expected %s to survive GC (reachable via cyclic uses:), keys=%v", bPin, actionKeys(store2.file.Dependencies))
}
}

Expand Down
Loading