diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index c31a024c..55c4d0cc 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -7,11 +7,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: docker://golangci/golangci-lint:v1.23.8 - with: - args: golangci-lint run + - uses: golangci/golangci-lint-action@v2 env: CGO_ENABLED: 0 + with: + version: v1.32.2 test: name: Test @@ -35,7 +35,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: GoReleaser - uses: goreleaser/goreleaser-action@v1 + uses: goreleaser/goreleaser-action@v2 with: version: latest args: release --snapshot --rm-dist diff --git a/.github/workflows/test-expressions.yml b/.github/workflows/test-expressions.yml index ec1ee43d..fe26f9eb 100644 --- a/.github/workflows/test-expressions.yml +++ b/.github/workflows/test-expressions.yml @@ -8,6 +8,7 @@ env: KEY_WITH_UNDERSCORES: value_with_underscores SOMETHING_TRUE: true SOMETHING_FALSE: false + ACT: true jobs: diff --git a/act/common/draw.go b/act/common/draw.go index 0d64e865..b5b21fe9 100644 --- a/act/common/draw.go +++ b/act/common/draw.go @@ -43,7 +43,6 @@ type styleDef struct { var styleDefs = []styleDef{ {"\u2554", "\u2557", "\u255a", "\u255d", "\u2550", "\u2551"}, - //{"\u250c", "\u2510", "\u2514", "\u2518", "\u2500", "\u2502"}, {"\u256d", "\u256e", "\u2570", "\u256f", "\u2500", "\u2502"}, {"\u250c", "\u2510", "\u2514", "\u2518", "\u254c", "\u254e"}, {" ", " ", " ", " ", " ", " "}, diff --git a/act/common/git.go b/act/common/git.go index c5db4add..7166b9eb 100644 --- a/act/common/git.go +++ b/act/common/git.go @@ -157,7 +157,6 @@ func findGitDirectory(fromFile string) (string, error) { return "", err } - //log.Debugf("Searching for git directory in %s", absPath) fi, err := os.Stat(absPath) if err != nil { return "", err diff --git a/act/container/docker_run.go b/act/container/docker_run.go index b042735d..2f7eeb24 100644 --- a/act/container/docker_run.go +++ b/act/container/docker_run.go @@ -2,6 +2,7 @@ package container import ( "archive/tar" + "bufio" "bytes" "context" "fmt" @@ -9,6 +10,7 @@ import ( "io/ioutil" "os" "path/filepath" + "regexp" "strings" "github.com/go-git/go-billy/v5/helper/polyfill" @@ -24,7 +26,7 @@ import ( "github.com/nektos/act/pkg/common" "github.com/pkg/errors" log "github.com/sirupsen/logrus" - "golang.org/x/crypto/ssh/terminal" + "golang.org/x/term" ) // NewContainerInput the input for the New function @@ -58,6 +60,7 @@ type Container interface { Pull(forcePull bool) common.Executor Start(attach bool) common.Executor Exec(command []string, env map[string]string) common.Executor + UpdateFromGithubEnv(env *map[string]string) common.Executor Remove() common.Executor } @@ -116,6 +119,10 @@ func (cr *containerReference) CopyDir(destPath string, srcPath string) common.Ex ).IfNot(common.Dryrun) } +func (cr *containerReference) UpdateFromGithubEnv(env *map[string]string) common.Executor { + return cr.extractGithubEnv(env).IfNot(common.Dryrun) +} + func (cr *containerReference) Exec(command []string, env map[string]string) common.Executor { return common.NewPipelineExecutor( @@ -196,10 +203,10 @@ func (cr *containerReference) find() common.Executor { return errors.WithStack(err) } - for _, container := range containers { - for _, name := range container.Names { + for _, c := range containers { + for _, name := range c.Names { if name[1:] == cr.input.Name { - cr.id = container.ID + cr.id = c.ID return nil } } @@ -237,7 +244,7 @@ func (cr *containerReference) create() common.Executor { return nil } logger := common.Logger(ctx) - isTerminal := terminal.IsTerminal(int(os.Stdout.Fd())) + isTerminal := term.IsTerminal(int(os.Stdout.Fd())) input := cr.input config := &container.Config{ @@ -275,11 +282,59 @@ func (cr *containerReference) create() common.Executor { } } +var singleLineEnvPattern, mulitiLineEnvPattern *regexp.Regexp + +func (cr *containerReference) extractGithubEnv(env *map[string]string) common.Executor { + if singleLineEnvPattern == nil { + singleLineEnvPattern = regexp.MustCompile("^([^=]+)=([^=]+)$") + mulitiLineEnvPattern = regexp.MustCompile(`^([^<]+)<<(\w+)$`) + } + + localEnv := *env + return func(ctx context.Context) error { + githubEnvTar, _, err := cr.cli.CopyFromContainer(ctx, cr.id, localEnv["GITHUB_ENV"]) + if err != nil { + return nil + } + reader := tar.NewReader(githubEnvTar) + _, err = reader.Next() + if err != nil && err != io.EOF { + return errors.WithStack(err) + } + s := bufio.NewScanner(reader) + multiLineEnvKey := "" + multiLineEnvDelimiter := "" + multiLineEnvContent := "" + for s.Scan() { + line := s.Text() + if singleLineEnv := singleLineEnvPattern.FindStringSubmatch(line); singleLineEnv != nil { + localEnv[singleLineEnv[1]] = singleLineEnv[2] + } + if line == multiLineEnvDelimiter { + localEnv[multiLineEnvKey] = multiLineEnvContent + multiLineEnvKey, multiLineEnvDelimiter, multiLineEnvContent = "", "", "" + } + if multiLineEnvKey != "" && multiLineEnvDelimiter != "" { + if multiLineEnvContent != "" { + multiLineEnvContent += "\n" + } + multiLineEnvContent += line + } + if mulitiLineEnvStart := mulitiLineEnvPattern.FindStringSubmatch(line); mulitiLineEnvStart != nil { + multiLineEnvKey = mulitiLineEnvStart[1] + multiLineEnvDelimiter = mulitiLineEnvStart[2] + } + } + env = &localEnv + return nil + } +} + func (cr *containerReference) exec(cmd []string, env map[string]string) common.Executor { return func(ctx context.Context) error { logger := common.Logger(ctx) logger.Debugf("Exec command '%s'", cmd) - isTerminal := terminal.IsTerminal(int(os.Stdout.Fd())) + isTerminal := term.IsTerminal(int(os.Stdout.Fd())) envList := make([]string, 0) for k, v := range env { envList = append(envList, fmt.Sprintf("%s=%s", k, v)) @@ -492,7 +547,7 @@ func (cr *containerReference) attach() common.Executor { if err != nil { return errors.WithStack(err) } - isTerminal := terminal.IsTerminal(int(os.Stdout.Fd())) + isTerminal := term.IsTerminal(int(os.Stdout.Fd())) var outWriter io.Writer outWriter = cr.input.Stdout diff --git a/act/model/workflow.go b/act/model/workflow.go index d981f23d..aa5422b1 100644 --- a/act/model/workflow.go +++ b/act/model/workflow.go @@ -283,13 +283,13 @@ const ( // StepTypeRun is all steps that have a `run` attribute StepTypeRun StepType = iota - //StepTypeUsesDockerURL is all steps that have a `uses` that is of the form `docker://...` + // StepTypeUsesDockerURL is all steps that have a `uses` that is of the form `docker://...` StepTypeUsesDockerURL - //StepTypeUsesActionLocal is all steps that have a `uses` that is a local action in a subdirectory + // StepTypeUsesActionLocal is all steps that have a `uses` that is a local action in a subdirectory StepTypeUsesActionLocal - //StepTypeUsesActionRemote is all steps that have a `uses` that is a reference to a github repo + // StepTypeUsesActionRemote is all steps that have a `uses` that is a reference to a github repo StepTypeUsesActionRemote ) diff --git a/act/runner/command.go b/act/runner/command.go index c635322e..0b230a52 100644 --- a/act/runner/command.go +++ b/act/runner/command.go @@ -13,7 +13,7 @@ var commandPatternADO *regexp.Regexp func init() { commandPatternGA = regexp.MustCompile("^::([^ ]+)( (.+))?::([^\r\n]*)[\r\n]+$") - commandPatternADO = regexp.MustCompile("^##\\[([^ ]+)( (.+))?\\]([^\r\n]*)[\r\n]+$") + commandPatternADO = regexp.MustCompile("^##\\[([^ ]+)( (.+))?]([^\r\n]*)[\r\n]+$") } func (rc *RunContext) commandHandler(ctx context.Context) common.LineHandler { @@ -103,7 +103,7 @@ func unescapeCommandData(arg string) string { "%0A": "\n", } for k, v := range escapeMap { - arg = strings.Replace(arg, k, v, -1) + arg = strings.ReplaceAll(arg, k, v) } return arg } @@ -116,7 +116,7 @@ func unescapeCommandProperty(arg string) string { "%2C": ",", } for k, v := range escapeMap { - arg = strings.Replace(arg, k, v, -1) + arg = strings.ReplaceAll(arg, k, v) } return arg } diff --git a/act/runner/command_test.go b/act/runner/command_test.go index 1274dbf7..0d66ded5 100644 --- a/act/runner/command_test.go +++ b/act/runner/command_test.go @@ -8,17 +8,17 @@ import ( ) func TestSetEnv(t *testing.T) { - assert := assert.New(t) + a := assert.New(t) ctx := context.Background() rc := new(RunContext) handler := rc.commandHandler(ctx) handler("::set-env name=x::valz\n") - assert.Equal("valz", rc.Env["x"]) + a.Equal("valz", rc.Env["x"]) } func TestSetOutput(t *testing.T) { - assert := assert.New(t) + a := assert.New(t) ctx := context.Background() rc := new(RunContext) rc.StepResults = make(map[string]*stepResult) @@ -29,62 +29,62 @@ func TestSetOutput(t *testing.T) { Outputs: make(map[string]string), } handler("::set-output name=x::valz\n") - assert.Equal("valz", rc.StepResults["my-step"].Outputs["x"]) + a.Equal("valz", rc.StepResults["my-step"].Outputs["x"]) handler("::set-output name=x::percent2%25\n") - assert.Equal("percent2%", rc.StepResults["my-step"].Outputs["x"]) + a.Equal("percent2%", rc.StepResults["my-step"].Outputs["x"]) handler("::set-output name=x::percent2%25%0Atest\n") - assert.Equal("percent2%\ntest", rc.StepResults["my-step"].Outputs["x"]) + a.Equal("percent2%\ntest", rc.StepResults["my-step"].Outputs["x"]) handler("::set-output name=x::percent2%25%0Atest another3%25test\n") - assert.Equal("percent2%\ntest another3%test", rc.StepResults["my-step"].Outputs["x"]) + a.Equal("percent2%\ntest another3%test", rc.StepResults["my-step"].Outputs["x"]) handler("::set-output name=x%3A::percent2%25%0Atest\n") - assert.Equal("percent2%\ntest", rc.StepResults["my-step"].Outputs["x:"]) + a.Equal("percent2%\ntest", rc.StepResults["my-step"].Outputs["x:"]) handler("::set-output name=x%3A%2C%0A%25%0D%3A::percent2%25%0Atest\n") - assert.Equal("percent2%\ntest", rc.StepResults["my-step"].Outputs["x:,\n%\r:"]) + a.Equal("percent2%\ntest", rc.StepResults["my-step"].Outputs["x:,\n%\r:"]) } func TestAddpath(t *testing.T) { - assert := assert.New(t) + a := assert.New(t) ctx := context.Background() rc := new(RunContext) handler := rc.commandHandler(ctx) handler("::add-path::/zoo\n") - assert.Equal("/zoo", rc.ExtraPath[0]) + a.Equal("/zoo", rc.ExtraPath[0]) handler("::add-path::/boo\n") - assert.Equal("/boo", rc.ExtraPath[1]) + a.Equal("/boo", rc.ExtraPath[1]) } func TestStopCommands(t *testing.T) { - assert := assert.New(t) + a := assert.New(t) ctx := context.Background() rc := new(RunContext) handler := rc.commandHandler(ctx) handler("::set-env name=x::valz\n") - assert.Equal("valz", rc.Env["x"]) + a.Equal("valz", rc.Env["x"]) handler("::stop-commands::my-end-token\n") handler("::set-env name=x::abcd\n") - assert.Equal("valz", rc.Env["x"]) + a.Equal("valz", rc.Env["x"]) handler("::my-end-token::\n") handler("::set-env name=x::abcd\n") - assert.Equal("abcd", rc.Env["x"]) + a.Equal("abcd", rc.Env["x"]) } func TestAddpathADO(t *testing.T) { - assert := assert.New(t) + a := assert.New(t) ctx := context.Background() rc := new(RunContext) handler := rc.commandHandler(ctx) handler("##[add-path]/zoo\n") - assert.Equal("/zoo", rc.ExtraPath[0]) + a.Equal("/zoo", rc.ExtraPath[0]) handler("##[add-path]/boo\n") - assert.Equal("/boo", rc.ExtraPath[1]) + a.Equal("/boo", rc.ExtraPath[1]) } diff --git a/act/runner/expression_test.go b/act/runner/expression_test.go index cd336e41..607d7e96 100644 --- a/act/runner/expression_test.go +++ b/act/runner/expression_test.go @@ -185,7 +185,6 @@ func TestInterpolate(t *testing.T) { func updateTestExpressionWorkflow(t *testing.T, tables []struct { in string out string - //wantErr bool }, rc *RunContext) { var envs string diff --git a/act/runner/logger.go b/act/runner/logger.go index 8c3bb373..ff3384cc 100644 --- a/act/runner/logger.go +++ b/act/runner/logger.go @@ -12,11 +12,11 @@ import ( "github.com/nektos/act/pkg/common" "github.com/sirupsen/logrus" - "golang.org/x/crypto/ssh/terminal" + "golang.org/x/term" ) const ( - //nocolor = 0 + // nocolor = 0 red = 31 green = 32 yellow = 33 @@ -126,7 +126,7 @@ func (f *stepLogFormatter) isColored(entry *logrus.Entry) bool { func checkIfTerminal(w io.Writer) bool { switch v := w.(type) { case *os.File: - return terminal.IsTerminal(int(v.Fd())) + return term.IsTerminal(int(v.Fd())) default: return false } diff --git a/act/runner/run_context.go b/act/runner/run_context.go index 384e70d3..0baf54e4 100644 --- a/act/runner/run_context.go +++ b/act/runner/run_context.go @@ -125,6 +125,10 @@ func (rc *RunContext) startJobContainer() common.Executor { Name: "workflow/event.json", Mode: 0644, Body: rc.EventJSON, + }, &container.FileEntry{ + Name: "workflow/envs.txt", + Mode: 0644, + Body: "", }, &container.FileEntry{ Name: "home/.act", Mode: 0644, @@ -199,6 +203,13 @@ func (rc *RunContext) newStepExecutor(step *model.Step) common.Executor { } _ = sc.setupEnv()(ctx) + + if sc.Env != nil { + err := rc.JobContainer.UpdateFromGithubEnv(&sc.Env)(ctx) + if err != nil { + return err + } + } rc.ExprEval = sc.NewExpressionEvaluator() runStep, err := rc.EvalBool(sc.Step.If) @@ -267,14 +278,17 @@ func (rc *RunContext) isEnabled(ctx context.Context) bool { return true } +var splitPattern *regexp.Regexp + // EvalBool evaluates an expression against current run context func (rc *RunContext) EvalBool(expr string) (bool, error) { + if splitPattern == nil { + splitPattern = regexp.MustCompile(fmt.Sprintf(`%s|%s|\S+`, expressionPattern.String(), operatorPattern.String())) + } if strings.HasPrefix(strings.TrimSpace(expr), "!") { return false, errors.New("expressions starting with ! must be wrapped in ${{ }}") } if expr != "" { - splitPattern := regexp.MustCompile(fmt.Sprintf(`%s|%s|\S+`, expressionPattern.String(), operatorPattern.String())) - parts := splitPattern.FindAllString(expr, -1) var evaluatedParts []string for i, part := range parts { @@ -550,6 +564,8 @@ func (rc *RunContext) withGithubEnv(env map[string]string) map[string]string { github := rc.getGithubContext() env["CI"] = "true" env["HOME"] = "/github/home" + env["GITHUB_ENV"] = "/github/workflow/envs.txt" + env["GITHUB_WORKFLOW"] = github.Workflow env["GITHUB_RUN_ID"] = github.RunID env["GITHUB_RUN_NUMBER"] = github.RunNumber diff --git a/act/runner/runner_test.go b/act/runner/runner_test.go index ba20ded0..b87d71b5 100644 --- a/act/runner/runner_test.go +++ b/act/runner/runner_test.go @@ -92,7 +92,7 @@ func TestRunEvent(t *testing.T) { {"testdata", "matrix-include-exclude", "push", "", platforms}, {"testdata", "commands", "push", "", platforms}, {"testdata", "workdir", "push", "", platforms}, - //{"testdata", "issue-228", "push", "", platforms}, // TODO [igni]: Remove this once everything passes + // {"testdata", "issue-228", "push", "", platforms}, // TODO [igni]: Remove this once everything passes {"testdata", "defaults-run", "push", "", platforms}, } log.SetLevel(log.DebugLevel) diff --git a/cmd/root.go b/cmd/root.go index c823f3c7..2fa2db4a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -11,7 +11,7 @@ import ( "github.com/nektos/act/pkg/common" - fswatch "github.com/andreaskoch/go-fswatch" + "github.com/andreaskoch/go-fswatch" "github.com/joho/godotenv" "github.com/nektos/act/pkg/model" "github.com/nektos/act/pkg/runner" @@ -79,7 +79,12 @@ func readArgsFile(file string) []string { if err != nil { return args } - defer f.Close() + defer func() { + err := f.Close() + if err != nil { + log.Errorf("Failed to close args file: %v", err) + } + }() scanner := bufio.NewScanner(f) for scanner.Scan() { arg := scanner.Text() @@ -91,7 +96,7 @@ func readArgsFile(file string) []string { } -func setupLogging(cmd *cobra.Command, args []string) { +func setupLogging(cmd *cobra.Command, _ []string) { verbose, _ := cmd.Flags().GetBool("verbose") if verbose { log.SetLevel(log.DebugLevel) @@ -189,7 +194,7 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str Platforms: input.newPlatforms(), Privileged: input.privileged, } - runner, err := runner.New(config) + r, err := runner.New(config) if err != nil { return err } @@ -198,10 +203,10 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str if watch, err := cmd.Flags().GetBool("watch"); err != nil { return err } else if watch { - return watchAndRun(ctx, runner.NewPlanExecutor(plan)) + return watchAndRun(ctx, r.NewPlanExecutor(plan)) } - return runner.NewPlanExecutor(plan)(ctx) + return r.NewPlanExecutor(plan)(ctx) } }