Skip to content

Commit 102e6cb

Browse files
feat: Validate GitHub Actions schema (#2416)
* feat: Validate GitHub Actions schema **BREAKING** previously accepted workflows are now invalid * update code * fix tests * Bump docker / fix lint * fix test action due to moving the file * remove unused function * fix parsing additional functions * fix allow int * update docker dep, due to linter
1 parent bda491e commit 102e6cb

File tree

12 files changed

+2854
-67
lines changed

12 files changed

+2854
-67
lines changed

go.mod

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ require (
88
github.com/adrg/xdg v0.5.0
99
github.com/andreaskoch/go-fswatch v1.0.0
1010
github.com/creack/pty v1.1.21
11-
github.com/docker/cli v26.1.4+incompatible
11+
github.com/docker/cli v26.1.5+incompatible
1212
github.com/docker/distribution v2.8.3+incompatible
13-
github.com/docker/docker v26.1.3+incompatible
13+
github.com/docker/docker v26.1.5+incompatible
1414
github.com/docker/go-connections v0.5.0
1515
github.com/go-git/go-billy/v5 v5.5.0
1616
github.com/go-git/go-git/v5 v5.12.0

go.sum

+6
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,16 @@ github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK
4848
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
4949
github.com/docker/cli v26.1.4+incompatible h1:I8PHdc0MtxEADqYJZvhBrW9bo8gawKwwenxRM7/rLu8=
5050
github.com/docker/cli v26.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
51+
github.com/docker/cli v26.1.5+incompatible h1:NxXGSdz2N+Ibdaw330TDO3d/6/f7MvHuiMbuFaIQDTk=
52+
github.com/docker/cli v26.1.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
5153
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
5254
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
5355
github.com/docker/docker v26.1.3+incompatible h1:lLCzRbrVZrljpVNobJu1J2FHk8V0s4BawoZippkc+xo=
5456
github.com/docker/docker v26.1.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
57+
github.com/docker/docker v26.1.4+incompatible h1:vuTpXDuoga+Z38m1OZHzl7NKisKWaWlhjQk7IDPSLsU=
58+
github.com/docker/docker v26.1.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
59+
github.com/docker/docker v26.1.5+incompatible h1:NEAxTwEjxV6VbBMBoGG3zPqbiJosIApZjxlbrG9q3/g=
60+
github.com/docker/docker v26.1.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
5561
github.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8=
5662
github.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHOZ32AWRqQdECoh/Mg0AlEYb40=
5763
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=

pkg/model/action.go

+13
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"io"
66
"strings"
77

8+
"github.com/nektos/act/pkg/schema"
89
"gopkg.in/yaml.v3"
910
)
1011

@@ -78,6 +79,18 @@ type Action struct {
7879
} `yaml:"branding"`
7980
}
8081

82+
func (a *Action) UnmarshalYAML(node *yaml.Node) error {
83+
// Validate the schema before deserializing it into our model
84+
if err := (&schema.Node{
85+
Definition: "action-root",
86+
Schema: schema.GetActionSchema(),
87+
}).UnmarshalYAML(node); err != nil {
88+
return err
89+
}
90+
type ActionDefault Action
91+
return node.Decode((*ActionDefault)(a))
92+
}
93+
8194
// Input parameters allow you to specify data that the action expects to use during runtime. GitHub stores input parameters as environment variables. Input ids with uppercase letters are converted to lowercase during runtime. We recommended using lowercase input ids.
8295
type Input struct {
8396
Description string `yaml:"description"`

pkg/model/workflow.go

+13
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"strings"
1010

1111
"github.com/nektos/act/pkg/common"
12+
"github.com/nektos/act/pkg/schema"
1213
log "github.com/sirupsen/logrus"
1314
"gopkg.in/yaml.v3"
1415
)
@@ -66,6 +67,18 @@ func (w *Workflow) OnEvent(event string) interface{} {
6667
return nil
6768
}
6869

70+
func (w *Workflow) UnmarshalYAML(node *yaml.Node) error {
71+
// Validate the schema before deserializing it into our model
72+
if err := (&schema.Node{
73+
Definition: "workflow-root-strict",
74+
Schema: schema.GetWorkflowSchema(),
75+
}).UnmarshalYAML(node); err != nil {
76+
return err
77+
}
78+
type WorkflowDefault Workflow
79+
return node.Decode((*WorkflowDefault)(w))
80+
}
81+
6982
type WorkflowDispatchInput struct {
7083
Description string `yaml:"description"`
7184
Required bool `yaml:"required"`

pkg/model/workflow_test.go

+2-9
Original file line numberDiff line numberDiff line change
@@ -280,15 +280,8 @@ jobs:
280280
uses: ./local-action
281281
`
282282

283-
workflow, err := ReadWorkflow(strings.NewReader(yaml))
284-
assert.NoError(t, err, "read workflow should succeed")
285-
assert.Len(t, workflow.Jobs, 1)
286-
assert.Len(t, workflow.Jobs["test"].Steps, 5)
287-
assert.Equal(t, workflow.Jobs["test"].Steps[0].Type(), StepTypeInvalid)
288-
assert.Equal(t, workflow.Jobs["test"].Steps[1].Type(), StepTypeRun)
289-
assert.Equal(t, workflow.Jobs["test"].Steps[2].Type(), StepTypeUsesActionRemote)
290-
assert.Equal(t, workflow.Jobs["test"].Steps[3].Type(), StepTypeUsesDockerURL)
291-
assert.Equal(t, workflow.Jobs["test"].Steps[4].Type(), StepTypeUsesActionLocal)
283+
_, err := ReadWorkflow(strings.NewReader(yaml))
284+
assert.Error(t, err, "read workflow should fail")
292285
}
293286

294287
// See: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idoutputs

pkg/runner/runner_test.go

+15-11
Original file line numberDiff line numberDiff line change
@@ -196,16 +196,20 @@ func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config
196196
assert.Nil(t, err, j.workflowPath)
197197

198198
planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true)
199-
assert.Nil(t, err, fullWorkflowPath)
200-
201-
plan, err := planner.PlanEvent(j.eventName)
202-
assert.True(t, (err == nil) != (plan == nil), "PlanEvent should return either a plan or an error")
203-
if err == nil && plan != nil {
204-
err = runner.NewPlanExecutor(plan)(ctx)
205-
if j.errorMessage == "" {
206-
assert.Nil(t, err, fullWorkflowPath)
207-
} else {
208-
assert.Error(t, err, j.errorMessage)
199+
if err != nil {
200+
assert.Error(t, err, j.errorMessage)
201+
} else {
202+
assert.Nil(t, err, fullWorkflowPath)
203+
204+
plan, err := planner.PlanEvent(j.eventName)
205+
assert.True(t, (err == nil) != (plan == nil), "PlanEvent should return either a plan or an error")
206+
if err == nil && plan != nil {
207+
err = runner.NewPlanExecutor(plan)(ctx)
208+
if j.errorMessage == "" {
209+
assert.Nil(t, err, fullWorkflowPath)
210+
} else {
211+
assert.Error(t, err, j.errorMessage)
212+
}
209213
}
210214
}
211215

@@ -334,7 +338,7 @@ func TestRunEvent(t *testing.T) {
334338
config.EventPath = eventFile
335339
}
336340

337-
testConfigFile := filepath.Join(workdir, table.workflowPath, "config.yml")
341+
testConfigFile := filepath.Join(workdir, table.workflowPath, "config/config.yml")
338342
if file, err := os.ReadFile(testConfigFile); err == nil {
339343
testConfig := &TestConfig{}
340344
if yaml.Unmarshal(file, testConfig) == nil {
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,44 @@
1-
inputs:
2-
who-to-greet:
3-
default: 'Mona the Octocat'
4-
runs:
5-
using: composite
6-
steps:
7-
# Test if GITHUB_ACTION_PATH is set correctly before all steps
8-
- run: stat $GITHUB_ACTION_PATH/push.yml
9-
shell: bash
10-
- run: stat $GITHUB_ACTION_PATH/action.yml
11-
shell: bash
12-
- run: '[[ "$GITHUB_ACTION_REPOSITORY" == "" ]] && [[ "$GITHUB_ACTION_REF" == "" ]]'
13-
shell: bash
14-
- uses: ./actions/docker-local
15-
id: dockerlocal
16-
with:
17-
who-to-greet: ${{inputs.who-to-greet}}
18-
- run: '[[ "${{ env.SOMEVAR }}" == "${{inputs.who-to-greet}}" ]]'
19-
shell: bash
20-
- run: '[ "${SOMEVAR}" = "Not Mona" ] || exit 1'
21-
shell: bash
22-
env:
23-
SOMEVAR: 'Not Mona'
24-
- run: '[[ "${{ steps.dockerlocal.outputs.whoami }}" == "${{inputs.who-to-greet}}" ]]'
25-
shell: bash
26-
# Test if overriding args doesn't leak inputs
27-
- uses: ./actions/docker-local-noargs
28-
with:
29-
args: ${{format('"{0}"', 'Mona is not the Octocat') }}
30-
who-to-greet: ${{inputs.who-to-greet}}
31-
- run: '[[ "${{ env.SOMEVAR }}" == "Mona is not the Octocat" ]]'
32-
shell: bash
33-
- uses: ./localdockerimagetest_
34-
# Also test a remote docker action here
35-
- uses: actions/hello-world-docker-action@v1
36-
with:
37-
who-to-greet: 'Mona the Octocat'
38-
# Test if GITHUB_ACTION_PATH is set correctly after all steps
39-
- run: stat $GITHUB_ACTION_PATH/push.yml
40-
shell: bash
41-
- run: stat $GITHUB_ACTION_PATH/action.yml
42-
shell: bash
43-
- run: '[[ "$GITHUB_ACTION_REPOSITORY" == "" ]] && [[ "$GITHUB_ACTION_REF" == "" ]]'
44-
shell: bash
1+
inputs:
2+
who-to-greet:
3+
default: 'Mona the Octocat'
4+
runs:
5+
using: composite
6+
steps:
7+
# Test if GITHUB_ACTION_PATH is set correctly before all steps
8+
- run: stat $GITHUB_ACTION_PATH/../push.yml
9+
shell: bash
10+
- run: stat $GITHUB_ACTION_PATH/action.yml
11+
shell: bash
12+
- run: '[[ "$GITHUB_ACTION_REPOSITORY" == "" ]] && [[ "$GITHUB_ACTION_REF" == "" ]]'
13+
shell: bash
14+
- uses: ./actions/docker-local
15+
id: dockerlocal
16+
with:
17+
who-to-greet: ${{inputs.who-to-greet}}
18+
- run: '[[ "${{ env.SOMEVAR }}" == "${{inputs.who-to-greet}}" ]]'
19+
shell: bash
20+
- run: '[ "${SOMEVAR}" = "Not Mona" ] || exit 1'
21+
shell: bash
22+
env:
23+
SOMEVAR: 'Not Mona'
24+
- run: '[[ "${{ steps.dockerlocal.outputs.whoami }}" == "${{inputs.who-to-greet}}" ]]'
25+
shell: bash
26+
# Test if overriding args doesn't leak inputs
27+
- uses: ./actions/docker-local-noargs
28+
with:
29+
args: ${{format('"{0}"', 'Mona is not the Octocat') }}
30+
who-to-greet: ${{inputs.who-to-greet}}
31+
- run: '[[ "${{ env.SOMEVAR }}" == "Mona is not the Octocat" ]]'
32+
shell: bash
33+
- uses: ./localdockerimagetest_
34+
# Also test a remote docker action here
35+
- uses: actions/hello-world-docker-action@v1
36+
with:
37+
who-to-greet: 'Mona the Octocat'
38+
# Test if GITHUB_ACTION_PATH is set correctly after all steps
39+
- run: stat $GITHUB_ACTION_PATH/../push.yml
40+
shell: bash
41+
- run: stat $GITHUB_ACTION_PATH/action.yml
42+
shell: bash
43+
- run: '[[ "$GITHUB_ACTION_REPOSITORY" == "" ]] && [[ "$GITHUB_ACTION_REF" == "" ]]'
44+
shell: bash

pkg/runner/testdata/local-action-via-composite-dockerfile/push.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ jobs:
66
runs-on: ubuntu-latest
77
steps:
88
- uses: actions/checkout@v2
9-
- uses: ./local-action-via-composite-dockerfile
9+
- uses: ./local-action-via-composite-dockerfile/action

0 commit comments

Comments
 (0)