fix(actions): keep step-level continue-on-error expressions unevaluated (#39141)

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
bircni
2026-08-27 07:46:46 +02:00
committed by GitHub
parent 2c6dacb015
commit 3b6ce586a7
2 changed files with 66 additions and 11 deletions
+24 -11
View File
@@ -7,6 +7,7 @@ import (
"bytes"
"errors"
"fmt"
"strings"
"gitea.dev/actionslib/pkg/expreval"
"gitea.dev/actionslib/pkg/exprparser"
@@ -175,17 +176,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
@@ -4,8 +4,11 @@
package jobparser
import (
"bytes"
"testing"
"gitea.dev/actionslib/pkg/model"
"github.com/stretchr/testify/require"
)
@@ -62,3 +65,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)
}
}