fix: fail the step or job whose expression cannot be interpolated (#1199)

An expression that failed to evaluate was logged and replaced by an empty string, so a `run:` step executed an empty script and reported success. The same swallow covered `shell:`, `working-directory:`, step `env:`, `with:`, `uses:`, the job's `env:`, `container:`, `services:`, `runs-on:` and outputs, and a called workflow's `with:` and `secrets:`. Every interpolation now propagates its error as actions/runner does: step-level values fail the step, job-level values fail the job at setup, `timeout-minutes` logs the error and runs unbounded, and a job or step name keeps its source text.

`defaults.run` and a called workflow's inputs and secrets are resolved once at job setup with the job context rather than per step, and the job's image is resolved once, so host mode and `ImageOS` derive from the image the job started with.

Closes https://gitea.com/gitea/runner/issues/392
Closes https://gitea.com/gitea/runner/issues/555

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1199
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni
2026-09-03 10:43:04 +00:00
committed by silverwind
co-authored by silverwind silverwind
parent 7f53eca716
commit 1745c7c841
20 changed files with 495 additions and 290 deletions
+28 -17
View File
@@ -6,6 +6,7 @@ package runner
import (
"bytes"
"cmp"
"context"
"io"
"os"
@@ -42,17 +43,12 @@ func TestStepRun(t *testing.T) {
JobID: "1",
Workflow: &model.Workflow{
Jobs: map[string]*model.Job{
"1": {
Defaults: model.Defaults{
Run: model.RunDefaults{
Shell: "bash",
},
},
},
"1": {},
},
},
},
JobContainer: cm,
JobContainer: cm,
jobRunDefaults: model.RunDefaults{Shell: "bash"},
},
Step: &model.Step{
ID: "1",
@@ -84,13 +80,13 @@ func TestStepRun(t *testing.T) {
func TestStepRunShellParity(t *testing.T) {
tests := []struct {
name, shell, workingDir string
env map[string]string
host bool
probeErr error
wantExt string
wantCmd []string
wantErr string
name, run, shell, workingDir string
env map[string]string
host bool
probeErr error
wantExt string
wantCmd []string
wantErr string
}{
{
name: "implicit host bash",
@@ -130,6 +126,21 @@ func TestStepRunShellParity(t *testing.T) {
wantExt: ".py",
wantCmd: []string{"python", "/var/run/act/workflow/1.py"},
},
{
name: "run expression without a context",
run: "echo ${{ COMMIT_SHA }}",
wantErr: "unable to interpolate the run script:",
},
{
name: "shell expression without a context",
shell: "${{ SHELL }}",
wantErr: "unable to interpolate the shell:",
},
{
name: "working directory expression without a context",
workingDir: "${{ DIR }}",
wantErr: "unable to interpolate the working directory:",
},
}
for _, test := range tests {
@@ -156,13 +167,13 @@ func TestStepRunShellParity(t *testing.T) {
Run: &model.Run{JobID: "1", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"1": {}}}},
JobContainer: jobContainer,
},
Step: &model.Step{ID: "1", Run: "echo hi", Shell: test.shell, WorkingDirectory: test.workingDir},
Step: &model.Step{ID: "1", Run: cmp.Or(test.run, "echo hi"), Shell: test.shell, WorkingDirectory: test.workingDir},
env: test.env,
}
name, script, err := sr.setupShellCommand(t.Context())
if test.wantErr != "" {
require.EqualError(t, err, test.wantErr)
require.ErrorContains(t, err, test.wantErr)
return
}
require.NoError(t, err)