diff --git a/modules/actions/jobparser/model.go b/modules/actions/jobparser/model.go index 501633972b5..727b30da931 100644 --- a/modules/actions/jobparser/model.go +++ b/modules/actions/jobparser/model.go @@ -7,6 +7,7 @@ import ( "bytes" "errors" "fmt" + "strings" "gitea.dev/modules/util" @@ -173,17 +174,29 @@ func (j *Job) RunsOn() []string { } type Step struct { - ID string `yaml:"id,omitempty"` - If yaml.Node `yaml:"if,omitempty"` - Name string `yaml:"name,omitempty"` - Uses string `yaml:"uses,omitempty"` - Run string `yaml:"run,omitempty"` - WorkingDirectory string `yaml:"working-directory,omitempty"` - Shell string `yaml:"shell,omitempty"` - Env yaml.Node `yaml:"env,omitempty"` - With map[string]string `yaml:"with,omitempty"` - ContinueOnError bool `yaml:"continue-on-error,omitempty"` - TimeoutMinutes string `yaml:"timeout-minutes,omitempty"` + ID string `yaml:"id,omitempty"` + If yaml.Node `yaml:"if,omitempty"` + Name string `yaml:"name,omitempty"` + Uses string `yaml:"uses,omitempty"` + Run string `yaml:"run,omitempty"` + WorkingDirectory string `yaml:"working-directory,omitempty"` + Shell string `yaml:"shell,omitempty"` + Env yaml.Node `yaml:"env,omitempty"` + With map[string]string `yaml:"with,omitempty"` + RawContinueOnError yaml.Node `yaml:"continue-on-error,omitempty"` // raw: the runner evaluates it with the steps context + TimeoutMinutes string `yaml:"timeout-minutes,omitempty"` +} + +// UnmarshalYAML canonicalizes booleans like continue-on-error +func (s *Step) UnmarshalYAML(node *yaml.Node) error { + type rawStep Step + if err := node.Decode((*rawStep)(s)); err != nil { + return err + } + if raw := &s.RawContinueOnError; raw.Tag == "!!bool" { + raw.Value = strings.ToLower(raw.Value) + } + return nil } // String gets the name of step diff --git a/modules/actions/jobparser/roundtrip_test.go b/modules/actions/jobparser/roundtrip_test.go index ae4fa4dee2e..400b8ccd455 100644 --- a/modules/actions/jobparser/roundtrip_test.go +++ b/modules/actions/jobparser/roundtrip_test.go @@ -4,8 +4,10 @@ package jobparser import ( + "bytes" "testing" + "gitea.com/gitea/runner/act/model" "github.com/stretchr/testify/require" ) @@ -62,3 +64,42 @@ jobs: require.Len(t, gotJob.Steps, 1) require.Equal(t, wantRun, gotJob.Steps[0].Run, "round-trip must preserve run content; got payload:\n%s", payload) } + +// Typing a step's continue-on-error as a bool used to reject the whole `jobs:` node. +func TestSingleWorkflowRoundTripStepContinueOnError(t *testing.T) { + const wf = `name: demo +on: push +jobs: + job1: + runs-on: ubuntu-latest + steps: + - id: quarantine + run: echo "q=true" >> "$GITHUB_OUTPUT" + - run: exit 1 + continue-on-error: ${{ steps.quarantine.outputs.q == 'true' }} + - run: exit 1 + continue-on-error: true + - run: exit 1 + continue-on-error: TRUE + - run: exit 1 + continue-on-error: false + - run: exit 1 + continue-on-error: yes +` + want := []string{"", "${{ steps.quarantine.outputs.q == 'true' }}", "true", "true", "false", "yes"} + + sws, err := Parse([]byte(wf)) + require.NoError(t, err) + require.Len(t, sws, 1) + + payload, err := sws[0].Marshal() + require.NoError(t, err) + + rw, err := model.ReadWorkflow(bytes.NewReader(payload)) + require.NoError(t, err, "payload:\n%s", payload) + steps := rw.Jobs["job1"].Steps + require.Len(t, steps, len(want)) + for i, w := range want { + require.Equal(t, w, steps[i].RawContinueOnError, "step %d, payload:\n%s", i, payload) + } +}