Skip to content
Draft
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func main() {
| elm | elm.json, elm-package.json | |
| gem | Gemfile, gems.rb, *.gemspec | Gemfile.lock, gems.locked |
| git | .gitmodules | |
| github-actions | .github/workflows/*.yml | |
| github-actions | .github/workflows/*.yml | actions.lock |
| golang | go.mod, Godeps, glide.yaml, Gopkg.toml | Godeps.json, glide.lock, Gopkg.lock, vendor.json, go-resolved-dependencies.json, vendor/manifest |
| guix | manifest.scm | |
| hackage | *.cabal | stack.yaml.lock, cabal.config, cabal.project.freeze |
Expand Down
87 changes: 87 additions & 0 deletions internal/github_actions/actions_lock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package github_actions

import (
"sort"
"strings"

"github.com/git-pkgs/manifests/internal/core"
"gopkg.in/yaml.v3"
)

func init() {
core.Register("github-actions", core.Lockfile, &actionsLockParser{}, core.ExactMatch("actions.lock"))
}

// actionsLockParser parses the GitHub Actions dependency lockfile written by
// gh actions-lock to .github/workflows/actions.lock. The format is pre-1.0;
// this parser is deliberately lenient (unknown fields ignored, no version
// gate) so older git-pkgs binaries keep working across schema bumps.
//
// See https://github.com/github/actions-lockfile for the schema.
type actionsLockParser struct{}

type actionsLockFile struct {
Version string `yaml:"version"`
Workflows map[string][]string `yaml:"workflows"`
Dependencies map[string]actionsLockAction `yaml:"dependencies"`
}

type actionsLockAction struct {
Ref string `yaml:"ref"`
Commit string `yaml:"commit"`
}

func (p *actionsLockParser) Parse(filename string, content []byte) (*core.Result, error) {
var lock actionsLockFile
if err := yaml.Unmarshal(content, &lock); err != nil {
return nil, &core.ParseError{Filename: filename, Err: err}
}

// Pin keys that appear in a workflows: list are direct dependencies of
// the repo; the rest arrived transitively via an action's uses: list.
direct := make(map[string]bool)
for _, pins := range lock.Workflows {
for _, pin := range pins {
direct[pin] = true
}
}

// Sort keys so output order is stable across runs.
keys := make([]string, 0, len(lock.Dependencies))
for k := range lock.Dependencies {
keys = append(keys, k)
}
sort.Strings(keys)

deps := make([]core.Dependency, 0, len(keys))
for _, key := range keys {
action := lock.Dependencies[key]
name, keyRef := splitPin(key)
if name == "" {
continue
}
// Prefer the entry's ref: field (the resolved tag). Fall back to the
// ref segment of the pin key when it's absent.
version := action.Ref
if version == "" {
version = keyRef
}
deps = append(deps, core.Dependency{
Name: name,
Version: version,
Scope: core.Runtime,
Integrity: action.Commit,
Direct: direct[key],
})
}

return &core.Result{Dependencies: deps}, nil
}

// splitPin splits an OWNER/REPO@REF pin key into name and ref.
func splitPin(key string) (name, ref string) {
if idx := strings.Index(key, "@"); idx > 0 {
return key[:idx], key[idx+1:]
}
return key, ""
}
72 changes: 72 additions & 0 deletions internal/github_actions/actions_lock_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package github_actions

import (
"os"
"testing"

"github.com/git-pkgs/manifests/internal/core"
)

func TestActionsLock(t *testing.T) {
content, err := os.ReadFile("../../testdata/github-actions/actions.lock")
if err != nil {
t.Fatalf("failed to read fixture: %v", err)
}

parser := &actionsLockParser{}
res, err := parser.Parse("actions.lock", content)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}

if len(res.Dependencies) != 5 {
t.Fatalf("expected 5 dependencies, got %d: %+v", len(res.Dependencies), res.Dependencies)
}

byKey := make(map[string]core.Dependency)
for _, d := range res.Dependencies {
byKey[d.Name+"@"+d.Version] = d
}

checkout5, ok := byKey["actions/checkout@v5.0.1"]
if !ok {
t.Fatalf("expected actions/checkout@v5.0.1, got %+v", res.Dependencies)
}
if checkout5.Integrity != "sha1-93cb6efe18208431cddfb8368fd83d5badbf9bfd" {
t.Errorf("checkout@v5.0.1 integrity = %q", checkout5.Integrity)
}
if !checkout5.Direct {
t.Error("checkout@v5.0.1 should be direct (listed in workflows)")
}
if checkout5.Scope != core.Runtime {
t.Errorf("checkout@v5.0.1 scope = %q, want runtime", checkout5.Scope)
}

if _, ok := byKey["actions/checkout@v6.0.2"]; !ok {
t.Error("expected second actions/checkout entry at v6.0.2")
}

// actions/attest is only reached via cli/gh-extension-precompile's uses:
// list, so it is transitive. Its pin key uses a SHA but ref: carries the
// tag, which is what should surface as the version.
attest, ok := byKey["actions/attest@v1.4.1"]
if !ok {
t.Fatalf("expected actions/attest@v1.4.1, got %+v", res.Dependencies)
}
if attest.Direct {
t.Error("actions/attest should not be direct")
}
}

func TestActionsLockIdentify(t *testing.T) {
parser, eco, kind := core.IdentifyParser(".github/workflows/actions.lock")
if parser == nil {
t.Fatal("expected .github/workflows/actions.lock to be identified")
}
if eco != "github-actions" {
t.Errorf("ecosystem = %q, want github-actions", eco)
}
if kind != core.Lockfile {
t.Errorf("kind = %q, want lockfile", kind)
}
}
4 changes: 4 additions & 0 deletions manifests_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ func TestIdentifyFiles(t *testing.T) {
{"Dockerfile", "docker", Manifest, true},
{"docker-compose.yml", "docker", Manifest, true},

// github-actions
{".github/workflows/ci.yml", "github-actions", Manifest, true},
{".github/workflows/actions.lock", "github-actions", Lockfile, true},

// unknown
{"unknown.txt", "", "", false},
{"random.file", "", "", false},
Expand Down
39 changes: 39 additions & 0 deletions testdata/github-actions/actions.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# This file is machine-generated by `gh actions-lock`.
# Do not edit by hand; run `gh actions-lock` to update.
# Docs: https://gh.io/actions-lockfile
version: 'v0.0.2'
workflows:
'.github/workflows/release.yml':
- 'actions/checkout@v6.0.2'
- 'cli/gh-extension-precompile@v2.1.0'
'.github/workflows/test.yml':
- 'actions/checkout@v5.0.1'
- 'actions/setup-go@v6.4.0'
dependencies:
'actions/attest@67422f5511b7ff725f4dbd6fb9bd2cd925c65a8d':
ref: 'v1.4.1'
commit: 'sha1-67422f5511b7ff725f4dbd6fb9bd2cd925c65a8d'
owner_id: 44036562
repo_id: 760701061
'actions/checkout@v5.0.1':
ref: 'v5.0.1'
commit: 'sha1-93cb6efe18208431cddfb8368fd83d5badbf9bfd'
owner_id: 44036562
repo_id: 197814629
'actions/checkout@v6.0.2':
ref: 'v6.0.2'
commit: 'sha1-de0fac2e4500dabe0009e67214ff5f5447ce83dd'
owner_id: 44036562
repo_id: 197814629
'actions/setup-go@v6.4.0':
ref: 'v6.4.0'
commit: 'sha1-4a3601121dd01d1626a1e23e37211e3254c1c06c'
owner_id: 44036562
repo_id: 192624594
'cli/gh-extension-precompile@v2.1.0':
ref: 'v2.1.0'
commit: 'sha1-9e2237c30f869ad3bcaed6a4be2cd43564dd421b'
owner_id: 59704711
repo_id: 421583656
uses:
- 'actions/attest@67422f5511b7ff725f4dbd6fb9bd2cd925c65a8d'