commit
0f796ff9f6
33 changed files with 1089 additions and 710 deletions
9
.github/workflows/check/Dockerfile
vendored
9
.github/workflows/check/Dockerfile
vendored
|
@ -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"]
|
|
8
.github/workflows/check/action.yml
vendored
8
.github/workflows/check/action.yml
vendored
|
@ -1,8 +0,0 @@
|
||||||
name: Check
|
|
||||||
description: Run static analysis and unit tests
|
|
||||||
branding:
|
|
||||||
icon: check-circle
|
|
||||||
color: green
|
|
||||||
runs:
|
|
||||||
using: 'docker'
|
|
||||||
image: 'Dockerfile'
|
|
4
.github/workflows/check/entrypoint.sh
vendored
4
.github/workflows/check/entrypoint.sh
vendored
|
@ -1,4 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
golangci-lint run
|
|
||||||
go test -cover -short ./...
|
|
7
.github/workflows/integration/Dockerfile
vendored
7
.github/workflows/integration/Dockerfile
vendored
|
@ -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"]
|
|
8
.github/workflows/integration/action.yml
vendored
8
.github/workflows/integration/action.yml
vendored
|
@ -1,8 +0,0 @@
|
||||||
name: Check
|
|
||||||
description: Run integration tests
|
|
||||||
branding:
|
|
||||||
icon: check-circle
|
|
||||||
color: green
|
|
||||||
runs:
|
|
||||||
using: 'docker'
|
|
||||||
image: 'Dockerfile'
|
|
3
.github/workflows/integration/entrypoint.sh
vendored
3
.github/workflows/integration/entrypoint.sh
vendored
|
@ -1,3 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
go test -cover ./pkg/runner
|
|
37
.github/workflows/push.yml
vendored
37
.github/workflows/push.yml
vendored
|
@ -2,9 +2,40 @@ name: push
|
||||||
on: push
|
on: push
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: ./.github/workflows/check
|
- uses: docker://golangci/golangci-lint:v1.23.6
|
||||||
#- uses: ./.github/workflows/integration
|
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 }}
|
||||||
|
|
20
.github/workflows/tag.yml
vendored
20
.github/workflows/tag.yml
vendored
|
@ -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-)
|
VERSION?=$(shell git describe --tags --dirty | 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
|
|
||||||
IS_SNAPSHOT = $(if $(findstring -, $(VERSION)),true,false)
|
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)
|
export GITHUB_TOKEN = $(shell cat ~/.config/github/token)
|
||||||
|
|
||||||
check:
|
build:
|
||||||
@golangci-lint run
|
go build -ldflags "-X main.version=$(VERSION)" -o dist/local/act main.go
|
||||||
@go test -cover ./...
|
|
||||||
|
|
||||||
build: check
|
test:
|
||||||
$(eval export SNAPSHOT_VERSION=$(VERSION))
|
$(ACT) -P ubuntu-latest=nektos/act-environments-ubuntu:18.04
|
||||||
$(ACT) -ra build
|
|
||||||
|
|
||||||
install: build
|
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
|
@chmod 755 /usr/local/bin/act
|
||||||
@act --version
|
@act --version
|
||||||
|
|
||||||
|
@ -33,7 +24,8 @@ installer:
|
||||||
godownloader -r nektos/act -o install.sh
|
godownloader -r nektos/act -o install.sh
|
||||||
|
|
||||||
promote: vendor
|
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))
|
ifeq (false,$(IS_SNAPSHOT))
|
||||||
@echo "Unable to promote a non-snapshot"
|
@echo "Unable to promote a non-snapshot"
|
||||||
@exit 1
|
@exit 1
|
||||||
|
@ -42,9 +34,8 @@ ifneq ($(shell git status -s),)
|
||||||
@echo "Unable to promote a dirty workspace"
|
@echo "Unable to promote a dirty workspace"
|
||||||
@exit 1
|
@exit 1
|
||||||
endif
|
endif
|
||||||
$(eval NEW_VERSION := $(word 1,$(subst -, , $(TAG_VERSION))))
|
git tag -a -m "releasing v$(NEW_VERSION)" v$(NEW_VERSION)
|
||||||
git tag -a -m "releasing $(NEW_VERSION)" $(NEW_VERSION)
|
git push origin v$(NEW_VERSION)
|
||||||
git push origin $(NEW_VERSION)
|
|
||||||
|
|
||||||
vendor:
|
vendor:
|
||||||
go mod vendor
|
go mod vendor
|
||||||
|
|
|
@ -56,9 +56,9 @@ GitHub Actions offers managed [virtual environments](https://help.github.com/en/
|
||||||
|
|
||||||
| GitHub Runner | Docker Image |
|
| GitHub Runner | Docker Image |
|
||||||
| --------------- | ------------ |
|
| --------------- | ------------ |
|
||||||
| ubuntu-latest | [ubuntu:18.04](https://hub.docker.com/_/ubuntu) |
|
| ubuntu-latest | [node:12.6-buster-slim](https://hub.docker.com/_/buildpack-deps) |
|
||||||
| ubuntu-18.04 | [ubuntu:18.04](https://hub.docker.com/_/ubuntu) |
|
| ubuntu-18.04 | [node:12.6-buster-slim](https://hub.docker.com/_/buildpack-deps) |
|
||||||
| ubuntu-16.04 | [ubuntu:16.04](https://hub.docker.com/_/ubuntu) |
|
| ubuntu-16.04 | [node:12.6-stretch-slim](https://hub.docker.com/_/buildpack-deps) |
|
||||||
| windows-latest | `unsupported` |
|
| windows-latest | `unsupported` |
|
||||||
| windows-2019 | `unsupported` |
|
| windows-2019 | `unsupported` |
|
||||||
| macos-latest | `unsupported` |
|
| macos-latest | `unsupported` |
|
||||||
|
|
|
@ -11,6 +11,7 @@ type Input struct {
|
||||||
workflowsPath string
|
workflowsPath string
|
||||||
eventPath string
|
eventPath string
|
||||||
reuseContainers bool
|
reuseContainers bool
|
||||||
|
bindWorkdir bool
|
||||||
secrets []string
|
secrets []string
|
||||||
platforms []string
|
platforms []string
|
||||||
dryrun bool
|
dryrun bool
|
||||||
|
|
|
@ -6,9 +6,9 @@ import (
|
||||||
|
|
||||||
func (i *Input) newPlatforms() map[string]string {
|
func (i *Input) newPlatforms() map[string]string {
|
||||||
platforms := map[string]string{
|
platforms := map[string]string{
|
||||||
"ubuntu-latest": "ubuntu:18.04",
|
"ubuntu-latest": "node:12.6-buster-slim",
|
||||||
"ubuntu-18.04": "ubuntu:18.04",
|
"ubuntu-18.04": "node:12.6-buster-slim",
|
||||||
"ubuntu-16.04": "ubuntu:16.04",
|
"ubuntu-16.04": "node:12.6-stretch-slim",
|
||||||
"windows-latest": "",
|
"windows-latest": "",
|
||||||
"windows-2019": "",
|
"windows-2019": "",
|
||||||
"macos-latest": "",
|
"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.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().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.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().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.Flags().StringVarP(&input.eventPath, "eventpath", "e", "", "path to event JSON file")
|
||||||
rootCmd.PersistentFlags().StringVarP(&input.workflowsPath, "workflows", "W", "./.github/workflows/", "path to workflow files")
|
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,
|
ForcePull: input.forcePull,
|
||||||
ReuseContainers: input.reuseContainers,
|
ReuseContainers: input.reuseContainers,
|
||||||
Workdir: input.Workdir(),
|
Workdir: input.Workdir(),
|
||||||
|
BindWorkdir: input.bindWorkdir,
|
||||||
LogOutput: !input.noOutput,
|
LogOutput: !input.noOutput,
|
||||||
Secrets: newSecrets(input.secrets),
|
Secrets: newSecrets(input.secrets),
|
||||||
Platforms: input.newPlatforms(),
|
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/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 h1:PyjxRdW1mqCmSoxy/6uP01P7CGbsD+woX+oOWbaUPwQ=
|
||||||
github.com/docker/engine v0.0.0-20181106193140-f5749085e9cb/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY=
|
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 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||||
github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk=
|
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
|
// NewPipelineExecutor creates a new executor from a series of other executors
|
||||||
func NewPipelineExecutor(executors ...Executor) Executor {
|
func NewPipelineExecutor(executors ...Executor) Executor {
|
||||||
if len(executors) == 0 {
|
if len(executors) == 0 {
|
||||||
|
|
|
@ -254,7 +254,7 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) Executor {
|
||||||
Force: true,
|
Force: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Unable to checkout %s: %v", refName, err)
|
logger.Errorf("Unable to checkout %s: %v", *hash, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// LineHandler is a callback function for handling a line
|
// LineHandler is a callback function for handling a line
|
||||||
type LineHandler func(line string)
|
type LineHandler func(line string) bool
|
||||||
|
|
||||||
type lineWriter struct {
|
type lineWriter struct {
|
||||||
buffer bytes.Buffer
|
buffer bytes.Buffer
|
||||||
|
@ -42,6 +42,9 @@ func (lw *lineWriter) Write(p []byte) (n int, err error) {
|
||||||
|
|
||||||
func (lw *lineWriter) handleLine(line string) {
|
func (lw *lineWriter) handleLine(line string) {
|
||||||
for _, h := range lw.handlers {
|
for _, h := range lw.handlers {
|
||||||
h(line)
|
ok := h(line)
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,9 @@ import (
|
||||||
|
|
||||||
func TestLineWriter(t *testing.T) {
|
func TestLineWriter(t *testing.T) {
|
||||||
lines := make([]string, 0)
|
lines := make([]string, 0)
|
||||||
lineHandler := func(s string) {
|
lineHandler := func(s string) bool {
|
||||||
lines = append(lines, s)
|
lines = append(lines, s)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
lineWriter := NewLineWriter(lineHandler)
|
lineWriter := NewLineWriter(lineHandler)
|
||||||
|
|
|
@ -21,7 +21,7 @@ type NewDockerPullExecutorInput struct {
|
||||||
func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
|
func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
logger.Infof("%sdocker pull %v", logPrefix, input.Image)
|
logger.Debugf("%sdocker pull %v", logPrefix, input.Image)
|
||||||
|
|
||||||
if common.Dryrun(ctx) {
|
if common.Dryrun(ctx) {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -1,67 +1,141 @@
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/mount"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/docker/docker/pkg/stdcopy"
|
"github.com/docker/docker/pkg/stdcopy"
|
||||||
"github.com/nektos/act/pkg/common"
|
"github.com/nektos/act/pkg/common"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/crypto/ssh/terminal"
|
"golang.org/x/crypto/ssh/terminal"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewDockerRunExecutorInput the input for the NewDockerRunExecutor function
|
// NewContainerInput the input for the New function
|
||||||
type NewDockerRunExecutorInput struct {
|
type NewContainerInput struct {
|
||||||
Image string
|
Image string
|
||||||
Entrypoint []string
|
Entrypoint []string
|
||||||
Cmd []string
|
Cmd []string
|
||||||
WorkingDir string
|
WorkingDir string
|
||||||
Env []string
|
Env []string
|
||||||
Binds []string
|
Binds []string
|
||||||
Content map[string]io.Reader
|
Mounts map[string]string
|
||||||
Volumes []string
|
Name string
|
||||||
Name string
|
Stdout io.Writer
|
||||||
ReuseContainers bool
|
Stderr io.Writer
|
||||||
Stdout io.Writer
|
|
||||||
Stderr io.Writer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDockerRunExecutor function to create a run executor for the container
|
// FileEntry is a file to copy to a container
|
||||||
func NewDockerRunExecutor(input NewDockerRunExecutorInput) common.Executor {
|
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 := new(containerReference)
|
||||||
cr.input = input
|
cr.input = input
|
||||||
|
return cr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *containerReference) Create() common.Executor {
|
||||||
return common.
|
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(
|
Then(
|
||||||
common.NewPipelineExecutor(
|
common.NewPipelineExecutor(
|
||||||
cr.connect(),
|
cr.connect(),
|
||||||
cr.find(),
|
cr.find(),
|
||||||
cr.remove().IfBool(!input.ReuseContainers),
|
|
||||||
cr.create(),
|
cr.create(),
|
||||||
cr.copyContent(),
|
|
||||||
cr.attach(),
|
|
||||||
cr.start(),
|
|
||||||
cr.wait(),
|
|
||||||
).Finally(
|
|
||||||
cr.remove().IfBool(!input.ReuseContainers),
|
|
||||||
).IfNot(common.Dryrun),
|
).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 {
|
type containerReference struct {
|
||||||
input NewDockerRunExecutorInput
|
|
||||||
cli *client.Client
|
cli *client.Client
|
||||||
id string
|
id string
|
||||||
|
input *NewContainerInput
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cr *containerReference) connect() common.Executor {
|
func (cr *containerReference) connect() common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
|
if cr.cli != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
cli, err := client.NewClientWithOpts(client.FromEnv)
|
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
|
@ -74,6 +148,9 @@ func (cr *containerReference) connect() common.Executor {
|
||||||
|
|
||||||
func (cr *containerReference) find() common.Executor {
|
func (cr *containerReference) find() common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
|
if cr.id != "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
containers, err := cr.cli.ContainerList(ctx, types.ContainerListOptions{
|
containers, err := cr.cli.ContainerList(ctx, types.ContainerListOptions{
|
||||||
All: true,
|
All: true,
|
||||||
})
|
})
|
||||||
|
@ -107,11 +184,11 @@ func (cr *containerReference) remove() common.Executor {
|
||||||
Force: true,
|
Force: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
logger.Error(errors.WithStack(err))
|
||||||
}
|
}
|
||||||
cr.id = ""
|
|
||||||
|
|
||||||
logger.Debugf("Removed container: %v", cr.id)
|
logger.Debugf("Removed container: %v", cr.id)
|
||||||
|
cr.id = ""
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -134,15 +211,18 @@ func (cr *containerReference) create() common.Executor {
|
||||||
Tty: isTerminal,
|
Tty: isTerminal,
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(input.Volumes) > 0 {
|
mounts := make([]mount.Mount, 0)
|
||||||
config.Volumes = make(map[string]struct{})
|
for mountSource, mountTarget := range input.Mounts {
|
||||||
for _, vol := range input.Volumes {
|
mounts = append(mounts, mount.Mount{
|
||||||
config.Volumes[vol] = struct{}{}
|
Type: mount.TypeVolume,
|
||||||
}
|
Source: mountSource,
|
||||||
|
Target: mountTarget,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := cr.cli.ContainerCreate(ctx, config, &container.HostConfig{
|
resp, err := cr.cli.ContainerCreate(ctx, config, &container.HostConfig{
|
||||||
Binds: input.Binds,
|
Binds: input.Binds,
|
||||||
|
Mounts: mounts,
|
||||||
}, nil, input.Name)
|
}, nil, input.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
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 {
|
return func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
for dstPath, srcReader := range cr.input.Content {
|
logger.Debugf("Exec command '%s'", cmd)
|
||||||
logger.Debugf("Extracting content to '%s'", dstPath)
|
isTerminal := terminal.IsTerminal(int(os.Stdout.Fd()))
|
||||||
err := cr.cli.CopyToContainer(ctx, cr.id, dstPath, srcReader, types.CopyToContainerOptions{})
|
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 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -207,7 +452,7 @@ func (cr *containerReference) attach() common.Executor {
|
||||||
func (cr *containerReference) start() common.Executor {
|
func (cr *containerReference) start() common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
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 {
|
if err := cr.cli.ContainerStart(ctx, cr.id, types.ContainerStartOptions{}); err != nil {
|
||||||
return errors.WithStack(err)
|
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!`)
|
|
||||||
}
|
|
29
pkg/container/docker_volume.go
Normal file
29
pkg/container/docker_volume.go
Normal file
|
@ -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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/common"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -94,6 +95,58 @@ func (j *Job) Needs() []string {
|
||||||
return nil
|
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
|
// ContainerSpec is the specification of the container to use for the job
|
||||||
type ContainerSpec struct {
|
type ContainerSpec struct {
|
||||||
Image string `yaml:"image"`
|
Image string `yaml:"image"`
|
||||||
|
@ -104,6 +157,7 @@ type ContainerSpec struct {
|
||||||
Entrypoint string
|
Entrypoint string
|
||||||
Args string
|
Args string
|
||||||
Name string
|
Name string
|
||||||
|
Reuse bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step is the structure of one step in a job
|
// Step is the structure of one step in a job
|
||||||
|
@ -147,6 +201,29 @@ func (s *Step) GetEnv() map[string]string {
|
||||||
return rtnEnv
|
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
|
// StepType describes what type of step we are about to run
|
||||||
type StepType int
|
type StepType int
|
||||||
|
|
||||||
|
|
|
@ -8,50 +8,63 @@ import (
|
||||||
"github.com/nektos/act/pkg/common"
|
"github.com/nektos/act/pkg/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
var commandPattern *regexp.Regexp
|
var commandPatternGA *regexp.Regexp
|
||||||
|
var commandPatternADO *regexp.Regexp
|
||||||
|
|
||||||
func init() {
|
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 {
|
func (rc *RunContext) commandHandler(ctx context.Context) common.LineHandler {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
resumeCommand := ""
|
resumeCommand := ""
|
||||||
return func(line string) {
|
return func(line string) bool {
|
||||||
if m := commandPattern.FindStringSubmatch(line); m != nil {
|
var command string
|
||||||
command := m[1]
|
var kvPairs map[string]string
|
||||||
kvPairs := parseKeyValuePairs(m[3])
|
var arg string
|
||||||
arg := m[4]
|
if m := commandPatternGA.FindStringSubmatch(line); m != nil {
|
||||||
|
command = m[1]
|
||||||
if resumeCommand != "" && command != resumeCommand {
|
kvPairs = parseKeyValuePairs(m[3], ",")
|
||||||
return
|
arg = m[4]
|
||||||
}
|
} else if m := commandPatternADO.FindStringSubmatch(line); m != nil {
|
||||||
|
command = m[1]
|
||||||
switch command {
|
kvPairs = parseKeyValuePairs(m[3], ";")
|
||||||
case "set-env":
|
arg = m[4]
|
||||||
rc.setEnv(ctx, kvPairs, arg)
|
} else {
|
||||||
case "set-output":
|
return true
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
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)
|
rtn := make(map[string]string)
|
||||||
kvPairList := strings.Split(kvPairs, ",")
|
kvPairList := strings.Split(kvPairs, separator)
|
||||||
for _, kvPair := range kvPairList {
|
for _, kvPair := range kvPairList {
|
||||||
kv := strings.Split(kvPair, "=")
|
kv := strings.Split(kvPair, "=")
|
||||||
if len(kv) == 2 {
|
if len(kv) == 2 {
|
||||||
|
|
|
@ -60,3 +60,16 @@ func TestStopCommands(t *testing.T) {
|
||||||
handler("::set-env name=x::abcd\n")
|
handler("::set-env name=x::abcd\n")
|
||||||
assert.Equal("abcd", rc.Env["x"])
|
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"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/model"
|
|
||||||
"github.com/robertkrimen/otto"
|
"github.com/robertkrimen/otto"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"gopkg.in/godo.v2/glob"
|
"gopkg.in/godo.v2/glob"
|
||||||
|
@ -34,11 +33,12 @@ func (rc *RunContext) NewExpressionEvaluator() ExpressionEvaluator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStepExpressionEvaluator creates a new evaluator
|
// NewExpressionEvaluator creates a new evaluator
|
||||||
func (rc *RunContext) NewStepExpressionEvaluator(step *model.Step) ExpressionEvaluator {
|
func (sc *StepContext) NewExpressionEvaluator() ExpressionEvaluator {
|
||||||
vm := rc.newVM()
|
vm := sc.RunContext.newVM()
|
||||||
configers := []func(*otto.Otto){
|
configers := []func(*otto.Otto){
|
||||||
rc.vmEnv(step),
|
sc.vmEnv(),
|
||||||
|
sc.vmInputs(),
|
||||||
}
|
}
|
||||||
for _, configer := range configers {
|
for _, configer := range configers {
|
||||||
configer(vm)
|
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) {
|
return func(vm *otto.Otto) {
|
||||||
env := rc.StepEnv(step)
|
_ = vm.Set("env", sc.Env)
|
||||||
_ = vm.Set("env", 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
|
package runner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/tar"
|
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
@ -24,16 +20,16 @@ import (
|
||||||
|
|
||||||
// RunContext contains info about current job
|
// RunContext contains info about current job
|
||||||
type RunContext struct {
|
type RunContext struct {
|
||||||
Config *Config
|
Config *Config
|
||||||
Matrix map[string]interface{}
|
Matrix map[string]interface{}
|
||||||
Run *model.Run
|
Run *model.Run
|
||||||
EventJSON string
|
EventJSON string
|
||||||
Env map[string]string
|
Env map[string]string
|
||||||
Tempdir string
|
ExtraPath []string
|
||||||
ExtraPath []string
|
CurrentStep string
|
||||||
CurrentStep string
|
StepResults map[string]*stepResult
|
||||||
StepResults map[string]*stepResult
|
ExprEval ExpressionEvaluator
|
||||||
ExprEval ExpressionEvaluator
|
JobContainer container.Container
|
||||||
}
|
}
|
||||||
|
|
||||||
type stepResult struct {
|
type stepResult struct {
|
||||||
|
@ -49,68 +45,175 @@ func (rc *RunContext) GetEnv() map[string]string {
|
||||||
return rc.Env
|
return rc.Env
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close cleans up temp dir
|
func (rc *RunContext) jobContainerName() string {
|
||||||
func (rc *RunContext) Close(ctx context.Context) error {
|
return createContainerName("act", rc.Run.String())
|
||||||
return os.RemoveAll(rc.Tempdir)
|
}
|
||||||
|
|
||||||
|
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
|
// Executor returns a pipeline executor for all the steps in the job
|
||||||
func (rc *RunContext) Executor() common.Executor {
|
func (rc *RunContext) Executor() common.Executor {
|
||||||
|
|
||||||
err := rc.setupTempDir()
|
|
||||||
if err != nil {
|
|
||||||
return common.NewErrorExecutor(err)
|
|
||||||
}
|
|
||||||
steps := make([]common.Executor, 0)
|
steps := make([]common.Executor, 0)
|
||||||
|
steps = append(steps, rc.startJobContainer())
|
||||||
|
|
||||||
for i, step := range rc.Run.Job().Steps {
|
for i, step := range rc.Run.Job().Steps {
|
||||||
if step.ID == "" {
|
if step.ID == "" {
|
||||||
step.ID = fmt.Sprintf("%d", i)
|
step.ID = fmt.Sprintf("%d", i)
|
||||||
}
|
}
|
||||||
s := step
|
steps = append(steps, rc.newStepExecutor(step))
|
||||||
steps = append(steps, func(ctx context.Context) error {
|
}
|
||||||
rc.CurrentStep = s.ID
|
steps = append(steps, rc.stopJobContainer())
|
||||||
rc.StepResults[rc.CurrentStep] = &stepResult{
|
|
||||||
Success: true,
|
|
||||||
Outputs: make(map[string]string),
|
|
||||||
}
|
|
||||||
rc.ExprEval = rc.NewStepExpressionEvaluator(s)
|
|
||||||
|
|
||||||
if !rc.EvalBool(s.If) {
|
return common.NewPipelineExecutor(steps...).If(rc.isEnabled)
|
||||||
log.Debugf("Skipping step '%s' due to '%s'", s.String(), s.If)
|
}
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
common.Logger(ctx).Infof("\u2B50 Run %s", s)
|
func (rc *RunContext) newStepExecutor(step *model.Step) common.Executor {
|
||||||
err := rc.newStepExecutor(s)(ctx)
|
sc := &StepContext{
|
||||||
if err == nil {
|
RunContext: rc,
|
||||||
common.Logger(ctx).Infof(" \u2705 Success - %s", s)
|
Step: step,
|
||||||
} else {
|
|
||||||
common.Logger(ctx).Errorf(" \u274C Failure - %s", s)
|
|
||||||
rc.StepResults[rc.CurrentStep].Success = false
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
defer rc.Close(ctx)
|
rc.CurrentStep = sc.Step.ID
|
||||||
job := rc.Run.Job()
|
rc.StepResults[rc.CurrentStep] = &stepResult{
|
||||||
log := common.Logger(ctx)
|
Success: true,
|
||||||
if !rc.EvalBool(job.If) {
|
Outputs: make(map[string]string),
|
||||||
log.Debugf("Skipping job '%s' due to '%s'", job.Name, job.If)
|
}
|
||||||
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
platformName := rc.ExprEval.Interpolate(rc.Run.Job().RunsOn)
|
common.Logger(ctx).Infof("\u2B50 Run %s", sc.Step)
|
||||||
if img, ok := rc.Config.Platforms[strings.ToLower(platformName)]; !ok || img == "" {
|
err := sc.Executor()(ctx)
|
||||||
log.Infof(" \U0001F6A7 Skipping unsupported platform '%s'", platformName)
|
if err == nil {
|
||||||
return 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 err
|
||||||
return common.NewPipelineExecutor(steps...)(ctx)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// EvalBool evaluates an expression against current run context
|
||||||
func (rc *RunContext) EvalBool(expr string) bool {
|
func (rc *RunContext) EvalBool(expr string) bool {
|
||||||
if expr != "" {
|
if expr != "" {
|
||||||
|
@ -134,124 +237,24 @@ func mergeMaps(maps ...map[string]string) map[string]string {
|
||||||
return rtnMap
|
return rtnMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rc *RunContext) setupTempDir() error {
|
func createContainerName(parts ...string) string {
|
||||||
var err error
|
name := make([]string, 0)
|
||||||
tempBase := ""
|
pattern := regexp.MustCompile("[^a-zA-Z0-9]")
|
||||||
if runtime.GOOS == "darwin" {
|
partLen := (30 / len(parts)) - 1
|
||||||
tempBase = "/tmp"
|
for i, part := range parts {
|
||||||
}
|
if i == len(parts)-1 {
|
||||||
rc.Tempdir, err = ioutil.TempDir(tempBase, "act-")
|
name = append(name, pattern.ReplaceAllString(part, "-"))
|
||||||
if err != nil {
|
} else {
|
||||||
return err
|
name = append(name, trimToLen(pattern.ReplaceAllString(part, "-"), partLen))
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := tw.Close(); err != nil {
|
return trimToLen(strings.Trim(strings.Join(name, "-"), "-"), 30)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func trimToLen(s string, l int) string {
|
func trimToLen(s string, l int) string {
|
||||||
|
if l < 0 {
|
||||||
|
l = 0
|
||||||
|
}
|
||||||
if len(s) > l {
|
if len(s) > l {
|
||||||
return 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_ID"] = github.RunID
|
||||||
env["GITHUB_RUN_NUMBER"] = github.RunNumber
|
env["GITHUB_RUN_NUMBER"] = github.RunNumber
|
||||||
env["GITHUB_ACTION"] = github.Action
|
env["GITHUB_ACTION"] = github.Action
|
||||||
|
env["GITHUB_ACTIONS"] = "true"
|
||||||
env["GITHUB_ACTOR"] = github.Actor
|
env["GITHUB_ACTOR"] = github.Actor
|
||||||
env["GITHUB_REPOSITORY"] = github.Repository
|
env["GITHUB_REPOSITORY"] = github.Repository
|
||||||
env["GITHUB_EVENT_NAME"] = github.EventName
|
env["GITHUB_EVENT_NAME"] = github.EventName
|
||||||
|
|
|
@ -13,12 +13,12 @@ import (
|
||||||
// Runner provides capabilities to run GitHub actions
|
// Runner provides capabilities to run GitHub actions
|
||||||
type Runner interface {
|
type Runner interface {
|
||||||
NewPlanExecutor(plan *model.Plan) common.Executor
|
NewPlanExecutor(plan *model.Plan) common.Executor
|
||||||
NewRunExecutor(run *model.Run, matrix map[string]interface{}) common.Executor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config contains the config for a new runner
|
// Config contains the config for a new runner
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Workdir string // path to working directory
|
Workdir string // path to working directory
|
||||||
|
BindWorkdir bool // bind the workdir to the job container
|
||||||
EventName string // name of event to run
|
EventName string // name of event to run
|
||||||
EventPath string // path to JSON file to use for event.json in containers
|
EventPath string // path to JSON file to use for event.json in containers
|
||||||
ReuseContainers bool // reuse containers to maintain state
|
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)
|
stageExecutor := make([]common.Executor, 0)
|
||||||
for _, run := range stage.Runs {
|
for _, run := range stage.Runs {
|
||||||
job := run.Job()
|
job := run.Job()
|
||||||
matrixes := make([]map[string]interface{}, 0)
|
matrixes := job.GetMatrixes()
|
||||||
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{}))
|
|
||||||
}
|
|
||||||
|
|
||||||
jobName := fmt.Sprintf("%-*s", maxJobNameLen, run.String())
|
jobName := fmt.Sprintf("%-*s", maxJobNameLen, run.String())
|
||||||
for _, matrix := range matrixes {
|
for _, matrix := range matrixes {
|
||||||
m := matrix
|
m := matrix
|
||||||
runExecutor := runner.NewRunExecutor(run, matrix)
|
runExecutor := runner.newRunExecutor(run, matrix)
|
||||||
stageExecutor = append(stageExecutor, func(ctx context.Context) error {
|
stageExecutor = append(stageExecutor, func(ctx context.Context) error {
|
||||||
ctx = WithJobLogger(ctx, jobName)
|
ctx = WithJobLogger(ctx, jobName)
|
||||||
if len(m) > 0 {
|
if len(m) > 0 {
|
||||||
|
@ -117,22 +80,14 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
|
||||||
return common.NewPipelineExecutor(pipeline...)
|
return common.NewPipelineExecutor(pipeline...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func commonKeysMatch(a map[string]interface{}, b map[string]interface{}) bool {
|
func (runner *runnerImpl) newRunExecutor(run *model.Run, matrix map[string]interface{}) common.Executor {
|
||||||
for aKey, aVal := range a {
|
rc := &RunContext{
|
||||||
if bVal, ok := b[aKey]; ok && aVal != bVal {
|
Config: runner.config,
|
||||||
return false
|
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()
|
rc.ExprEval = rc.NewExpressionEvaluator()
|
||||||
return rc.Executor()
|
return rc.Executor()
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package runner
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/model"
|
"github.com/nektos/act/pkg/model"
|
||||||
|
@ -57,13 +58,16 @@ func TestRunEvent(t *testing.T) {
|
||||||
table := table
|
table := table
|
||||||
t.Run(table.workflowPath, func(t *testing.T) {
|
t.Run(table.workflowPath, func(t *testing.T) {
|
||||||
platforms := map[string]string{
|
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{
|
runnerConfig := &Config{
|
||||||
Workdir: "testdata",
|
Workdir: workdir,
|
||||||
EventName: table.eventName,
|
EventName: table.eventName,
|
||||||
Platforms: platforms,
|
Platforms: platforms,
|
||||||
ReuseContainers: true,
|
ReuseContainers: false,
|
||||||
}
|
}
|
||||||
runner, err := New(runnerConfig)
|
runner, err := New(runnerConfig)
|
||||||
assert.NilError(t, err, table.workflowPath)
|
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
|
|
||||||
}
|
|
326
pkg/runner/step_context.go
Normal file
326
pkg/runner/step_context.go
Normal file
|
@ -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
|
||||||
|
}
|
11
pkg/runner/testdata/basic/push.yml
vendored
11
pkg/runner/testdata/basic/push.yml
vendored
|
@ -5,7 +5,10 @@ jobs:
|
||||||
check:
|
check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
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:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -20,4 +23,8 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: docker://ubuntu:18.04
|
- uses: docker://ubuntu:18.04
|
||||||
with:
|
with:
|
||||||
args: echo ${GITHUB_REF} | grep nektos/act
|
args: env
|
||||||
|
- uses: docker://ubuntu:18.04
|
||||||
|
with:
|
||||||
|
entrypoint: /bin/echo
|
||||||
|
args: ${{github.event_name}}
|
||||||
|
|
16
pkg/runner/testdata/node/push.yml
vendored
Normal file
16
pkg/runner/testdata/node/push.yml
vendored
Normal file
|
@ -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 a new issue