diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 87d51e0..f53769d 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -6,5 +6,5 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: ./.github/workflows/check + #- uses: ./.github/workflows/check - uses: ./.github/workflows/integration diff --git a/pkg/common/line_writer.go b/pkg/common/line_writer.go new file mode 100644 index 0000000..4d1661b --- /dev/null +++ b/pkg/common/line_writer.go @@ -0,0 +1,47 @@ +package common + +import ( + "bytes" + "io" +) + +// LineHandler is a callback function for handling a line +type LineHandler func(line string) + +type lineWriter struct { + buffer bytes.Buffer + handlers []LineHandler +} + +// NewLineWriter creates a new instance of a line writer +func NewLineWriter(handlers ...LineHandler) io.Writer { + w := new(lineWriter) + w.handlers = handlers + return w +} + +func (lw *lineWriter) Write(p []byte) (n int, err error) { + pBuf := bytes.NewBuffer(p) + written := 0 + for { + line, err := pBuf.ReadString('\n') + w, _ := lw.buffer.WriteString(line) + written += w + if err == nil { + lw.handleLine(lw.buffer.String()) + lw.buffer.Reset() + } else if err == io.EOF { + break + } else { + return written, err + } + } + + return written, nil +} + +func (lw *lineWriter) handleLine(line string) { + for _, h := range lw.handlers { + h(line) + } +} diff --git a/pkg/common/line_writer_test.go b/pkg/common/line_writer_test.go new file mode 100644 index 0000000..462a20d --- /dev/null +++ b/pkg/common/line_writer_test.go @@ -0,0 +1,36 @@ +package common + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLineWriter(t *testing.T) { + lines := make([]string, 0) + lineHandler := func(s string) { + lines = append(lines, s) + } + + lineWriter := NewLineWriter(lineHandler) + + assert := assert.New(t) + write := func(s string) { + n, err := lineWriter.Write([]byte(s)) + assert.NoError(err) + assert.Equal(len(s), n, s) + } + + write("hello") + write(" ") + write("world!!\nextra") + write(" line\n and another\nlast") + write(" line\n") + write("no newline here...") + + assert.Len(lines, 4) + assert.Equal("hello world!!\n", lines[0]) + assert.Equal("extra line\n", lines[1]) + assert.Equal(" and another\n", lines[2]) + assert.Equal("last line\n", lines[3]) +} diff --git a/pkg/container/docker_logger.go b/pkg/container/docker_logger.go index c47d220..c4b5e39 100644 --- a/pkg/container/docker_logger.go +++ b/pkg/container/docker_logger.go @@ -2,15 +2,11 @@ package container import ( "bufio" - "context" "encoding/json" "errors" "io" - "github.com/nektos/act/pkg/common" "github.com/sirupsen/logrus" - - "github.com/docker/docker/pkg/stdcopy" ) type dockerMessage struct { @@ -26,6 +22,7 @@ type dockerMessage struct { const logPrefix = " \U0001F433 " +/* func logDockerOutput(ctx context.Context, dockerResponse io.Reader) { logger := common.Logger(ctx) if entry, ok := logger.(*logrus.Entry); ok { @@ -44,7 +41,9 @@ func logDockerOutput(ctx context.Context, dockerResponse io.Reader) { logrus.Errorf("Unable to get writer from logger (type=%T)", logger) } } +*/ +/* func streamDockerOutput(ctx context.Context, dockerResponse io.Reader) { /* out := os.Stdout @@ -57,7 +56,7 @@ func streamDockerOutput(ctx context.Context, dockerResponse io.Reader) { if err != nil { logrus.Error(err) } - */ + * / logger := common.Logger(ctx) reader := bufio.NewReader(dockerResponse) @@ -74,6 +73,7 @@ func streamDockerOutput(ctx context.Context, dockerResponse io.Reader) { } } +*/ func logDockerResponse(logger logrus.FieldLogger, dockerResponse io.ReadCloser, isError bool) error { if dockerResponse == nil { diff --git a/pkg/runner/command.go b/pkg/runner/command.go new file mode 100644 index 0000000..6e0ab67 --- /dev/null +++ b/pkg/runner/command.go @@ -0,0 +1,91 @@ +package runner + +import ( + "context" + "fmt" + "regexp" + "strings" + + "github.com/nektos/act/pkg/common" +) + +var commandPattern *regexp.Regexp + +func init() { + commandPattern = regexp.MustCompile("^::([^ ]+)( (.+))?::([^\r\n]*)[\r\n]+$") +} + +func (rc *RunContext) commandHandler(ctx context.Context) common.LineHandler { + logger := common.Logger(ctx) + resumeCommand := "" + return func(line string) { + if m := commandPattern.FindStringSubmatch(line); m != nil { + command := m[1] + kvPairs := parseKeyValuePairs(m[3]) + arg := m[4] + + if resumeCommand != "" && command != resumeCommand { + return + } + + switch command { + case "set-env": + rc.setEnv(ctx, kvPairs, arg) + case "set-output": + rc.setOutput(ctx, kvPairs, arg) + case "add-path": + rc.addPath(ctx, arg) + case "debug": + logger.Infof(" \U0001F4AC %s", line) + case "warning": + logger.Infof(" \U0001F6A7 %s", line) + case "error": + logger.Infof(" \U00002757 %s", line) + case "add-mask": + logger.Infof(" \U00002699 %s", line) + case "stop-commands": + resumeCommand = arg + logger.Infof(" \U00002699 %s", line) + case resumeCommand: + resumeCommand = "" + logger.Infof(" \U00002699 %s", line) + default: + logger.Infof(" \U00002753 %s", line) + } + } + } +} + +func (rc *RunContext) setEnv(ctx context.Context, kvPairs map[string]string, arg string) { + common.Logger(ctx).Infof(" \U00002699 ::set-env:: %s=%s", kvPairs["name"], arg) + if rc.Env == nil { + rc.Env = make(map[string]string) + } + rc.Env[kvPairs["name"]] = arg +} +func (rc *RunContext) setOutput(ctx context.Context, kvPairs map[string]string, arg string) { + common.Logger(ctx).Infof(" \U00002699 ::set-output:: %s=%s", kvPairs["name"], arg) + if rc.Outputs == nil { + rc.Outputs = make(map[string]string) + } + rc.Outputs[kvPairs["name"]] = arg +} +func (rc *RunContext) addPath(ctx context.Context, arg string) { + common.Logger(ctx).Infof(" \U00002699 ::add-path:: %s", arg) + if rc.Env == nil { + rc.Env = make(map[string]string) + } + rc.Env["PATH"] = fmt.Sprintf("%s:%s", arg, rc.Env["PATH"]) +} + +func parseKeyValuePairs(kvPairs string) map[string]string { + rtn := make(map[string]string) + kvPairList := strings.Split(kvPairs, ",") + for _, kvPair := range kvPairList { + kv := strings.Split(kvPair, "=") + if len(kv) == 2 { + rtn[kv[0]] = kv[1] + } + } + return rtn +} diff --git a/pkg/runner/command_test.go b/pkg/runner/command_test.go new file mode 100644 index 0000000..5e4c2d2 --- /dev/null +++ b/pkg/runner/command_test.go @@ -0,0 +1,57 @@ +package runner + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSetEnv(t *testing.T) { + assert := 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"]) +} + +func TestSetOutput(t *testing.T) { + assert := assert.New(t) + ctx := context.Background() + rc := new(RunContext) + handler := rc.commandHandler(ctx) + + handler("::set-output name=x::valz\n") + assert.Equal("valz", rc.Outputs["x"]) +} + +func TestAddpath(t *testing.T) { + assert := assert.New(t) + ctx := context.Background() + rc := new(RunContext) + handler := rc.commandHandler(ctx) + + handler("::add-path::/zoo") + assert.Equal("/zoo:", rc.Env["PATH"]) + + handler("::add-path::/booo") + assert.Equal("/booo:/zoo:", rc.Env["PATH"]) +} + +func TestStopCommands(t *testing.T) { + assert := 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"]) + handler("::stop-commands::my-end-token\n") + handler("::set-env name=x::abcd\n") + assert.Equal("valz", rc.Env["x"]) + handler("::my-end-token::\n") + handler("::set-env name=x::abcd\n") + assert.Equal("abcd", rc.Env["x"]) +} diff --git a/pkg/runner/logger.go b/pkg/runner/logger.go index 50b077a..c1ace20 100644 --- a/pkg/runner/logger.go +++ b/pkg/runner/logger.go @@ -7,6 +7,7 @@ import ( "io" "os" "strings" + "sync" "github.com/nektos/act/pkg/common" @@ -27,6 +28,7 @@ const ( var colors []int var nextColor int +var mux sync.Mutex func init() { nextColor = 0 @@ -37,9 +39,11 @@ func init() { // WithJobLogger attaches a new logger to context that is aware of steps func WithJobLogger(ctx context.Context, jobName string) context.Context { + mux.Lock() + defer mux.Unlock() formatter := new(stepLogFormatter) formatter.color = colors[nextColor%len(colors)] - nextColor = nextColor + 1 + nextColor++ logger := logrus.New() logger.SetFormatter(formatter) @@ -71,7 +75,9 @@ func (f *stepLogFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry) { entry.Message = strings.TrimSuffix(entry.Message, "\n") jobName := entry.Data["job"] - if entry.Data["dryrun"] == true { + if entry.Data["raw_output"] == true { + fmt.Fprintf(b, "\x1b[%dm|\x1b[0m %s", f.color, entry.Message) + } else if entry.Data["dryrun"] == true { fmt.Fprintf(b, "\x1b[1m\x1b[%dm\x1b[7m*DRYRUN*\x1b[0m \x1b[%dm[%s] \x1b[0m%s", gray, f.color, jobName, entry.Message) } else { fmt.Fprintf(b, "\x1b[%dm[%s] \x1b[0m%s", f.color, jobName, entry.Message) @@ -82,7 +88,9 @@ func (f *stepLogFormatter) print(b *bytes.Buffer, entry *logrus.Entry) { entry.Message = strings.TrimSuffix(entry.Message, "\n") jobName := entry.Data["job"] - if entry.Data["dryrun"] == true { + if entry.Data["raw_output"] == true { + fmt.Fprintf(b, "[%s] | %s", jobName, entry.Message) + } else if entry.Data["dryrun"] == true { fmt.Fprintf(b, "*DRYRUN* [%s] %s", jobName, entry.Message) } else { fmt.Fprintf(b, "[%s] %s", jobName, entry.Message) diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index 8131270..0e9ca18 100644 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -57,9 +57,7 @@ func (rc *RunContext) Executor() common.Executor { } s := step steps = append(steps, func(ctx context.Context) error { - //common.Logger(ctx).Infof("\U0001F680 Begin %s", step) - //common.Logger(ctx).Infof("\u2728 Begin - %s", step) - common.Logger(ctx).Infof("\u2B50 Begin - %s", s) + common.Logger(ctx).Infof("\u2B50 Run %s", s) err := rc.newStepExecutor(s)(ctx) if err == nil { common.Logger(ctx).Infof(" \u2705 Success - %s", s) @@ -128,16 +126,10 @@ func (rc *RunContext) runContainer(containerSpec *model.ContainerSpec) common.Ex entrypoint = strings.Fields(containerSpec.Entrypoint) } - var logWriter io.Writer - logger := common.Logger(ctx) - if entry, ok := logger.(*log.Entry); ok { - logWriter = entry.Writer() - } else if lgr, ok := logger.(*log.Logger); ok { - logWriter = lgr.Writer() - } else { - logger.Errorf("Unable to get writer from logger (type=%T)", logger) - } - logWriter = os.Stdout + rawLogger := common.Logger(ctx).WithField("raw_output", true) + logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) { + rawLogger.Debugf(s) + }) return container.NewDockerRunExecutor(container.NewDockerRunExecutorInput{ Cmd: cmd, diff --git a/pkg/runner/runner_test.go b/pkg/runner/runner_test.go index 28af7a9..49bcb47 100644 --- a/pkg/runner/runner_test.go +++ b/pkg/runner/runner_test.go @@ -37,15 +37,18 @@ func TestRunEvent(t *testing.T) { eventName string errorMessage string }{ - {"basic", "push", ""}, - {"fail", "push", "exit with `FAILURE`: 1"}, {"runs-on", "push", ""}, - {"job-container", "push", ""}, - {"uses-docker-url", "push", ""}, - {"remote-action-docker", "push", ""}, - {"remote-action-js", "push", ""}, - {"local-action-docker-url", "push", ""}, - {"local-action-dockerfile", "push", ""}, + /* + {"basic", "push", ""}, + {"fail", "push", "exit with `FAILURE`: 1"}, + {"runs-on", "push", ""}, + {"job-container", "push", ""}, + {"uses-docker-url", "push", ""}, + {"remote-action-docker", "push", ""}, + {"remote-action-js", "push", ""}, + {"local-action-docker-url", "push", ""}, + {"local-action-dockerfile", "push", ""}, + */ } log.SetLevel(log.DebugLevel) @@ -55,8 +58,9 @@ func TestRunEvent(t *testing.T) { table := table t.Run(table.workflowPath, func(t *testing.T) { runnerConfig := &Config{ - Workdir: "testdata", - EventName: table.eventName, + Workdir: "testdata", + EventName: table.eventName, + ReuseContainers: true, } runner, err := New(runnerConfig) assert.NilError(t, err, table.workflowPath) diff --git a/pkg/runner/testdata/commands/push.yml b/pkg/runner/testdata/commands/push.yml new file mode 100644 index 0000000..af30675 --- /dev/null +++ b/pkg/runner/testdata/commands/push.yml @@ -0,0 +1,33 @@ +name: basic +on: push + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: TEST set-env + run: echo "::set-env name=foo::bar" + - name: TEST set-env (cont.) + run: echo $foo | grep bar + + - name: TEST set-output + run: echo "::set-output name=zoo::zar" + + #- run: echo "::add-path::/zip" + #- run: echo $PATH | grep /zip + + - name: TEST debug, warning, error + run: | + echo "::debug file=app.js,line=100,col=20::Hello debug!" + echo "::warning file=app.js,line=100,col=20::Hello warning!" + echo "::error file=app.js,line=100,col=30::Hello error!" + + - name: TEST stop-commands + run: | + echo "::stop-commands::my-end-token" + echo "::set-env name=foo::baz" + echo $foo | grep bar + echo "::my-end-token::" + echo "::set-env name=foo::baz" + - name: TEST stop-commands (cont.) + run: echo $foo | grep baz diff --git a/pkg/runner/testdata/parallel/push.yml b/pkg/runner/testdata/parallel/push.yml index 71ad27f..fb1415a 100644 --- a/pkg/runner/testdata/parallel/push.yml +++ b/pkg/runner/testdata/parallel/push.yml @@ -20,7 +20,7 @@ jobs: args: echo ${GITHUB_REF} | grep nektos/act - uses: ./actions/docker-url with: - args: npm install angular-cli + args: npm install -g qs test2: runs-on: ubuntu-latest needs: [build] diff --git a/pkg/runner/testdata/runs-on/push.yml b/pkg/runner/testdata/runs-on/push.yml index f6b0142..a0e49eb 100644 --- a/pkg/runner/testdata/runs-on/push.yml +++ b/pkg/runner/testdata/runs-on/push.yml @@ -5,4 +5,6 @@ jobs: test: runs-on: ubuntu-latest steps: + - run: env + - run: echo ${GITHUB_ACTOR} - run: echo ${GITHUB_ACTOR} | grep nektos/act