Skip to content

Commit 259bddb

Browse files
Handle Dockerfile action manifest
Avoid erroring when an action used by a project has a `Dockerfile` manifest. Also update related documentation.
1 parent 69dc672 commit 259bddb

File tree

9 files changed

+97
-30
lines changed

9 files changed

+97
-30
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ Versioning].
1212

1313
## [Unreleased]
1414

15+
### Bugs
16+
17+
- Fix errors for actions with a `Dockerfile` manifest.
18+
19+
## [v0.5.0] - 2025-05-03
20+
1521
### Enhancements
1622

1723
- Correct typo in the `ghasum help verify` output.

README.md

+6-3
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,15 @@ recommended to use commit SHAs instead to avoid failing verification by ghasum.
5959
6060
- Requires manual intervention when an Action is updated.
6161
- The hashing algorithm used for checksums is not configurable.
62-
- Checksums do not provide protection against [unpinnable actions].[^1]
62+
- `ghasum` does not (yet, [#216]) handle Docker-based [unpinnable actions].
63+
- Checksums do not provide protection against code-based [unpinnable actions].
6364

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

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

6972
## Background
7073

SPECIFICATION.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,7 @@ version 1
203203

204204
## Definitions
205205

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

internal/gha/actions.go

+15-8
Original file line numberDiff line numberDiff line change
@@ -142,14 +142,21 @@ func workflowInRepo(repo fs.FS, path string) ([]byte, error) {
142142

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

153-
data, _ := io.ReadAll(file)
154-
return data, nil
150+
path = filepath.Join(dir, "action.yaml")
151+
if file, err := repo.Open(path); err == nil {
152+
data, _ := io.ReadAll(file)
153+
return data, nil
154+
}
155+
156+
path = filepath.Join(dir, "Dockerfile")
157+
if _, err := repo.Open(path); err == nil {
158+
return nil, ErrDockerfileManifest
159+
}
160+
161+
return nil, ErrNoManifest
155162
}

internal/gha/actions_test.go

+24-4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package gha
1616

1717
import (
1818
"bytes"
19+
"errors"
1920
"testing"
2021
"testing/quick"
2122

@@ -702,7 +703,7 @@ func TestManifestInRepo(t *testing.T) {
702703
type TestCase struct {
703704
fs map[string]mockFsEntry
704705
dir string
705-
want []byte
706+
want error
706707
}
707708

708709
testCases := map[string]TestCase{
@@ -713,7 +714,7 @@ func TestManifestInRepo(t *testing.T) {
713714
},
714715
},
715716
dir: "nested",
716-
want: []byte(manifestWithStep),
717+
want: ErrNoManifest,
717718
},
718719
".yaml manifest in different dir": {
719720
fs: map[string]mockFsEntry{
@@ -722,17 +723,36 @@ func TestManifestInRepo(t *testing.T) {
722723
},
723724
},
724725
dir: "nested",
725-
want: []byte(manifestWithStep),
726+
want: ErrNoManifest,
727+
},
728+
"Dockerfile manifest": {
729+
fs: map[string]mockFsEntry{
730+
"Dockerfile": {
731+
Content: []byte(manifestDockerfile),
732+
},
733+
},
734+
dir: "",
735+
want: ErrDockerfileManifest,
726736
},
727737
}
728738

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

733-
if _, err := mockRepo(tt.fs); err != nil {
743+
repo, err := mockRepo(tt.fs)
744+
if err != nil {
745+
t.Fatalf("Could not initialize file system: %+v", err)
746+
}
747+
748+
_, err = manifestInRepo(repo, tt.dir)
749+
if err == nil {
734750
t.Fatal("Unexpected success")
735751
}
752+
753+
if got, want := err, tt.want; !errors.Is(got, want) {
754+
t.Errorf("Unexpected error (got %q, want %q)", got, want)
755+
}
736756
})
737757
}
738758
})

internal/gha/errors.go

+3
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,7 @@ var (
2222
ErrInvalidUsesRepo = errors.New("invalid repository in uses")
2323
ErrInvalidUsesPath = errors.New("invalid repository path in uses")
2424
ErrLocalAction = errors.New("uses is a local action")
25+
26+
ErrDockerfileManifest = errors.New("found a Dockerfile manifest")
27+
ErrNoManifest = errors.New("could not find a manifest")
2528
)

internal/gha/gha.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package gha
1616

1717
import (
18+
"errors"
1819
"fmt"
1920
"io/fs"
2021
"path/filepath"
@@ -147,7 +148,9 @@ func JobActions(repo fs.FS, path, name string) ([]GitHubAction, error) {
147148
// specified directory in the given file system hierarchy.
148149
func ManifestActions(repo fs.FS, path string) ([]GitHubAction, error) {
149150
data, err := manifestInRepo(repo, path)
150-
if err != nil {
151+
if errors.Is(err, ErrDockerfileManifest) {
152+
return nil, nil
153+
} else if err != nil {
151154
return nil, err
152155
}
153156

internal/gha/gha_test.go

+35-12
Original file line numberDiff line numberDiff line change
@@ -667,8 +667,8 @@ func TestManifestActions(t *testing.T) {
667667
wantErr bool
668668
}
669669

670-
testCases := []TestCase{
671-
{
670+
testCases := map[string]TestCase{
671+
"root manifest without transitive actions": {
672672
fs: map[string]mockFsEntry{
673673
"action.yml": {
674674
Content: []byte(manifestWithNoSteps),
@@ -677,7 +677,7 @@ func TestManifestActions(t *testing.T) {
677677
path: "",
678678
wantErr: false,
679679
},
680-
{
680+
"root manifest with transitive actions": {
681681
fs: map[string]mockFsEntry{
682682
"action.yml": {
683683
Content: []byte(manifestWithStep),
@@ -686,7 +686,7 @@ func TestManifestActions(t *testing.T) {
686686
path: "",
687687
wantErr: false,
688688
},
689-
{
689+
"root manifest using nested actions": {
690690
fs: map[string]mockFsEntry{
691691
"action.yml": {
692692
Content: []byte(manifestWithNestedActions),
@@ -695,7 +695,7 @@ func TestManifestActions(t *testing.T) {
695695
path: "",
696696
wantErr: false,
697697
},
698-
{
698+
"nested .yml manifest": {
699699
fs: map[string]mockFsEntry{
700700
"nested": {
701701
Dir: true,
@@ -709,7 +709,7 @@ func TestManifestActions(t *testing.T) {
709709
path: "nested",
710710
wantErr: false,
711711
},
712-
{
712+
"root .yaml manifest": {
713713
fs: map[string]mockFsEntry{
714714
"action.yaml": {
715715
Content: []byte(manifestWithStep),
@@ -718,7 +718,7 @@ func TestManifestActions(t *testing.T) {
718718
path: "",
719719
wantErr: false,
720720
},
721-
{
721+
"nested .yaml manifest": {
722722
fs: map[string]mockFsEntry{
723723
"nested": {
724724
Dir: true,
@@ -732,7 +732,30 @@ func TestManifestActions(t *testing.T) {
732732
path: "nested",
733733
wantErr: false,
734734
},
735-
{
735+
"root Dockerfile manifest": {
736+
fs: map[string]mockFsEntry{
737+
"Dockerfile": {
738+
Content: []byte(manifestDockerfile),
739+
},
740+
},
741+
path: "",
742+
wantErr: false,
743+
},
744+
"nested Dockerfile manifest": {
745+
fs: map[string]mockFsEntry{
746+
"nested": {
747+
Dir: true,
748+
Children: map[string]mockFsEntry{
749+
"Dockerfile": {
750+
Content: []byte(manifestDockerfile),
751+
},
752+
},
753+
},
754+
},
755+
path: "nested",
756+
wantErr: false,
757+
},
758+
"manifest with syntax error": {
736759
fs: map[string]mockFsEntry{
737760
"action.yml": {
738761
Content: []byte(yamlWithSyntaxError),
@@ -741,7 +764,7 @@ func TestManifestActions(t *testing.T) {
741764
path: "",
742765
wantErr: true,
743766
},
744-
{
767+
"manifest with invalid uses value": {
745768
fs: map[string]mockFsEntry{
746769
"action.yml": {
747770
Content: []byte(manifestWithInvalidUses),
@@ -750,15 +773,15 @@ func TestManifestActions(t *testing.T) {
750773
path: "",
751774
wantErr: true,
752775
},
753-
{
776+
"empty repo": {
754777
fs: map[string]mockFsEntry{},
755778
path: "",
756779
wantErr: true,
757780
},
758781
}
759782

760-
for i, tt := range testCases {
761-
t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) {
783+
for name, tt := range testCases {
784+
t.Run(name, func(t *testing.T) {
762785
t.Parallel()
763786

764787
repo, err := mockRepo(tt.fs)

internal/gha/shared_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ runs:
5151
runs:
5252
steps:
5353
- uses: this-is-not-an-action
54+
`
55+
manifestDockerfile = `FROM docker.io/alpine:3.21.3
56+
ENTRYPOINT ["echo", "Hello world!"]
5457
`
5558

5659
workflowWithNoJobs = `name: workflow with no jobs

0 commit comments

Comments
 (0)