Skip to content

Handle Dockerfile action manifest #225

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 3, 2025
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ Versioning].

## [Unreleased]

### Bugs

- Fix errors for actions with a `Dockerfile` manifest.

## [v0.5.0] - 2025-05-03

### Enhancements

- Correct typo in the `ghasum help verify` output.
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,15 @@ recommended to use commit SHAs instead to avoid failing verification by ghasum.

- Requires manual intervention when an Action is updated.
- The hashing algorithm used for checksums is not configurable.
- Checksums do not provide protection against [unpinnable actions].[^1]
- `ghasum` does not (yet, [#216]) handle Docker-based [unpinnable actions].
- Checksums do not provide protection against code-based [unpinnable actions].

[^1]: See [github/roadmap#592] for work on unpinnable actions by GitHub.
Some of these limitations may be addressed by Github's Immutable Actions
initiative, see [github/roadmap#592] for more information.

[unpinnable actions]: https://www.paloaltonetworks.com/blog/prisma-cloud/unpinnable-actions-github-security/
[#216]: https://github.com/chains-project/ghasum/issues/216
[github/roadmap#592]: https://github.com/github/roadmap/issues/592
[unpinnable actions]: https://www.paloaltonetworks.com/blog/prisma-cloud/unpinnable-actions-github-security/

## Background

Expand Down
3 changes: 1 addition & 2 deletions SPECIFICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,7 @@ version 1

## Definitions

- _action manifest_ is the file `action.yml` or `action.yaml` (mutually
exclusive).
- _action manifest_ is the file `action.yml`, `action.yaml`, or `Dockerfile`.
- _checksum file_ is the file `.github/workflows/gha.sum`.
- _workflows directory_ is the directory `.github/workflows`.

Expand Down
23 changes: 15 additions & 8 deletions internal/gha/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,21 @@ func workflowInRepo(repo fs.FS, path string) ([]byte, error) {

func manifestInRepo(repo fs.FS, dir string) ([]byte, error) {
path := filepath.Join(dir, "action.yml")
file, err := repo.Open(path)
if err != nil {
path = filepath.Join(dir, "action.yaml")
if file, err = repo.Open(path); err != nil {
return nil, fmt.Errorf("could not open manifest (action.yml or action.yaml) at %s: %v", dir, err)
}
if file, err := repo.Open(path); err == nil {
data, _ := io.ReadAll(file)
return data, nil
}

data, _ := io.ReadAll(file)
return data, nil
path = filepath.Join(dir, "action.yaml")
if file, err := repo.Open(path); err == nil {
data, _ := io.ReadAll(file)
return data, nil
}

path = filepath.Join(dir, "Dockerfile")
if _, err := repo.Open(path); err == nil {
return nil, ErrDockerfileManifest
}

return nil, ErrNoManifest
}
28 changes: 24 additions & 4 deletions internal/gha/actions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package gha

import (
"bytes"
"errors"
"testing"
"testing/quick"

Expand Down Expand Up @@ -702,7 +703,7 @@ func TestManifestInRepo(t *testing.T) {
type TestCase struct {
fs map[string]mockFsEntry
dir string
want []byte
want error
}

testCases := map[string]TestCase{
Expand All @@ -713,7 +714,7 @@ func TestManifestInRepo(t *testing.T) {
},
},
dir: "nested",
want: []byte(manifestWithStep),
want: ErrNoManifest,
},
".yaml manifest in different dir": {
fs: map[string]mockFsEntry{
Expand All @@ -722,17 +723,36 @@ func TestManifestInRepo(t *testing.T) {
},
},
dir: "nested",
want: []byte(manifestWithStep),
want: ErrNoManifest,
},
"Dockerfile manifest": {
fs: map[string]mockFsEntry{
"Dockerfile": {
Content: []byte(manifestDockerfile),
},
},
dir: "",
want: ErrDockerfileManifest,
},
}

for name, tt := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()

if _, err := mockRepo(tt.fs); err != nil {
repo, err := mockRepo(tt.fs)
if err != nil {
t.Fatalf("Could not initialize file system: %+v", err)
}

_, err = manifestInRepo(repo, tt.dir)
if err == nil {
t.Fatal("Unexpected success")
}

if got, want := err, tt.want; !errors.Is(got, want) {
t.Errorf("Unexpected error (got %q, want %q)", got, want)
}
})
}
})
Expand Down
3 changes: 3 additions & 0 deletions internal/gha/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@ var (
ErrInvalidUsesRepo = errors.New("invalid repository in uses")
ErrInvalidUsesPath = errors.New("invalid repository path in uses")
ErrLocalAction = errors.New("uses is a local action")

ErrDockerfileManifest = errors.New("found a Dockerfile manifest")
ErrNoManifest = errors.New("could not find a manifest")
)
5 changes: 4 additions & 1 deletion internal/gha/gha.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package gha

import (
"errors"
"fmt"
"io/fs"
"path/filepath"
Expand Down Expand Up @@ -147,7 +148,9 @@ func JobActions(repo fs.FS, path, name string) ([]GitHubAction, error) {
// specified directory in the given file system hierarchy.
func ManifestActions(repo fs.FS, path string) ([]GitHubAction, error) {
data, err := manifestInRepo(repo, path)
if err != nil {
if errors.Is(err, ErrDockerfileManifest) {
return nil, nil
} else if err != nil {
return nil, err
}

Expand Down
47 changes: 35 additions & 12 deletions internal/gha/gha_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -667,8 +667,8 @@ func TestManifestActions(t *testing.T) {
wantErr bool
}

testCases := []TestCase{
{
testCases := map[string]TestCase{
"root manifest without transitive actions": {
fs: map[string]mockFsEntry{
"action.yml": {
Content: []byte(manifestWithNoSteps),
Expand All @@ -677,7 +677,7 @@ func TestManifestActions(t *testing.T) {
path: "",
wantErr: false,
},
{
"root manifest with transitive actions": {
fs: map[string]mockFsEntry{
"action.yml": {
Content: []byte(manifestWithStep),
Expand All @@ -686,7 +686,7 @@ func TestManifestActions(t *testing.T) {
path: "",
wantErr: false,
},
{
"root manifest using nested actions": {
fs: map[string]mockFsEntry{
"action.yml": {
Content: []byte(manifestWithNestedActions),
Expand All @@ -695,7 +695,7 @@ func TestManifestActions(t *testing.T) {
path: "",
wantErr: false,
},
{
"nested .yml manifest": {
fs: map[string]mockFsEntry{
"nested": {
Dir: true,
Expand All @@ -709,7 +709,7 @@ func TestManifestActions(t *testing.T) {
path: "nested",
wantErr: false,
},
{
"root .yaml manifest": {
fs: map[string]mockFsEntry{
"action.yaml": {
Content: []byte(manifestWithStep),
Expand All @@ -718,7 +718,7 @@ func TestManifestActions(t *testing.T) {
path: "",
wantErr: false,
},
{
"nested .yaml manifest": {
fs: map[string]mockFsEntry{
"nested": {
Dir: true,
Expand All @@ -732,7 +732,30 @@ func TestManifestActions(t *testing.T) {
path: "nested",
wantErr: false,
},
{
"root Dockerfile manifest": {
fs: map[string]mockFsEntry{
"Dockerfile": {
Content: []byte(manifestDockerfile),
},
},
path: "",
wantErr: false,
},
"nested Dockerfile manifest": {
fs: map[string]mockFsEntry{
"nested": {
Dir: true,
Children: map[string]mockFsEntry{
"Dockerfile": {
Content: []byte(manifestDockerfile),
},
},
},
},
path: "nested",
wantErr: false,
},
"manifest with syntax error": {
fs: map[string]mockFsEntry{
"action.yml": {
Content: []byte(yamlWithSyntaxError),
Expand All @@ -741,7 +764,7 @@ func TestManifestActions(t *testing.T) {
path: "",
wantErr: true,
},
{
"manifest with invalid uses value": {
fs: map[string]mockFsEntry{
"action.yml": {
Content: []byte(manifestWithInvalidUses),
Expand All @@ -750,15 +773,15 @@ func TestManifestActions(t *testing.T) {
path: "",
wantErr: true,
},
{
"empty repo": {
fs: map[string]mockFsEntry{},
path: "",
wantErr: true,
},
}

for i, tt := range testCases {
t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) {
for name, tt := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()

repo, err := mockRepo(tt.fs)
Expand Down
3 changes: 3 additions & 0 deletions internal/gha/shared_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ runs:
runs:
steps:
- uses: this-is-not-an-action
`
manifestDockerfile = `FROM docker.io/alpine:3.21.3
ENTRYPOINT ["echo", "Hello world!"]
`

workflowWithNoJobs = `name: workflow with no jobs
Expand Down
Loading