mirror of
https://github.com/nektos/act.git
synced 2024-09-11 12:35:25 +03:00
Merge branch 'master' into act-dind
This commit is contained in:
commit
ea7100f778
4
go.mod
4
go.mod
@ -8,9 +8,9 @@ require (
|
||||
github.com/adrg/xdg v0.5.0
|
||||
github.com/andreaskoch/go-fswatch v1.0.0
|
||||
github.com/creack/pty v1.1.21
|
||||
github.com/docker/cli v26.1.4+incompatible
|
||||
github.com/docker/cli v26.1.5+incompatible
|
||||
github.com/docker/distribution v2.8.3+incompatible
|
||||
github.com/docker/docker v26.1.3+incompatible
|
||||
github.com/docker/docker v26.1.5+incompatible
|
||||
github.com/docker/go-connections v0.5.0
|
||||
github.com/go-git/go-billy/v5 v5.5.0
|
||||
github.com/go-git/go-git/v5 v5.12.0
|
||||
|
6
go.sum
6
go.sum
@ -48,10 +48,16 @@ github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK
|
||||
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/cli v26.1.4+incompatible h1:I8PHdc0MtxEADqYJZvhBrW9bo8gawKwwenxRM7/rLu8=
|
||||
github.com/docker/cli v26.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v26.1.5+incompatible h1:NxXGSdz2N+Ibdaw330TDO3d/6/f7MvHuiMbuFaIQDTk=
|
||||
github.com/docker/cli v26.1.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v26.1.3+incompatible h1:lLCzRbrVZrljpVNobJu1J2FHk8V0s4BawoZippkc+xo=
|
||||
github.com/docker/docker v26.1.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v26.1.4+incompatible h1:vuTpXDuoga+Z38m1OZHzl7NKisKWaWlhjQk7IDPSLsU=
|
||||
github.com/docker/docker v26.1.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v26.1.5+incompatible h1:NEAxTwEjxV6VbBMBoGG3zPqbiJosIApZjxlbrG9q3/g=
|
||||
github.com/docker/docker v26.1.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8=
|
||||
github.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHOZ32AWRqQdECoh/Mg0AlEYb40=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/nektos/act/pkg/schema"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@ -78,6 +79,18 @@ type Action struct {
|
||||
} `yaml:"branding"`
|
||||
}
|
||||
|
||||
func (a *Action) UnmarshalYAML(node *yaml.Node) error {
|
||||
// Validate the schema before deserializing it into our model
|
||||
if err := (&schema.Node{
|
||||
Definition: "action-root",
|
||||
Schema: schema.GetActionSchema(),
|
||||
}).UnmarshalYAML(node); err != nil {
|
||||
return err
|
||||
}
|
||||
type ActionDefault Action
|
||||
return node.Decode((*ActionDefault)(a))
|
||||
}
|
||||
|
||||
// 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.
|
||||
type Input struct {
|
||||
Description string `yaml:"description"`
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/schema"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
@ -66,6 +67,18 @@ func (w *Workflow) OnEvent(event string) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Workflow) UnmarshalYAML(node *yaml.Node) error {
|
||||
// Validate the schema before deserializing it into our model
|
||||
if err := (&schema.Node{
|
||||
Definition: "workflow-root-strict",
|
||||
Schema: schema.GetWorkflowSchema(),
|
||||
}).UnmarshalYAML(node); err != nil {
|
||||
return err
|
||||
}
|
||||
type WorkflowDefault Workflow
|
||||
return node.Decode((*WorkflowDefault)(w))
|
||||
}
|
||||
|
||||
type WorkflowDispatchInput struct {
|
||||
Description string `yaml:"description"`
|
||||
Required bool `yaml:"required"`
|
||||
|
@ -280,15 +280,8 @@ jobs:
|
||||
uses: ./local-action
|
||||
`
|
||||
|
||||
workflow, err := ReadWorkflow(strings.NewReader(yaml))
|
||||
assert.NoError(t, err, "read workflow should succeed")
|
||||
assert.Len(t, workflow.Jobs, 1)
|
||||
assert.Len(t, workflow.Jobs["test"].Steps, 5)
|
||||
assert.Equal(t, workflow.Jobs["test"].Steps[0].Type(), StepTypeInvalid)
|
||||
assert.Equal(t, workflow.Jobs["test"].Steps[1].Type(), StepTypeRun)
|
||||
assert.Equal(t, workflow.Jobs["test"].Steps[2].Type(), StepTypeUsesActionRemote)
|
||||
assert.Equal(t, workflow.Jobs["test"].Steps[3].Type(), StepTypeUsesDockerURL)
|
||||
assert.Equal(t, workflow.Jobs["test"].Steps[4].Type(), StepTypeUsesActionLocal)
|
||||
_, err := ReadWorkflow(strings.NewReader(yaml))
|
||||
assert.Error(t, err, "read workflow should fail")
|
||||
}
|
||||
|
||||
// See: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idoutputs
|
||||
|
@ -196,16 +196,20 @@ func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config
|
||||
assert.Nil(t, err, j.workflowPath)
|
||||
|
||||
planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true)
|
||||
assert.Nil(t, err, fullWorkflowPath)
|
||||
if err != nil {
|
||||
assert.Error(t, err, j.errorMessage)
|
||||
} else {
|
||||
assert.Nil(t, err, fullWorkflowPath)
|
||||
|
||||
plan, err := planner.PlanEvent(j.eventName)
|
||||
assert.True(t, (err == nil) != (plan == nil), "PlanEvent should return either a plan or an error")
|
||||
if err == nil && plan != nil {
|
||||
err = runner.NewPlanExecutor(plan)(ctx)
|
||||
if j.errorMessage == "" {
|
||||
assert.Nil(t, err, fullWorkflowPath)
|
||||
} else {
|
||||
assert.Error(t, err, j.errorMessage)
|
||||
plan, err := planner.PlanEvent(j.eventName)
|
||||
assert.True(t, (err == nil) != (plan == nil), "PlanEvent should return either a plan or an error")
|
||||
if err == nil && plan != nil {
|
||||
err = runner.NewPlanExecutor(plan)(ctx)
|
||||
if j.errorMessage == "" {
|
||||
assert.Nil(t, err, fullWorkflowPath)
|
||||
} else {
|
||||
assert.Error(t, err, j.errorMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -334,7 +338,7 @@ func TestRunEvent(t *testing.T) {
|
||||
config.EventPath = eventFile
|
||||
}
|
||||
|
||||
testConfigFile := filepath.Join(workdir, table.workflowPath, "config.yml")
|
||||
testConfigFile := filepath.Join(workdir, table.workflowPath, "config/config.yml")
|
||||
if file, err := os.ReadFile(testConfigFile); err == nil {
|
||||
testConfig := &TestConfig{}
|
||||
if yaml.Unmarshal(file, testConfig) == nil {
|
||||
|
@ -1,44 +1,44 @@
|
||||
inputs:
|
||||
who-to-greet:
|
||||
default: 'Mona the Octocat'
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
# Test if GITHUB_ACTION_PATH is set correctly before all steps
|
||||
- run: stat $GITHUB_ACTION_PATH/push.yml
|
||||
shell: bash
|
||||
- run: stat $GITHUB_ACTION_PATH/action.yml
|
||||
shell: bash
|
||||
- run: '[[ "$GITHUB_ACTION_REPOSITORY" == "" ]] && [[ "$GITHUB_ACTION_REF" == "" ]]'
|
||||
shell: bash
|
||||
- uses: ./actions/docker-local
|
||||
id: dockerlocal
|
||||
with:
|
||||
who-to-greet: ${{inputs.who-to-greet}}
|
||||
- run: '[[ "${{ env.SOMEVAR }}" == "${{inputs.who-to-greet}}" ]]'
|
||||
shell: bash
|
||||
- run: '[ "${SOMEVAR}" = "Not Mona" ] || exit 1'
|
||||
shell: bash
|
||||
env:
|
||||
SOMEVAR: 'Not Mona'
|
||||
- run: '[[ "${{ steps.dockerlocal.outputs.whoami }}" == "${{inputs.who-to-greet}}" ]]'
|
||||
shell: bash
|
||||
# Test if overriding args doesn't leak inputs
|
||||
- uses: ./actions/docker-local-noargs
|
||||
with:
|
||||
args: ${{format('"{0}"', 'Mona is not the Octocat') }}
|
||||
who-to-greet: ${{inputs.who-to-greet}}
|
||||
- run: '[[ "${{ env.SOMEVAR }}" == "Mona is not the Octocat" ]]'
|
||||
shell: bash
|
||||
- uses: ./localdockerimagetest_
|
||||
# Also test a remote docker action here
|
||||
- uses: actions/hello-world-docker-action@v1
|
||||
with:
|
||||
who-to-greet: 'Mona the Octocat'
|
||||
# Test if GITHUB_ACTION_PATH is set correctly after all steps
|
||||
- run: stat $GITHUB_ACTION_PATH/push.yml
|
||||
shell: bash
|
||||
- run: stat $GITHUB_ACTION_PATH/action.yml
|
||||
shell: bash
|
||||
- run: '[[ "$GITHUB_ACTION_REPOSITORY" == "" ]] && [[ "$GITHUB_ACTION_REF" == "" ]]'
|
||||
shell: bash
|
||||
inputs:
|
||||
who-to-greet:
|
||||
default: 'Mona the Octocat'
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
# Test if GITHUB_ACTION_PATH is set correctly before all steps
|
||||
- run: stat $GITHUB_ACTION_PATH/../push.yml
|
||||
shell: bash
|
||||
- run: stat $GITHUB_ACTION_PATH/action.yml
|
||||
shell: bash
|
||||
- run: '[[ "$GITHUB_ACTION_REPOSITORY" == "" ]] && [[ "$GITHUB_ACTION_REF" == "" ]]'
|
||||
shell: bash
|
||||
- uses: ./actions/docker-local
|
||||
id: dockerlocal
|
||||
with:
|
||||
who-to-greet: ${{inputs.who-to-greet}}
|
||||
- run: '[[ "${{ env.SOMEVAR }}" == "${{inputs.who-to-greet}}" ]]'
|
||||
shell: bash
|
||||
- run: '[ "${SOMEVAR}" = "Not Mona" ] || exit 1'
|
||||
shell: bash
|
||||
env:
|
||||
SOMEVAR: 'Not Mona'
|
||||
- run: '[[ "${{ steps.dockerlocal.outputs.whoami }}" == "${{inputs.who-to-greet}}" ]]'
|
||||
shell: bash
|
||||
# Test if overriding args doesn't leak inputs
|
||||
- uses: ./actions/docker-local-noargs
|
||||
with:
|
||||
args: ${{format('"{0}"', 'Mona is not the Octocat') }}
|
||||
who-to-greet: ${{inputs.who-to-greet}}
|
||||
- run: '[[ "${{ env.SOMEVAR }}" == "Mona is not the Octocat" ]]'
|
||||
shell: bash
|
||||
- uses: ./localdockerimagetest_
|
||||
# Also test a remote docker action here
|
||||
- uses: actions/hello-world-docker-action@v1
|
||||
with:
|
||||
who-to-greet: 'Mona the Octocat'
|
||||
# Test if GITHUB_ACTION_PATH is set correctly after all steps
|
||||
- run: stat $GITHUB_ACTION_PATH/../push.yml
|
||||
shell: bash
|
||||
- run: stat $GITHUB_ACTION_PATH/action.yml
|
||||
shell: bash
|
||||
- run: '[[ "$GITHUB_ACTION_REPOSITORY" == "" ]] && [[ "$GITHUB_ACTION_REF" == "" ]]'
|
||||
shell: bash
|
@ -6,4 +6,4 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: ./local-action-via-composite-dockerfile
|
||||
- uses: ./local-action-via-composite-dockerfile/action
|
261
pkg/schema/action_schema.json
Normal file
261
pkg/schema/action_schema.json
Normal file
@ -0,0 +1,261 @@
|
||||
{
|
||||
"definitions": {
|
||||
"action-root": {
|
||||
"description": "Action file",
|
||||
"mapping": {
|
||||
"properties": {
|
||||
"name": "string",
|
||||
"description": "string",
|
||||
"inputs": "inputs",
|
||||
"runs": "runs",
|
||||
"outputs": "outputs"
|
||||
},
|
||||
"loose-key-type": "non-empty-string",
|
||||
"loose-value-type": "any"
|
||||
}
|
||||
},
|
||||
"inputs": {
|
||||
"mapping": {
|
||||
"loose-key-type": "non-empty-string",
|
||||
"loose-value-type": "input"
|
||||
}
|
||||
},
|
||||
"input": {
|
||||
"mapping": {
|
||||
"properties": {
|
||||
"default": "input-default-context"
|
||||
},
|
||||
"loose-key-type": "non-empty-string",
|
||||
"loose-value-type": "any"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"mapping": {
|
||||
"loose-key-type": "non-empty-string",
|
||||
"loose-value-type": "output-definition"
|
||||
}
|
||||
},
|
||||
"output-definition": {
|
||||
"mapping": {
|
||||
"properties": {
|
||||
"description": "string",
|
||||
"value": "output-value"
|
||||
}
|
||||
}
|
||||
},
|
||||
"runs": {
|
||||
"one-of": ["container-runs", "node-runs", "plugin-runs", "composite-runs"]
|
||||
},
|
||||
"container-runs": {
|
||||
"mapping": {
|
||||
"properties": {
|
||||
"using": "non-empty-string",
|
||||
"image": "non-empty-string",
|
||||
"entrypoint": "non-empty-string",
|
||||
"args": "container-runs-args",
|
||||
"env": "container-runs-env",
|
||||
"pre-entrypoint": "non-empty-string",
|
||||
"pre-if": "non-empty-string",
|
||||
"post-entrypoint": "non-empty-string",
|
||||
"post-if": "non-empty-string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"container-runs-args": {
|
||||
"sequence": {
|
||||
"item-type": "container-runs-context"
|
||||
}
|
||||
},
|
||||
"container-runs-env": {
|
||||
"context": ["inputs"],
|
||||
"mapping": {
|
||||
"loose-key-type": "non-empty-string",
|
||||
"loose-value-type": "string"
|
||||
}
|
||||
},
|
||||
"node-runs": {
|
||||
"mapping": {
|
||||
"properties": {
|
||||
"using": "non-empty-string",
|
||||
"main": "non-empty-string",
|
||||
"pre": "non-empty-string",
|
||||
"pre-if": "non-empty-string",
|
||||
"post": "non-empty-string",
|
||||
"post-if": "non-empty-string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugin-runs": {
|
||||
"mapping": {
|
||||
"properties": {
|
||||
"plugin": "non-empty-string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"composite-runs": {
|
||||
"mapping": {
|
||||
"properties": {
|
||||
"using": "non-empty-string",
|
||||
"steps": "composite-steps"
|
||||
}
|
||||
}
|
||||
},
|
||||
"composite-steps": {
|
||||
"sequence": {
|
||||
"item-type": "composite-step"
|
||||
}
|
||||
},
|
||||
"composite-step": {
|
||||
"one-of": ["run-step", "uses-step"]
|
||||
},
|
||||
"run-step": {
|
||||
"mapping": {
|
||||
"properties": {
|
||||
"name": "string-steps-context",
|
||||
"id": "non-empty-string",
|
||||
"if": "step-if",
|
||||
"run": {
|
||||
"type": "string-steps-context",
|
||||
"required": true
|
||||
},
|
||||
"env": "step-env",
|
||||
"continue-on-error": "boolean-steps-context",
|
||||
"working-directory": "string-steps-context",
|
||||
"shell": {
|
||||
"type": "string-steps-context",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"uses-step": {
|
||||
"mapping": {
|
||||
"properties": {
|
||||
"name": "string-steps-context",
|
||||
"id": "non-empty-string",
|
||||
"if": "step-if",
|
||||
"uses": {
|
||||
"type": "non-empty-string",
|
||||
"required": true
|
||||
},
|
||||
"continue-on-error": "boolean-steps-context",
|
||||
"with": "step-with",
|
||||
"env": "step-env"
|
||||
}
|
||||
}
|
||||
},
|
||||
"container-runs-context": {
|
||||
"context": ["inputs"],
|
||||
"string": {}
|
||||
},
|
||||
"output-value": {
|
||||
"context": [
|
||||
"github",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"steps",
|
||||
"inputs",
|
||||
"job",
|
||||
"runner",
|
||||
"env"
|
||||
],
|
||||
"string": {}
|
||||
},
|
||||
"input-default-context": {
|
||||
"context": [
|
||||
"github",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"job",
|
||||
"runner",
|
||||
"hashFiles(1,255)"
|
||||
],
|
||||
"string": {}
|
||||
},
|
||||
"non-empty-string": {
|
||||
"string": {
|
||||
"require-non-empty": true
|
||||
}
|
||||
},
|
||||
"string-steps-context": {
|
||||
"context": [
|
||||
"github",
|
||||
"inputs",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"steps",
|
||||
"job",
|
||||
"runner",
|
||||
"env",
|
||||
"hashFiles(1,255)"
|
||||
],
|
||||
"string": {}
|
||||
},
|
||||
"boolean-steps-context": {
|
||||
"context": [
|
||||
"github",
|
||||
"inputs",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"steps",
|
||||
"job",
|
||||
"runner",
|
||||
"env",
|
||||
"hashFiles(1,255)"
|
||||
],
|
||||
"boolean": {}
|
||||
},
|
||||
"step-env": {
|
||||
"context": [
|
||||
"github",
|
||||
"inputs",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"steps",
|
||||
"job",
|
||||
"runner",
|
||||
"env",
|
||||
"hashFiles(1,255)"
|
||||
],
|
||||
"mapping": {
|
||||
"loose-key-type": "non-empty-string",
|
||||
"loose-value-type": "string"
|
||||
}
|
||||
},
|
||||
"step-if": {
|
||||
"context": [
|
||||
"github",
|
||||
"inputs",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"steps",
|
||||
"job",
|
||||
"runner",
|
||||
"env",
|
||||
"always(0,0)",
|
||||
"failure(0,0)",
|
||||
"cancelled(0,0)",
|
||||
"success(0,0)",
|
||||
"hashFiles(1,255)"
|
||||
],
|
||||
"string": {}
|
||||
},
|
||||
"step-with": {
|
||||
"context": [
|
||||
"github",
|
||||
"inputs",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"steps",
|
||||
"job",
|
||||
"runner",
|
||||
"env",
|
||||
"hashFiles(1,255)"
|
||||
],
|
||||
"mapping": {
|
||||
"loose-key-type": "non-empty-string",
|
||||
"loose-value-type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
387
pkg/schema/schema.go
Normal file
387
pkg/schema/schema.go
Normal file
@ -0,0 +1,387 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/rhysd/actionlint"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
//go:embed workflow_schema.json
|
||||
var workflowSchema string
|
||||
|
||||
//go:embed action_schema.json
|
||||
var actionSchema string
|
||||
|
||||
type Schema struct {
|
||||
Definitions map[string]Definition
|
||||
}
|
||||
|
||||
func (s *Schema) GetDefinition(name string) Definition {
|
||||
def, ok := s.Definitions[name]
|
||||
if !ok {
|
||||
switch name {
|
||||
case "any":
|
||||
return Definition{OneOf: &[]string{"sequence", "mapping", "number", "boolean", "string", "null"}}
|
||||
case "sequence":
|
||||
return Definition{Sequence: &SequenceDefinition{ItemType: "any"}}
|
||||
case "mapping":
|
||||
return Definition{Mapping: &MappingDefinition{LooseKeyType: "any", LooseValueType: "any"}}
|
||||
case "number":
|
||||
return Definition{Number: &NumberDefinition{}}
|
||||
case "string":
|
||||
return Definition{String: &StringDefinition{}}
|
||||
case "boolean":
|
||||
return Definition{Boolean: &BooleanDefinition{}}
|
||||
case "null":
|
||||
return Definition{Null: &NullDefinition{}}
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
type Definition struct {
|
||||
Context []string
|
||||
Mapping *MappingDefinition
|
||||
Sequence *SequenceDefinition
|
||||
OneOf *[]string `json:"one-of"`
|
||||
AllowedValues *[]string `json:"allowed-values"`
|
||||
String *StringDefinition
|
||||
Number *NumberDefinition
|
||||
Boolean *BooleanDefinition
|
||||
Null *NullDefinition
|
||||
}
|
||||
|
||||
type MappingDefinition struct {
|
||||
Properties map[string]MappingProperty
|
||||
LooseKeyType string `json:"loose-key-type"`
|
||||
LooseValueType string `json:"loose-value-type"`
|
||||
}
|
||||
|
||||
type MappingProperty struct {
|
||||
Type string
|
||||
Required bool
|
||||
}
|
||||
|
||||
func (s *MappingProperty) UnmarshalJSON(data []byte) error {
|
||||
if json.Unmarshal(data, &s.Type) != nil {
|
||||
type MProp MappingProperty
|
||||
return json.Unmarshal(data, (*MProp)(s))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SequenceDefinition struct {
|
||||
ItemType string `json:"item-type"`
|
||||
}
|
||||
|
||||
type StringDefinition struct {
|
||||
Constant string
|
||||
IsExpression bool `json:"is-expression"`
|
||||
}
|
||||
|
||||
type NumberDefinition struct {
|
||||
}
|
||||
|
||||
type BooleanDefinition struct {
|
||||
}
|
||||
|
||||
type NullDefinition struct {
|
||||
}
|
||||
|
||||
func GetWorkflowSchema() *Schema {
|
||||
sh := &Schema{}
|
||||
_ = json.Unmarshal([]byte(workflowSchema), sh)
|
||||
return sh
|
||||
}
|
||||
|
||||
func GetActionSchema() *Schema {
|
||||
sh := &Schema{}
|
||||
_ = json.Unmarshal([]byte(actionSchema), sh)
|
||||
return sh
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
Definition string
|
||||
Schema *Schema
|
||||
Context []string
|
||||
}
|
||||
|
||||
type FunctionInfo struct {
|
||||
name string
|
||||
min int
|
||||
max int
|
||||
}
|
||||
|
||||
func (s *Node) checkSingleExpression(exprNode actionlint.ExprNode) error {
|
||||
if len(s.Context) == 0 {
|
||||
switch exprNode.Token().Kind {
|
||||
case actionlint.TokenKindInt:
|
||||
case actionlint.TokenKindFloat:
|
||||
case actionlint.TokenKindString:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("expressions are not allowed here")
|
||||
}
|
||||
}
|
||||
|
||||
funcs := s.GetFunctions()
|
||||
|
||||
var err error
|
||||
actionlint.VisitExprNode(exprNode, func(node, _ actionlint.ExprNode, entering bool) {
|
||||
if funcCallNode, ok := node.(*actionlint.FuncCallNode); entering && ok {
|
||||
for _, v := range *funcs {
|
||||
if strings.EqualFold(funcCallNode.Callee, v.name) {
|
||||
if v.min > len(funcCallNode.Args) {
|
||||
err = errors.Join(err, fmt.Errorf("Missing parameters for %s expected > %v got %v", funcCallNode.Callee, v.min, len(funcCallNode.Args)))
|
||||
}
|
||||
if v.max < len(funcCallNode.Args) {
|
||||
err = errors.Join(err, fmt.Errorf("To many parameters for %s expected < %v got %v", funcCallNode.Callee, v.max, len(funcCallNode.Args)))
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
err = errors.Join(err, fmt.Errorf("Unknown Function Call %s", funcCallNode.Callee))
|
||||
}
|
||||
if varNode, ok := node.(*actionlint.VariableNode); entering && ok {
|
||||
for _, v := range s.Context {
|
||||
if strings.EqualFold(varNode.Name, v) {
|
||||
return
|
||||
}
|
||||
}
|
||||
err = errors.Join(err, fmt.Errorf("Unknown Variable Access %s", varNode.Name))
|
||||
}
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Node) GetFunctions() *[]FunctionInfo {
|
||||
funcs := &[]FunctionInfo{}
|
||||
AddFunction(funcs, "contains", 2, 2)
|
||||
AddFunction(funcs, "endsWith", 2, 2)
|
||||
AddFunction(funcs, "format", 1, 255)
|
||||
AddFunction(funcs, "join", 1, 2)
|
||||
AddFunction(funcs, "startsWith", 2, 2)
|
||||
AddFunction(funcs, "toJson", 1, 1)
|
||||
AddFunction(funcs, "fromJson", 1, 1)
|
||||
for _, v := range s.Context {
|
||||
i := strings.Index(v, "(")
|
||||
if i == -1 {
|
||||
continue
|
||||
}
|
||||
fun := FunctionInfo{
|
||||
name: v[:i],
|
||||
}
|
||||
if n, err := fmt.Sscanf(v[i:], "(%d,%d)", &fun.min, &fun.max); n == 2 && err == nil {
|
||||
*funcs = append(*funcs, fun)
|
||||
}
|
||||
}
|
||||
return funcs
|
||||
}
|
||||
|
||||
func (s *Node) checkExpression(node *yaml.Node) (bool, error) {
|
||||
val := node.Value
|
||||
hadExpr := false
|
||||
var err error
|
||||
for {
|
||||
if i := strings.Index(val, "${{"); i != -1 {
|
||||
val = val[i+3:]
|
||||
} else {
|
||||
return hadExpr, err
|
||||
}
|
||||
hadExpr = true
|
||||
|
||||
parser := actionlint.NewExprParser()
|
||||
lexer := actionlint.NewExprLexer(val)
|
||||
exprNode, parseErr := parser.Parse(lexer)
|
||||
if parseErr != nil {
|
||||
err = errors.Join(err, fmt.Errorf("%sFailed to parse: %s", formatLocation(node), parseErr.Message))
|
||||
continue
|
||||
}
|
||||
val = val[lexer.Offset():]
|
||||
cerr := s.checkSingleExpression(exprNode)
|
||||
if cerr != nil {
|
||||
err = errors.Join(err, fmt.Errorf("%s%w", formatLocation(node), cerr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func AddFunction(funcs *[]FunctionInfo, s string, i1, i2 int) {
|
||||
*funcs = append(*funcs, FunctionInfo{
|
||||
name: s,
|
||||
min: i1,
|
||||
max: i2,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Node) UnmarshalYAML(node *yaml.Node) error {
|
||||
def := s.Schema.GetDefinition(s.Definition)
|
||||
if s.Context == nil {
|
||||
s.Context = def.Context
|
||||
}
|
||||
|
||||
isExpr, err := s.checkExpression(node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isExpr {
|
||||
return nil
|
||||
}
|
||||
if def.Mapping != nil {
|
||||
return s.checkMapping(node, def)
|
||||
} else if def.Sequence != nil {
|
||||
return s.checkSequence(node, def)
|
||||
} else if def.OneOf != nil {
|
||||
return s.checkOneOf(def, node)
|
||||
}
|
||||
|
||||
if node.Kind != yaml.ScalarNode {
|
||||
return fmt.Errorf("%sExpected a scalar got %v", formatLocation(node), getStringKind(node.Kind))
|
||||
}
|
||||
|
||||
if def.String != nil {
|
||||
return s.checkString(node, def)
|
||||
} else if def.Number != nil {
|
||||
var num float64
|
||||
return node.Decode(&num)
|
||||
} else if def.Boolean != nil {
|
||||
var b bool
|
||||
return node.Decode(&b)
|
||||
} else if def.AllowedValues != nil {
|
||||
s := node.Value
|
||||
for _, v := range *def.AllowedValues {
|
||||
if s == v {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%sExpected one of %s got %s", formatLocation(node), strings.Join(*def.AllowedValues, ","), s)
|
||||
} else if def.Null != nil {
|
||||
var myNull *byte
|
||||
return node.Decode(&myNull)
|
||||
}
|
||||
return errors.ErrUnsupported
|
||||
}
|
||||
|
||||
func (s *Node) checkString(node *yaml.Node, def Definition) error {
|
||||
val := node.Value
|
||||
if def.String.Constant != "" && def.String.Constant != val {
|
||||
return fmt.Errorf("%sExpected %s got %s", formatLocation(node), def.String.Constant, val)
|
||||
}
|
||||
if def.String.IsExpression {
|
||||
parser := actionlint.NewExprParser()
|
||||
lexer := actionlint.NewExprLexer(val + "}}")
|
||||
exprNode, parseErr := parser.Parse(lexer)
|
||||
if parseErr != nil {
|
||||
return fmt.Errorf("%sFailed to parse: %s", formatLocation(node), parseErr.Message)
|
||||
}
|
||||
cerr := s.checkSingleExpression(exprNode)
|
||||
if cerr != nil {
|
||||
return fmt.Errorf("%s%w", formatLocation(node), cerr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Node) checkOneOf(def Definition, node *yaml.Node) error {
|
||||
var allErrors error
|
||||
for _, v := range *def.OneOf {
|
||||
sub := &Node{
|
||||
Definition: v,
|
||||
Schema: s.Schema,
|
||||
Context: append(append([]string{}, s.Context...), s.Schema.GetDefinition(v).Context...),
|
||||
}
|
||||
|
||||
err := sub.UnmarshalYAML(node)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
allErrors = errors.Join(allErrors, fmt.Errorf("%sFailed to match %s: %w", formatLocation(node), v, err))
|
||||
}
|
||||
return allErrors
|
||||
}
|
||||
|
||||
func getStringKind(k yaml.Kind) string {
|
||||
switch k {
|
||||
case yaml.DocumentNode:
|
||||
return "document"
|
||||
case yaml.SequenceNode:
|
||||
return "sequence"
|
||||
case yaml.MappingNode:
|
||||
return "mapping"
|
||||
case yaml.ScalarNode:
|
||||
return "scalar"
|
||||
case yaml.AliasNode:
|
||||
return "alias"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Node) checkSequence(node *yaml.Node, def Definition) error {
|
||||
if node.Kind != yaml.SequenceNode {
|
||||
return fmt.Errorf("%sExpected a sequence got %v", formatLocation(node), getStringKind(node.Kind))
|
||||
}
|
||||
var allErrors error
|
||||
for _, v := range node.Content {
|
||||
allErrors = errors.Join(allErrors, (&Node{
|
||||
Definition: def.Sequence.ItemType,
|
||||
Schema: s.Schema,
|
||||
Context: append(append([]string{}, s.Context...), s.Schema.GetDefinition(def.Sequence.ItemType).Context...),
|
||||
}).UnmarshalYAML(v))
|
||||
}
|
||||
return allErrors
|
||||
}
|
||||
|
||||
func formatLocation(node *yaml.Node) string {
|
||||
return fmt.Sprintf("Line: %v Column %v: ", node.Line, node.Column)
|
||||
}
|
||||
|
||||
func (s *Node) checkMapping(node *yaml.Node, def Definition) error {
|
||||
if node.Kind != yaml.MappingNode {
|
||||
return fmt.Errorf("%sExpected a mapping got %v", formatLocation(node), getStringKind(node.Kind))
|
||||
}
|
||||
insertDirective := regexp.MustCompile(`\${{\s*insert\s*}}`)
|
||||
var allErrors error
|
||||
for i, k := range node.Content {
|
||||
if i%2 == 0 {
|
||||
if insertDirective.MatchString(k.Value) {
|
||||
if len(s.Context) == 0 {
|
||||
allErrors = errors.Join(allErrors, fmt.Errorf("%sinsert is not allowed here", formatLocation(k)))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
isExpr, err := s.checkExpression(k)
|
||||
if err != nil {
|
||||
allErrors = errors.Join(allErrors, err)
|
||||
continue
|
||||
}
|
||||
if isExpr {
|
||||
continue
|
||||
}
|
||||
vdef, ok := def.Mapping.Properties[k.Value]
|
||||
if !ok {
|
||||
if def.Mapping.LooseValueType == "" {
|
||||
allErrors = errors.Join(allErrors, fmt.Errorf("%sUnknown Property %v", formatLocation(k), k.Value))
|
||||
continue
|
||||
}
|
||||
vdef = MappingProperty{Type: def.Mapping.LooseValueType}
|
||||
}
|
||||
|
||||
if err := (&Node{
|
||||
Definition: vdef.Type,
|
||||
Schema: s.Schema,
|
||||
Context: append(append([]string{}, s.Context...), s.Schema.GetDefinition(vdef.Type).Context...),
|
||||
}).UnmarshalYAML(node.Content[i+1]); err != nil {
|
||||
allErrors = errors.Join(allErrors, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return allErrors
|
||||
}
|
2110
pkg/schema/workflow_schema.json
Normal file
2110
pkg/schema/workflow_schema.json
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user