mirror of
https://gitea.com/gitea/act_runner
synced 2026-11-03 10:57:22 +01:00
Without a docker daemon six `act/runner` tests failed instead of passing. A dry run still created and removed job networks, the job teardown dropped the dry-run flag along with every other context value, and the container mocks asked the daemon for the runner arch. This also speeds up the tests and CI without dropping assertions. Most of the time went to git and docker round trips, which are now pinned, batched or coalesced, and the dind and e2e steps cache or overlap more of their setup. On CI, `act/runner` went from 77s to 55s and `test-dind` from 108s to 55s. Locally, `act/runner` without docker went from 53s to 7s. Split out of https://gitea.com/gitea/runner/pulls/1348, which adds Windows CI on top. The go cache changes moved to https://gitea.com/gitea/runner/pulls/1350. Written by Claude Code. --------- Co-authored-by: bircni <bircni@icloud.com> Reviewed-on: https://gitea.com/gitea/runner/pulls/1349 Reviewed-by: bircni <bircni@icloud.com> Co-authored-by: silverwind <me@silverwind.io>
588 lines
20 KiB
Go
588 lines
20 KiB
Go
// Copyright 2022 The Gitea Authors. All rights reserved.
|
|
// Copyright 2020 The nektos/act Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package runner
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.com/gitea/runner/act/common"
|
|
"gitea.com/gitea/runner/act/common/logging/logtest"
|
|
"gitea.com/gitea/runner/act/container"
|
|
|
|
"gitea.dev/actionslib/pkg/exprparser"
|
|
"gitea.dev/actionslib/pkg/model"
|
|
assert "github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
yaml "go.yaml.in/yaml/v4"
|
|
)
|
|
|
|
func createRunContext(t *testing.T) *RunContext {
|
|
var yml yaml.Node
|
|
err := yml.Encode(map[string][]any{
|
|
"os": {"Linux", "Windows"},
|
|
"foo": {"bar", "baz"},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
return &RunContext{
|
|
Config: &Config{
|
|
Env: testCheckoutEnv(),
|
|
Workdir: ".",
|
|
Secrets: map[string]string{
|
|
"CASE_INSENSITIVE_SECRET": "value",
|
|
},
|
|
Vars: map[string]string{
|
|
"CASE_INSENSITIVE_VAR": "value",
|
|
},
|
|
},
|
|
Env: map[string]string{
|
|
"key": "value",
|
|
},
|
|
Run: &model.Run{
|
|
JobID: "job1",
|
|
Workflow: &model.Workflow{
|
|
Name: "test-workflow",
|
|
Jobs: map[string]*model.Job{
|
|
"job1": {
|
|
Strategy: &model.Strategy{
|
|
RawMatrix: yml,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Matrix: map[string]any{
|
|
"os": "Linux",
|
|
"foo": "bar",
|
|
},
|
|
StepResults: map[string]*model.StepResult{
|
|
"idwithnothing": {
|
|
Conclusion: model.StepStatusSuccess,
|
|
Outcome: model.StepStatusFailure,
|
|
Outputs: map[string]string{
|
|
"foowithnothing": "barwithnothing",
|
|
},
|
|
},
|
|
"id-with-hyphens": {
|
|
Conclusion: model.StepStatusSuccess,
|
|
Outcome: model.StepStatusFailure,
|
|
Outputs: map[string]string{
|
|
"foo-with-hyphens": "bar-with-hyphens",
|
|
},
|
|
},
|
|
"id_with_underscores": {
|
|
Conclusion: model.StepStatusSuccess,
|
|
Outcome: model.StepStatusFailure,
|
|
Outputs: map[string]string{
|
|
"foo_with_underscores": "bar_with_underscores",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestEvaluateRunContext(t *testing.T) {
|
|
rc := createRunContext(t)
|
|
ee := rc.NewExpressionEvaluator(t.Context())
|
|
|
|
tables := []struct {
|
|
in string
|
|
out any
|
|
}{
|
|
{" 1 ", 1.0},
|
|
{"'my text'", "my text"},
|
|
{"contains('my text', 'te')", true},
|
|
{"contains('my TEXT', 'te')", true},
|
|
{"contains(fromJSON('[\"my text\"]'), 'te')", false},
|
|
{"contains(fromJSON('[\"foo\",\"bar\"]'), 'bar')", true},
|
|
{"startsWith('hello world', 'He')", true},
|
|
{"endsWith('hello world', 'ld')", true},
|
|
{"format('0:{0} 2:{2} 1:{1}', 'zero', 'one', 'two')", "0:zero 2:two 1:one"},
|
|
{"join(fromJSON('[\"hello\"]'),'octocat')", "hello"},
|
|
{"join(fromJSON('[\"hello\",\"mona\",\"the\"]'),'octocat')", "hellooctocatmonaoctocatthe"},
|
|
{"join('hello','mona')", "hello"},
|
|
{"toJSON(env)", "{\n \"ACT\": \"true\",\n \"ACT_SKIP_CHECKOUT\": \"true\",\n \"key\": \"value\"\n}"},
|
|
{"toJson(env)", "{\n \"ACT\": \"true\",\n \"ACT_SKIP_CHECKOUT\": \"true\",\n \"key\": \"value\"\n}"},
|
|
{"(fromJSON('{\"foo\":\"bar\"}')).foo", "bar"},
|
|
{"(fromJson('{\"foo\":\"bar\"}')).foo", "bar"},
|
|
{"(fromJson('[\"foo\",\"bar\"]'))[1]", "bar"},
|
|
{"hashFiles('**/non-extant-files')", ""},
|
|
{"hashFiles('**/non-extant-files', '**/more-non-extant-files')", ""},
|
|
{"hashFiles('**/non.extant.files')", ""},
|
|
{"hashFiles('**/non''extant''files')", ""},
|
|
{"success()", true},
|
|
{"failure()", false},
|
|
{"always()", true},
|
|
{"cancelled()", false},
|
|
{"github.workflow", "test-workflow"},
|
|
{"github.actor", "nektos/act"},
|
|
{"github.run_id", "1"},
|
|
{"github.run_number", "1"},
|
|
{"job.status", "success"},
|
|
{"matrix.os", "Linux"},
|
|
{"matrix.foo", "bar"},
|
|
{"env.key", "value"},
|
|
{"secrets.CASE_INSENSITIVE_SECRET", "value"},
|
|
{"secrets.case_insensitive_secret", "value"},
|
|
{"vars.CASE_INSENSITIVE_VAR", "value"},
|
|
{"vars.case_insensitive_var", "value"},
|
|
{"format('{{0}}', 'test')", "{0}"},
|
|
{"format('{{{0}}}', 'test')", "{test}"},
|
|
{"format('}}')", "}"},
|
|
{"format('echo Hello {0} ${{Test}}', 'World')", "echo Hello World ${Test}"},
|
|
{"format('echo Hello {0} ${{Test}}', github.undefined_property)", "echo Hello ${Test}"},
|
|
{"format('echo Hello {0}{1} ${{Te{0}st}}', github.undefined_property, 'World')", "echo Hello World ${Test}"},
|
|
{"format('{0}', '{1}', 'World')", "{1}"},
|
|
{"format('{{{0}', '{1}', 'World')", "{{1}"},
|
|
}
|
|
|
|
for _, table := range tables {
|
|
t.Run(table.in, func(t *testing.T) {
|
|
out, err := ee.evaluate(t.Context(), table.in, exprparser.DefaultStatusCheckNone)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, table.out, out)
|
|
})
|
|
}
|
|
}
|
|
|
|
type hashFilesContainer struct {
|
|
fakeContainer
|
|
stdout, stderr io.Writer
|
|
copyErr error
|
|
execute func([]string, map[string]string, string, string) common.Executor
|
|
}
|
|
|
|
func (c *hashFilesContainer) Copy(_destPath string, _files ...*container.FileEntry) common.Executor {
|
|
return common.NewErrorExecutor(c.copyErr)
|
|
}
|
|
|
|
func (c *hashFilesContainer) Exec(command []string, env map[string]string, user, workdir string) common.Executor {
|
|
return c.execute(command, env, user, workdir)
|
|
}
|
|
|
|
func (c *hashFilesContainer) ReplaceLogWriter(stdout, stderr io.Writer) (io.Writer, io.Writer) {
|
|
previousOut, previousErr := c.stdout, c.stderr
|
|
c.stdout, c.stderr = stdout, stderr
|
|
return previousOut, previousErr
|
|
}
|
|
|
|
func TestGetHashFilesFunction(t *testing.T) {
|
|
const failureMessage = "hashFiles('dir''file, *.sum') failed. Fail to hash files under directory '/workspace'"
|
|
for _, test := range []struct {
|
|
name string
|
|
output string
|
|
execErr error
|
|
copyErr error
|
|
hash string
|
|
err string
|
|
warning bool
|
|
expired bool
|
|
}{
|
|
{name: "success", output: "patterns __OUTPUT__wrong__OUTPUT__ echoed\n__OUTPUT__digest__OUTPUT__", hash: "digest"},
|
|
{name: "no matches", output: "__OUTPUT____OUTPUT__"},
|
|
{name: "script failure", output: "Cannot find: node in PATH\n__OUTPUT__digest__OUTPUT__", execErr: container.ExitCodeError(1), err: failureMessage},
|
|
{name: "timeout", execErr: context.DeadlineExceeded, err: "hashFiles('dir''file, *.sum') couldn't finish within 120 seconds"},
|
|
{name: "job deadline", execErr: context.DeadlineExceeded, expired: true, err: context.DeadlineExceeded.Error()},
|
|
{name: "node missing", execErr: fmt.Errorf("wrapped: %w", container.ExitCodeError(127)), warning: true},
|
|
{name: "node not executable", execErr: container.ExitCodeError(126), warning: true},
|
|
{name: "host node missing", execErr: errors.New("Cannot find: node in PATH"), warning: true},
|
|
{name: "copy failure", copyErr: errors.New("open /runner/cache: permission denied"), err: failureMessage},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
logger, hook := logtest.NewLogger()
|
|
stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
|
|
jobContainer := &hashFilesContainer{stdout: stdout, stderr: stderr, copyErr: test.copyErr}
|
|
jobContainer.execute = func(command []string, env map[string]string, user, workdir string) common.Executor {
|
|
assert.Equal(t, []string{"node", "/var/run/act/workflow/hashfiles/index.js"}, command)
|
|
assert.Equal(t, map[string]string{"patterns": "dir'file\n*.sum", "followSymbolicLinks": "true"}, env)
|
|
assert.Empty(t, user)
|
|
assert.Empty(t, workdir)
|
|
return func(_ctx context.Context) error {
|
|
_, err := io.WriteString(jobContainer.stderr, test.output)
|
|
require.NoError(t, err)
|
|
return test.execErr
|
|
}
|
|
}
|
|
ctx := common.WithLogger(t.Context(), logger)
|
|
if test.expired {
|
|
var cancel context.CancelFunc
|
|
ctx, cancel = context.WithDeadline(ctx, time.Now())
|
|
defer cancel()
|
|
}
|
|
result, err := getHashFilesFunction(ctx, &RunContext{
|
|
Config: &Config{Workdir: "/workspace"},
|
|
JobContainer: jobContainer,
|
|
})([]reflect.Value{reflect.ValueOf("--follow-symbolic-links"), reflect.ValueOf("dir'file"), reflect.ValueOf("*.sum")})
|
|
if test.err == "" {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.EqualError(t, err, test.err)
|
|
}
|
|
assert.Equal(t, test.hash, result)
|
|
assert.Same(t, stdout, jobContainer.stdout)
|
|
assert.Same(t, stderr, jobContainer.stderr)
|
|
if test.warning {
|
|
require.Len(t, hook.AllRecords(), 1)
|
|
assert.Equal(t, "##[warning]hashFiles could not run because node is unavailable in the job environment. Returning an empty string.", hook.LastRecord().Message)
|
|
} else {
|
|
assert.Empty(t, hook.AllRecords())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEvaluateStep(t *testing.T) {
|
|
rc := createRunContext(t)
|
|
rc.Env["INPUT_FORGED"] = "leaked"
|
|
step := &stepRun{
|
|
RunContext: rc,
|
|
env: map[string]string{"INPUT_FORGED": "leaked"},
|
|
}
|
|
|
|
ee := rc.NewStepExpressionEvaluator(t.Context(), step)
|
|
|
|
tables := []struct {
|
|
in string
|
|
out any
|
|
}{
|
|
{"steps.idwithnothing.conclusion", model.StepStatusSuccess.String()},
|
|
{"steps.idwithnothing.outcome", model.StepStatusFailure.String()},
|
|
{"steps.idwithnothing.outputs.foowithnothing", "barwithnothing"},
|
|
{"steps.id-with-hyphens.conclusion", model.StepStatusSuccess.String()},
|
|
{"steps.id-with-hyphens.outcome", model.StepStatusFailure.String()},
|
|
{"steps.id-with-hyphens.outputs.foo-with-hyphens", "bar-with-hyphens"},
|
|
{"steps.id_with_underscores.conclusion", model.StepStatusSuccess.String()},
|
|
{"steps.id_with_underscores.outcome", model.StepStatusFailure.String()},
|
|
{"steps.id_with_underscores.outputs.foo_with_underscores", "bar_with_underscores"},
|
|
{"inputs.forged", nil},
|
|
}
|
|
|
|
for _, table := range tables {
|
|
t.Run(table.in, func(t *testing.T) {
|
|
out, err := ee.evaluate(t.Context(), table.in, exprparser.DefaultStatusCheckNone)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, table.out, out)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInterpolate(t *testing.T) {
|
|
rc := &RunContext{
|
|
Config: &Config{
|
|
Env: testCheckoutEnv(),
|
|
Workdir: ".",
|
|
Secrets: map[string]string{
|
|
"CASE_INSENSITIVE_SECRET": "value",
|
|
},
|
|
Vars: map[string]string{
|
|
"CASE_INSENSITIVE_VAR": "value",
|
|
},
|
|
},
|
|
Env: map[string]string{
|
|
"KEYWITHNOTHING": "valuewithnothing",
|
|
"KEY-WITH-HYPHENS": "value-with-hyphens",
|
|
"KEY_WITH_UNDERSCORES": "value_with_underscores",
|
|
"SOMETHING_TRUE": "true",
|
|
"SOMETHING_FALSE": "false",
|
|
},
|
|
Run: &model.Run{
|
|
JobID: "job1",
|
|
Workflow: &model.Workflow{
|
|
Name: "test-workflow",
|
|
Jobs: map[string]*model.Job{
|
|
"job1": {},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
ee := rc.NewExpressionEvaluator(t.Context())
|
|
tables := []struct {
|
|
in string
|
|
out string
|
|
}{
|
|
{" text ", " text "},
|
|
{" $text ", " $text "},
|
|
{" ${text} ", " ${text} "},
|
|
{" ${{ 1 }} to ${{2}} ", " 1 to 2 "},
|
|
{" ${{ (true || false) }} to ${{2}} ", " true to 2 "},
|
|
{" ${{ (false || '}}' ) }} to ${{2}} ", " }} to 2 "},
|
|
{" ${{ env.KEYWITHNOTHING }} ", " valuewithnothing "},
|
|
{" ${{ env.KEY-WITH-HYPHENS }} ", " value-with-hyphens "},
|
|
{" ${{ env.KEY_WITH_UNDERSCORES }} ", " value_with_underscores "},
|
|
{"${{ secrets.CASE_INSENSITIVE_SECRET }}", "value"},
|
|
{"${{ secrets.case_insensitive_secret }}", "value"},
|
|
{"${{ vars.CASE_INSENSITIVE_VAR }}", "value"},
|
|
{"${{ vars.case_insensitive_var }}", "value"},
|
|
{"${{ env.UNKNOWN }}", ""},
|
|
{"${{ env.SOMETHING_TRUE }}", "true"},
|
|
{"${{ env.SOMETHING_FALSE }}", "false"},
|
|
{"${{ !env.SOMETHING_TRUE }}", "false"},
|
|
{"${{ !env.SOMETHING_FALSE }}", "false"},
|
|
{"${{ !env.SOMETHING_TRUE && true }}", "false"},
|
|
{"${{ !env.SOMETHING_FALSE && true }}", "false"},
|
|
{"${{ env.SOMETHING_TRUE && true }}", "true"},
|
|
{"${{ env.SOMETHING_FALSE && true }}", "true"},
|
|
{"${{ !env.SOMETHING_TRUE || true }}", "true"},
|
|
{"${{ !env.SOMETHING_FALSE || true }}", "true"},
|
|
{"${{ !env.SOMETHING_TRUE && false }}", "false"},
|
|
{"${{ !env.SOMETHING_FALSE && false }}", "false"},
|
|
{"${{ !env.SOMETHING_TRUE || false }}", "false"},
|
|
{"${{ !env.SOMETHING_FALSE || false }}", "false"},
|
|
{"${{ env.SOMETHING_TRUE || false }}", "true"},
|
|
{"${{ env.SOMETHING_FALSE || false }}", "false"},
|
|
{"${{ env.SOMETHING_FALSE }} && ${{ env.SOMETHING_TRUE }}", "false && true"},
|
|
{"${{ fromJSON('{}') < 2 }}", "false"},
|
|
{"${{ 1 }}", "1"},
|
|
{"${{ 1.0 }}", "1"},
|
|
{"${{ null }}", ""},
|
|
{"${{ fromJSON('[1,2]') }}", "Array"},
|
|
{"${{ fromJSON('{\"a\":1}') }}", "Object"},
|
|
}
|
|
|
|
for _, table := range tables {
|
|
t.Run("interpolate", func(t *testing.T) {
|
|
out, err := ee.Interpolate(t.Context(), table.in)
|
|
require.NoError(t, err, table.in)
|
|
assert.Equal(t, table.out, out, table.in)
|
|
})
|
|
}
|
|
|
|
for _, in := range []string{"${{ 1) && (2 }}", "run ${{ 1) && (2 }} now", "${{ 1"} {
|
|
_, err := ee.Interpolate(t.Context(), in)
|
|
assert.Error(t, err, in)
|
|
}
|
|
}
|
|
|
|
func TestGetEvaluatorInputsBoolean(t *testing.T) {
|
|
workflow := func(event string) string {
|
|
return "on:\n " + event + `:
|
|
inputs:
|
|
flag:
|
|
type: boolean
|
|
default: true
|
|
name:
|
|
type: string
|
|
default: gitea
|
|
unset:
|
|
type: boolean
|
|
`
|
|
}
|
|
|
|
tables := []struct {
|
|
name string
|
|
event map[string]any
|
|
flag any
|
|
}{
|
|
{
|
|
name: "native bool true",
|
|
event: map[string]any{"inputs": map[string]any{"flag": true}},
|
|
flag: true,
|
|
},
|
|
{
|
|
name: "native bool false",
|
|
event: map[string]any{"inputs": map[string]any{"flag": false}},
|
|
flag: false,
|
|
},
|
|
{
|
|
name: "string true",
|
|
event: map[string]any{"inputs": map[string]any{"flag": "true"}},
|
|
flag: true,
|
|
},
|
|
{
|
|
name: "string false",
|
|
event: map[string]any{"inputs": map[string]any{"flag": "false"}},
|
|
flag: false,
|
|
},
|
|
{
|
|
name: "default is used when the event carries no inputs",
|
|
event: map[string]any{},
|
|
flag: true,
|
|
},
|
|
}
|
|
|
|
for _, eventName := range []string{"workflow_call", "workflow_dispatch"} {
|
|
for _, table := range tables {
|
|
t.Run(eventName+"/"+table.name, func(t *testing.T) {
|
|
wf, err := model.ReadWorkflow(strings.NewReader(workflow(eventName)))
|
|
require.NoError(t, err)
|
|
|
|
rc := &RunContext{
|
|
Config: &Config{Workdir: "."},
|
|
Run: &model.Run{JobID: "job1", Workflow: wf},
|
|
}
|
|
ghc := &model.GithubContext{EventName: eventName, Event: table.event}
|
|
|
|
inputs := getEvaluatorInputs(rc, nil, ghc)
|
|
assert.Equal(t, table.flag, inputs["flag"])
|
|
assert.Equal(t, "gitea", inputs["name"])
|
|
assert.Equal(t, false, inputs["unset"])
|
|
})
|
|
}
|
|
}
|
|
|
|
t.Run("deferred workflow call inputs", func(t *testing.T) {
|
|
workflow, err := model.ReadWorkflow(strings.NewReader(workflow("workflow_call") + `
|
|
jobs:
|
|
test: {}
|
|
call:
|
|
uses: ./reuse.yml
|
|
with: ${{ fromJSON(matrix.args) }}
|
|
`))
|
|
require.NoError(t, err)
|
|
workflow.GetJob("test")
|
|
job := workflow.GetJob("call")
|
|
rawWith := model.CloneYamlNode(job.RawWith)
|
|
for _, value := range []string{"true", "false"} {
|
|
t.Run(value, func(t *testing.T) {
|
|
t.Parallel()
|
|
parent, err := (&runnerImpl{config: &Config{PresetGitHubContext: &model.GithubContext{}}}).newRunContext(t.Context(), &model.Run{Workflow: workflow, JobID: "call"}, map[string]any{"args": `{"flag":` + value + `,"name":"${{ 'runner' }}"}`})
|
|
require.NoError(t, err)
|
|
child, err := (&runnerImpl{config: &Config{PresetGitHubContext: &model.GithubContext{}}, caller: &caller{runContext: parent}}).newRunContext(t.Context(), &model.Run{Workflow: workflow, JobID: "test"}, nil)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, map[string]any{"flag": value == "true", "name": "${{ 'runner' }}", "unset": false}, child.workflowCallInputs)
|
|
assert.Equal(t, rawWith, job.RawWith)
|
|
assert.Nil(t, job.With)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGetEvaluatorInputsResolvedByGitea(t *testing.T) {
|
|
wf, err := model.ReadWorkflow(strings.NewReader(`
|
|
on:
|
|
workflow_call:
|
|
inputs:
|
|
name: {type: string, default: callee}
|
|
workflow_dispatch:
|
|
inputs:
|
|
name: {type: string}
|
|
`))
|
|
require.NoError(t, err)
|
|
config := &Config{Workdir: ".", WorkflowCallInputs: map[string]any{"name": "caller", "target": "dispatch"}}
|
|
ghc := &model.GithubContext{EventName: "workflow_dispatch", Event: map[string]any{"inputs": map[string]any{"name": "dispatch"}}}
|
|
|
|
job := &RunContext{Config: config, Run: &model.Run{JobID: "job1", Workflow: wf}}
|
|
assert.Equal(t, map[string]any{"name": "caller", "target": "dispatch"}, getEvaluatorInputs(job, nil, ghc))
|
|
|
|
composite := &RunContext{Config: config, Run: &model.Run{JobID: "job1", Workflow: &model.Workflow{}}, Parent: job}
|
|
assert.Equal(t, map[string]any{"arg": "x"}, getEvaluatorInputs(composite, map[string]any{"arg": "x"}, ghc))
|
|
}
|
|
|
|
func TestActionInputsShadowDispatchInputs(t *testing.T) {
|
|
wf, err := model.ReadWorkflow(strings.NewReader(`
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
message: {type: string}
|
|
jobs:
|
|
job1: {}
|
|
`))
|
|
require.NoError(t, err)
|
|
rc := newTestRC(wf, nil)
|
|
rc.Config.EventName = "workflow_dispatch"
|
|
rc.EventJSON = `{"inputs":{"message":"workflow"}}`
|
|
step := &stepActionRemote{RunContext: rc, env: map[string]string{"INPUT_MESSAGE": "step"}}
|
|
assert.Equal(t, "step", rc.NewActionInputsExpressionEvaluator(t.Context(), step).InterpolateName(t.Context(), "${{ inputs.message }}"))
|
|
assert.Equal(t, "workflow", rc.NewStepExpressionEvaluator(t.Context(), step).InterpolateName(t.Context(), "${{ inputs.message }}"))
|
|
enabled, err := isStepEnabled(t.Context(), "inputs.message == 'workflow'", step, stepStageMain)
|
|
require.NoError(t, err)
|
|
assert.True(t, enabled)
|
|
step.Step, step.action = &model.Step{}, &model.Action{Inputs: map[string]model.Input{"fallback": {Default: "${{ inputs.message }}"}}}
|
|
env, err := evaluateCompositeInputAndEnv(t.Context(), rc, step)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "workflow", env["INPUT_FALLBACK"])
|
|
}
|
|
|
|
func TestResolveWorkflowCallToleratesMismatchesAndPassesInheritedSecretsVerbatim(t *testing.T) {
|
|
callerWorkflow, err := model.ReadWorkflow(strings.NewReader(`
|
|
jobs:
|
|
call:
|
|
uses: ./reuse.yml
|
|
with: {flag: not-a-boolean}
|
|
secrets: {undeclared: "${{ secrets.token }}"}
|
|
inherit:
|
|
uses: ./reuse.yml
|
|
secrets: inherit
|
|
`))
|
|
require.NoError(t, err)
|
|
callee, err := model.ReadWorkflow(strings.NewReader(`
|
|
on:
|
|
workflow_call:
|
|
inputs:
|
|
flag: {type: boolean, default: true}
|
|
jobs:
|
|
test: {}
|
|
`))
|
|
require.NoError(t, err)
|
|
notCallable, err := model.ReadWorkflow(strings.NewReader("on: push\njobs:\n test: {}\n"))
|
|
require.NoError(t, err)
|
|
config := &Config{Secrets: map[string]string{"token": "${{ github.token }}"}, PresetGitHubContext: &model.GithubContext{}}
|
|
resolve := func(callerJobID string, workflow *model.Workflow) *RunContext {
|
|
parent, err := (&runnerImpl{config: config}).newRunContext(t.Context(), &model.Run{Workflow: callerWorkflow, JobID: callerJobID}, nil)
|
|
require.NoError(t, err)
|
|
child, err := (&runnerImpl{config: config, caller: &caller{runContext: parent}}).newRunContext(t.Context(), &model.Run{Workflow: workflow, JobID: "test"}, nil)
|
|
require.NoError(t, err)
|
|
return child
|
|
}
|
|
|
|
called := resolve("call", callee)
|
|
assert.Equal(t, map[string]any{"flag": true}, called.workflowCallInputs)
|
|
assert.Equal(t, map[string]string{"undeclared": "${{ github.token }}"}, called.workflowCallSecrets)
|
|
inherited := resolve("inherit", notCallable)
|
|
assert.Empty(t, inherited.workflowCallInputs)
|
|
assert.Equal(t, config.Secrets, inherited.workflowCallSecrets)
|
|
}
|
|
|
|
func TestJobNameMasksSecrets(t *testing.T) {
|
|
workflow, err := model.ReadWorkflow(strings.NewReader(`
|
|
jobs:
|
|
a:
|
|
name: deploy ${{ secrets.A }}
|
|
b:
|
|
name: deploy ${{ secrets.B }}
|
|
`))
|
|
require.NoError(t, err)
|
|
|
|
runner := &runnerImpl{config: &Config{Secrets: map[string]string{"A": "s3cr3t-a", "B": "s3cr3t-b"}, PresetGitHubContext: &model.GithubContext{}}}
|
|
containerName := func(jobID string) string {
|
|
rc, err := runner.newRunContext(t.Context(), &model.Run{JobID: jobID, Workflow: workflow}, nil)
|
|
require.NoError(t, err)
|
|
assert.NotContains(t, rc.Name, "s3cr3t")
|
|
return rc.jobContainerName()
|
|
}
|
|
|
|
a, b := containerName("a"), containerName("b")
|
|
assert.NotContains(t, a, "s3cr3t")
|
|
assert.NotEqual(t, a, b)
|
|
}
|
|
|
|
func TestNewExpressionEvaluatorReadsJobOutputsWhileCombinationsWriteThem(t *testing.T) {
|
|
rc := createRunContext(t)
|
|
rc.caller = &caller{runContext: createRunContext(t)}
|
|
rc.ExprEval = rc.NewExpressionEvaluator(t.Context())
|
|
job := rc.Run.Job()
|
|
var writes sync.WaitGroup
|
|
writes.Go(func() {
|
|
for range 100 {
|
|
unlock := lockJob(job)
|
|
job.Outputs = map[string]string{"out": "value"}
|
|
unlock()
|
|
}
|
|
})
|
|
for range 100 {
|
|
rc.NewExpressionEvaluator(t.Context())
|
|
}
|
|
writes.Wait()
|
|
}
|