mirror of
https://code.forgejo.org/forgejo/runner.git
synced 2025-08-11 17:50:58 +00:00
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 (cherry picked from commit 64219df0f2155d75ffc4423dc93c1e80bb4740bc) Conflicts: go.mod go.sum pkg/model/workflow.go trivial context conflict & go.mod upgrades
This commit is contained in:
parent
7eb547faa5
commit
65ae238f17
10 changed files with 2847 additions and 66 deletions
|
@ -5,6 +5,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/schema"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -82,6 +83,18 @@ type Action struct {
|
||||||
} `yaml:"branding"`
|
} `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.
|
// 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 {
|
type Input struct {
|
||||||
Description string `yaml:"description"`
|
Description string `yaml:"description"`
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/common"
|
"github.com/nektos/act/pkg/common"
|
||||||
|
"github.com/nektos/act/pkg/schema"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
@ -92,6 +93,18 @@ func (w *Workflow) OnSchedule() []string {
|
||||||
return []string{}
|
return []string{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
type WorkflowDispatchInput struct {
|
||||||
Description string `yaml:"description"`
|
Description string `yaml:"description"`
|
||||||
Required bool `yaml:"required"`
|
Required bool `yaml:"required"`
|
||||||
|
@ -488,7 +501,7 @@ func (j *Job) GetMatrixes() ([]map[string]interface{}, error) {
|
||||||
return matrixes, nil
|
return matrixes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func commonKeysMatch(a map[string]interface{}, b map[string]interface{}) bool {
|
func commonKeysMatch(a, b map[string]interface{}) bool {
|
||||||
for aKey, aVal := range a {
|
for aKey, aVal := range a {
|
||||||
if bVal, ok := b[aKey]; ok && !reflect.DeepEqual(aVal, bVal) {
|
if bVal, ok := b[aKey]; ok && !reflect.DeepEqual(aVal, bVal) {
|
||||||
return false
|
return false
|
||||||
|
@ -497,7 +510,7 @@ func commonKeysMatch(a map[string]interface{}, b map[string]interface{}) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func commonKeysMatch2(a map[string]interface{}, b map[string]interface{}, m map[string][]interface{}) bool {
|
func commonKeysMatch2(a, b map[string]interface{}, m map[string][]interface{}) bool {
|
||||||
for aKey, aVal := range a {
|
for aKey, aVal := range a {
|
||||||
_, useKey := m[aKey]
|
_, useKey := m[aKey]
|
||||||
if bVal, ok := b[aKey]; useKey && ok && !reflect.DeepEqual(aVal, bVal) {
|
if bVal, ok := b[aKey]; useKey && ok && !reflect.DeepEqual(aVal, bVal) {
|
||||||
|
|
|
@ -440,15 +440,8 @@ jobs:
|
||||||
uses: ./local-action
|
uses: ./local-action
|
||||||
`
|
`
|
||||||
|
|
||||||
workflow, err := ReadWorkflow(strings.NewReader(yaml))
|
_, err := ReadWorkflow(strings.NewReader(yaml))
|
||||||
assert.NoError(t, err, "read workflow should succeed")
|
assert.Error(t, err, "read workflow should fail")
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// See: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idoutputs
|
// See: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idoutputs
|
||||||
|
|
|
@ -197,6 +197,9 @@ func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config
|
||||||
assert.Nil(t, err, j.workflowPath)
|
assert.Nil(t, err, j.workflowPath)
|
||||||
|
|
||||||
planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true)
|
planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true)
|
||||||
|
if err != nil {
|
||||||
|
assert.Error(t, err, j.errorMessage)
|
||||||
|
} else {
|
||||||
assert.Nil(t, err, fullWorkflowPath)
|
assert.Nil(t, err, fullWorkflowPath)
|
||||||
|
|
||||||
plan, err := planner.PlanEvent(j.eventName)
|
plan, err := planner.PlanEvent(j.eventName)
|
||||||
|
@ -209,6 +212,7 @@ func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config
|
||||||
assert.Error(t, err, j.errorMessage)
|
assert.Error(t, err, j.errorMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("::endgroup::")
|
fmt.Println("::endgroup::")
|
||||||
}
|
}
|
||||||
|
@ -331,7 +335,7 @@ func TestRunEvent(t *testing.T) {
|
||||||
config.EventPath = eventFile
|
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 {
|
if file, err := os.ReadFile(testConfigFile); err == nil {
|
||||||
testConfig := &TestConfig{}
|
testConfig := &TestConfig{}
|
||||||
if yaml.Unmarshal(file, testConfig) == nil {
|
if yaml.Unmarshal(file, testConfig) == nil {
|
||||||
|
|
|
@ -5,7 +5,7 @@ runs:
|
||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
# Test if GITHUB_ACTION_PATH is set correctly before all steps
|
# Test if GITHUB_ACTION_PATH is set correctly before all steps
|
||||||
- run: stat $GITHUB_ACTION_PATH/push.yml
|
- run: stat $GITHUB_ACTION_PATH/../push.yml
|
||||||
shell: bash
|
shell: bash
|
||||||
- run: stat $GITHUB_ACTION_PATH/action.yml
|
- run: stat $GITHUB_ACTION_PATH/action.yml
|
||||||
shell: bash
|
shell: bash
|
||||||
|
@ -36,7 +36,7 @@ runs:
|
||||||
with:
|
with:
|
||||||
who-to-greet: 'Mona the Octocat'
|
who-to-greet: 'Mona the Octocat'
|
||||||
# Test if GITHUB_ACTION_PATH is set correctly after all steps
|
# Test if GITHUB_ACTION_PATH is set correctly after all steps
|
||||||
- run: stat $GITHUB_ACTION_PATH/push.yml
|
- run: stat $GITHUB_ACTION_PATH/../push.yml
|
||||||
shell: bash
|
shell: bash
|
||||||
- run: stat $GITHUB_ACTION_PATH/action.yml
|
- run: stat $GITHUB_ACTION_PATH/action.yml
|
||||||
shell: bash
|
shell: bash
|
|
@ -6,4 +6,4 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: ./local-action-via-composite-dockerfile
|
- uses: ./local-action-via-composite-dockerfile/action
|
261
act/schema/action_schema.json
Normal file
261
act/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
act/schema/schema.go
Normal file
387
act/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
act/schema/workflow_schema.json
Normal file
2110
act/schema/workflow_schema.json
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue