Skip to content

Commit 018af0b

Browse files
Compute checksums for transitive Actions
Expand the initialization, updating, and verification to recursively compute the checksums of transitive Actions. What are transitive Actions? Those are the actions defined in an Action manifest using the composite Action type [1]. To capture these, `ghasum` looks at every Action repository it downloads from GitHub and sees if the manifest (at the used path in that repository) uses any other actions. If it does, it adds it to the list of actions to compute a checksum for. -- 1. https://docs.github.com/en/actions/sharing-automations/creating-actions/creating-a-composite-action
1 parent fb31e83 commit 018af0b

File tree

22 files changed

+1360
-362
lines changed

22 files changed

+1360
-362
lines changed

SPECIFICATION.md

Lines changed: 65 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -6,42 +6,6 @@ The specification aims to clarify how `ghasum` operates. Any discrepancy with
66
the implementation or ambiguity in the specification can be reported as a bug.
77
There is no guarantee on whether the specification or implementation is correct.
88

9-
## Scope
10-
11-
The scope of `ghasum` are reusable GitHub Actions used in the GitHub Actions
12-
Workflow of a repository. This excludes
13-
14-
- Actions in the same repository as the workflow ("local actions"). Example:
15-
16-
```yaml
17-
steps:
18-
- uses: ./.github/actions/hello-world-action
19-
```
20-
21-
- Docker Hub Actions ([#216]). Examples:
22-
23-
```yaml
24-
steps:
25-
- uses: docker://alpine:3.8
26-
- uses: docker://ghcr.io/OWNER/IMAGE_NAME
27-
- uses: docker://gcr.io/cloud-builders/gradle
28-
```
29-
30-
- Reusable workflows ([#215]). Examples:
31-
32-
```yaml
33-
jobs:
34-
call-workflow-1-in-local-repo:
35-
uses: octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89
36-
call-workflow-2-in-local-repo:
37-
uses: ./.github/workflows/workflow-2.yml
38-
call-workflow-in-another-repo:
39-
uses: octo-org/another-repo/.github/workflows/workflow.yml@v1
40-
```
41-
42-
[#215]: https://github.com/chains-project/ghasum/issues/215
43-
[#216]: https://github.com/chains-project/ghasum/issues/216
44-
459
## Actions
4610

4711
### `ghasum init`
@@ -54,10 +18,11 @@ immediately (it means either 1. the file has been created since it was checked
5418
and so is not owned by us, or 2. the file could not be created and so cannot be
5519
initialized).
5620

57-
If the file lock is obtained, the process will compute checksums for all actions
58-
used in the repository (see [Computing Checksums]) using the best available
59-
hashing algorithm. Then it stores them in a sumfile (see [Storing Checksums])
60-
using the latest sumfile version and releases the lock.
21+
If the file lock is obtained, the process will compute checksums (see [Computing
22+
Checksums]) for all actions used in the repository (see [Collecting Actions])
23+
using the best available hashing algorithm. Then it stores them in a sumfile
24+
(see [Storing Checksums]) using the latest sumfile version. Finally the process
25+
will releases the lock on the file.
6126

6227
If the process fails an attempt should be made to remove the created file (if
6328
removing fails the error is ignored).
@@ -74,14 +39,14 @@ by another process leading to an inconsistent state).
7439
If the file lock is obtained, the process shall first read it and parse it
7540
completely to extract the sumfile version. If this fails the process shall exit
7641
immediately unless the `-force` flag is used (see details below). Else it shall
77-
compute checksums for all new actions used in the repository (see [Computing
78-
Checksums]) using the best available hashing algorithm. Here, a new action also
79-
includes a new version of a previously used action. Additionally, it should
80-
remove any entry which is no longer in use. No existing checksum for a used
81-
action shall be updated unless the `-force` flag is used. It shall then store
82-
them in a sumfile (see [Storing Checksums]) using the same sumfile version as
83-
before and releases the lock. In short, updating will only add new and remove
84-
old checksums from an existing sumfile.
42+
compute checksums (see [Computing Checksums]) for all new actions used in the
43+
repository (see [Collecting Actions]) using the same hashing algorithm as was
44+
used for the existing checksums. New actions also include new versions of a
45+
previously used actions. Additionally, it should remove any entry which is no
46+
longer in use. No existing checksum for a used action shall be updated. It shall
47+
then store them in a sumfile (see [Storing Checksums]) using the same sumfile
48+
version as before and releases the lock. In short, updating will only add new
49+
and remove old checksums from an existing sumfile.
8550

8651
With the `-force` flag the process will ignore errors in the sumfile and fix
8752
those while updating. It will also update existing checksums that are incorrect.
@@ -99,9 +64,9 @@ error.
9964

10065
If the checksum file exists the process shall read and parse it fully. If this
10166
fails the process shall exit immediately. Else it shall recompute the checksums
102-
(see [Computing Checksums]) for all actions in the target using the same hashing
103-
algorithm as was used for the stored checksums. It shall compare the computed
104-
checksums against the stored checksums.
67+
(see [Computing Checksums]) for all actions in the target (see [Collecting
68+
Actions]) using the same hashing algorithm as was used for the stored checksums.
69+
It shall compare the computed checksums against the stored checksums.
10570

10671
If any of the checksums does not match or is missing the process shall exit with
10772
a non-zero exit code, for usability all values should be compared (and all
@@ -117,6 +82,51 @@ Redundant checksums are ignored by this process.
11782

11883
## Procedures
11984

85+
### Collecting Actions
86+
87+
To determine the set of actions a target depends on, first find all `uses:`
88+
entries in the target. For a repository this covers all workflows in the
89+
workflows directory, otherwise it covers only the target.
90+
91+
For each `uses:` value, excluding the list below, it is added to the set and the
92+
repository declared by the `uses:` value is fetched. The action manifest at the
93+
path specified in the `uses:` value is parsed for additional `uses:` values. For
94+
each of these transitive `uses:` values, this process is repeated.
95+
96+
The following `uses:` values are to be excluded from the set of actions a
97+
repository depends on.
98+
99+
- Actions in the same repository as the workflow ("local actions"). Example:
100+
101+
```yaml
102+
steps:
103+
- uses: ./.github/actions/hello-world-action
104+
```
105+
106+
- Docker Hub Actions ([#216]). Examples:
107+
108+
```yaml
109+
steps:
110+
- uses: docker://alpine:3.8
111+
- uses: docker://ghcr.io/OWNER/IMAGE_NAME
112+
- uses: docker://gcr.io/cloud-builders/gradle
113+
```
114+
115+
- Reusable workflows ([#215]). Examples:
116+
117+
```yaml
118+
jobs:
119+
call-workflow-1-in-local-repo:
120+
uses: octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89
121+
call-workflow-2-in-local-repo:
122+
uses: ./.github/workflows/workflow-2.yml
123+
call-workflow-in-another-repo:
124+
uses: octo-org/another-repo/.github/workflows/workflow.yml@v1
125+
```
126+
127+
[#215]: https://github.com/chains-project/ghasum/issues/215
128+
[#216]: https://github.com/chains-project/ghasum/issues/216
129+
120130
### Computing Checksums
121131
122132
To compute checksums `ghasum` will pull the repository of an action, either at
@@ -181,8 +191,12 @@ version 1
181191

182192
## Definitions
183193

194+
- _action manifest_ is the file `action.yml` or `action.yaml` (mutually
195+
exclusive).
184196
- _checksum file_ is the file `.github/workflows/gha.sum`.
197+
- _workflows directory_ is the directory `.github/workflows`.
185198

199+
[collecting actions]: #collecting-actions
186200
[computing checksums]: #computing-checksums
187201
[storing checksums]: #storing-checksums
188202
[sumfile versions]: #sumfile-versions

cmd/ghasum/version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024 Eric Cornelissen
1+
// Copyright 2024-2025 Eric Cornelissen
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.

internal/gha/actions.go

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,45 +19,60 @@ import (
1919
"fmt"
2020
"io"
2121
"io/fs"
22+
"maps"
2223
"path"
24+
"path/filepath"
25+
"slices"
2326
)
2427

2528
type workflowFile struct {
2629
path string
2730
content []byte
2831
}
2932

33+
func actionsInManifest(manifest manifest) ([]GitHubAction, error) {
34+
unique := make(map[string]GitHubAction, 0)
35+
err := actionsInSteps(manifest.Runs.Steps, unique)
36+
if err != nil {
37+
return nil, err
38+
}
39+
40+
return slices.Collect(maps.Values(unique)), nil
41+
}
42+
3043
func actionsInWorkflows(workflows []workflow) ([]GitHubAction, error) {
3144
unique := make(map[string]GitHubAction, 0)
3245
for _, workflow := range workflows {
3346
for _, job := range workflow.Jobs {
34-
for _, step := range job.Steps {
35-
uses := step.Uses
36-
if uses == "" {
37-
continue
38-
}
39-
40-
action, err := parseUses(uses)
41-
if errors.Is(err, ErrLocalAction) || errors.Is(err, ErrDockerUses) {
42-
continue
43-
} else if err != nil {
44-
return nil, err
45-
}
46-
47-
id := fmt.Sprintf("%s%s%s", action.Owner, action.Project, action.Ref)
48-
unique[id] = action
47+
err := actionsInSteps(job.Steps, unique)
48+
if err != nil {
49+
return nil, err
4950
}
5051
}
5152
}
5253

53-
i := 0
54-
actions := make([]GitHubAction, len(unique))
55-
for _, action := range unique {
56-
actions[i] = action
57-
i++
54+
return slices.Collect(maps.Values(unique)), nil
55+
}
56+
57+
func actionsInSteps(steps []step, m map[string]GitHubAction) error {
58+
for _, step := range steps {
59+
uses := step.Uses
60+
if uses == "" {
61+
continue
62+
}
63+
64+
action, err := parseUses(uses)
65+
if errors.Is(err, ErrLocalAction) || errors.Is(err, ErrDockerUses) {
66+
continue
67+
} else if err != nil {
68+
return err
69+
}
70+
71+
id := fmt.Sprintf("%s%s%s%s", action.Owner, action.Project, action.Path, action.Ref)
72+
m[id] = action
5873
}
5974

60-
return actions, nil
75+
return nil
6176
}
6277

6378
func workflowsInRepo(repo fs.FS) ([]workflowFile, error) {
@@ -108,3 +123,17 @@ func workflowInRepo(repo fs.FS, path string) ([]byte, error) {
108123
data, _ := io.ReadAll(file)
109124
return data, nil
110125
}
126+
127+
func manifestInRepo(repo fs.FS, dir string) ([]byte, error) {
128+
path := filepath.Join(dir, "action.yml")
129+
file, err := repo.Open(path)
130+
if err != nil {
131+
path = filepath.Join(dir, "action.yaml")
132+
if file, err = repo.Open(path); err != nil {
133+
return nil, fmt.Errorf("could not open manifest (action.yml or action.yaml) at %s: %v", dir, err)
134+
}
135+
}
136+
137+
data, _ := io.ReadAll(file)
138+
return data, nil
139+
}

0 commit comments

Comments
 (0)