4391a10d5a
* feat: use logger from context wherever possible Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * feat: add step/job id and results to json logs Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * test: value to be masked should not be hard-coded in the action Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * fix: replace values following ::add-mask:: in evaluated strings Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * feat: [DEBUG] identifier for debug logs to distinguish them Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * feat: replace logger with step logger The container gets injected a job logger, but during the time that steps are run, we want to use the step logger. This commit wraps pre/main/post steps in an executor that replaces the job logger with a step logger. Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * feat: add pre/post stage identifier fields to json log output Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * feat: add job/step result status to skipped steps/jobs Co-authored-by: Markus Wolf <markus.wolf@new-work.se> Co-authored-by: Markus Wolf <markus.wolf@new-work.se> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
367 lines
11 KiB
Go
367 lines
11 KiB
Go
package runner
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/nektos/act/pkg/exprparser"
|
|
"github.com/nektos/act/pkg/model"
|
|
assert "github.com/stretchr/testify/assert"
|
|
yaml "gopkg.in/yaml.v3"
|
|
)
|
|
|
|
func createRunContext(t *testing.T) *RunContext {
|
|
var yml yaml.Node
|
|
err := yml.Encode(map[string][]interface{}{
|
|
"os": {"Linux", "Windows"},
|
|
"foo": {"bar", "baz"},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
return &RunContext{
|
|
Config: &Config{
|
|
Workdir: ".",
|
|
Secrets: map[string]string{
|
|
"CASE_INSENSITIVE_SECRET": "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]interface{}{
|
|
"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(context.Background())
|
|
|
|
tables := []struct {
|
|
in string
|
|
out interface{}
|
|
errMesg string
|
|
}{
|
|
{" 1 ", 1, ""},
|
|
// {"1 + 3", "4", ""},
|
|
// {"(1 + 3) * -2", "-8", ""},
|
|
{"'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 \"key\": \"value\"\n}", ""},
|
|
{"toJson(env)", "{\n \"ACT\": \"true\",\n \"key\": \"value\"\n}", ""},
|
|
{"(fromJSON('{\"foo\":\"bar\"}')).foo", "bar", ""},
|
|
{"(fromJson('{\"foo\":\"bar\"}')).foo", "bar", ""},
|
|
{"(fromJson('[\"foo\",\"bar\"]'))[1]", "bar", ""},
|
|
// github does return an empty string for non-existent files
|
|
{"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", ""},
|
|
{"runner.os", "Linux", ""},
|
|
{"matrix.os", "Linux", ""},
|
|
{"matrix.foo", "bar", ""},
|
|
{"env.key", "value", ""},
|
|
{"secrets.CASE_INSENSITIVE_SECRET", "value", ""},
|
|
{"secrets.case_insensitive_secret", "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 {
|
|
table := table
|
|
t.Run(table.in, func(t *testing.T) {
|
|
assertObject := assert.New(t)
|
|
out, err := ee.evaluate(context.Background(), table.in, exprparser.DefaultStatusCheckNone)
|
|
if table.errMesg == "" {
|
|
assertObject.NoError(err, table.in)
|
|
assertObject.Equal(table.out, out, table.in)
|
|
} else {
|
|
assertObject.Error(err, table.in)
|
|
assertObject.Equal(table.errMesg, err.Error(), table.in)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEvaluateStep(t *testing.T) {
|
|
rc := createRunContext(t)
|
|
step := &stepRun{
|
|
RunContext: rc,
|
|
}
|
|
|
|
ee := rc.NewStepExpressionEvaluator(context.Background(), step)
|
|
|
|
tables := []struct {
|
|
in string
|
|
out interface{}
|
|
errMesg string
|
|
}{
|
|
{"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", ""},
|
|
}
|
|
|
|
for _, table := range tables {
|
|
table := table
|
|
t.Run(table.in, func(t *testing.T) {
|
|
assertObject := assert.New(t)
|
|
out, err := ee.evaluate(context.Background(), table.in, exprparser.DefaultStatusCheckNone)
|
|
if table.errMesg == "" {
|
|
assertObject.NoError(err, table.in)
|
|
assertObject.Equal(table.out, out, table.in)
|
|
} else {
|
|
assertObject.Error(err, table.in)
|
|
assertObject.Equal(table.errMesg, err.Error(), table.in)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInterpolate(t *testing.T) {
|
|
rc := &RunContext{
|
|
Config: &Config{
|
|
Workdir: ".",
|
|
Secrets: map[string]string{
|
|
"CASE_INSENSITIVE_SECRET": "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(context.Background())
|
|
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"},
|
|
{"${{ 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"},
|
|
}
|
|
|
|
updateTestExpressionWorkflow(t, tables, rc)
|
|
for _, table := range tables {
|
|
table := table
|
|
t.Run("interpolate", func(t *testing.T) {
|
|
assertObject := assert.New(t)
|
|
out := ee.Interpolate(context.Background(), table.in)
|
|
assertObject.Equal(table.out, out, table.in)
|
|
})
|
|
}
|
|
}
|
|
|
|
func updateTestExpressionWorkflow(t *testing.T, tables []struct {
|
|
in string
|
|
out string
|
|
}, rc *RunContext) {
|
|
var envs string
|
|
keys := make([]string, 0, len(rc.Env))
|
|
for k := range rc.Env {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, k := range keys {
|
|
envs += fmt.Sprintf(" %s: %s\n", k, rc.Env[k])
|
|
}
|
|
|
|
// editorconfig-checker-disable
|
|
workflow := fmt.Sprintf(`
|
|
name: "Test how expressions are handled on GitHub"
|
|
on: push
|
|
|
|
env:
|
|
%s
|
|
|
|
jobs:
|
|
test-espressions:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
`, envs)
|
|
// editorconfig-checker-enable
|
|
for _, table := range tables {
|
|
expressionPattern := regexp.MustCompile(`\${{\s*(.+?)\s*}}`)
|
|
|
|
expr := expressionPattern.ReplaceAllStringFunc(table.in, func(match string) string {
|
|
return fmt.Sprintf("€{{ %s }}", expressionPattern.ReplaceAllString(match, "$1"))
|
|
})
|
|
name := fmt.Sprintf(`%s -> %s should be equal to %s`, expr, table.in, table.out)
|
|
echo := `run: echo "Done "`
|
|
workflow += fmt.Sprintf("\n - name: %s\n %s\n", name, echo)
|
|
}
|
|
|
|
file, err := os.Create("../../.github/workflows/test-expressions.yml")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = file.WriteString(workflow)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestRewriteSubExpression(t *testing.T) {
|
|
table := []struct {
|
|
in string
|
|
out string
|
|
}{
|
|
{in: "Hello World", out: "Hello World"},
|
|
{in: "${{ true }}", out: "${{ true }}"},
|
|
{in: "${{ true }} ${{ true }}", out: "format('{0} {1}', true, true)"},
|
|
{in: "${{ true || false }} ${{ true && true }}", out: "format('{0} {1}', true || false, true && true)"},
|
|
{in: "${{ '}}' }}", out: "${{ '}}' }}"},
|
|
{in: "${{ '''}}''' }}", out: "${{ '''}}''' }}"},
|
|
{in: "${{ '''' }}", out: "${{ '''' }}"},
|
|
{in: `${{ fromJSON('"}}"') }}`, out: `${{ fromJSON('"}}"') }}`},
|
|
{in: `${{ fromJSON('"\"}}\""') }}`, out: `${{ fromJSON('"\"}}\""') }}`},
|
|
{in: `${{ fromJSON('"''}}"') }}`, out: `${{ fromJSON('"''}}"') }}`},
|
|
{in: "Hello ${{ 'World' }}", out: "format('Hello {0}', 'World')"},
|
|
}
|
|
|
|
for _, table := range table {
|
|
t.Run("TestRewriteSubExpression", func(t *testing.T) {
|
|
assertObject := assert.New(t)
|
|
out, err := rewriteSubExpression(context.Background(), table.in, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertObject.Equal(table.out, out, table.in)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRewriteSubExpressionForceFormat(t *testing.T) {
|
|
table := []struct {
|
|
in string
|
|
out string
|
|
}{
|
|
{in: "Hello World", out: "Hello World"},
|
|
{in: "${{ true }}", out: "format('{0}', true)"},
|
|
{in: "${{ '}}' }}", out: "format('{0}', '}}')"},
|
|
{in: `${{ fromJSON('"}}"') }}`, out: `format('{0}', fromJSON('"}}"'))`},
|
|
{in: "Hello ${{ 'World' }}", out: "format('Hello {0}', 'World')"},
|
|
}
|
|
|
|
for _, table := range table {
|
|
t.Run("TestRewriteSubExpressionForceFormat", func(t *testing.T) {
|
|
assertObject := assert.New(t)
|
|
out, err := rewriteSubExpression(context.Background(), table.in, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertObject.Equal(table.out, out, table.in)
|
|
})
|
|
}
|
|
}
|