mirror of
https://gitea.com/gitea/runner.git
synced 2026-08-27 22:37:46 +00:00
12dc9d26a2
Fixes 51 bugs discovered via comparison with `actions/runner`. Every fix has test coverage.
### Secrets
- A short secret registered no shifted-base64 form, so `base64("user:$TOKEN")` printed in the clear
- Encoded forms came only from the whole trimmed value, missing padded and per-line spellings
- Masks split only on `\n`, so `::add-mask::a%0Db` registered neither half
- Adds XML, expression-string and quote-trimming encoders
### Workflow commands
- Split at the last `::` or `]` rather than the first, so `::add-mask::a::b` registered no mask
- A command on the last line without a newline was ignored, and `::ADD-MASK::` did nothing
- `##[...]` did not decode `%3B`/`%5D`, properties lost anything after a second `=`
- `$GITHUB_ENV` and `::set-env::` now refuse `NODE_OPTIONS`
### Status
- `continue-on-error` reported failed, a cancelled job reported success, an `if:` error reported cancelled
- File commands ran after `continue-on-error`, failing the job while the step stayed green
- A bad job output aborted the whole run instead of that job
### Steps and actions
- `${{ matrix.* }}` and `${{ strategy.* }}` were empty inside composite actions
- Composite inputs leaked into nested actions as `INPUT_*`, `with:` matched case-sensitively, `pre` failures were dropped
- Docker actions dropped `runs.env` when the caller passed `with: args:`, and caller `args`/`entrypoint` beat the manifest
- An implicit shell ran with `pipefail`, a `shell:` without `{0}` passed without running
- `container.env` overrode job env and every `$GITHUB_ENV` write, heredocs lost leading blank lines, `$GITHUB_PATH` was not BOM-decoded
Written by Claude Opus 5.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1194
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
499 lines
18 KiB
Go
499 lines
18 KiB
Go
// Copyright 2022 The Gitea Authors. All rights reserved.
|
|
// Copyright 2022 The nektos/act Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package runner
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"testing"
|
|
|
|
"gitea.com/gitea/runner/act/common"
|
|
"gitea.com/gitea/runner/act/container"
|
|
|
|
"gitea.dev/actionslib/pkg/model"
|
|
log "github.com/sirupsen/logrus"
|
|
logrustest "github.com/sirupsen/logrus/hooks/test"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
yaml "go.yaml.in/yaml/v4"
|
|
)
|
|
|
|
func TestMergeIntoMap(t *testing.T) {
|
|
table := []struct {
|
|
name string
|
|
target map[string]string
|
|
maps []map[string]string
|
|
expected map[string]string
|
|
}{
|
|
{
|
|
name: "testEmptyMap",
|
|
target: map[string]string{},
|
|
maps: []map[string]string{},
|
|
expected: map[string]string{},
|
|
},
|
|
{
|
|
name: "testMergeIntoEmptyMap",
|
|
target: map[string]string{},
|
|
maps: []map[string]string{
|
|
{
|
|
"key1": "value1",
|
|
"key2": "value2",
|
|
}, {
|
|
"key2": "overridden",
|
|
"key3": "value3",
|
|
},
|
|
},
|
|
expected: map[string]string{
|
|
"key1": "value1",
|
|
"key2": "overridden",
|
|
"key3": "value3",
|
|
},
|
|
},
|
|
{
|
|
name: "testMergeIntoExistingMap",
|
|
target: map[string]string{
|
|
"key1": "value1",
|
|
"key2": "value2",
|
|
},
|
|
maps: []map[string]string{
|
|
{
|
|
"key1": "overridden",
|
|
},
|
|
},
|
|
expected: map[string]string{
|
|
"key1": "overridden",
|
|
"key2": "value2",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range table {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mergeIntoMapCaseSensitive(tt.target, tt.maps...)
|
|
assert.Equal(t, tt.expected, tt.target)
|
|
mergeIntoMapCaseInsensitive(tt.target, tt.maps...)
|
|
assert.Equal(t, tt.expected, tt.target)
|
|
})
|
|
}
|
|
}
|
|
|
|
type stepMock struct {
|
|
mock.Mock
|
|
step
|
|
}
|
|
|
|
func (sm *stepMock) pre() common.Executor {
|
|
args := sm.Called()
|
|
return args.Get(0).(func(context.Context) error)
|
|
}
|
|
|
|
func (sm *stepMock) main() common.Executor {
|
|
args := sm.Called()
|
|
return args.Get(0).(func(context.Context) error)
|
|
}
|
|
|
|
func (sm *stepMock) post() common.Executor {
|
|
args := sm.Called()
|
|
return args.Get(0).(func(context.Context) error)
|
|
}
|
|
|
|
func (sm *stepMock) getRunContext() *RunContext {
|
|
args := sm.Called()
|
|
return args.Get(0).(*RunContext)
|
|
}
|
|
|
|
func (sm *stepMock) getGithubContext(ctx context.Context) *model.GithubContext {
|
|
args := sm.Called()
|
|
return args.Get(0).(*RunContext).getGithubContext(ctx)
|
|
}
|
|
|
|
func (sm *stepMock) getStepModel() *model.Step {
|
|
args := sm.Called()
|
|
return args.Get(0).(*model.Step)
|
|
}
|
|
|
|
func (sm *stepMock) getEnv() *map[string]string {
|
|
args := sm.Called()
|
|
return args.Get(0).(*map[string]string)
|
|
}
|
|
|
|
func TestSetupEnv(t *testing.T) {
|
|
cm := &containerMock{}
|
|
sm := &stepMock{}
|
|
|
|
rc := &RunContext{
|
|
Config: &Config{
|
|
Env: map[string]string{
|
|
"GITHUB_RUN_ID": "runId",
|
|
},
|
|
},
|
|
Run: &model.Run{
|
|
JobID: "1",
|
|
Workflow: &model.Workflow{
|
|
Jobs: map[string]*model.Job{
|
|
"1": {
|
|
Env: yaml.Node{
|
|
Value: "JOB_KEY: jobvalue",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Env: map[string]string{
|
|
"RC_KEY": "rcvalue",
|
|
},
|
|
JobContainer: cm,
|
|
}
|
|
step := &model.Step{
|
|
With: map[string]string{
|
|
"STEP_WITH": "with-value",
|
|
},
|
|
}
|
|
env := map[string]string{}
|
|
|
|
sm.On("getRunContext").Return(rc)
|
|
sm.On("getGithubContext").Return(rc)
|
|
sm.On("getStepModel").Return(step)
|
|
sm.On("getEnv").Return(&env)
|
|
|
|
setupEnv(context.Background(), sm)
|
|
|
|
// These are commit or system specific
|
|
delete(env, "GITHUB_REF")
|
|
delete(env, "GITHUB_REF_NAME")
|
|
delete(env, "GITHUB_REF_TYPE")
|
|
delete(env, "GITHUB_SHA")
|
|
delete(env, "GITHUB_WORKSPACE")
|
|
delete(env, "GITHUB_REPOSITORY")
|
|
delete(env, "GITHUB_REPOSITORY_OWNER")
|
|
delete(env, "GITHUB_ACTOR")
|
|
// Host-dependent, asserted in TestRunContextWithGithubEnvRunnerValues instead.
|
|
delete(env, "RUNNER_NAME")
|
|
delete(env, "RUNNER_WORKSPACE")
|
|
|
|
assert.Equal(t, map[string]string{
|
|
"ACT": "true",
|
|
"ACT_SKIP_CHECKOUT": "true",
|
|
"CI": "true",
|
|
"GITHUB_ACTION": "",
|
|
"GITHUB_ACTIONS": "true",
|
|
"GITHUB_ACTION_PATH": "",
|
|
"GITHUB_ACTION_REF": "",
|
|
"GITHUB_ACTION_REPOSITORY": "",
|
|
"GITHUB_API_URL": "https:///api/v1", // Gitea uses api/v1 (upstream GitHub: api/v3)
|
|
"GITHUB_BASE_REF": "",
|
|
"GITHUB_EVENT_NAME": "",
|
|
"GITHUB_EVENT_PATH": "/var/run/act/workflow/event.json",
|
|
"GITHUB_GRAPHQL_URL": "",
|
|
"GITHUB_HEAD_REF": "",
|
|
"GITHUB_JOB": "1",
|
|
"GITHUB_RETENTION_DAYS": "0",
|
|
"GITHUB_RUN_ATTEMPT": "",
|
|
"GITHUB_RUN_ID": "runId",
|
|
"GITHUB_RUN_NUMBER": "1",
|
|
"GITHUB_SERVER_URL": "https://",
|
|
"GITHUB_WORKFLOW": "",
|
|
"INPUT_STEP_WITH": "with-value",
|
|
"RC_KEY": "rcvalue",
|
|
"RUNNER_ENVIRONMENT": "self-hosted",
|
|
"RUNNER_PERFLOG": "/dev/null",
|
|
"RUNNER_TRACKING_ID": "",
|
|
}, env)
|
|
|
|
cm.AssertExpectations(t)
|
|
}
|
|
|
|
func TestIsStepEnabled(t *testing.T) {
|
|
createTestStep := func(t *testing.T, input string) step {
|
|
var step *model.Step
|
|
err := yaml.Unmarshal([]byte(input), &step)
|
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
|
|
|
return &stepRun{
|
|
RunContext: &RunContext{
|
|
Config: &Config{Workdir: ".", PlatformPicker: func([]string) string { return "ubuntu-latest" }},
|
|
StepResults: map[string]*model.StepResult{},
|
|
Env: map[string]string{},
|
|
Run: &model.Run{
|
|
JobID: "job1",
|
|
Workflow: &model.Workflow{
|
|
Name: "workflow1",
|
|
Jobs: map[string]*model.Job{
|
|
"job1": createJob(t, `runs-on: ubuntu-latest`, ""),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Step: step,
|
|
}
|
|
}
|
|
|
|
log.SetLevel(log.DebugLevel)
|
|
assertObject := assert.New(t)
|
|
|
|
// success()
|
|
step := createTestStep(t, "if: success()")
|
|
assertObject.True(isStepEnabled(context.Background(), step.getIfExpression(context.Background(), stepStageMain), step, stepStageMain))
|
|
|
|
step = createTestStep(t, "if: success()")
|
|
step.getRunContext().StepResults["a"] = &model.StepResult{
|
|
Conclusion: model.StepStatusSuccess,
|
|
}
|
|
assertObject.True(isStepEnabled(context.Background(), step.getStepModel().If.Value, step, stepStageMain))
|
|
|
|
step = createTestStep(t, "if: success()")
|
|
step.getRunContext().StepResults["a"] = &model.StepResult{
|
|
Conclusion: model.StepStatusFailure,
|
|
}
|
|
assertObject.False(isStepEnabled(context.Background(), step.getStepModel().If.Value, step, stepStageMain))
|
|
|
|
// failure()
|
|
step = createTestStep(t, "if: failure()")
|
|
assertObject.False(isStepEnabled(context.Background(), step.getStepModel().If.Value, step, stepStageMain))
|
|
|
|
step = createTestStep(t, "if: failure()")
|
|
step.getRunContext().StepResults["a"] = &model.StepResult{
|
|
Conclusion: model.StepStatusSuccess,
|
|
}
|
|
assertObject.False(isStepEnabled(context.Background(), step.getStepModel().If.Value, step, stepStageMain))
|
|
|
|
step = createTestStep(t, "if: failure()")
|
|
step.getRunContext().StepResults["a"] = &model.StepResult{
|
|
Conclusion: model.StepStatusFailure,
|
|
}
|
|
assertObject.True(isStepEnabled(context.Background(), step.getStepModel().If.Value, step, stepStageMain))
|
|
|
|
// always()
|
|
step = createTestStep(t, "if: always()")
|
|
assertObject.True(isStepEnabled(context.Background(), step.getStepModel().If.Value, step, stepStageMain))
|
|
|
|
step = createTestStep(t, "if: always()")
|
|
step.getRunContext().StepResults["a"] = &model.StepResult{
|
|
Conclusion: model.StepStatusSuccess,
|
|
}
|
|
assertObject.True(isStepEnabled(context.Background(), step.getStepModel().If.Value, step, stepStageMain))
|
|
|
|
step = createTestStep(t, "if: always()")
|
|
step.getRunContext().StepResults["a"] = &model.StepResult{
|
|
Conclusion: model.StepStatusFailure,
|
|
}
|
|
assertObject.True(isStepEnabled(context.Background(), step.getStepModel().If.Value, step, stepStageMain))
|
|
|
|
// neither env nor the step's own with: values are inputs, at any stage
|
|
step = createTestStep(t, "if: inputs.forged")
|
|
step.getRunContext().Env["INPUT_FORGED"] = "leaked"
|
|
*step.getEnv() = map[string]string{"INPUT_FORGED": "leaked"}
|
|
assertObject.False(isStepEnabled(context.Background(), step.getStepModel().If.Value, step, stepStageMain))
|
|
assertObject.False(isStepEnabled(context.Background(), step.getStepModel().If.Value, step, stepStagePost))
|
|
}
|
|
|
|
func TestIsContinueOnError(t *testing.T) {
|
|
createTestStep := func(t *testing.T, input string) step {
|
|
var step *model.Step
|
|
err := yaml.Unmarshal([]byte(input), &step)
|
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
|
|
|
return &stepRun{
|
|
RunContext: &RunContext{
|
|
Config: &Config{Workdir: ".", PlatformPicker: func([]string) string { return "ubuntu-latest" }},
|
|
StepResults: map[string]*model.StepResult{},
|
|
Env: map[string]string{},
|
|
Run: &model.Run{
|
|
JobID: "job1",
|
|
Workflow: &model.Workflow{
|
|
Name: "workflow1",
|
|
Jobs: map[string]*model.Job{
|
|
"job1": createJob(t, `runs-on: ubuntu-latest`, ""),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Step: step,
|
|
}
|
|
}
|
|
|
|
log.SetLevel(log.DebugLevel)
|
|
assertObject := assert.New(t)
|
|
|
|
// absent
|
|
step := createTestStep(t, "name: test")
|
|
continueOnError, err := isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
|
assertObject.False(continueOnError)
|
|
assertObject.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
|
|
|
// explcit true
|
|
step = createTestStep(t, "continue-on-error: true")
|
|
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
|
assertObject.True(continueOnError)
|
|
assertObject.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
|
|
|
// explicit false
|
|
step = createTestStep(t, "continue-on-error: false")
|
|
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
|
assertObject.False(continueOnError)
|
|
assertObject.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
|
|
|
// expression true
|
|
step = createTestStep(t, "continue-on-error: ${{ 'test' == 'test' }}")
|
|
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
|
assertObject.True(continueOnError)
|
|
assertObject.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
|
|
|
// expression false
|
|
step = createTestStep(t, "continue-on-error: ${{ 'test' != 'test' }}")
|
|
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
|
assertObject.False(continueOnError)
|
|
assertObject.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
|
|
|
// the step's own with: values are not inputs
|
|
step = createTestStep(t, "continue-on-error: ${{ inputs.forged }}")
|
|
*step.getEnv() = map[string]string{"INPUT_FORGED": "true"}
|
|
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
|
assertObject.False(continueOnError)
|
|
require.NoError(t, err)
|
|
|
|
// expression parse error
|
|
step = createTestStep(t, "continue-on-error: ${{ 'test' != test }}")
|
|
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
|
assertObject.False(continueOnError)
|
|
assertObject.Error(err)
|
|
}
|
|
|
|
// A refused ::set-env::/::add-path:: records a job-scoped error. When the step that
|
|
// produced it also fails on its own, the refusal must be cleared at the step boundary, so
|
|
// it fails only that step and never leaks onto a later step that runs anyway (if: always()).
|
|
func TestRunStepExecutorDoesNotLeakRefusalToNextStep(t *testing.T) {
|
|
cm := &containerMock{}
|
|
noop := func(context.Context) error { return nil }
|
|
cm.On("Copy", mock.Anything, mock.Anything).Return(noop)
|
|
cm.On("UpdateFromEnv", mock.Anything, mock.Anything).Return(noop)
|
|
|
|
rc := &RunContext{
|
|
Config: &Config{Env: map[string]string{}},
|
|
Run: &model.Run{
|
|
JobID: "1",
|
|
Workflow: &model.Workflow{Jobs: map[string]*model.Job{"1": {}}},
|
|
},
|
|
Env: map[string]string{},
|
|
StepResults: map[string]*model.StepResult{},
|
|
JobContainer: cm,
|
|
}
|
|
rc.ExprEval = rc.NewExpressionEvaluator(context.Background())
|
|
// Dryrun skips reading the path file back from the (mocked) container.
|
|
ctx := common.WithDryrun(context.Background(), true)
|
|
|
|
// A refusal parsed out of the job container's own output belongs to no step, so the
|
|
// first step must not be failed by it.
|
|
rc.commandHandler(ctx)("::set-env name=setup::y\n")
|
|
stepSetup := &stepRun{RunContext: rc, Step: &model.Step{ID: "setup"}, env: map[string]string{}}
|
|
require.NoError(t, runStepExecutor(stepSetup, stepStageMain, func(context.Context) error { return nil })(ctx))
|
|
|
|
// Step A refuses a ::set-env:: and then fails on its own.
|
|
stepA := &stepRun{RunContext: rc, Step: &model.Step{ID: "a"}, env: map[string]string{}}
|
|
errA := runStepExecutor(stepA, stepStageMain, func(context.Context) error {
|
|
rc.commandHandler(ctx)("::set-env name=x::y\n")
|
|
return errors.New("boom")
|
|
})(ctx)
|
|
// The step fails with its own error, not the refusal.
|
|
require.ErrorContains(t, errA, "boom")
|
|
|
|
// Step B runs despite step A's failure (if: always()) and issues no unsecure command;
|
|
// it must not inherit step A's refusal.
|
|
stepB := &stepRun{RunContext: rc, Step: &model.Step{ID: "b", If: yaml.Node{Value: "always()"}}, env: map[string]string{}}
|
|
errB := runStepExecutor(stepB, stepStageMain, func(context.Context) error { return nil })(ctx)
|
|
require.NoError(t, errB)
|
|
}
|
|
|
|
func TestRunStepExecutorParity(t *testing.T) {
|
|
newStep := func(t *testing.T, stepModel *model.Step) *stepRun {
|
|
rc := createRunContext(t)
|
|
rc.JobContainer = &container.HostEnvironment{ActPath: t.TempDir()}
|
|
rc.ExprEval = rc.NewExpressionEvaluator(context.Background())
|
|
return &stepRun{RunContext: rc, Step: stepModel, env: map[string]string{}}
|
|
}
|
|
badExpression := "${{ 'test' != test }}"
|
|
for _, test := range []struct {
|
|
name, wantError string
|
|
step *model.Step
|
|
executor common.Executor
|
|
}{
|
|
{"condition error", "if-expression", &model.Step{ID: "condition", If: yaml.Node{Value: badExpression}}, noopExecutor},
|
|
{"continue-on-error expression error", "continue-on-error expression", &model.Step{ID: "continue", RawContinueOnError: badExpression}, common.NewErrorExecutor(assert.AnError)},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
step := newStep(t, test.step)
|
|
logger, hook := logrustest.NewNullLogger()
|
|
err := runStepExecutor(step, stepStageMain, test.executor)(common.WithLogger(context.Background(), logger))
|
|
|
|
require.ErrorContains(t, err, test.wantError)
|
|
assert.Equal(t, model.StepStatusFailure, step.RunContext.StepResults[test.step.ID].Conclusion)
|
|
assert.Equal(t, model.StepStatusFailure, hook.LastEntry().Data["stepResult"])
|
|
})
|
|
}
|
|
|
|
t.Run("file command error honors continue-on-error", func(t *testing.T) {
|
|
step := newStep(t, &model.Step{ID: "commands", RawContinueOnError: "true"})
|
|
logger, hook := logrustest.NewNullLogger()
|
|
err := runStepExecutor(step, stepStageMain, func(context.Context) error {
|
|
require.NoError(t, os.WriteFile(step.env["GITHUB_ENV"], []byte("GOOD=1\nmalformed\n"), 0o600))
|
|
require.NoError(t, os.WriteFile(step.env["GITHUB_OUTPUT"], []byte("kept=value\n"), 0o600))
|
|
require.NoError(t, os.WriteFile(step.env["GITHUB_STATE"], []byte("saved=value\n"), 0o600))
|
|
return nil
|
|
})(common.WithLogger(context.Background(), logger))
|
|
|
|
require.NoError(t, err)
|
|
result := step.RunContext.StepResults[step.Step.ID]
|
|
assert.Equal(t, model.StepStatusFailure, result.Outcome)
|
|
assert.Equal(t, model.StepStatusSuccess, result.Conclusion)
|
|
assert.Equal(t, model.StepStatusSuccess, hook.LastEntry().Data["stepResult"])
|
|
assert.Equal(t, "1", step.RunContext.Env["GOOD"])
|
|
assert.Equal(t, "value", result.Outputs["kept"])
|
|
assert.Equal(t, "value", step.RunContext.IntraActionState[step.Step.ID]["saved"])
|
|
for _, name := range []string{"GITHUB_ENV", "GITHUB_OUTPUT", "GITHUB_STATE", "GITHUB_PATH"} {
|
|
contents, readErr := os.ReadFile(step.env[name])
|
|
require.NoError(t, readErr)
|
|
assert.Empty(t, contents)
|
|
}
|
|
})
|
|
|
|
t.Run("composite child files do not leak", func(t *testing.T) {
|
|
outer := newStep(t, &model.Step{ID: "outer"})
|
|
childRC := &RunContext{
|
|
Config: outer.RunContext.Config, Run: outer.RunContext.Run, Env: map[string]string{}, StepResults: map[string]*model.StepResult{},
|
|
JobContainer: outer.RunContext.JobContainer, Parent: outer.RunContext,
|
|
}
|
|
childRC.ExprEval = childRC.NewExpressionEvaluator(context.Background())
|
|
child := &stepRun{RunContext: childRC, Step: &model.Step{ID: "child"}, env: map[string]string{}}
|
|
|
|
err := runStepExecutor(outer, stepStageMain, func(ctx context.Context) error {
|
|
require.NoError(t, runStepExecutor(child, stepStageMain, func(context.Context) error {
|
|
require.NoError(t, os.WriteFile(child.env["GITHUB_OUTPUT"], []byte("declared=child\nundeclared=leak\n"), 0o600))
|
|
require.NoError(t, os.WriteFile(child.env["GITHUB_STATE"], []byte("saved=child\n"), 0o600))
|
|
return nil
|
|
})(ctx))
|
|
outer.RunContext.setOutput(ctx, map[string]string{"name": "declared"}, "outer")
|
|
return nil
|
|
})(context.Background())
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, map[string]string{"declared": "outer"}, outer.RunContext.StepResults[outer.Step.ID].Outputs)
|
|
assert.Equal(t, map[string]string{"declared": "child", "undeclared": "leak"}, childRC.StepResults[child.Step.ID].Outputs)
|
|
assert.Empty(t, outer.RunContext.IntraActionState)
|
|
assert.Equal(t, "child", childRC.IntraActionState[child.Step.ID]["saved"])
|
|
})
|
|
|
|
t.Run("timeout must be positive", func(t *testing.T) {
|
|
exprEval := createRunContext(t).NewExpressionEvaluator(context.Background())
|
|
for timeout, wantDeadline := range map[string]bool{"-1": false, "0": false, "1": true} {
|
|
ctx, cancel := evaluateStepTimeout(context.Background(), exprEval, &model.Step{TimeoutMinutes: timeout})
|
|
_, hasDeadline := ctx.Deadline()
|
|
cancel()
|
|
assert.Equal(t, wantDeadline, hasDeadline, timeout)
|
|
}
|
|
})
|
|
}
|