diff --git a/.github/workflows/check/Dockerfile b/.github/workflows/check/Dockerfile deleted file mode 100644 index 5c67ab28..00000000 --- a/.github/workflows/check/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM golangci/golangci-lint:v1.23.6 - -RUN apt-get install git - -COPY "entrypoint.sh" "/entrypoint.sh" -RUN chmod +x /entrypoint.sh - -ENV GOFLAGS -mod=vendor -ENTRYPOINT ["/entrypoint.sh"] diff --git a/.github/workflows/check/action.yml b/.github/workflows/check/action.yml deleted file mode 100644 index 5222118d..00000000 --- a/.github/workflows/check/action.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: Check -description: Run static analysis and unit tests -branding: - icon: check-circle - color: green -runs: - using: 'docker' - image: 'Dockerfile' diff --git a/.github/workflows/check/entrypoint.sh b/.github/workflows/check/entrypoint.sh deleted file mode 100644 index bbfc168d..00000000 --- a/.github/workflows/check/entrypoint.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -set -e -golangci-lint run -go test -cover -short ./... diff --git a/.github/workflows/integration/Dockerfile b/.github/workflows/integration/Dockerfile deleted file mode 100644 index 9a5c1083..00000000 --- a/.github/workflows/integration/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM golangci/golangci-lint:v1.23.6 - -COPY "entrypoint.sh" "/entrypoint.sh" -RUN chmod +x /entrypoint.sh - -ENV GOFLAGS -mod=vendor -ENTRYPOINT ["/entrypoint.sh"] diff --git a/.github/workflows/integration/action.yml b/.github/workflows/integration/action.yml deleted file mode 100644 index 8c73f49a..00000000 --- a/.github/workflows/integration/action.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: Check -description: Run integration tests -branding: - icon: check-circle - color: green -runs: - using: 'docker' - image: 'Dockerfile' diff --git a/.github/workflows/integration/entrypoint.sh b/.github/workflows/integration/entrypoint.sh deleted file mode 100644 index 262d0515..00000000 --- a/.github/workflows/integration/entrypoint.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -set -e -go test -cover ./pkg/runner diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index d1937db1..78450ff0 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -2,9 +2,40 @@ name: push on: push jobs: - ci: + lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: ./.github/workflows/check - #- uses: ./.github/workflows/integration + - uses: docker://golangci/golangci-lint:v1.23.6 + with: + args: golangci-lint run + env: + CGO_ENABLED: 0 + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v1 + with: + go-version: 1.13 + - run: go test -cover ./... + env: + CGO_ENABLED: 0 + GOFLAGS: -mod=vendor + + release: + if: startsWith(github.ref, 'refs/tags/v') + needs: + - lint + - test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: GoReleaser + uses: goreleaser/goreleaser-action@v1 + with: + version: latest + args: release --rm-dist + env: + GITHUB_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN }} diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml deleted file mode 100644 index f64b244a..00000000 --- a/.github/workflows/tag.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: tag -on: - push: - tags: - - 'v*' - -jobs: - release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: ./.github/workflows/check - #- uses: ./.github/workflows/integration - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v1 - with: - version: latest - args: release --rm-dist - env: - GITHUB_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN }} diff --git a/Makefile b/Makefile index c8fd328c..abd9ec83 100644 --- a/Makefile +++ b/Makefile @@ -1,30 +1,21 @@ -LATEST_VERSION := $(shell git tag -l --sort=creatordate | grep "^v[0-9]*.[0-9]*.[0-9]*$$" | tail -1 | cut -c 2-) -ifeq "$(shell git tag -l v$(LATEST_VERSION) --points-at HEAD)" "v$(LATEST_VERSION)" -### latest tag points to current commit, this is a release build -VERSION ?= $(LATEST_VERSION) -else -### latest tag points to prior commit, this is a snapshot build -MAJOR_VERSION := $(word 1, $(subst ., ,$(LATEST_VERSION))) -MINOR_VERSION := $(word 2, $(subst ., ,$(LATEST_VERSION))) -PATCH_VERSION := $(word 3, $(subst ., ,$(LATEST_VERSION))) -VERSION ?= $(MAJOR_VERSION).$(MINOR_VERSION).$(shell echo $$(( $(PATCH_VERSION) + 1)) )-develop -endif +VERSION?=$(shell git describe --tags --dirty | cut -c 2-) IS_SNAPSHOT = $(if $(findstring -, $(VERSION)),true,false) -TAG_VERSION = v$(VERSION) +MAJOR_VERSION = $(word 1, $(subst ., ,$(VERSION))) +MINOR_VERSION = $(word 2, $(subst ., ,$(VERSION))) +PATCH_VERSION = $(word 3, $(subst ., ,$(word 1,$(subst -, , $(VERSION))))) +NEW_VERSION ?= $(MAJOR_VERSION).$(MINOR_VERSION).$(shell echo $$(( $(PATCH_VERSION) + 1)) ) -ACT ?= go run -mod=vendor main.go +ACT ?= go run main.go export GITHUB_TOKEN = $(shell cat ~/.config/github/token) -check: - @golangci-lint run - @go test -cover ./... +build: + go build -ldflags "-X main.version=$(VERSION)" -o dist/local/act main.go -build: check - $(eval export SNAPSHOT_VERSION=$(VERSION)) - $(ACT) -ra build +test: + $(ACT) -P ubuntu-latest=nektos/act-environments-ubuntu:18.04 install: build - @cp dist/$(shell go env GOOS)_$(shell go env GOARCH)/act /usr/local/bin/act + @cp dist/local/act /usr/local/bin/act @chmod 755 /usr/local/bin/act @act --version @@ -33,7 +24,8 @@ installer: godownloader -r nektos/act -o install.sh promote: vendor - @echo "VERSION:$(VERSION) IS_SNAPSHOT:$(IS_SNAPSHOT) LATEST_VERSION:$(LATEST_VERSION)" + @git fetch --tags + @echo "VERSION:$(VERSION) IS_SNAPSHOT:$(IS_SNAPSHOT) NEW_VERSION:$(NEW_VERSION)" ifeq (false,$(IS_SNAPSHOT)) @echo "Unable to promote a non-snapshot" @exit 1 @@ -42,9 +34,8 @@ ifneq ($(shell git status -s),) @echo "Unable to promote a dirty workspace" @exit 1 endif - $(eval NEW_VERSION := $(word 1,$(subst -, , $(TAG_VERSION)))) - git tag -a -m "releasing $(NEW_VERSION)" $(NEW_VERSION) - git push origin $(NEW_VERSION) + git tag -a -m "releasing v$(NEW_VERSION)" v$(NEW_VERSION) + git push origin v$(NEW_VERSION) vendor: go mod vendor diff --git a/README.md b/README.md index bd1a4609..813a3b81 100644 --- a/README.md +++ b/README.md @@ -56,9 +56,9 @@ GitHub Actions offers managed [virtual environments](https://help.github.com/en/ | GitHub Runner | Docker Image | | --------------- | ------------ | -| ubuntu-latest | [ubuntu:18.04](https://hub.docker.com/_/ubuntu) | -| ubuntu-18.04 | [ubuntu:18.04](https://hub.docker.com/_/ubuntu) | -| ubuntu-16.04 | [ubuntu:16.04](https://hub.docker.com/_/ubuntu) | +| ubuntu-latest | [node:12.6-buster-slim](https://hub.docker.com/_/buildpack-deps) | +| ubuntu-18.04 | [node:12.6-buster-slim](https://hub.docker.com/_/buildpack-deps) | +| ubuntu-16.04 | [node:12.6-stretch-slim](https://hub.docker.com/_/buildpack-deps) | | windows-latest | `unsupported` | | windows-2019 | `unsupported` | | macos-latest | `unsupported` | diff --git a/cmd/input.go b/cmd/input.go index 63a64adc..3b1b34f9 100644 --- a/cmd/input.go +++ b/cmd/input.go @@ -11,6 +11,7 @@ type Input struct { workflowsPath string eventPath string reuseContainers bool + bindWorkdir bool secrets []string platforms []string dryrun bool diff --git a/cmd/platforms.go b/cmd/platforms.go index 46d45b19..b79e8edd 100644 --- a/cmd/platforms.go +++ b/cmd/platforms.go @@ -6,9 +6,9 @@ import ( func (i *Input) newPlatforms() map[string]string { platforms := map[string]string{ - "ubuntu-latest": "ubuntu:18.04", - "ubuntu-18.04": "ubuntu:18.04", - "ubuntu-16.04": "ubuntu:16.04", + "ubuntu-latest": "node:12.6-buster-slim", + "ubuntu-18.04": "node:12.6-buster-slim", + "ubuntu-16.04": "node:12.6-stretch-slim", "windows-latest": "", "windows-2019": "", "macos-latest": "", diff --git a/cmd/root.go b/cmd/root.go index a22b10f7..efbc05c0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -33,6 +33,7 @@ func Execute(ctx context.Context, version string) { rootCmd.Flags().StringArrayVarP(&input.secrets, "secret", "s", []string{}, "secret to make available to actions with optional value (e.g. -s mysecret=foo or -s mysecret)") rootCmd.Flags().StringArrayVarP(&input.platforms, "platform", "P", []string{}, "custom image to use per platform (e.g. -P ubuntu-18.04=nektos/act-environments-ubuntu:18.04)") rootCmd.Flags().BoolVarP(&input.reuseContainers, "reuse", "r", false, "reuse action containers to maintain state") + rootCmd.Flags().BoolVarP(&input.bindWorkdir, "bind", "b", false, "bind working directory to container, rather than copy") rootCmd.Flags().BoolVarP(&input.forcePull, "pull", "p", false, "pull docker image(s) if already present") rootCmd.Flags().StringVarP(&input.eventPath, "eventpath", "e", "", "path to event JSON file") rootCmd.PersistentFlags().StringVarP(&input.workflowsPath, "workflows", "W", "./.github/workflows/", "path to workflow files") @@ -97,6 +98,7 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str ForcePull: input.forcePull, ReuseContainers: input.reuseContainers, Workdir: input.Workdir(), + BindWorkdir: input.bindWorkdir, LogOutput: !input.noOutput, Secrets: newSecrets(input.secrets), Platforms: input.newPlatforms(), diff --git a/go.sum b/go.sum index c747a1dc..1a0a56fd 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,7 @@ github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BU github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/engine v0.0.0-20181106193140-f5749085e9cb h1:PyjxRdW1mqCmSoxy/6uP01P7CGbsD+woX+oOWbaUPwQ= github.com/docker/engine v0.0.0-20181106193140-f5749085e9cb/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY= +github.com/docker/engine v1.13.1 h1:Cks33UT9YBW5Xyc3MtGDq2IPgqfJtJ+qkFaxc2b0Euc= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk= diff --git a/pkg/common/executor.go b/pkg/common/executor.go index f16de41d..197cd5b3 100644 --- a/pkg/common/executor.go +++ b/pkg/common/executor.go @@ -40,6 +40,15 @@ func NewInfoExecutor(format string, args ...interface{}) Executor { } } +// NewDebugExecutor is an executor that logs messages +func NewDebugExecutor(format string, args ...interface{}) Executor { + return func(ctx context.Context) error { + logger := Logger(ctx) + logger.Debugf(format, args...) + return nil + } +} + // NewPipelineExecutor creates a new executor from a series of other executors func NewPipelineExecutor(executors ...Executor) Executor { if len(executors) == 0 { diff --git a/pkg/common/git.go b/pkg/common/git.go index 159141fa..c7a059ca 100644 --- a/pkg/common/git.go +++ b/pkg/common/git.go @@ -254,7 +254,7 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) Executor { Force: true, }) if err != nil { - logger.Errorf("Unable to checkout %s: %v", refName, err) + logger.Errorf("Unable to checkout %s: %v", *hash, err) return err } diff --git a/pkg/common/line_writer.go b/pkg/common/line_writer.go index 4d1661b2..20351997 100644 --- a/pkg/common/line_writer.go +++ b/pkg/common/line_writer.go @@ -6,7 +6,7 @@ import ( ) // LineHandler is a callback function for handling a line -type LineHandler func(line string) +type LineHandler func(line string) bool type lineWriter struct { buffer bytes.Buffer @@ -42,6 +42,9 @@ func (lw *lineWriter) Write(p []byte) (n int, err error) { func (lw *lineWriter) handleLine(line string) { for _, h := range lw.handlers { - h(line) + ok := h(line) + if !ok { + break + } } } diff --git a/pkg/common/line_writer_test.go b/pkg/common/line_writer_test.go index 462a20d9..44e11ef5 100644 --- a/pkg/common/line_writer_test.go +++ b/pkg/common/line_writer_test.go @@ -8,8 +8,9 @@ import ( func TestLineWriter(t *testing.T) { lines := make([]string, 0) - lineHandler := func(s string) { + lineHandler := func(s string) bool { lines = append(lines, s) + return true } lineWriter := NewLineWriter(lineHandler) diff --git a/pkg/container/docker_pull.go b/pkg/container/docker_pull.go index 7aa9b893..69a2e2f4 100644 --- a/pkg/container/docker_pull.go +++ b/pkg/container/docker_pull.go @@ -21,7 +21,7 @@ type NewDockerPullExecutorInput struct { func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor { return func(ctx context.Context) error { logger := common.Logger(ctx) - logger.Infof("%sdocker pull %v", logPrefix, input.Image) + logger.Debugf("%sdocker pull %v", logPrefix, input.Image) if common.Dryrun(ctx) { return nil diff --git a/pkg/container/docker_run.go b/pkg/container/docker_run.go index b54ab875..faf436b0 100644 --- a/pkg/container/docker_run.go +++ b/pkg/container/docker_run.go @@ -1,67 +1,141 @@ package container import ( + "archive/tar" + "bytes" "context" "fmt" "io" + "io/ioutil" "os" + "path/filepath" + "strings" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/mount" "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" "github.com/nektos/act/pkg/common" "github.com/pkg/errors" + log "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh/terminal" ) -// NewDockerRunExecutorInput the input for the NewDockerRunExecutor function -type NewDockerRunExecutorInput struct { - Image string - Entrypoint []string - Cmd []string - WorkingDir string - Env []string - Binds []string - Content map[string]io.Reader - Volumes []string - Name string - ReuseContainers bool - Stdout io.Writer - Stderr io.Writer +// NewContainerInput the input for the New function +type NewContainerInput struct { + Image string + Entrypoint []string + Cmd []string + WorkingDir string + Env []string + Binds []string + Mounts map[string]string + Name string + Stdout io.Writer + Stderr io.Writer } -// NewDockerRunExecutor function to create a run executor for the container -func NewDockerRunExecutor(input NewDockerRunExecutorInput) common.Executor { +// FileEntry is a file to copy to a container +type FileEntry struct { + Name string + Mode int64 + Body string +} + +// Container for managing docker run containers +type Container interface { + Create() common.Executor + Copy(destPath string, files ...*FileEntry) common.Executor + CopyDir(destPath string, srcPath string) common.Executor + Pull(forcePull bool) common.Executor + Start(attach bool) common.Executor + Exec(command []string, env map[string]string) common.Executor + Remove() common.Executor +} + +// NewContainer creates a reference to a container +func NewContainer(input *NewContainerInput) Container { cr := new(containerReference) cr.input = input + return cr +} +func (cr *containerReference) Create() common.Executor { return common. - NewInfoExecutor("%sdocker run image=%s entrypoint=%+q cmd=%+q", logPrefix, input.Image, input.Entrypoint, input.Cmd). + NewDebugExecutor("%sdocker create image=%s entrypoint=%+q cmd=%+q", logPrefix, cr.input.Image, cr.input.Entrypoint, cr.input.Cmd). Then( common.NewPipelineExecutor( cr.connect(), cr.find(), - cr.remove().IfBool(!input.ReuseContainers), cr.create(), - cr.copyContent(), - cr.attach(), - cr.start(), - cr.wait(), - ).Finally( - cr.remove().IfBool(!input.ReuseContainers), ).IfNot(common.Dryrun), ) } +func (cr *containerReference) Start(attach bool) common.Executor { + return common. + NewInfoExecutor("%sdocker run image=%s entrypoint=%+q cmd=%+q", logPrefix, cr.input.Image, cr.input.Entrypoint, cr.input.Cmd). + Then( + common.NewPipelineExecutor( + cr.connect(), + cr.find(), + cr.attach().IfBool(attach), + cr.start(), + cr.wait().IfBool(attach), + ).IfNot(common.Dryrun), + ) +} +func (cr *containerReference) Pull(forcePull bool) common.Executor { + return NewDockerPullExecutor(NewDockerPullExecutorInput{ + Image: cr.input.Image, + ForcePull: forcePull, + }) +} +func (cr *containerReference) Copy(destPath string, files ...*FileEntry) common.Executor { + return common.NewPipelineExecutor( + cr.connect(), + cr.find(), + cr.copyContent(destPath, files...), + ).IfNot(common.Dryrun) +} + +func (cr *containerReference) CopyDir(destPath string, srcPath string) common.Executor { + return common.NewPipelineExecutor( + common.NewInfoExecutor("%sdocker cp src=%s dst=%s", logPrefix, srcPath, destPath), + cr.connect(), + cr.find(), + cr.copyDir(destPath, srcPath), + ).IfNot(common.Dryrun) +} + +func (cr *containerReference) Exec(command []string, env map[string]string) common.Executor { + + return common.NewPipelineExecutor( + cr.connect(), + cr.find(), + cr.exec(command, env), + ).IfNot(common.Dryrun) +} +func (cr *containerReference) Remove() common.Executor { + return common.NewPipelineExecutor( + cr.connect(), + cr.find(), + ).Finally( + cr.remove(), + ).IfNot(common.Dryrun) +} type containerReference struct { - input NewDockerRunExecutorInput cli *client.Client id string + input *NewContainerInput } func (cr *containerReference) connect() common.Executor { return func(ctx context.Context) error { + if cr.cli != nil { + return nil + } cli, err := client.NewClientWithOpts(client.FromEnv) if err != nil { return errors.WithStack(err) @@ -74,6 +148,9 @@ func (cr *containerReference) connect() common.Executor { func (cr *containerReference) find() common.Executor { return func(ctx context.Context) error { + if cr.id != "" { + return nil + } containers, err := cr.cli.ContainerList(ctx, types.ContainerListOptions{ All: true, }) @@ -107,11 +184,11 @@ func (cr *containerReference) remove() common.Executor { Force: true, }) if err != nil { - return errors.WithStack(err) + logger.Error(errors.WithStack(err)) } - cr.id = "" logger.Debugf("Removed container: %v", cr.id) + cr.id = "" return nil } } @@ -134,15 +211,18 @@ func (cr *containerReference) create() common.Executor { Tty: isTerminal, } - if len(input.Volumes) > 0 { - config.Volumes = make(map[string]struct{}) - for _, vol := range input.Volumes { - config.Volumes[vol] = struct{}{} - } + mounts := make([]mount.Mount, 0) + for mountSource, mountTarget := range input.Mounts { + mounts = append(mounts, mount.Mount{ + Type: mount.TypeVolume, + Source: mountSource, + Target: mountTarget, + }) } resp, err := cr.cli.ContainerCreate(ctx, config, &container.HostConfig{ - Binds: input.Binds, + Binds: input.Binds, + Mounts: mounts, }, nil, input.Name) if err != nil { return errors.WithStack(err) @@ -155,15 +235,180 @@ func (cr *containerReference) create() common.Executor { } } -func (cr *containerReference) copyContent() common.Executor { +func (cr *containerReference) exec(cmd []string, env map[string]string) common.Executor { return func(ctx context.Context) error { logger := common.Logger(ctx) - for dstPath, srcReader := range cr.input.Content { - logger.Debugf("Extracting content to '%s'", dstPath) - err := cr.cli.CopyToContainer(ctx, cr.id, dstPath, srcReader, types.CopyToContainerOptions{}) + logger.Debugf("Exec command '%s'", cmd) + isTerminal := terminal.IsTerminal(int(os.Stdout.Fd())) + envList := make([]string, 0) + for k, v := range env { + envList = append(envList, fmt.Sprintf("%s=%s", k, v)) + } + + idResp, err := cr.cli.ContainerExecCreate(ctx, cr.id, types.ExecConfig{ + Cmd: cmd, + WorkingDir: cr.input.WorkingDir, + Env: envList, + Tty: isTerminal, + AttachStderr: true, + AttachStdout: true, + }) + if err != nil { + return errors.WithStack(err) + } + + resp, err := cr.cli.ContainerExecAttach(ctx, idResp.ID, types.ExecStartCheck{ + Tty: isTerminal, + }) + if err != nil { + return errors.WithStack(err) + } + var outWriter io.Writer + outWriter = cr.input.Stdout + if outWriter == nil { + outWriter = os.Stdout + } + errWriter := cr.input.Stderr + if errWriter == nil { + errWriter = os.Stderr + } + + err = cr.cli.ContainerExecStart(ctx, idResp.ID, types.ExecStartCheck{ + Tty: isTerminal, + }) + if err != nil { + return errors.WithStack(err) + } + + if !isTerminal || os.Getenv("NORAW") != "" { + _, err = stdcopy.StdCopy(outWriter, errWriter, resp.Reader) + } else { + _, err = io.Copy(outWriter, resp.Reader) + } + if err != nil { + logger.Error(err) + } + + inspectResp, err := cr.cli.ContainerExecInspect(ctx, idResp.ID) + if err != nil { + return errors.WithStack(err) + } + + if inspectResp.ExitCode == 0 { + return nil + } + + return fmt.Errorf("exit with `FAILURE`: %v", inspectResp.ExitCode) + } +} + +func (cr *containerReference) copyDir(dstPath string, srcPath string) common.Executor { + return func(ctx context.Context) error { + logger := common.Logger(ctx) + tarFile, err := ioutil.TempFile("", "act") + if err != nil { + return err + } + log.Debugf("Writing tarball %s from %s", tarFile.Name(), srcPath) + defer tarFile.Close() + defer os.Remove(tarFile.Name()) + tw := tar.NewWriter(tarFile) + + srcPrefix := filepath.Dir(srcPath) + if !strings.HasSuffix(srcPrefix, string(filepath.Separator)) { + srcPrefix += string(filepath.Separator) + } + log.Debugf("Stripping prefix:%s src:%s", srcPrefix, srcPath) + + err = filepath.Walk(srcPath, func(file string, fi os.FileInfo, err error) error { if err != nil { - return errors.WithStack(err) + return err } + + // return on non-regular files (thanks to [kumo](https://medium.com/@komuw/just-like-you-did-fbdd7df829d3) for this suggested update) + if !fi.Mode().IsRegular() { + return nil + } + + // create a new dir/file header + header, err := tar.FileInfoHeader(fi, fi.Name()) + if err != nil { + return err + } + + // update the name to correctly reflect the desired destination when untaring + header.Name = strings.TrimPrefix(file, srcPrefix) + log.Debugf("%s -> %s", file, header.Name) + + // write the header + if err := tw.WriteHeader(header); err != nil { + return err + } + + // open files for taring + f, err := os.Open(file) + if err != nil { + return err + } + + // copy file data into tar writer + if _, err := io.Copy(tw, f); err != nil { + return err + } + + // manually close here after each file operation; defering would cause each file close + // to wait until all operations have completed. + f.Close() + + return nil + }) + if err != nil { + return err + } + if err := tw.Close(); err != nil { + return err + } + + logger.Debugf("Extracting content from '%s' to '%s'", tarFile.Name(), dstPath) + _, err = tarFile.Seek(0, 0) + if err != nil { + return errors.WithStack(err) + } + err = cr.cli.CopyToContainer(ctx, cr.id, dstPath, tarFile, types.CopyToContainerOptions{}) + if err != nil { + return errors.WithStack(err) + } + return nil + } +} + +func (cr *containerReference) copyContent(dstPath string, files ...*FileEntry) common.Executor { + return func(ctx context.Context) error { + logger := common.Logger(ctx) + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + for _, file := range files { + log.Debugf("Writing entry to tarball %s len:%d", file.Name, len(file.Body)) + hdr := &tar.Header{ + Name: file.Name, + Mode: file.Mode, + Size: int64(len(file.Body)), + } + if err := tw.WriteHeader(hdr); err != nil { + return err + } + if _, err := tw.Write([]byte(file.Body)); err != nil { + return err + } + } + if err := tw.Close(); err != nil { + return err + } + + logger.Debugf("Extracting content to '%s'", dstPath) + err := cr.cli.CopyToContainer(ctx, cr.id, dstPath, &buf, types.CopyToContainerOptions{}) + if err != nil { + return errors.WithStack(err) } return nil } @@ -207,7 +452,7 @@ func (cr *containerReference) attach() common.Executor { func (cr *containerReference) start() common.Executor { return func(ctx context.Context) error { logger := common.Logger(ctx) - logger.Debugf("STARTING image=%s entrypoint=%s cmd=%v", cr.input.Image, cr.input.Entrypoint, cr.input.Cmd) + logger.Debugf("Starting container: %v", cr.id) if err := cr.cli.ContainerStart(ctx, cr.id, types.ContainerStartOptions{}); err != nil { return errors.WithStack(err) diff --git a/pkg/container/docker_run_test.go b/pkg/container/docker_run_test.go deleted file mode 100644 index b68d329b..00000000 --- a/pkg/container/docker_run_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package container - -import ( - "bytes" - "context" - "io/ioutil" - "testing" - - "github.com/nektos/act/pkg/common" - "github.com/sirupsen/logrus" - "github.com/stretchr/testify/assert" -) - -type rawFormatter struct{} - -func (f *rawFormatter) Format(entry *logrus.Entry) ([]byte, error) { - return []byte(entry.Message), nil -} - -func TestNewDockerRunExecutor(t *testing.T) { - if testing.Short() { - t.Skip("skipping slower test") - } - - noopLogger := logrus.New() - noopLogger.SetOutput(ioutil.Discard) - - buf := &bytes.Buffer{} - logger := logrus.New() - logger.SetOutput(buf) - logger.SetFormatter(&rawFormatter{}) - - ctx := common.WithLogger(context.Background(), logger) - - runner := NewDockerRunExecutor(NewDockerRunExecutorInput{ - Image: "hello-world", - Stdout: buf, - }) - - puller := NewDockerPullExecutor(NewDockerPullExecutorInput{ - Image: "hello-world", - }) - - pipeline := common.NewPipelineExecutor(puller, runner) - err := pipeline(ctx) - assert.NoError(t, err) - - actual := buf.String() - assert.Contains(t, actual, `docker pull hello-world`) - assert.Contains(t, actual, `docker run image=hello-world entrypoint=[] cmd=[]`) - assert.Contains(t, actual, `Hello from Docker!`) -} diff --git a/pkg/container/docker_volume.go b/pkg/container/docker_volume.go new file mode 100644 index 00000000..a0b533f5 --- /dev/null +++ b/pkg/container/docker_volume.go @@ -0,0 +1,29 @@ +package container + +import ( + "context" + + "github.com/docker/docker/client" + "github.com/nektos/act/pkg/common" +) + +// NewDockerVolumeRemoveExecutor function +func NewDockerVolumeRemoveExecutor(volume string, force bool) common.Executor { + return func(ctx context.Context) error { + logger := common.Logger(ctx) + logger.Debugf("%sdocker volume rm %s", logPrefix, volume) + + if common.Dryrun(ctx) { + return nil + } + + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + return err + } + cli.NegotiateAPIVersion(ctx) + + return cli.VolumeRemove(ctx, volume, force) + } + +} diff --git a/pkg/model/workflow.go b/pkg/model/workflow.go index c25cf5cd..33cf6e7f 100644 --- a/pkg/model/workflow.go +++ b/pkg/model/workflow.go @@ -3,10 +3,11 @@ package model import ( "fmt" "io" - "log" "regexp" "strings" + "github.com/nektos/act/pkg/common" + log "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" ) @@ -94,6 +95,58 @@ func (j *Job) Needs() []string { return nil } +// GetMatrixes returns the matrix cross product +func (j *Job) GetMatrixes() []map[string]interface{} { + matrixes := make([]map[string]interface{}, 0) + if j.Strategy != nil { + includes := make([]map[string]interface{}, 0) + for _, v := range j.Strategy.Matrix["include"] { + includes = append(includes, v.(map[string]interface{})) + } + delete(j.Strategy.Matrix, "include") + + excludes := make([]map[string]interface{}, 0) + for _, v := range j.Strategy.Matrix["exclude"] { + excludes = append(excludes, v.(map[string]interface{})) + } + delete(j.Strategy.Matrix, "exclude") + + matrixProduct := common.CartesianProduct(j.Strategy.Matrix) + + MATRIX: + for _, matrix := range matrixProduct { + for _, exclude := range excludes { + if commonKeysMatch(matrix, exclude) { + log.Debugf("Skipping matrix '%v' due to exclude '%v'", matrix, exclude) + continue MATRIX + } + } + for _, include := range includes { + if commonKeysMatch(matrix, include) { + log.Debugf("Setting add'l values on matrix '%v' due to include '%v'", matrix, include) + for k, v := range include { + matrix[k] = v + } + } + } + matrixes = append(matrixes, matrix) + } + + } else { + matrixes = append(matrixes, make(map[string]interface{})) + } + return matrixes +} + +func commonKeysMatch(a map[string]interface{}, b map[string]interface{}) bool { + for aKey, aVal := range a { + if bVal, ok := b[aKey]; ok && aVal != bVal { + return false + } + } + return true +} + // ContainerSpec is the specification of the container to use for the job type ContainerSpec struct { Image string `yaml:"image"` @@ -104,6 +157,7 @@ type ContainerSpec struct { Entrypoint string Args string Name string + Reuse bool } // Step is the structure of one step in a job @@ -147,6 +201,29 @@ func (s *Step) GetEnv() map[string]string { return rtnEnv } +// ShellCommand returns the command for the shell +func (s *Step) ShellCommand() string { + shellCommand := "" + + switch s.Shell { + case "", "bash": + shellCommand = "bash --noprofile --norc -eo pipefail {0}" + case "pwsh": + shellCommand = "pwsh -command \"& '{0}'\"" + case "python": + shellCommand = "python {0}" + case "sh": + shellCommand = "sh -e -c {0}" + case "cmd": + shellCommand = "%ComSpec% /D /E:ON /V:OFF /S /C \"CALL \"{0}\"\"" + case "powershell": + shellCommand = "powershell -command \"& '{0}'\"" + default: + shellCommand = s.Shell + } + return shellCommand +} + // StepType describes what type of step we are about to run type StepType int diff --git a/pkg/runner/command.go b/pkg/runner/command.go index 08c1ac90..9e27b742 100644 --- a/pkg/runner/command.go +++ b/pkg/runner/command.go @@ -8,50 +8,63 @@ import ( "github.com/nektos/act/pkg/common" ) -var commandPattern *regexp.Regexp +var commandPatternGA *regexp.Regexp +var commandPatternADO *regexp.Regexp func init() { - commandPattern = regexp.MustCompile("^::([^ ]+)( (.+))?::([^\r\n]*)[\r\n]+$") + commandPatternGA = regexp.MustCompile("^::([^ ]+)( (.+))?::([^\r\n]*)[\r\n]+$") + commandPatternADO = 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) - } + return func(line string) bool { + var command string + var kvPairs map[string]string + var arg string + if m := commandPatternGA.FindStringSubmatch(line); m != nil { + command = m[1] + kvPairs = parseKeyValuePairs(m[3], ",") + arg = m[4] + } else if m := commandPatternADO.FindStringSubmatch(line); m != nil { + command = m[1] + kvPairs = parseKeyValuePairs(m[3], ";") + arg = m[4] + } else { + return true } + + if resumeCommand != "" && command != resumeCommand { + return false + } + + 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) + } + + return false } } @@ -71,9 +84,9 @@ func (rc *RunContext) addPath(ctx context.Context, arg string) { rc.ExtraPath = append(rc.ExtraPath, arg) } -func parseKeyValuePairs(kvPairs string) map[string]string { +func parseKeyValuePairs(kvPairs string, separator string) map[string]string { rtn := make(map[string]string) - kvPairList := strings.Split(kvPairs, ",") + kvPairList := strings.Split(kvPairs, separator) for _, kvPair := range kvPairList { kv := strings.Split(kvPair, "=") if len(kv) == 2 { diff --git a/pkg/runner/command_test.go b/pkg/runner/command_test.go index 4279bc02..6abe5275 100644 --- a/pkg/runner/command_test.go +++ b/pkg/runner/command_test.go @@ -60,3 +60,16 @@ func TestStopCommands(t *testing.T) { handler("::set-env name=x::abcd\n") assert.Equal("abcd", rc.Env["x"]) } + +func TestAddpathADO(t *testing.T) { + assert := assert.New(t) + ctx := context.Background() + rc := new(RunContext) + handler := rc.commandHandler(ctx) + + handler("##[add-path]/zoo\n") + assert.Equal("/zoo", rc.ExtraPath[0]) + + handler("##[add-path]/boo\n") + assert.Equal("/boo", rc.ExtraPath[1]) +} diff --git a/pkg/runner/expression.go b/pkg/runner/expression.go index 9226faf1..652800ea 100644 --- a/pkg/runner/expression.go +++ b/pkg/runner/expression.go @@ -11,7 +11,6 @@ import ( "regexp" "strings" - "github.com/nektos/act/pkg/model" "github.com/robertkrimen/otto" "github.com/sirupsen/logrus" "gopkg.in/godo.v2/glob" @@ -34,11 +33,12 @@ func (rc *RunContext) NewExpressionEvaluator() ExpressionEvaluator { } } -// NewStepExpressionEvaluator creates a new evaluator -func (rc *RunContext) NewStepExpressionEvaluator(step *model.Step) ExpressionEvaluator { - vm := rc.newVM() +// NewExpressionEvaluator creates a new evaluator +func (sc *StepContext) NewExpressionEvaluator() ExpressionEvaluator { + vm := sc.RunContext.newVM() configers := []func(*otto.Otto){ - rc.vmEnv(step), + sc.vmEnv(), + sc.vmInputs(), } for _, configer := range configers { configer(vm) @@ -236,10 +236,19 @@ func (rc *RunContext) vmGithub() func(*otto.Otto) { } } -func (rc *RunContext) vmEnv(step *model.Step) func(*otto.Otto) { +func (sc *StepContext) vmEnv() func(*otto.Otto) { return func(vm *otto.Otto) { - env := rc.StepEnv(step) - _ = vm.Set("env", env) + _ = vm.Set("env", sc.Env) + } +} + +func (sc *StepContext) vmInputs() func(*otto.Otto) { + inputs := make(map[string]string) + for k, v := range sc.Step.With { + inputs[k] = v + } + return func(vm *otto.Otto) { + _ = vm.Set("inputs", inputs) } } diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index 2289847a..09d3ed2c 100644 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -1,13 +1,9 @@ package runner import ( - "archive/tar" - "bytes" "context" "encoding/json" "fmt" - "io" - "io/ioutil" "os" "path/filepath" "regexp" @@ -24,16 +20,16 @@ import ( // RunContext contains info about current job type RunContext struct { - Config *Config - Matrix map[string]interface{} - Run *model.Run - EventJSON string - Env map[string]string - Tempdir string - ExtraPath []string - CurrentStep string - StepResults map[string]*stepResult - ExprEval ExpressionEvaluator + Config *Config + Matrix map[string]interface{} + Run *model.Run + EventJSON string + Env map[string]string + ExtraPath []string + CurrentStep string + StepResults map[string]*stepResult + ExprEval ExpressionEvaluator + JobContainer container.Container } type stepResult struct { @@ -49,68 +45,175 @@ func (rc *RunContext) GetEnv() map[string]string { return rc.Env } -// Close cleans up temp dir -func (rc *RunContext) Close(ctx context.Context) error { - return os.RemoveAll(rc.Tempdir) +func (rc *RunContext) jobContainerName() string { + return createContainerName("act", rc.Run.String()) +} + +func (rc *RunContext) startJobContainer() common.Executor { + job := rc.Run.Job() + + var image string + if job.Container != nil { + image = job.Container.Image + } else { + platformName := rc.ExprEval.Interpolate(job.RunsOn) + image = rc.Config.Platforms[strings.ToLower(platformName)] + } + + return func(ctx context.Context) error { + rawLogger := common.Logger(ctx).WithField("raw_output", true) + logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool { + if rc.Config.LogOutput { + rawLogger.Infof(s) + } else { + rawLogger.Debugf(s) + } + return true + }) + + common.Logger(ctx).Infof("\U0001f680 Start image=%s", image) + name := rc.jobContainerName() + + envList := make([]string, 0) + bindModifiers := "" + if runtime.GOOS == "darwin" { + bindModifiers = ":delegated" + } + + envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TOOL_CACHE", "/toolcache")) + + binds := []string{ + fmt.Sprintf("%s:%s", "/var/run/docker.sock", "/var/run/docker.sock"), + } + if rc.Config.BindWorkdir { + binds = append(binds, fmt.Sprintf("%s:%s%s", rc.Config.Workdir, "/github/workspace", bindModifiers)) + } + + rc.JobContainer = container.NewContainer(&container.NewContainerInput{ + Cmd: nil, + Entrypoint: []string{"/usr/bin/tail", "-f", "/dev/null"}, + WorkingDir: "/github/workspace", + Image: image, + Name: name, + Env: envList, + Mounts: map[string]string{ + name: "/github", + "act-toolcache": "/toolcache", + "act-actions": "/actions", + }, + + Binds: binds, + Stdout: logWriter, + Stderr: logWriter, + }) + + return common.NewPipelineExecutor( + rc.JobContainer.Pull(rc.Config.ForcePull), + rc.JobContainer.Remove().IfBool(!rc.Config.ReuseContainers), + rc.JobContainer.Create(), + rc.JobContainer.Start(false), + rc.JobContainer.CopyDir("/github/workspace", rc.Config.Workdir+"/.").IfBool(!rc.Config.BindWorkdir), + rc.JobContainer.Copy("/github/", &container.FileEntry{ + Name: "workflow/event.json", + Mode: 644, + Body: rc.EventJSON, + }, &container.FileEntry{ + Name: "home/.act", + Mode: 644, + Body: "", + }), + )(ctx) + } +} +func (rc *RunContext) execJobContainer(cmd []string, env map[string]string) common.Executor { + return func(ctx context.Context) error { + return rc.JobContainer.Exec(cmd, env)(ctx) + } +} +func (rc *RunContext) stopJobContainer() common.Executor { + return func(ctx context.Context) error { + if rc.JobContainer != nil && !rc.Config.ReuseContainers { + return rc.JobContainer.Remove(). + Then(container.NewDockerVolumeRemoveExecutor(rc.jobContainerName(), false))(ctx) + } + return nil + } +} + +// ActionCacheDir is for rc +func (rc *RunContext) ActionCacheDir() string { + var xdgCache string + var ok bool + if xdgCache, ok = os.LookupEnv("XDG_CACHE_HOME"); !ok { + if home, ok := os.LookupEnv("HOME"); ok { + xdgCache = fmt.Sprintf("%s/.cache", home) + } + } + return filepath.Join(xdgCache, "act") } // Executor returns a pipeline executor for all the steps in the job func (rc *RunContext) Executor() common.Executor { - - err := rc.setupTempDir() - if err != nil { - return common.NewErrorExecutor(err) - } steps := make([]common.Executor, 0) + steps = append(steps, rc.startJobContainer()) for i, step := range rc.Run.Job().Steps { if step.ID == "" { step.ID = fmt.Sprintf("%d", i) } - s := step - steps = append(steps, func(ctx context.Context) error { - rc.CurrentStep = s.ID - rc.StepResults[rc.CurrentStep] = &stepResult{ - Success: true, - Outputs: make(map[string]string), - } - rc.ExprEval = rc.NewStepExpressionEvaluator(s) + steps = append(steps, rc.newStepExecutor(step)) + } + steps = append(steps, rc.stopJobContainer()) - if !rc.EvalBool(s.If) { - log.Debugf("Skipping step '%s' due to '%s'", s.String(), s.If) - return nil - } + return common.NewPipelineExecutor(steps...).If(rc.isEnabled) +} - common.Logger(ctx).Infof("\u2B50 Run %s", s) - err := rc.newStepExecutor(s)(ctx) - if err == nil { - common.Logger(ctx).Infof(" \u2705 Success - %s", s) - } else { - common.Logger(ctx).Errorf(" \u274C Failure - %s", s) - rc.StepResults[rc.CurrentStep].Success = false - } - return err - }) +func (rc *RunContext) newStepExecutor(step *model.Step) common.Executor { + sc := &StepContext{ + RunContext: rc, + Step: step, } return func(ctx context.Context) error { - defer rc.Close(ctx) - job := rc.Run.Job() - log := common.Logger(ctx) - if !rc.EvalBool(job.If) { - log.Debugf("Skipping job '%s' due to '%s'", job.Name, job.If) + rc.CurrentStep = sc.Step.ID + rc.StepResults[rc.CurrentStep] = &stepResult{ + Success: true, + Outputs: make(map[string]string), + } + rc.ExprEval = sc.NewExpressionEvaluator() + + if !rc.EvalBool(sc.Step.If) { + log.Debugf("Skipping step '%s' due to '%s'", sc.Step.String(), sc.Step.If) return nil } - platformName := rc.ExprEval.Interpolate(rc.Run.Job().RunsOn) - if img, ok := rc.Config.Platforms[strings.ToLower(platformName)]; !ok || img == "" { - log.Infof(" \U0001F6A7 Skipping unsupported platform '%s'", platformName) - return nil + common.Logger(ctx).Infof("\u2B50 Run %s", sc.Step) + err := sc.Executor()(ctx) + if err == nil { + common.Logger(ctx).Infof(" \u2705 Success - %s", sc.Step) + } else { + common.Logger(ctx).Errorf(" \u274C Failure - %s", sc.Step) + rc.StepResults[rc.CurrentStep].Success = false } - - return common.NewPipelineExecutor(steps...)(ctx) + return err } } +func (rc *RunContext) isEnabled(ctx context.Context) bool { + job := rc.Run.Job() + log := common.Logger(ctx) + if !rc.EvalBool(job.If) { + log.Debugf("Skipping job '%s' due to '%s'", job.Name, job.If) + return false + } + + platformName := rc.ExprEval.Interpolate(rc.Run.Job().RunsOn) + if img, ok := rc.Config.Platforms[strings.ToLower(platformName)]; !ok || img == "" { + log.Infof(" \U0001F6A7 Skipping unsupported platform '%s'", platformName) + return false + } + return true +} + // EvalBool evaluates an expression against current run context func (rc *RunContext) EvalBool(expr string) bool { if expr != "" { @@ -134,124 +237,24 @@ func mergeMaps(maps ...map[string]string) map[string]string { return rtnMap } -func (rc *RunContext) setupTempDir() error { - var err error - tempBase := "" - if runtime.GOOS == "darwin" { - tempBase = "/tmp" - } - rc.Tempdir, err = ioutil.TempDir(tempBase, "act-") - if err != nil { - return err - } - err = os.Chmod(rc.Tempdir, 0755) - if err != nil { - return err - } - log.Debugf("Setup tempdir %s", rc.Tempdir) - return err -} - -func (rc *RunContext) pullImage(containerSpec *model.ContainerSpec) common.Executor { - return func(ctx context.Context) error { - return container.NewDockerPullExecutor(container.NewDockerPullExecutorInput{ - Image: containerSpec.Image, - ForcePull: rc.Config.ForcePull, - })(ctx) - } -} - -func (rc *RunContext) runContainer(containerSpec *model.ContainerSpec) common.Executor { - return func(ctx context.Context) error { - ghReader, err := rc.createGithubTarball() - if err != nil { - return err - } - - envList := make([]string, 0) - for k, v := range containerSpec.Env { - envList = append(envList, fmt.Sprintf("%s=%s", k, v)) - } - var cmd, entrypoint []string - if containerSpec.Args != "" { - cmd = strings.Fields(rc.ExprEval.Interpolate(containerSpec.Args)) - } - if containerSpec.Entrypoint != "" { - entrypoint = strings.Fields(rc.ExprEval.Interpolate(containerSpec.Entrypoint)) - } - - rawLogger := common.Logger(ctx).WithField("raw_output", true) - logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) { - if rc.Config.LogOutput { - rawLogger.Infof(s) - } else { - rawLogger.Debugf(s) - } - }) - - return container.NewDockerRunExecutor(container.NewDockerRunExecutorInput{ - Cmd: cmd, - Entrypoint: entrypoint, - Image: containerSpec.Image, - WorkingDir: "/github/workspace", - Env: envList, - Name: containerSpec.Name, - Binds: []string{ - fmt.Sprintf("%s:%s", rc.Config.Workdir, "/github/workspace"), - fmt.Sprintf("%s:%s", rc.Tempdir, "/github/home"), - fmt.Sprintf("%s:%s", "/var/run/docker.sock", "/var/run/docker.sock"), - }, - Content: map[string]io.Reader{"/github": ghReader}, - ReuseContainers: rc.Config.ReuseContainers, - Stdout: logWriter, - Stderr: logWriter, - })(ctx) - } -} - -func (rc *RunContext) createGithubTarball() (io.Reader, error) { - var buf bytes.Buffer - tw := tar.NewWriter(&buf) - var files = []struct { - Name string - Mode int64 - Body string - }{ - {"workflow/event.json", 0644, rc.EventJSON}, - } - for _, file := range files { - log.Debugf("Writing entry to tarball %s len:%d", file.Name, len(rc.EventJSON)) - hdr := &tar.Header{ - Name: file.Name, - Mode: file.Mode, - Size: int64(len(rc.EventJSON)), - } - if err := tw.WriteHeader(hdr); err != nil { - return nil, err - } - if _, err := tw.Write([]byte(rc.EventJSON)); err != nil { - return nil, err +func createContainerName(parts ...string) string { + name := make([]string, 0) + pattern := regexp.MustCompile("[^a-zA-Z0-9]") + partLen := (30 / len(parts)) - 1 + for i, part := range parts { + if i == len(parts)-1 { + name = append(name, pattern.ReplaceAllString(part, "-")) + } else { + name = append(name, trimToLen(pattern.ReplaceAllString(part, "-"), partLen)) } } - if err := tw.Close(); err != nil { - return nil, err - } - - return &buf, nil - -} - -func (rc *RunContext) createContainerName(stepID string) string { - containerName := fmt.Sprintf("%s-%s", stepID, rc.Tempdir) - containerName = regexp.MustCompile("[^a-zA-Z0-9]").ReplaceAllString(containerName, "-") - - prefix := fmt.Sprintf("%s-", trimToLen(filepath.Base(rc.Config.Workdir), 10)) - suffix := "" - containerName = trimToLen(containerName, 30-(len(prefix)+len(suffix))) - return fmt.Sprintf("%s%s%s", prefix, containerName, suffix) + return trimToLen(strings.Trim(strings.Join(name, "-"), "-"), 30) } func trimToLen(s string, l int) string { + if l < 0 { + l = 0 + } if len(s) > l { return s[:l] } @@ -355,6 +358,7 @@ func (rc *RunContext) withGithubEnv(env map[string]string) map[string]string { env["GITHUB_RUN_ID"] = github.RunID env["GITHUB_RUN_NUMBER"] = github.RunNumber env["GITHUB_ACTION"] = github.Action + env["GITHUB_ACTIONS"] = "true" env["GITHUB_ACTOR"] = github.Actor env["GITHUB_REPOSITORY"] = github.Repository env["GITHUB_EVENT_NAME"] = github.EventName diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index 2c1888a4..3befe0c8 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -13,12 +13,12 @@ import ( // Runner provides capabilities to run GitHub actions type Runner interface { NewPlanExecutor(plan *model.Plan) common.Executor - NewRunExecutor(run *model.Run, matrix map[string]interface{}) common.Executor } // Config contains the config for a new runner type Config struct { Workdir string // path to working directory + BindWorkdir bool // bind the workdir to the job container EventName string // name of event to run EventPath string // path to JSON file to use for event.json in containers ReuseContainers bool // reuse containers to maintain state @@ -59,49 +59,12 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor { stageExecutor := make([]common.Executor, 0) for _, run := range stage.Runs { job := run.Job() - matrixes := make([]map[string]interface{}, 0) - if job.Strategy != nil { - includes := make([]map[string]interface{}, 0) - for _, v := range job.Strategy.Matrix["include"] { - includes = append(includes, v.(map[string]interface{})) - } - delete(job.Strategy.Matrix, "include") - - excludes := make([]map[string]interface{}, 0) - for _, v := range job.Strategy.Matrix["exclude"] { - excludes = append(excludes, v.(map[string]interface{})) - } - delete(job.Strategy.Matrix, "exclude") - - matrixProduct := common.CartesianProduct(job.Strategy.Matrix) - - MATRIX: - for _, matrix := range matrixProduct { - for _, exclude := range excludes { - if commonKeysMatch(matrix, exclude) { - log.Debugf("Skipping matrix '%v' due to exclude '%v'", matrix, exclude) - continue MATRIX - } - } - for _, include := range includes { - if commonKeysMatch(matrix, include) { - log.Debugf("Setting add'l values on matrix '%v' due to include '%v'", matrix, include) - for k, v := range include { - matrix[k] = v - } - } - } - matrixes = append(matrixes, matrix) - } - - } else { - matrixes = append(matrixes, make(map[string]interface{})) - } + matrixes := job.GetMatrixes() jobName := fmt.Sprintf("%-*s", maxJobNameLen, run.String()) for _, matrix := range matrixes { m := matrix - runExecutor := runner.NewRunExecutor(run, matrix) + runExecutor := runner.newRunExecutor(run, matrix) stageExecutor = append(stageExecutor, func(ctx context.Context) error { ctx = WithJobLogger(ctx, jobName) if len(m) > 0 { @@ -117,22 +80,14 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor { return common.NewPipelineExecutor(pipeline...) } -func commonKeysMatch(a map[string]interface{}, b map[string]interface{}) bool { - for aKey, aVal := range a { - if bVal, ok := b[aKey]; ok && aVal != bVal { - return false - } +func (runner *runnerImpl) newRunExecutor(run *model.Run, matrix map[string]interface{}) common.Executor { + rc := &RunContext{ + Config: runner.config, + Run: run, + EventJSON: runner.eventJSON, + StepResults: make(map[string]*stepResult), + Matrix: matrix, } - return true -} - -func (runner *runnerImpl) NewRunExecutor(run *model.Run, matrix map[string]interface{}) common.Executor { - rc := new(RunContext) - rc.Config = runner.config - rc.Run = run - rc.EventJSON = runner.eventJSON - rc.StepResults = make(map[string]*stepResult) - rc.Matrix = matrix rc.ExprEval = rc.NewExpressionEvaluator() return rc.Executor() } diff --git a/pkg/runner/runner_test.go b/pkg/runner/runner_test.go index b6f2c863..3be32aec 100644 --- a/pkg/runner/runner_test.go +++ b/pkg/runner/runner_test.go @@ -3,6 +3,7 @@ package runner import ( "context" "fmt" + "path/filepath" "testing" "github.com/nektos/act/pkg/model" @@ -57,13 +58,16 @@ func TestRunEvent(t *testing.T) { table := table t.Run(table.workflowPath, func(t *testing.T) { platforms := map[string]string{ - "ubuntu-latest": "ubuntu:18.04", + "ubuntu-latest": "node:12.6-buster-slim", } + + workdir, err := filepath.Abs("testdata") + assert.NilError(t, err, table.workflowPath) runnerConfig := &Config{ - Workdir: "testdata", + Workdir: workdir, EventName: table.eventName, Platforms: platforms, - ReuseContainers: true, + ReuseContainers: false, } runner, err := New(runnerConfig) assert.NilError(t, err, table.workflowPath) diff --git a/pkg/runner/step.go b/pkg/runner/step.go deleted file mode 100644 index 408f0330..00000000 --- a/pkg/runner/step.go +++ /dev/null @@ -1,247 +0,0 @@ -package runner - -import ( - "context" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "regexp" - "strings" - - "github.com/nektos/act/pkg/common" - "github.com/nektos/act/pkg/container" - "github.com/nektos/act/pkg/model" - log "github.com/sirupsen/logrus" -) - -func (rc *RunContext) StepEnv(step *model.Step) map[string]string { - var env map[string]string - job := rc.Run.Job() - if job.Container != nil { - env = mergeMaps(rc.GetEnv(), job.Container.Env, step.GetEnv()) - } else { - env = mergeMaps(rc.GetEnv(), step.GetEnv()) - } - - for k, v := range env { - env[k] = rc.ExprEval.Interpolate(v) - } - return env -} - -func (rc *RunContext) setupEnv(containerSpec *model.ContainerSpec, step *model.Step) common.Executor { - return func(ctx context.Context) error { - containerSpec.Env = rc.withGithubEnv(rc.StepEnv(step)) - return nil - } -} - -func (rc *RunContext) newStepExecutor(step *model.Step) common.Executor { - job := rc.Run.Job() - containerSpec := new(model.ContainerSpec) - containerSpec.Name = rc.createContainerName(step.ID) - - switch step.Type() { - case model.StepTypeRun: - if job.Container != nil { - containerSpec.Image = job.Container.Image - containerSpec.Ports = job.Container.Ports - containerSpec.Volumes = job.Container.Volumes - containerSpec.Options = job.Container.Options - } else { - platformName := rc.ExprEval.Interpolate(rc.Run.Job().RunsOn) - containerSpec.Image = rc.Config.Platforms[strings.ToLower(platformName)] - } - return common.NewPipelineExecutor( - rc.setupEnv(containerSpec, step), - rc.setupShellCommand(containerSpec, step.Shell, step.Run), - rc.pullImage(containerSpec), - rc.runContainer(containerSpec), - ) - - case model.StepTypeUsesDockerURL: - containerSpec.Image = strings.TrimPrefix(step.Uses, "docker://") - containerSpec.Entrypoint = step.With["entrypoint"] - containerSpec.Args = step.With["args"] - return common.NewPipelineExecutor( - rc.setupEnv(containerSpec, step), - rc.pullImage(containerSpec), - rc.runContainer(containerSpec), - ) - - case model.StepTypeUsesActionLocal: - containerSpec.Image = fmt.Sprintf("%s:%s", containerSpec.Name, "latest") - return common.NewPipelineExecutor( - rc.setupEnv(containerSpec, step), - rc.setupAction(containerSpec, filepath.Join(rc.Config.Workdir, step.Uses)), - applyWith(containerSpec, step), - rc.pullImage(containerSpec), - rc.runContainer(containerSpec), - ) - case model.StepTypeUsesActionRemote: - remoteAction := newRemoteAction(step.Uses) - if remoteAction.Org == "actions" && remoteAction.Repo == "checkout" { - return func(ctx context.Context) error { - common.Logger(ctx).Debugf("Skipping actions/checkout") - return nil - } - } - cloneDir, err := ioutil.TempDir(rc.Tempdir, remoteAction.Repo) - if err != nil { - return common.NewErrorExecutor(err) - } - containerSpec.Image = fmt.Sprintf("%s:%s", remoteAction.Repo, remoteAction.Ref) - return common.NewPipelineExecutor( - common.NewGitCloneExecutor(common.NewGitCloneExecutorInput{ - URL: remoteAction.CloneURL(), - Ref: remoteAction.Ref, - Dir: cloneDir, - }), - rc.setupEnv(containerSpec, step), - rc.setupAction(containerSpec, filepath.Join(cloneDir, remoteAction.Path)), - applyWith(containerSpec, step), - rc.pullImage(containerSpec), - rc.runContainer(containerSpec), - ) - } - - return common.NewErrorExecutor(fmt.Errorf("Unable to determine how to run job:%s step:%+v", rc.Run, step)) -} - -func applyWith(containerSpec *model.ContainerSpec, step *model.Step) common.Executor { - return func(ctx context.Context) error { - if entrypoint, ok := step.With["entrypoint"]; ok { - containerSpec.Entrypoint = entrypoint - } - if args, ok := step.With["args"]; ok { - containerSpec.Args = args - } - return nil - } -} - -func (rc *RunContext) setupShellCommand(containerSpec *model.ContainerSpec, shell string, run string) common.Executor { - return func(ctx context.Context) error { - shellCommand := "" - - switch shell { - case "", "bash": - shellCommand = "bash --noprofile --norc -eo pipefail {0}" - case "pwsh": - shellCommand = "pwsh -command \"& '{0}'\"" - case "python": - shellCommand = "python {0}" - case "sh": - shellCommand = "sh -e -c {0}" - case "cmd": - shellCommand = "%ComSpec% /D /E:ON /V:OFF /S /C \"CALL \"{0}\"\"" - case "powershell": - shellCommand = "powershell -command \"& '{0}'\"" - default: - shellCommand = shell - } - - tempScript, err := ioutil.TempFile(rc.Tempdir, ".temp-script-") - if err != nil { - return err - } - - _, err = tempScript.WriteString(fmt.Sprintf("PATH=\"%s:${PATH}\"\n", strings.Join(rc.ExtraPath, ":"))) - if err != nil { - return err - } - - run = rc.ExprEval.Interpolate(run) - - if _, err := tempScript.WriteString(run); err != nil { - return err - } - log.Debugf("Wrote command '%s' to '%s'", run, tempScript.Name()) - if err := tempScript.Close(); err != nil { - return err - } - containerPath := fmt.Sprintf("/github/home/%s", filepath.Base(tempScript.Name())) - containerSpec.Entrypoint = strings.Replace(shellCommand, "{0}", containerPath, 1) - return nil - } -} - -func (rc *RunContext) setupAction(containerSpec *model.ContainerSpec, actionDir string) common.Executor { - return func(ctx context.Context) error { - f, err := os.Open(filepath.Join(actionDir, "action.yml")) - if os.IsNotExist(err) { - f, err = os.Open(filepath.Join(actionDir, "action.yaml")) - if err != nil { - return err - } - } else if err != nil { - return err - } - - action, err := model.ReadAction(f) - if err != nil { - return err - } - - for inputID, input := range action.Inputs { - envKey := regexp.MustCompile("[^A-Z0-9-]").ReplaceAllString(strings.ToUpper(inputID), "_") - envKey = fmt.Sprintf("INPUT_%s", envKey) - if _, ok := containerSpec.Env[envKey]; !ok { - containerSpec.Env[envKey] = input.Default - } - } - - switch action.Runs.Using { - case model.ActionRunsUsingNode12: - containerSpec.Image = "node:12-alpine" - if strings.HasPrefix(actionDir, rc.Config.Workdir) { - containerSpec.Args = fmt.Sprintf("node /github/workspace/%s/%s", strings.TrimPrefix(actionDir, rc.Config.Workdir), action.Runs.Main) - } else if strings.HasPrefix(actionDir, rc.Tempdir) { - containerSpec.Args = fmt.Sprintf("node /github/home/%s/%s", strings.TrimPrefix(actionDir, rc.Tempdir), action.Runs.Main) - } - case model.ActionRunsUsingDocker: - if strings.HasPrefix(action.Runs.Image, "docker://") { - containerSpec.Image = strings.TrimPrefix(action.Runs.Image, "docker://") - containerSpec.Entrypoint = strings.Join(action.Runs.Entrypoint, " ") - containerSpec.Args = strings.Join(action.Runs.Args, " ") - } else { - contextDir := filepath.Join(actionDir, action.Runs.Main) - return container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{ - ContextDir: contextDir, - ImageTag: containerSpec.Image, - })(ctx) - } - } - return nil - } -} - -type remoteAction struct { - Org string - Repo string - Path string - Ref string -} - -func (ra *remoteAction) CloneURL() string { - return fmt.Sprintf("https://github.com/%s/%s", ra.Org, ra.Repo) -} - -func newRemoteAction(action string) *remoteAction { - r := regexp.MustCompile(`^([^/@]+)/([^/@]+)(/([^@]*))?(@(.*))?$`) - matches := r.FindStringSubmatch(action) - - ra := new(remoteAction) - ra.Org = matches[1] - ra.Repo = matches[2] - ra.Path = "" - ra.Ref = "master" - if len(matches) >= 5 { - ra.Path = matches[4] - } - if len(matches) >= 7 { - ra.Ref = matches[6] - } - return ra -} diff --git a/pkg/runner/step_context.go b/pkg/runner/step_context.go new file mode 100644 index 00000000..cece632c --- /dev/null +++ b/pkg/runner/step_context.go @@ -0,0 +1,326 @@ +package runner + +import ( + "context" + "fmt" + "os" + "path/filepath" + "regexp" + "runtime" + "strings" + + "github.com/nektos/act/pkg/common" + "github.com/nektos/act/pkg/container" + "github.com/nektos/act/pkg/model" + log "github.com/sirupsen/logrus" +) + +// StepContext contains info about current job +type StepContext struct { + RunContext *RunContext + Step *model.Step + Env map[string]string + Cmd []string + Action *model.Action +} + +func (sc *StepContext) execJobContainer() common.Executor { + return func(ctx context.Context) error { + return sc.RunContext.execJobContainer(sc.Cmd, sc.Env)(ctx) + } +} + +// Executor for a step context +func (sc *StepContext) Executor() common.Executor { + rc := sc.RunContext + step := sc.Step + + switch step.Type() { + case model.StepTypeRun: + return common.NewPipelineExecutor( + sc.setupEnv(), + sc.setupShellCommand(), + sc.execJobContainer(), + ) + + case model.StepTypeUsesDockerURL: + return common.NewPipelineExecutor( + sc.setupEnv(), + sc.runUsesContainer(), + ) + + case model.StepTypeUsesActionLocal: + actionDir := filepath.Join(rc.Config.Workdir, step.Uses) + return common.NewPipelineExecutor( + sc.setupEnv(), + sc.setupAction(actionDir), + sc.runAction(actionDir), + ) + case model.StepTypeUsesActionRemote: + remoteAction := newRemoteAction(step.Uses) + if remoteAction.Org == "actions" && remoteAction.Repo == "checkout" { + return func(ctx context.Context) error { + common.Logger(ctx).Debugf("Skipping actions/checkout") + return nil + } + } + + actionDir := fmt.Sprintf("%s/%s", rc.ActionCacheDir(), strings.ReplaceAll(step.Uses, "/", "-")) + return common.NewPipelineExecutor( + common.NewGitCloneExecutor(common.NewGitCloneExecutorInput{ + URL: remoteAction.CloneURL(), + Ref: remoteAction.Ref, + Dir: actionDir, + }), + sc.setupEnv(), + sc.setupAction(actionDir), + sc.runAction(actionDir), + ) + } + + return common.NewErrorExecutor(fmt.Errorf("Unable to determine how to run job:%s step:%+v", rc.Run, step)) +} + +func (sc *StepContext) setupEnv() common.Executor { + rc := sc.RunContext + job := rc.Run.Job() + step := sc.Step + return func(ctx context.Context) error { + var env map[string]string + if job.Container != nil { + env = mergeMaps(rc.GetEnv(), job.Container.Env, step.GetEnv()) + } else { + env = mergeMaps(rc.GetEnv(), step.GetEnv()) + } + + for k, v := range env { + env[k] = rc.ExprEval.Interpolate(v) + } + sc.Env = rc.withGithubEnv(env) + return nil + } +} + +func (sc *StepContext) setupShellCommand() common.Executor { + rc := sc.RunContext + step := sc.Step + return func(ctx context.Context) error { + var script strings.Builder + + _, err := script.WriteString(fmt.Sprintf("PATH=\"%s:${PATH}\"\n", strings.Join(rc.ExtraPath, ":"))) + if err != nil { + return err + } + + run := rc.ExprEval.Interpolate(step.Run) + + if _, err = script.WriteString(run); err != nil { + return err + } + scriptName := fmt.Sprintf("workflow/%s", step.ID) + log.Debugf("Wrote command '%s' to '%s'", run, scriptName) + containerPath := fmt.Sprintf("/github/%s", scriptName) + sc.Cmd = strings.Fields(strings.Replace(step.ShellCommand(), "{0}", containerPath, 1)) + return rc.JobContainer.Copy("/github/", &container.FileEntry{ + Name: scriptName, + Mode: 755, + Body: script.String(), + })(ctx) + } +} + +func (sc *StepContext) newStepContainer(ctx context.Context, image string, cmd []string, entrypoint []string) container.Container { + rc := sc.RunContext + step := sc.Step + rawLogger := common.Logger(ctx).WithField("raw_output", true) + logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool { + if rc.Config.LogOutput { + rawLogger.Infof(s) + } else { + rawLogger.Debugf(s) + } + return true + }) + envList := make([]string, 0) + for k, v := range sc.Env { + envList = append(envList, fmt.Sprintf("%s=%s", k, v)) + } + stepEE := sc.NewExpressionEvaluator() + for i, v := range cmd { + cmd[i] = stepEE.Interpolate(v) + } + for i, v := range entrypoint { + entrypoint[i] = stepEE.Interpolate(v) + } + + bindModifiers := "" + if runtime.GOOS == "darwin" { + bindModifiers = ":delegated" + } + + envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TOOL_CACHE", "/toolcache")) + + binds := []string{ + fmt.Sprintf("%s:%s", "/var/run/docker.sock", "/var/run/docker.sock"), + } + if rc.Config.BindWorkdir { + binds = append(binds, fmt.Sprintf("%s:%s%s", rc.Config.Workdir, "/github/workspace", bindModifiers)) + } + + stepContainer := container.NewContainer(&container.NewContainerInput{ + Cmd: cmd, + Entrypoint: entrypoint, + WorkingDir: "/github/workspace", + Image: image, + Name: createContainerName(rc.jobContainerName(), step.ID), + Env: envList, + Mounts: map[string]string{ + rc.jobContainerName(): "/github", + "act-toolcache": "/toolcache", + "act-actions": "/actions", + }, + Binds: binds, + Stdout: logWriter, + Stderr: logWriter, + }) + return stepContainer +} +func (sc *StepContext) runUsesContainer() common.Executor { + rc := sc.RunContext + step := sc.Step + return func(ctx context.Context) error { + image := strings.TrimPrefix(step.Uses, "docker://") + cmd := strings.Fields(step.With["args"]) + entrypoint := strings.Fields(step.With["entrypoint"]) + stepContainer := sc.newStepContainer(ctx, image, cmd, entrypoint) + + return common.NewPipelineExecutor( + stepContainer.Pull(rc.Config.ForcePull), + stepContainer.Remove().IfBool(!rc.Config.ReuseContainers), + stepContainer.Create(), + stepContainer.Start(true), + ).Finally( + stepContainer.Remove().IfBool(!rc.Config.ReuseContainers), + )(ctx) + } +} + +func (sc *StepContext) setupAction(actionDir string) common.Executor { + return func(ctx context.Context) error { + f, err := os.Open(filepath.Join(actionDir, "action.yml")) + if os.IsNotExist(err) { + f, err = os.Open(filepath.Join(actionDir, "action.yaml")) + if err != nil { + return err + } + } else if err != nil { + return err + } + + sc.Action, err = model.ReadAction(f) + log.Debugf("Read action %v from '%s'", sc.Action, f.Name()) + return err + } +} + +func (sc *StepContext) runAction(actionDir string) common.Executor { + rc := sc.RunContext + step := sc.Step + return func(ctx context.Context) error { + action := sc.Action + log.Debugf("About to run action %v", action) + for inputID, input := range action.Inputs { + envKey := regexp.MustCompile("[^A-Z0-9-]").ReplaceAllString(strings.ToUpper(inputID), "_") + envKey = fmt.Sprintf("INPUT_%s", envKey) + if _, ok := sc.Env[envKey]; !ok { + sc.Env[envKey] = input.Default + } + } + + actionName := "" + containerActionDir := "." + if step.Type() == model.StepTypeUsesActionLocal { + actionName = strings.TrimPrefix(strings.TrimPrefix(actionDir, rc.Config.Workdir), string(filepath.Separator)) + containerActionDir = "/github/workspace" + } else if step.Type() == model.StepTypeUsesActionRemote { + actionName = strings.TrimPrefix(strings.TrimPrefix(actionDir, rc.ActionCacheDir()), string(filepath.Separator)) + containerActionDir = "/actions" + } + + log.Debugf("actionDir=%s Workdir=%s ActionCacheDir=%s actionName=%s containerActionDir=%s", actionDir, rc.Config.Workdir, rc.ActionCacheDir(), actionName, containerActionDir) + + switch action.Runs.Using { + case model.ActionRunsUsingNode12: + if step.Type() == model.StepTypeUsesActionRemote { + err := rc.JobContainer.CopyDir(containerActionDir+string(filepath.Separator), actionDir)(ctx) + if err != nil { + return err + } + } + return rc.execJobContainer([]string{"node", fmt.Sprintf("%s/%s/%s", containerActionDir, actionName, action.Runs.Main)}, sc.Env)(ctx) + case model.ActionRunsUsingDocker: + var prepImage common.Executor + var image string + if strings.HasPrefix(action.Runs.Image, "docker://") { + image = strings.TrimPrefix(action.Runs.Image, "docker://") + } else { + image = fmt.Sprintf("%s:%s", regexp.MustCompile("[^a-zA-Z0-9]").ReplaceAllString(actionName, "-"), "latest") + image = fmt.Sprintf("act-%s", strings.TrimLeft(image, "-")) + contextDir := filepath.Join(actionDir, action.Runs.Main) + prepImage = container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{ + ContextDir: contextDir, + ImageTag: image, + }) + } + + cmd := strings.Fields(step.With["args"]) + if len(cmd) == 0 { + cmd = action.Runs.Args + } + entrypoint := strings.Fields(step.With["entrypoint"]) + if len(entrypoint) == 0 { + entrypoint = action.Runs.Entrypoint + } + stepContainer := sc.newStepContainer(ctx, image, cmd, entrypoint) + return common.NewPipelineExecutor( + prepImage, + stepContainer.Pull(rc.Config.ForcePull), + stepContainer.Remove().IfBool(!rc.Config.ReuseContainers), + stepContainer.Create(), + stepContainer.Start(true), + ).Finally( + stepContainer.Remove().IfBool(!rc.Config.ReuseContainers), + )(ctx) + } + return nil + } +} + +type remoteAction struct { + Org string + Repo string + Path string + Ref string +} + +func (ra *remoteAction) CloneURL() string { + return fmt.Sprintf("https://github.com/%s/%s", ra.Org, ra.Repo) +} + +func newRemoteAction(action string) *remoteAction { + r := regexp.MustCompile(`^([^/@]+)/([^/@]+)(/([^@]*))?(@(.*))?$`) + matches := r.FindStringSubmatch(action) + + ra := new(remoteAction) + ra.Org = matches[1] + ra.Repo = matches[2] + ra.Path = "" + ra.Ref = "master" + if len(matches) >= 5 { + ra.Path = matches[4] + } + if len(matches) >= 7 { + ra.Ref = matches[6] + } + return ra +} diff --git a/pkg/runner/testdata/basic/push.yml b/pkg/runner/testdata/basic/push.yml index 720ca259..2ae1ea5e 100644 --- a/pkg/runner/testdata/basic/push.yml +++ b/pkg/runner/testdata/basic/push.yml @@ -5,7 +5,10 @@ jobs: check: runs-on: ubuntu-latest steps: - - run: echo 'hello world' + - run: ls + - run: echo 'hello world' + - run: echo ${GITHUB_SHA} >> /github/sha.txt + - run: cat /github/sha.txt | grep ${GITHUB_SHA} build: runs-on: ubuntu-latest @@ -20,4 +23,8 @@ jobs: steps: - uses: docker://ubuntu:18.04 with: - args: echo ${GITHUB_REF} | grep nektos/act + args: env + - uses: docker://ubuntu:18.04 + with: + entrypoint: /bin/echo + args: ${{github.event_name}} diff --git a/pkg/runner/testdata/node/push.yml b/pkg/runner/testdata/node/push.yml new file mode 100644 index 00000000..e8a284d3 --- /dev/null +++ b/pkg/runner/testdata/node/push.yml @@ -0,0 +1,16 @@ +name: NodeJS Test + +on: push + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 12 + registry-url: https://registry.npmjs.org/ + - run: which node + - name: Install Dependencies + run: npm install