package runner import ( "context" "fmt" "os" "path/filepath" "runtime" "strings" "testing" "github.com/joho/godotenv" log "github.com/sirupsen/logrus" assert "github.com/stretchr/testify/assert" "github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/model" ) var ( baseImage = "node:16-buster-slim" platforms map[string]string logLevel = log.DebugLevel workdir = "testdata" ) func init() { if p := os.Getenv("ACT_TEST_IMAGE"); p != "" { baseImage = p } platforms = map[string]string{ "ubuntu-latest": baseImage, } if l := os.Getenv("ACT_TEST_LOG_LEVEL"); l != "" { if lvl, err := log.ParseLevel(l); err == nil { logLevel = lvl } } if wd, err := filepath.Abs(workdir); err == nil { workdir = wd } } func TestGraphEvent(t *testing.T) { planner, err := model.NewWorkflowPlanner("testdata/basic", true) assert.Nil(t, err) plan := planner.PlanEvent("push") assert.Nil(t, err) assert.Equal(t, len(plan.Stages), 3, "stages") assert.Equal(t, len(plan.Stages[0].Runs), 1, "stage0.runs") assert.Equal(t, len(plan.Stages[1].Runs), 1, "stage1.runs") assert.Equal(t, len(plan.Stages[2].Runs), 1, "stage2.runs") assert.Equal(t, plan.Stages[0].Runs[0].JobID, "check", "jobid") assert.Equal(t, plan.Stages[1].Runs[0].JobID, "build", "jobid") assert.Equal(t, plan.Stages[2].Runs[0].JobID, "test", "jobid") plan = planner.PlanEvent("release") assert.Equal(t, len(plan.Stages), 0, "stages") } type TestJobFileInfo struct { workdir string workflowPath string eventName string errorMessage string platforms map[string]string } func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config) { fmt.Printf("::group::%s\n", j.workflowPath) log.SetLevel(logLevel) workdir, err := filepath.Abs(j.workdir) assert.Nil(t, err, workdir) fullWorkflowPath := filepath.Join(workdir, j.workflowPath) runnerConfig := &Config{ Workdir: workdir, BindWorkdir: false, EventName: j.eventName, EventPath: cfg.EventPath, Platforms: j.platforms, ReuseContainers: false, Env: cfg.Env, Secrets: cfg.Secrets, GitHubInstance: "github.com", ContainerArchitecture: cfg.ContainerArchitecture, } runner, err := New(runnerConfig) assert.Nil(t, err, j.workflowPath) planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true) assert.Nil(t, err, fullWorkflowPath) plan := planner.PlanEvent(j.eventName) err = runner.NewPlanExecutor(plan)(ctx) if j.errorMessage == "" { assert.Nil(t, err, fullWorkflowPath) } else { assert.Error(t, err, j.errorMessage) } fmt.Println("::endgroup::") } func TestRunEvent(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } ctx := context.Background() tables := []TestJobFileInfo{ // Shells {workdir, "shells/defaults", "push", "", platforms}, // TODO: figure out why it fails // {workdir, "shells/custom", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:pwsh-latest"}, }, // custom image with pwsh {workdir, "shells/pwsh", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:pwsh-latest"}}, // custom image with pwsh {workdir, "shells/bash", "push", "", platforms}, {workdir, "shells/python", "push", "", map[string]string{"ubuntu-latest": "node:16-buster"}}, // slim doesn't have python {workdir, "shells/sh", "push", "", platforms}, // Local action {workdir, "local-action-docker-url", "push", "", platforms}, {workdir, "local-action-dockerfile", "push", "", platforms}, {workdir, "local-action-via-composite-dockerfile", "push", "", platforms}, {workdir, "local-action-js", "push", "", platforms}, // Uses {workdir, "uses-composite", "push", "", platforms}, {workdir, "uses-composite-with-error", "push", "Job 'failing-composite-action' failed", platforms}, {workdir, "uses-nested-composite", "push", "", platforms}, {workdir, "remote-action-composite-js-pre-with-defaults", "push", "", platforms}, {workdir, "uses-workflow", "push", "reusable workflows are currently not supported (see https://github.com/nektos/act/issues/826 for updates)", platforms}, {workdir, "uses-docker-url", "push", "", platforms}, {workdir, "act-composite-env-test", "push", "", platforms}, // Eval {workdir, "evalmatrix", "push", "", platforms}, {workdir, "evalmatrixneeds", "push", "", platforms}, {workdir, "evalmatrixneeds2", "push", "", platforms}, {workdir, "evalmatrix-merge-map", "push", "", platforms}, {workdir, "evalmatrix-merge-array", "push", "", platforms}, {workdir, "issue-1195", "push", "", platforms}, {workdir, "basic", "push", "", platforms}, {workdir, "fail", "push", "exit with `FAILURE`: 1", platforms}, {workdir, "runs-on", "push", "", platforms}, {workdir, "checkout", "push", "", platforms}, {workdir, "job-container", "push", "", platforms}, {workdir, "job-container-non-root", "push", "", platforms}, {workdir, "job-container-invalid-credentials", "push", "failed to handle credentials: failed to interpolate container.credentials.password", platforms}, {workdir, "container-hostname", "push", "", platforms}, {workdir, "remote-action-docker", "push", "", platforms}, {workdir, "remote-action-js", "push", "", platforms}, {workdir, "remote-action-js", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:runner-latest"}}, // Test if this works with non root container {workdir, "matrix", "push", "", platforms}, {workdir, "matrix-include-exclude", "push", "", platforms}, {workdir, "commands", "push", "", platforms}, {workdir, "workdir", "push", "", platforms}, {workdir, "defaults-run", "push", "", platforms}, {workdir, "composite-fail-with-output", "push", "", platforms}, {workdir, "issue-597", "push", "", platforms}, {workdir, "issue-598", "push", "", platforms}, {workdir, "if-env-act", "push", "", platforms}, {workdir, "env-and-path", "push", "", platforms}, {workdir, "non-existent-action", "push", "Job 'nopanic' failed", platforms}, {workdir, "outputs", "push", "", platforms}, {workdir, "networking", "push", "", platforms}, {workdir, "steps-context/conclusion", "push", "", platforms}, {workdir, "steps-context/outcome", "push", "", platforms}, {workdir, "job-status-check", "push", "job 'fail' failed", platforms}, {workdir, "if-expressions", "push", "Job 'mytest' failed", platforms}, {workdir, "actions-environment-and-context-tests", "push", "", platforms}, {workdir, "uses-action-with-pre-and-post-step", "push", "", platforms}, {workdir, "evalenv", "push", "", platforms}, {workdir, "ensure-post-steps", "push", "Job 'second-post-step-should-fail' failed", platforms}, {workdir, "workflow_dispatch", "workflow_dispatch", "", platforms}, {workdir, "workflow_dispatch_no_inputs_mapping", "workflow_dispatch", "", platforms}, {workdir, "workflow_dispatch-scalar", "workflow_dispatch", "", platforms}, {workdir, "workflow_dispatch-scalar-composite-action", "workflow_dispatch", "", platforms}, {"../model/testdata", "strategy", "push", "", platforms}, // TODO: move all testdata into pkg so we can validate it with planner and runner // {"testdata", "issue-228", "push", "", platforms, }, // TODO [igni]: Remove this once everything passes {"../model/testdata", "container-volumes", "push", "", platforms}, } for _, table := range tables { t.Run(table.workflowPath, func(t *testing.T) { config := &Config{} eventFile := filepath.Join(workdir, table.workflowPath, "event.json") if _, err := os.Stat(eventFile); err == nil { config.EventPath = eventFile } table.runTest(ctx, t, config) }) } } func TestRunEventHostEnvironment(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } ctx := context.Background() tables := []TestJobFileInfo{} if runtime.GOOS == "linux" { platforms := map[string]string{ "ubuntu-latest": "-self-hosted", } tables = append(tables, []TestJobFileInfo{ // Shells {workdir, "shells/defaults", "push", "", platforms}, {workdir, "shells/pwsh", "push", "", platforms}, {workdir, "shells/bash", "push", "", platforms}, {workdir, "shells/python", "push", "", platforms}, {workdir, "shells/sh", "push", "", platforms}, // Local action {workdir, "local-action-js", "push", "", platforms}, // Uses {workdir, "uses-composite", "push", "", platforms}, {workdir, "uses-composite-with-error", "push", "Job 'failing-composite-action' failed", platforms}, {workdir, "uses-nested-composite", "push", "", platforms}, {workdir, "act-composite-env-test", "push", "", platforms}, // Eval {workdir, "evalmatrix", "push", "", platforms}, {workdir, "evalmatrixneeds", "push", "", platforms}, {workdir, "evalmatrixneeds2", "push", "", platforms}, {workdir, "evalmatrix-merge-map", "push", "", platforms}, {workdir, "evalmatrix-merge-array", "push", "", platforms}, {workdir, "issue-1195", "push", "", platforms}, {workdir, "fail", "push", "exit with `FAILURE`: 1", platforms}, {workdir, "runs-on", "push", "", platforms}, {workdir, "checkout", "push", "", platforms}, {workdir, "remote-action-js", "push", "", platforms}, {workdir, "matrix", "push", "", platforms}, {workdir, "matrix-include-exclude", "push", "", platforms}, {workdir, "commands", "push", "", platforms}, {workdir, "defaults-run", "push", "", platforms}, {workdir, "composite-fail-with-output", "push", "", platforms}, {workdir, "issue-597", "push", "", platforms}, {workdir, "issue-598", "push", "", platforms}, {workdir, "if-env-act", "push", "", platforms}, {workdir, "env-and-path", "push", "", platforms}, {workdir, "non-existent-action", "push", "Job 'nopanic' failed", platforms}, {workdir, "outputs", "push", "", platforms}, {workdir, "steps-context/conclusion", "push", "", platforms}, {workdir, "steps-context/outcome", "push", "", platforms}, {workdir, "job-status-check", "push", "job 'fail' failed", platforms}, {workdir, "if-expressions", "push", "Job 'mytest' failed", platforms}, {workdir, "uses-action-with-pre-and-post-step", "push", "", platforms}, {workdir, "evalenv", "push", "", platforms}, {workdir, "ensure-post-steps", "push", "Job 'second-post-step-should-fail' failed", platforms}, }...) } if runtime.GOOS == "windows" { platforms := map[string]string{ "windows-latest": "-self-hosted", } tables = append(tables, []TestJobFileInfo{ {workdir, "windows-prepend-path", "push", "", platforms}, {workdir, "windows-add-env", "push", "", platforms}, }...) } else { platforms := map[string]string{ "self-hosted": "-self-hosted", } tables = append(tables, []TestJobFileInfo{ {workdir, "nix-prepend-path", "push", "", platforms}, }...) } for _, table := range tables { t.Run(table.workflowPath, func(t *testing.T) { table.runTest(ctx, t, &Config{}) }) } } func TestDryrunEvent(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } ctx := common.WithDryrun(context.Background(), true) tables := []TestJobFileInfo{ // Shells {workdir, "shells/defaults", "push", "", platforms}, {workdir, "shells/pwsh", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:pwsh-latest"}}, // custom image with pwsh {workdir, "shells/bash", "push", "", platforms}, {workdir, "shells/python", "push", "", map[string]string{"ubuntu-latest": "node:16-buster"}}, // slim doesn't have python {workdir, "shells/sh", "push", "", platforms}, // Local action {workdir, "local-action-docker-url", "push", "", platforms}, {workdir, "local-action-dockerfile", "push", "", platforms}, {workdir, "local-action-via-composite-dockerfile", "push", "", platforms}, {workdir, "local-action-js", "push", "", platforms}, } for _, table := range tables { t.Run(table.workflowPath, func(t *testing.T) { table.runTest(ctx, t, &Config{}) }) } } func TestRunDifferentArchitecture(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } tjfi := TestJobFileInfo{ workdir: workdir, workflowPath: "basic", eventName: "push", errorMessage: "", platforms: platforms, } tjfi.runTest(context.Background(), t, &Config{ContainerArchitecture: "linux/arm64"}) } func TestMaskValues(t *testing.T) { assertNoSecret := func(text string, secret string) { index := strings.Index(text, "composite secret") if index > -1 { fmt.Printf("\nFound Secret in the given text:\n%s\n", text) } assert.False(t, strings.Contains(text, "composite secret")) } if testing.Short() { t.Skip("skipping integration test") } log.SetLevel(log.DebugLevel) tjfi := TestJobFileInfo{ workdir: workdir, workflowPath: "mask-values", eventName: "push", errorMessage: "", platforms: platforms, } output := captureOutput(t, func() { tjfi.runTest(context.Background(), t, &Config{}) }) assertNoSecret(output, "secret value") assertNoSecret(output, "YWJjCg==") } func TestRunEventSecrets(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } workflowPath := "secrets" tjfi := TestJobFileInfo{ workdir: workdir, workflowPath: workflowPath, eventName: "push", errorMessage: "", platforms: platforms, } env, err := godotenv.Read(filepath.Join(workdir, workflowPath, ".env")) assert.NoError(t, err, "Failed to read .env") secrets, _ := godotenv.Read(filepath.Join(workdir, workflowPath, ".secrets")) assert.NoError(t, err, "Failed to read .secrets") tjfi.runTest(context.Background(), t, &Config{Secrets: secrets, Env: env}) } func TestRunEventPullRequest(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } workflowPath := "pull-request" tjfi := TestJobFileInfo{ workdir: workdir, workflowPath: workflowPath, eventName: "pull_request", errorMessage: "", platforms: platforms, } tjfi.runTest(context.Background(), t, &Config{EventPath: filepath.Join(workdir, workflowPath, "event.json")}) }