mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-09 16:50:28 +00:00
fix: align runner behaviours (#1338)
Contains a number of small bug fixes where jobs behaved differently from GitHub or failed silently.
1. Hooks fail on a refused `::set-env::` or `::add-path::` and no longer inherit the last step's identity
1. Docker build, exec and wait errors are returned instead of swallowed, token-only registry credentials work, logs of `--tty` services decode
1. Env files split lines like GitHub on each OS, so a carriage return is kept on Linux and CRLF ends a line on Windows, empty heredoc names or delimiters are rejected
1. Post steps run after their pre step even when main was skipped, composites see undeclared inputs, `runs.args` wins without `with.args` being parsed
1. Docker actions skip evaluating defaults of inputs given in `with` and read `entryPoint` and `args` in any case, a lowercase `dockerfile` is found, undeclared inputs get GitHub's warning
1. A user-set `CI` is kept, `GITHUB_REPOSITORY_ID` and the other missing `GITHUB_*` variables are exported
1. `timeout-minutes` can read the step's own `env`, a step-level `ACTIONS_ALLOW_UNSECURE_COMMANDS: false` beats a job-level `true` while the runner's own opt-in still wins, `pwsh` quotes the script path
1. A step `env:` given as one expression such as `${{ fromJSON(matrix.env) }}` is applied, `hashFiles` fails with GitHub's messages on script errors and after 120 seconds, a job image without node still gets an empty hash
1. A step whose command files cannot be reset fails instead of importing stale ones, a timeout between steps fails the job instead of reporting success
1. One named volume can be mounted at several paths, host-mode PTY steps keep their last output when they fail, host scratch directories are removed when job setup fails
1. Cancelling a step right after it starts kills its whole process tree
1. Cache prefix restores pick the newest entry created within the same second, also on S3, cache commits with a wrong size are rejected
1. Artifact chunks are written at their offset, download URLs are escaped, listing a run without artifacts no longer crashes
1. Poll backoff reaches `fetch_interval_max`, proxy passwords are masked in more forms, secrets are also masked in step names and in their quoted form in the runner log, a failed final report is logged
1. On Windows hosts a differently cased unsecure-commands opt-in or proxy variable counts, and an early cancel no longer leaks a job object handle
1. Interactive `register` prompts for a missing token and stops on SIGTERM, `exec --list --detect-event` no longer panics without events, `config set` works on `cache.s3` keys
Reviewed-on: https://gitea.com/gitea/runner/pulls/1338
Reviewed-by: bircni <[email protected]>
Co-authored-by: silverwind <[email protected]>
This commit is contained in:
1 parent
c719a88f36
commit
fe2a66dc25
68 files changed
+1413
-281
No files matched your search
+97
-12
@@ -19,6 +19,7 @@ import (
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
"gitea.com/gitea/runner/act/common/git"
|
||||
"gitea.com/gitea/runner/act/common/logging/logtest"
|
||||
"gitea.com/gitea/runner/act/container"
|
||||
|
||||
"gitea.dev/actionslib/pkg/model"
|
||||
@@ -85,6 +86,19 @@ runs:
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "readLowercaseDockerfile",
|
||||
step: &model.Step{},
|
||||
filename: "dockerfile",
|
||||
fileContent: "FROM ubuntu:20.04",
|
||||
expected: &model.Action{
|
||||
Name: "(Synthetic)",
|
||||
Runs: model.ActionRuns{
|
||||
Using: "docker",
|
||||
Image: "dockerfile",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "readWithArgs",
|
||||
step: &model.Step{
|
||||
@@ -95,6 +109,7 @@ runs:
|
||||
expected: &model.Action{
|
||||
Name: "(Synthetic)",
|
||||
Inputs: map[string]model.Input{
|
||||
"args": {},
|
||||
"cwd": {
|
||||
Description: "(Actual working directory)",
|
||||
Required: false,
|
||||
@@ -147,14 +162,33 @@ func TestActionRunner(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
inputs map[string]model.Input
|
||||
with map[string]string
|
||||
state map[string]string
|
||||
expectedEnv map[string]string
|
||||
warning string
|
||||
}{
|
||||
{
|
||||
name: "with-input",
|
||||
inputs: map[string]model.Input{"key": {Default: "default value"}},
|
||||
expectedEnv: map[string]string{"INPUT_KEY": "default value"},
|
||||
},
|
||||
{
|
||||
name: "declared input ignores case",
|
||||
inputs: map[string]model.Input{"key": {Default: "default value"}},
|
||||
with: map[string]string{"KEY": "provided value"},
|
||||
expectedEnv: map[string]string{"INPUT_KEY": "provided value"},
|
||||
},
|
||||
{
|
||||
name: "unexpected inputs warn once across stages",
|
||||
inputs: map[string]model.Input{"key": {}},
|
||||
with: map[string]string{"KEY": "provided value", "UNKNOWN": "value", "other%0A\r\n": "value"},
|
||||
warning: "##[warning]Unexpected input(s) 'UNKNOWN', 'other%250A%0D%0A', valid inputs are ['key']",
|
||||
},
|
||||
{
|
||||
name: "no declared inputs",
|
||||
with: map[string]string{"unknown": "value"},
|
||||
warning: "##[warning]Unexpected input(s) 'unknown', valid inputs are ['']",
|
||||
},
|
||||
{
|
||||
name: "restore-saved-state",
|
||||
state: map[string]string{"name": "state value"},
|
||||
@@ -162,6 +196,8 @@ func TestActionRunner(t *testing.T) {
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
logger, hook := logtest.NewLogger()
|
||||
ctx := common.WithLogger(t.Context(), logger)
|
||||
cm := &containerMock{}
|
||||
containerActionDir := "/var/run/act/actions/" + model.UsesHash("/org/repo/path@ref") + "/"
|
||||
cm.On("CopyDir", containerActionDir, "dir/", false, true).Return(func(_ctx context.Context) error { return nil })
|
||||
@@ -175,21 +211,36 @@ func TestActionRunner(t *testing.T) {
|
||||
return true
|
||||
})
|
||||
cm.On("Exec", []string{"node", "--preserve-symlinks-main", containerActionDir + "path"}, envMatcher, "", "").Return(func(_ctx context.Context) error { return nil })
|
||||
cm.On("Exec", nodeActionCommand(containerActionDir+"path/pre.js"), mock.Anything, "", "").Return(func(_ctx context.Context) error { return nil })
|
||||
cm.On("Exec", nodeActionCommand(containerActionDir+"path/post.js"), envMatcher, "", "").Return(func(_ctx context.Context) error { return nil })
|
||||
|
||||
remoteAction := mustNewRemoteAction(t, "org/repo/path@ref")
|
||||
step := &stepActionRemote{
|
||||
Step: &model.Step{ID: "step", Uses: "org/repo/path@ref"},
|
||||
Step: &model.Step{ID: "step", Uses: "org/repo/path@ref", With: tt.with},
|
||||
RunContext: &RunContext{
|
||||
Config: &Config{},
|
||||
Config: &Config{PresetGitHubContext: &model.GithubContext{}},
|
||||
Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job": {}}}},
|
||||
IntraActionState: map[string]map[string]string{"step": tt.state},
|
||||
JobContainer: cm,
|
||||
},
|
||||
action: &model.Action{Inputs: tt.inputs, Runs: model.ActionRuns{Using: "node16"}},
|
||||
env: map[string]string{},
|
||||
action: &model.Action{Inputs: tt.inputs, Runs: model.ActionRuns{Using: "node16", Pre: "pre.js", Post: "post.js"}},
|
||||
actionDir: "dir",
|
||||
env: (&model.Step{With: tt.with}).GetEnv(),
|
||||
remoteAction: remoteAction,
|
||||
}
|
||||
require.NoError(t, runActionImpl(step, "dir", remoteAction)(t.Context()))
|
||||
require.NoError(t, runPreStep(step)(ctx))
|
||||
assert.Empty(t, hook.AllRecords())
|
||||
require.NoError(t, runActionImpl(step, "dir", remoteAction)(ctx))
|
||||
require.NoError(t, runPostStep(step)(ctx))
|
||||
if tt.warning == "" {
|
||||
assert.Empty(t, hook.AllRecords())
|
||||
} else {
|
||||
require.Len(t, hook.AllRecords(), 1)
|
||||
assert.Equal(t, tt.warning, hook.LastRecord().Message)
|
||||
rawOutput, ok := hook.LastRecord().Attr(rawOutputField)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, true, rawOutput)
|
||||
}
|
||||
cm.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
@@ -473,10 +524,12 @@ func TestExecAsDockerStageEntrypoint(t *testing.T) {
|
||||
name string
|
||||
stage stepStage
|
||||
with map[string]string
|
||||
inputs map[string]model.Input
|
||||
runs model.ActionRuns
|
||||
env map[string]string
|
||||
wantCmd []string
|
||||
wantEntrypoint []string
|
||||
warning string
|
||||
}{
|
||||
{
|
||||
name: "main stage prefers manifest values",
|
||||
@@ -491,17 +544,36 @@ func TestExecAsDockerStageEntrypoint(t *testing.T) {
|
||||
wantEntrypoint: []string{"main.sh"},
|
||||
},
|
||||
{
|
||||
name: "main stage uses caller fallbacks",
|
||||
name: "main stage uses caller fallbacks in any case and warns about undeclared inputs",
|
||||
stage: stepStageMain,
|
||||
with: map[string]string{"args": "caller --flag", "entrypoint": "input.sh --verbose"},
|
||||
with: map[string]string{"ARGS": "caller --flag", "entryPoint": "input.sh --verbose", "unknown": "value"},
|
||||
inputs: map[string]model.Input{"message": {}},
|
||||
runs: model.ActionRuns{Env: map[string]string{"ACTION_ONLY": "manifest"}},
|
||||
wantCmd: []string{"caller", "--flag"},
|
||||
wantEntrypoint: []string{"input.sh", "--verbose"},
|
||||
warning: "##[warning]Unexpected input(s) 'unknown', valid inputs are ['entryPoint', 'args', 'message']",
|
||||
},
|
||||
{
|
||||
name: "manifest args keep caller input quotes literal",
|
||||
stage: stepStageMain,
|
||||
with: map[string]string{"args": "it's literal"},
|
||||
runs: model.ActionRuns{Args: []string{"${{ inputs.args }}"}},
|
||||
env: map[string]string{"INPUT_ARGS": "it's literal"},
|
||||
wantCmd: []string{"it's literal"},
|
||||
},
|
||||
{
|
||||
name: "caller input skips its default expression",
|
||||
stage: stepStageMain,
|
||||
with: map[string]string{"MESSAGE": "provided"},
|
||||
inputs: map[string]model.Input{"message": {Default: "${{ fromJSON('') }}"}},
|
||||
runs: model.ActionRuns{Args: []string{"${{ inputs.message }}"}},
|
||||
env: map[string]string{"INPUT_MESSAGE": "provided"},
|
||||
wantCmd: []string{"provided"},
|
||||
},
|
||||
{
|
||||
name: "explicit empty manifest args suppress caller args",
|
||||
stage: stepStageMain,
|
||||
with: map[string]string{"args": "caller"},
|
||||
with: map[string]string{"args": "it's ignored"},
|
||||
runs: model.ActionRuns{Args: []string{}},
|
||||
wantCmd: []string{},
|
||||
},
|
||||
@@ -527,6 +599,8 @@ func TestExecAsDockerStageEntrypoint(t *testing.T) {
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
logger, hook := logtest.NewLogger()
|
||||
ctx := common.WithLogger(t.Context(), logger)
|
||||
tc.runs.Using, tc.runs.Image = "docker", "docker://node:14"
|
||||
cm := &containerMock{}
|
||||
var input *container.NewContainerInput
|
||||
@@ -538,12 +612,13 @@ func TestExecAsDockerStageEntrypoint(t *testing.T) {
|
||||
step := &stepActionRemote{
|
||||
Step: &model.Step{ID: "1", Uses: "org/action@v1", With: tc.with},
|
||||
RunContext: &RunContext{
|
||||
Config: &Config{},
|
||||
Config: &Config{PresetGitHubContext: &model.GithubContext{}},
|
||||
Run: &model.Run{JobID: "1", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"1": {}}}},
|
||||
JobContainer: cm,
|
||||
},
|
||||
action: &model.Action{Runs: tc.runs},
|
||||
env: mergeMaps(tc.env),
|
||||
action: &model.Action{Inputs: tc.inputs, Runs: tc.runs},
|
||||
env: mergeMaps(tc.env),
|
||||
remoteAction: mustNewRemoteAction(t, "org/action@v1"),
|
||||
}
|
||||
|
||||
cm.On("Pull", false).Return(func(context.Context) error { return nil })
|
||||
@@ -552,7 +627,17 @@ func TestExecAsDockerStageEntrypoint(t *testing.T) {
|
||||
cm.On("Start", true).Return(func(context.Context) error { return nil })
|
||||
cm.On("Close").Return(func(context.Context) error { return nil })
|
||||
|
||||
require.NoError(t, execAsDocker(t.Context(), step, "action", t.TempDir(), t.TempDir(), false, tc.stage))
|
||||
if tc.stage == stepStageMain {
|
||||
require.NoError(t, runActionImpl(step, t.TempDir(), step.remoteAction)(ctx))
|
||||
} else {
|
||||
require.NoError(t, execAsDocker(ctx, step, "action", t.TempDir(), t.TempDir(), false, tc.stage))
|
||||
}
|
||||
if tc.warning == "" {
|
||||
assert.Empty(t, hook.AllRecords())
|
||||
} else {
|
||||
require.Len(t, hook.AllRecords(), 1)
|
||||
assert.Equal(t, tc.warning, hook.LastRecord().Message)
|
||||
}
|
||||
require.NotNil(t, input)
|
||||
assert.Equal(t, tc.wantCmd, input.Cmd)
|
||||
assert.Equal(t, tc.wantEntrypoint, input.Entrypoint)
|
||||
|
||||
Reference in new issue
Block a user