mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-10-29 03:16:43 +08:00
`exec` forwarded the toolkit's ArtifactService calls to an instance it never registered, and its random runtime token held no backend IDs, so `actions/upload-artifact` and `actions/download-artifact` v4 and later failed. The local artifact server now serves the v4 API, including the single Put Blob uploads docker/build-push-action uses. `exec` gives each workflow its own unsigned JWT runtime token and artifact run, so artifact names conflict and carry over between runs as on GitHub. Artifacts default to a temporary directory instead of the workspace that jobs copy. Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Reviewed-on: https://gitea.com/gitea/runner/pulls/1341 Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com> Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com> Co-authored-by: bircni <bircni@icloud.com>
593 lines
20 KiB
Go
593 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 TestResolveWorkflowCallToleratesMismatchesAndPassesInheritedSecretsAndRuntimeToken(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{},
|
|
RuntimeTokens: map[*model.Workflow]string{callerWorkflow: "caller-run"},
|
|
}
|
|
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)
|
|
assert.Equal(t, "caller-run", called.GetEnv()["ACTIONS_RUNTIME_TOKEN"])
|
|
inherited := resolve("inherit", notCallable)
|
|
assert.Empty(t, inherited.workflowCallInputs)
|
|
assert.Equal(t, map[string]string{"token": "${{ github.token }}", "ACTIONS_RUNTIME_TOKEN": "caller-run"}, 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()
|
|
}
|