commit
0f796ff9f6
|
@ -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"]
|
|
@ -1,8 +0,0 @@
|
|||
name: Check
|
||||
description: Run static analysis and unit tests
|
||||
branding:
|
||||
icon: check-circle
|
||||
color: green
|
||||
runs:
|
||||
using: 'docker'
|
||||
image: 'Dockerfile'
|
|
@ -1,4 +0,0 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
golangci-lint run
|
||||
go test -cover -short ./...
|
|
@ -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"]
|
|
@ -1,8 +0,0 @@
|
|||
name: Check
|
||||
description: Run integration tests
|
||||
branding:
|
||||
icon: check-circle
|
||||
color: green
|
||||
runs:
|
||||
using: 'docker'
|
||||
image: 'Dockerfile'
|
|
@ -1,3 +0,0 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
go test -cover ./pkg/runner
|
|
@ -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 }}
|
||||
|
|
|
@ -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 }}
|
39
Makefile
39
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
|
||||
|
|
|
@ -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` |
|
||||
|
|
|
@ -11,6 +11,7 @@ type Input struct {
|
|||
workflowsPath string
|
||||
eventPath string
|
||||
reuseContainers bool
|
||||
bindWorkdir bool
|
||||
secrets []string
|
||||
platforms []string
|
||||
dryrun bool
|
||||
|
|
|
@ -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": "",
|
||||
|
|
|
@ -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(),
|
||||
|
|
1
go.sum
1
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=
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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!`)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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}}
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue