shared container for job
This commit is contained in:
parent
9179d8924d
commit
01876438c2
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 {
|
||||||
|
|
|
@ -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,6 +1,8 @@
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -8,60 +10,119 @@ import (
|
||||||
|
|
||||||
"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
|
||||||
|
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.
|
||||||
|
NewDebugExecutor("%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) 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 +135,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,
|
||||||
})
|
})
|
||||||
|
@ -134,15 +198,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 +222,100 @@ 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)
|
||||||
if err != nil {
|
for k, v := range env {
|
||||||
return errors.WithStack(err)
|
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) 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 +359,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)
|
||||||
|
|
|
@ -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"`
|
||||||
|
@ -148,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
|
||||||
|
|
||||||
|
|
|
@ -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,11 @@ 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(),
|
||||||
}
|
}
|
||||||
for _, configer := range configers {
|
for _, configer := range configers {
|
||||||
configer(vm)
|
configer(vm)
|
||||||
|
@ -236,10 +235,9 @@ 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,12 @@
|
||||||
package runner
|
package runner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/tar"
|
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/container"
|
"github.com/nektos/act/pkg/container"
|
||||||
|
@ -23,16 +19,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 {
|
||||||
|
@ -48,80 +44,141 @@ 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(filepath.Base(rc.Config.Workdir), 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) {
|
||||||
|
if rc.Config.LogOutput {
|
||||||
|
rawLogger.Infof(s)
|
||||||
|
} else {
|
||||||
|
rawLogger.Debugf(s)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
common.Logger(ctx).Infof("\U0001f680 Start image=%s", image)
|
||||||
|
name := rc.jobContainerName()
|
||||||
|
|
||||||
|
rc.JobContainer = container.NewContainer(&container.NewContainerInput{
|
||||||
|
Cmd: nil,
|
||||||
|
Entrypoint: []string{"/bin/cat"},
|
||||||
|
WorkingDir: "/github/workspace",
|
||||||
|
Image: image,
|
||||||
|
Name: name,
|
||||||
|
Mounts: map[string]string{
|
||||||
|
name: "/github",
|
||||||
|
},
|
||||||
|
Binds: []string{
|
||||||
|
fmt.Sprintf("%s:%s", rc.Config.Workdir, "/github/workspace"),
|
||||||
|
fmt.Sprintf("%s:%s", "/var/run/docker.sock", "/var/run/docker.sock"),
|
||||||
|
},
|
||||||
|
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.Copy("/github/", &container.FileEntry{
|
||||||
|
Name: "workflow/event.json",
|
||||||
|
Mode: 644,
|
||||||
|
Body: rc.EventJSON,
|
||||||
|
}),
|
||||||
|
)(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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
nullLogger := logrus.New()
|
|
||||||
nullLogger.Out = ioutil.Discard
|
|
||||||
if !rc.Config.ReuseContainers {
|
|
||||||
_ = rc.newContainerCleaner()(common.WithLogger(ctx, nullLogger))
|
|
||||||
}
|
|
||||||
|
|
||||||
err := common.NewPipelineExecutor(steps...)(ctx)
|
|
||||||
|
|
||||||
if !rc.Config.ReuseContainers {
|
|
||||||
_ = rc.newContainerCleaner()(common.WithLogger(ctx, nullLogger))
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
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
|
// 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 != "" {
|
||||||
|
@ -145,33 +202,7 @@ func mergeMaps(maps ...map[string]string) map[string]string {
|
||||||
return rtnMap
|
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 {
|
func (rc *RunContext) runContainer(containerSpec *model.ContainerSpec) common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
ghReader, err := rc.createGithubTarball()
|
ghReader, err := rc.createGithubTarball()
|
||||||
|
@ -200,7 +231,7 @@ func (rc *RunContext) runContainer(containerSpec *model.ContainerSpec) common.Ex
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return container.NewDockerRunExecutor(container.NewDockerRunExecutorInput{
|
c := container.NewContainer(&container.NewContainerInput{
|
||||||
Cmd: cmd,
|
Cmd: cmd,
|
||||||
Entrypoint: entrypoint,
|
Entrypoint: entrypoint,
|
||||||
Image: containerSpec.Image,
|
Image: containerSpec.Image,
|
||||||
|
@ -212,64 +243,27 @@ func (rc *RunContext) runContainer(containerSpec *model.ContainerSpec) common.Ex
|
||||||
fmt.Sprintf("%s:%s", rc.Tempdir, "/github/home"),
|
fmt.Sprintf("%s:%s", rc.Tempdir, "/github/home"),
|
||||||
fmt.Sprintf("%s:%s", "/var/run/docker.sock", "/var/run/docker.sock"),
|
fmt.Sprintf("%s:%s", "/var/run/docker.sock", "/var/run/docker.sock"),
|
||||||
},
|
},
|
||||||
Content: map[string]io.Reader{"/github": ghReader},
|
Stdout: logWriter,
|
||||||
ReuseContainers: containerSpec.Reuse,
|
Stderr: logWriter,
|
||||||
Stdout: logWriter,
|
})
|
||||||
Stderr: logWriter,
|
|
||||||
})(ctx)
|
return c.Create().
|
||||||
|
Then(c.Copy("/github", ghReader)).
|
||||||
|
Then(c.Start()).
|
||||||
|
Finally(c.Remove().IfBool(!rc.Config.ReuseContainers))(ctx)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rc *RunContext) createGithubTarball() (io.Reader, error) {
|
*/
|
||||||
var buf bytes.Buffer
|
|
||||||
tw := tar.NewWriter(&buf)
|
func createContainerName(parts ...string) string {
|
||||||
var files = []struct {
|
name := make([]string, 0)
|
||||||
Name string
|
pattern := regexp.MustCompile("[^a-zA-Z0-9]")
|
||||||
Mode int64
|
for _, part := range parts {
|
||||||
Body string
|
name = append(name, pattern.ReplaceAllString(part, "-"))
|
||||||
}{
|
|
||||||
{"workflow/event.json", 0644, rc.EventJSON},
|
|
||||||
}
|
}
|
||||||
for _, file := range files {
|
return trimToLen(strings.Join(name, "-"), 30)
|
||||||
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 nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &buf, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *RunContext) createContainerName() string {
|
|
||||||
containerName := rc.Run.String()
|
|
||||||
containerName = regexp.MustCompile("[^a-zA-Z0-9]").ReplaceAllString(containerName, "-")
|
|
||||||
|
|
||||||
prefix := ""
|
|
||||||
suffix := ""
|
|
||||||
containerName = trimToLen(containerName, 30-(len(prefix)+len(suffix)))
|
|
||||||
return fmt.Sprintf("%s%s%s", prefix, containerName, suffix)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *RunContext) createStepContainerName(stepID string) string {
|
|
||||||
|
|
||||||
prefix := regexp.MustCompile("[^a-zA-Z0-9]").ReplaceAllString(rc.createContainerName(), "-")
|
|
||||||
suffix := regexp.MustCompile("[^a-zA-Z0-9]").ReplaceAllString(stepID, "-")
|
|
||||||
prefix = trimToLen(prefix, 30-(1+len(suffix)))
|
|
||||||
name := strings.Trim(fmt.Sprintf("%s-%s", prefix, suffix), "-")
|
|
||||||
return name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func trimToLen(s string, l int) string {
|
func trimToLen(s string, l int) string {
|
||||||
|
|
|
@ -13,7 +13,6 @@ 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
|
||||||
|
@ -59,49 +58,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 +79,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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,276 +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) newContainerCleaner() common.Executor {
|
|
||||||
job := rc.Run.Job()
|
|
||||||
containerSpec := new(model.ContainerSpec)
|
|
||||||
containerSpec.Name = rc.createContainerName()
|
|
||||||
containerSpec.Reuse = false
|
|
||||||
|
|
||||||
if job.Container != nil {
|
|
||||||
containerSpec.Image = job.Container.Image
|
|
||||||
} else {
|
|
||||||
platformName := rc.ExprEval.Interpolate(rc.Run.Job().RunsOn)
|
|
||||||
containerSpec.Image = rc.Config.Platforms[strings.ToLower(platformName)]
|
|
||||||
}
|
|
||||||
containerSpec.Entrypoint = "bash --noprofile --norc -o pipefail -c echo 'cleaning up'"
|
|
||||||
return common.NewPipelineExecutor(
|
|
||||||
rc.pullImage(containerSpec),
|
|
||||||
rc.runContainer(containerSpec),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *RunContext) newStepExecutor(step *model.Step) common.Executor {
|
|
||||||
job := rc.Run.Job()
|
|
||||||
containerSpec := new(model.ContainerSpec)
|
|
||||||
containerSpec.Name = rc.createContainerName()
|
|
||||||
containerSpec.Reuse = true
|
|
||||||
|
|
||||||
if job.Container != nil {
|
|
||||||
containerSpec.Image = job.Container.Image
|
|
||||||
} else {
|
|
||||||
platformName := rc.ExprEval.Interpolate(rc.Run.Job().RunsOn)
|
|
||||||
containerSpec.Image = rc.Config.Platforms[strings.ToLower(platformName)]
|
|
||||||
}
|
|
||||||
|
|
||||||
switch step.Type() {
|
|
||||||
case model.StepTypeRun:
|
|
||||||
if job.Container != nil {
|
|
||||||
containerSpec.Ports = job.Container.Ports
|
|
||||||
containerSpec.Volumes = job.Container.Volumes
|
|
||||||
containerSpec.Options = job.Container.Options
|
|
||||||
}
|
|
||||||
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.Name = rc.createStepContainerName(step.ID)
|
|
||||||
containerSpec.Entrypoint = step.With["entrypoint"]
|
|
||||||
containerSpec.Args = step.With["args"]
|
|
||||||
containerSpec.Reuse = rc.Config.ReuseContainers
|
|
||||||
return common.NewPipelineExecutor(
|
|
||||||
rc.setupEnv(containerSpec, step),
|
|
||||||
rc.pullImage(containerSpec),
|
|
||||||
rc.runContainer(containerSpec),
|
|
||||||
)
|
|
||||||
|
|
||||||
case model.StepTypeUsesActionLocal:
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
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:
|
|
||||||
if strings.HasPrefix(actionDir, rc.Config.Workdir) {
|
|
||||||
containerSpec.Entrypoint = fmt.Sprintf("node /github/workspace/%s/%s", strings.TrimPrefix(actionDir, rc.Config.Workdir), action.Runs.Main)
|
|
||||||
} else if strings.HasPrefix(actionDir, rc.Tempdir) {
|
|
||||||
containerSpec.Entrypoint = fmt.Sprintf("node /github/home/%s/%s", strings.TrimPrefix(actionDir, rc.Tempdir), action.Runs.Main)
|
|
||||||
}
|
|
||||||
case model.ActionRunsUsingDocker:
|
|
||||||
if strings.HasPrefix(actionDir, rc.Config.Workdir) {
|
|
||||||
containerSpec.Name = rc.createStepContainerName(strings.TrimPrefix(actionDir, rc.Config.Workdir))
|
|
||||||
} else if strings.HasPrefix(actionDir, rc.Tempdir) {
|
|
||||||
containerSpec.Name = rc.createStepContainerName(strings.TrimPrefix(actionDir, rc.Tempdir))
|
|
||||||
}
|
|
||||||
containerSpec.Reuse = rc.Config.ReuseContainers
|
|
||||||
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 {
|
|
||||||
containerSpec.Image = fmt.Sprintf("%s:%s", containerSpec.Name, "latest")
|
|
||||||
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
|
|
||||||
}
|
|
|
@ -6,8 +6,11 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- run: echo 'hello world'
|
- run: echo 'hello world'
|
||||||
|
- run: echo ${GITHUB_SHA} >> /github/sha.txt
|
||||||
|
- run: cat /github/sha.txt | grep ${GITHUB_SHA}
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
if: false
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [check]
|
needs: [check]
|
||||||
steps:
|
steps:
|
||||||
|
@ -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}}
|
||||||
|
|
|
@ -6,7 +6,6 @@ jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- run: which node
|
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
|
|
Loading…
Reference in New Issue