Merge tag 'nektos/v0.2.34'

This commit is contained in:
Jason Song
2022-12-05 17:08:17 +08:00
57 changed files with 1720 additions and 282 deletions

View File

@@ -352,7 +352,7 @@ func newStepContainer(ctx context.Context, step step, image string, cmd []string
stepContainer := container.NewContainer(&container.NewContainerInput{
Cmd: cmd,
Entrypoint: entrypoint,
WorkingDir: rc.Config.ContainerWorkdir(),
WorkingDir: rc.JobContainer.ToContainerPath(rc.Config.Workdir),
Image: image,
Username: rc.Config.Secrets["DOCKER_USERNAME"],
Password: rc.Config.Secrets["DOCKER_PASSWORD"],
@@ -397,11 +397,11 @@ func getContainerActionPaths(step *model.Step, actionDir string, rc *RunContext)
containerActionDir := "."
if step.Type() != model.StepTypeUsesActionRemote {
actionName = getOsSafeRelativePath(actionDir, rc.Config.Workdir)
containerActionDir = rc.Config.ContainerWorkdir() + "/" + actionName
containerActionDir = rc.JobContainer.ToContainerPath(rc.Config.Workdir) + "/" + actionName
actionName = "./" + actionName
} else if step.Type() == model.StepTypeUsesActionRemote {
actionName = getOsSafeRelativePath(actionDir, rc.ActionCacheDir())
containerActionDir = ActPath + "/actions/" + actionName
containerActionDir = rc.JobContainer.GetActPath() + "/actions/" + actionName
}
if actionName == "" {

View File

@@ -69,7 +69,9 @@ func newCompositeRunContext(ctx context.Context, parent *RunContext, step action
Masks: parent.Masks,
ExtraPath: parent.ExtraPath,
Parent: parent,
EventJSON: parent.EventJSON,
}
compositerc.ExprEval = compositerc.NewExpressionEvaluator(ctx)
return compositerc
}

View File

@@ -11,6 +11,7 @@ import (
type containerMock struct {
mock.Mock
container.Container
container.LinuxContainerEnvironmentExtensions
}
func (cm *containerMock) Create(capAdd []string, capDrop []string) common.Executor {

View File

@@ -7,7 +7,6 @@ import (
"strings"
"github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/container"
"github.com/nektos/act/pkg/exprparser"
"github.com/nektos/act/pkg/model"
"gopkg.in/yaml.v3"
@@ -23,20 +22,22 @@ type ExpressionEvaluator interface {
// NewExpressionEvaluator creates a new evaluator
func (rc *RunContext) NewExpressionEvaluator(ctx context.Context) ExpressionEvaluator {
// todo: cleanup EvaluationEnvironment creation
job := rc.Run.Job()
strategy := make(map[string]interface{})
if job.Strategy != nil {
strategy["fail-fast"] = job.Strategy.FailFast
strategy["max-parallel"] = job.Strategy.MaxParallel
}
jobs := rc.Run.Workflow.Jobs
jobNeeds := rc.Run.Job().Needs()
using := make(map[string]map[string]map[string]string)
for _, needs := range jobNeeds {
using[needs] = map[string]map[string]string{
"outputs": jobs[needs].Outputs,
strategy := make(map[string]interface{})
if rc.Run != nil {
job := rc.Run.Job()
if job != nil && job.Strategy != nil {
strategy["fail-fast"] = job.Strategy.FailFast
strategy["max-parallel"] = job.Strategy.MaxParallel
}
jobs := rc.Run.Workflow.Jobs
jobNeeds := rc.Run.Job().Needs()
for _, needs := range jobNeeds {
using[needs] = map[string]map[string]string{
"outputs": jobs[needs].Outputs,
}
}
}
@@ -49,19 +50,16 @@ func (rc *RunContext) NewExpressionEvaluator(ctx context.Context) ExpressionEval
Job: rc.getJobContext(),
// todo: should be unavailable
// but required to interpolate/evaluate the step outputs on the job
Steps: rc.getStepsContext(),
Runner: map[string]interface{}{
"os": "Linux",
"arch": container.RunnerArch(ctx),
"temp": "/tmp",
"tool_cache": "/opt/hostedtoolcache",
},
Steps: rc.getStepsContext(),
Secrets: rc.Config.Secrets,
Strategy: strategy,
Matrix: rc.Matrix,
Needs: using,
Inputs: inputs,
}
if rc.JobContainer != nil {
ee.Runner = rc.JobContainer.GetRunnerContext(ctx)
}
return expressionEvaluator{
interpreter: exprparser.NewInterpeter(ee, exprparser.Config{
Run: rc.Run,
@@ -95,16 +93,10 @@ func (rc *RunContext) NewStepExpressionEvaluator(ctx context.Context, step step)
inputs := getEvaluatorInputs(ctx, rc, step, ghc)
ee := &exprparser.EvaluationEnvironment{
Github: step.getGithubContext(ctx),
Env: *step.getEnv(),
Job: rc.getJobContext(),
Steps: rc.getStepsContext(),
Runner: map[string]interface{}{
"os": "Linux",
"arch": container.RunnerArch(ctx),
"temp": "/tmp",
"tool_cache": "/opt/hostedtoolcache",
},
Github: step.getGithubContext(ctx),
Env: *step.getEnv(),
Job: rc.getJobContext(),
Steps: rc.getStepsContext(),
Secrets: rc.Config.Secrets,
Strategy: strategy,
Matrix: rc.Matrix,
@@ -113,6 +105,9 @@ func (rc *RunContext) NewStepExpressionEvaluator(ctx context.Context, step step)
// but required to interpolate/evaluate the inputs in actions/composite
Inputs: inputs,
}
if rc.JobContainer != nil {
ee.Runner = rc.JobContainer.GetRunnerContext(ctx)
}
return expressionEvaluator{
interpreter: exprparser.NewInterpeter(ee, exprparser.Config{
Run: rc.Run,
@@ -331,15 +326,17 @@ func getEvaluatorInputs(ctx context.Context, rc *RunContext, step step, ghc *mod
if ghc.EventName == "workflow_dispatch" {
config := rc.Run.Workflow.WorkflowDispatchConfig()
for k, v := range config.Inputs {
value := nestedMapLookup(ghc.Event, "inputs", k)
if value == nil {
value = v.Default
}
if v.Type == "boolean" {
inputs[k] = value == "true"
} else {
inputs[k] = value
if config != nil && config.Inputs != nil {
for k, v := range config.Inputs {
value := nestedMapLookup(ghc.Event, "inputs", k)
if value == nil {
value = v.Default
}
if v.Type == "boolean" {
inputs[k] = value == "true"
} else {
inputs[k] = value
}
}
}
}

View File

@@ -117,7 +117,6 @@ func TestEvaluateRunContext(t *testing.T) {
{"github.run_id", "1", ""},
{"github.run_number", "1", ""},
{"job.status", "success", ""},
{"runner.os", "Linux", ""},
{"matrix.os", "Linux", ""},
{"matrix.foo", "bar", ""},
{"env.key", "value", ""},

View File

@@ -38,6 +38,20 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
return common.NewDebugExecutor("No steps found")
}
preSteps = append(preSteps, func(ctx context.Context) error {
// Have to be skipped for some Tests
if rc.Run == nil {
return nil
}
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
// evaluate environment variables since they can contain
// GitHub's special environment variables.
for k, v := range rc.GetEnv() {
rc.Env[k] = rc.ExprEval.Interpolate(ctx, v)
}
return nil
})
for i, stepModel := range infoSteps {
stepModel := stepModel
if stepModel == nil {
@@ -120,7 +134,7 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
func useStepLogger(rc *RunContext, stepModel *model.Step, stage stepStage, executor common.Executor) common.Executor {
return func(ctx context.Context) error {
ctx = withStepLogger(ctx, stepModel.Number, stepModel.ID, stepModel.String(), stage.String())
ctx = withStepLogger(ctx, stepModel.Number, stepModel.ID, rc.ExprEval.Interpolate(ctx, stepModel.String()), stage.String())
rawLogger := common.Logger(ctx).WithField("raw_output", true)
logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {

View File

@@ -79,6 +79,7 @@ func (jim *jobInfoMock) result(result string) {
type jobContainerMock struct {
container.Container
container.LinuxContainerEnvironmentExtensions
}
func (jcm *jobContainerMock) ReplaceLogWriter(stdout, stderr io.Writer) (io.Writer, io.Writer) {
@@ -248,7 +249,17 @@ func TestNewJobExecutor(t *testing.T) {
sfm := &stepFactoryMock{}
rc := &RunContext{
JobContainer: &jobContainerMock{},
Run: &model.Run{
JobID: "test",
Workflow: &model.Workflow{
Jobs: map[string]*model.Job{
"test": {},
},
},
},
Config: &Config{},
}
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
executorOrder := make([]string, 0)
jim.On("steps").Return(tt.steps)

View File

@@ -2,7 +2,10 @@ package runner
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
@@ -22,26 +25,25 @@ import (
"github.com/nektos/act/pkg/model"
)
const ActPath string = "/var/run/act"
// RunContext contains info about current job
type RunContext struct {
Name string
Config *Config
Matrix map[string]interface{}
Run *model.Run
EventJSON string
Env map[string]string
ExtraPath []string
CurrentStep string
StepResults map[string]*model.StepResult
ExprEval ExpressionEvaluator
JobContainer container.Container
OutputMappings map[MappableOutput]MappableOutput
JobName string
ActionPath string
Parent *RunContext
Masks []string
Name string
Config *Config
Matrix map[string]interface{}
Run *model.Run
EventJSON string
Env map[string]string
ExtraPath []string
CurrentStep string
StepResults map[string]*model.StepResult
ExprEval ExpressionEvaluator
JobContainer container.ExecutionsEnvironment
OutputMappings map[MappableOutput]MappableOutput
JobName string
ActionPath string
Parent *RunContext
Masks []string
cleanUpJobContainer common.Executor
}
func (rc *RunContext) AddMask(mask string) {
@@ -60,7 +62,13 @@ func (rc *RunContext) String() string {
// GetEnv returns the env for the context
func (rc *RunContext) GetEnv() map[string]string {
if rc.Env == nil {
rc.Env = mergeMaps(rc.Run.Workflow.Env, rc.Run.Job().Environment(), rc.Config.Env)
rc.Env = map[string]string{}
if rc.Run != nil && rc.Run.Workflow != nil && rc.Config != nil {
job := rc.Run.Job()
if job != nil {
rc.Env = mergeMaps(rc.Run.Workflow.Env, job.Environment(), rc.Config.Env)
}
}
}
rc.Env["ACT"] = "true"
return rc.Env
@@ -82,9 +90,11 @@ func (rc *RunContext) GetBindsAndMounts() ([]string, map[string]string) {
fmt.Sprintf("%s:%s", rc.Config.ContainerDaemonSocket, "/var/run/docker.sock"),
}
ext := container.LinuxContainerEnvironmentExtensions{}
mounts := map[string]string{
"act-toolcache": "/toolcache",
name + "-env": ActPath,
name + "-env": ext.GetActPath(),
}
if job := rc.Run.Job(); job != nil {
@@ -110,14 +120,84 @@ func (rc *RunContext) GetBindsAndMounts() ([]string, map[string]string) {
if selinux.GetEnabled() {
bindModifiers = ":z"
}
binds = append(binds, fmt.Sprintf("%s:%s%s", rc.Config.Workdir, rc.Config.ContainerWorkdir(), bindModifiers))
binds = append(binds, fmt.Sprintf("%s:%s%s", rc.Config.Workdir, ext.ToContainerPath(rc.Config.Workdir), bindModifiers))
} else {
mounts[name] = rc.Config.ContainerWorkdir()
mounts[name] = ext.ToContainerPath(rc.Config.Workdir)
}
return binds, mounts
}
func (rc *RunContext) startHostEnvironment() common.Executor {
return func(ctx context.Context) error {
logger := common.Logger(ctx)
rawLogger := logger.WithField("raw_output", true)
logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
if rc.Config.LogOutput {
rawLogger.Infof("%s", s)
} else {
rawLogger.Debugf("%s", s)
}
return true
})
cacheDir := rc.ActionCacheDir()
randBytes := make([]byte, 8)
_, _ = rand.Read(randBytes)
miscpath := filepath.Join(cacheDir, hex.EncodeToString(randBytes))
actPath := filepath.Join(miscpath, "act")
if err := os.MkdirAll(actPath, 0777); err != nil {
return err
}
path := filepath.Join(miscpath, "hostexecutor")
if err := os.MkdirAll(path, 0777); err != nil {
return err
}
runnerTmp := filepath.Join(miscpath, "tmp")
if err := os.MkdirAll(runnerTmp, 0777); err != nil {
return err
}
toolCache := filepath.Join(cacheDir, "tool_cache")
rc.JobContainer = &container.HostEnvironment{
Path: path,
TmpDir: runnerTmp,
ToolCache: toolCache,
Workdir: rc.Config.Workdir,
ActPath: actPath,
CleanUp: func() {
os.RemoveAll(miscpath)
},
StdOut: logWriter,
}
rc.cleanUpJobContainer = rc.JobContainer.Remove()
rc.Env["RUNNER_TOOL_CACHE"] = toolCache
rc.Env["RUNNER_OS"] = runtime.GOOS
rc.Env["RUNNER_ARCH"] = runtime.GOARCH
rc.Env["RUNNER_TEMP"] = runnerTmp
for _, env := range os.Environ() {
i := strings.Index(env, "=")
if i > 0 {
rc.Env[env[0:i]] = env[i+1:]
}
}
return common.NewPipelineExecutor(
rc.JobContainer.Copy(rc.JobContainer.GetActPath()+"/", &container.FileEntry{
Name: "workflow/event.json",
Mode: 0644,
Body: rc.EventJSON,
}, &container.FileEntry{
Name: "workflow/envs.txt",
Mode: 0666,
Body: "",
}, &container.FileEntry{
Name: "workflow/paths.txt",
Mode: 0666,
Body: "",
}),
)(ctx)
}
}
func (rc *RunContext) startJobContainer() common.Executor {
return func(ctx context.Context) error {
logger := common.Logger(ctx)
@@ -147,12 +227,22 @@ func (rc *RunContext) startJobContainer() common.Executor {
envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_ARCH", container.RunnerArch(ctx)))
envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TEMP", "/tmp"))
ext := container.LinuxContainerEnvironmentExtensions{}
binds, mounts := rc.GetBindsAndMounts()
rc.cleanUpJobContainer = func(ctx context.Context) error {
if rc.JobContainer != nil && !rc.Config.ReuseContainers {
return rc.JobContainer.Remove().
Then(container.NewDockerVolumeRemoveExecutor(rc.jobContainerName(), false)).
Then(container.NewDockerVolumeRemoveExecutor(rc.jobContainerName()+"-env", false))(ctx)
}
return nil
}
rc.JobContainer = container.NewContainer(&container.NewContainerInput{
Cmd: nil,
Entrypoint: []string{"/bin/sleep", fmt.Sprint(rc.Config.ContainerMaxLifetime.Round(time.Second).Seconds())},
WorkingDir: rc.Config.ContainerWorkdir(),
WorkingDir: ext.ToContainerPath(rc.Config.Workdir),
Image: image,
Username: username,
Password: password,
@@ -169,6 +259,9 @@ func (rc *RunContext) startJobContainer() common.Executor {
Options: rc.options(ctx),
AutoRemove: rc.Config.AutoRemove,
})
if rc.JobContainer == nil {
return errors.New("Failed to create job container")
}
return common.NewPipelineExecutor(
rc.JobContainer.Pull(rc.Config.ForcePull),
@@ -177,17 +270,17 @@ func (rc *RunContext) startJobContainer() common.Executor {
rc.JobContainer.Start(false),
rc.JobContainer.UpdateFromImageEnv(&rc.Env),
rc.JobContainer.UpdateFromEnv("/etc/environment", &rc.Env),
rc.JobContainer.Copy(ActPath+"/", &container.FileEntry{
rc.JobContainer.Copy(rc.JobContainer.GetActPath()+"/", &container.FileEntry{
Name: "workflow/event.json",
Mode: 0o644,
Mode: 0644,
Body: rc.EventJSON,
}, &container.FileEntry{
Name: "workflow/envs.txt",
Mode: 0o666,
Mode: 0666,
Body: "",
}, &container.FileEntry{
Name: "workflow/paths.txt",
Mode: 0o666,
Mode: 0666,
Body: "",
}),
)(ctx)
@@ -203,10 +296,8 @@ func (rc *RunContext) execJobContainer(cmd []string, env map[string]string, user
// stopJobContainer removes the job container (if it exists) and its volume (if it exists) if !rc.Config.ReuseContainers
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)).
Then(container.NewDockerVolumeRemoveExecutor(rc.jobContainerName()+"-env", false))(ctx)
if rc.cleanUpJobContainer != nil && !rc.Config.ReuseContainers {
return rc.cleanUpJobContainer(ctx)
}
return nil
}
@@ -243,7 +334,13 @@ func (rc *RunContext) interpolateOutputs() common.Executor {
}
func (rc *RunContext) startContainer() common.Executor {
return rc.startJobContainer()
return func(ctx context.Context) error {
image := rc.platformImage(ctx)
if strings.EqualFold(image, "-self-hosted") {
return rc.startHostEnvironment()(ctx)
}
return rc.startJobContainer()(ctx)
}
}
func (rc *RunContext) stopContainer() common.Executor {
@@ -438,13 +535,11 @@ func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext
logger := common.Logger(ctx)
ghc := &model.GithubContext{
Event: make(map[string]interface{}),
EventPath: ActPath + "/workflow/event.json",
Workflow: rc.Run.Workflow.Name,
RunID: rc.Config.Env["GITHUB_RUN_ID"],
RunNumber: rc.Config.Env["GITHUB_RUN_NUMBER"],
Actor: rc.Config.Actor,
EventName: rc.Config.EventName,
Workspace: rc.Config.ContainerWorkdir(),
Action: rc.CurrentStep,
Token: rc.Config.Token,
ActionPath: rc.ActionPath,
@@ -453,6 +548,10 @@ func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext
RunnerPerflog: rc.Config.Env["RUNNER_PERFLOG"],
RunnerTrackingID: rc.Config.Env["RUNNER_TRACKING_ID"],
}
if rc.JobContainer != nil {
ghc.EventPath = rc.JobContainer.GetActPath() + "/workflow/event.json"
ghc.Workspace = rc.JobContainer.ToContainerPath(rc.Config.Workdir)
}
if ghc.RunID == "" {
ghc.RunID = "1"
@@ -586,8 +685,8 @@ func nestedMapLookup(m map[string]interface{}, ks ...string) (rval interface{})
func (rc *RunContext) withGithubEnv(ctx context.Context, github *model.GithubContext, env map[string]string) map[string]string {
env["CI"] = "true"
env["GITHUB_ENV"] = ActPath + "/workflow/envs.txt"
env["GITHUB_PATH"] = ActPath + "/workflow/paths.txt"
env["GITHUB_ENV"] = rc.JobContainer.GetActPath() + "/workflow/envs.txt"
env["GITHUB_PATH"] = rc.JobContainer.GetActPath() + "/workflow/paths.txt"
env["GITHUB_WORKFLOW"] = github.Workflow
env["GITHUB_RUN_ID"] = github.RunID
env["GITHUB_RUN_NUMBER"] = github.RunNumber

View File

@@ -385,14 +385,12 @@ func TestGetGitHubContext(t *testing.T) {
}
assert.Equal(t, ghc.RunID, "1")
assert.Equal(t, ghc.Workspace, rc.Config.containerPath(cwd))
assert.Equal(t, ghc.RunNumber, "1")
assert.Equal(t, ghc.RetentionDays, "0")
assert.Equal(t, ghc.Actor, actor)
assert.Equal(t, ghc.Repository, repo)
assert.Equal(t, ghc.RepositoryOwner, owner)
assert.Equal(t, ghc.RunnerPerflog, "/dev/null")
assert.Equal(t, ghc.EventPath, ActPath+"/workflow/event.json")
assert.Equal(t, ghc.Token, rc.Config.Secrets["GITHUB_TOKEN"])
}

View File

@@ -4,10 +4,6 @@ import (
"context"
"fmt"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"time"
log "github.com/sirupsen/logrus"
@@ -65,46 +61,6 @@ type Config struct {
PlatformPicker func(labels []string) string // platform picker, it will take precedence over Platforms if isn't nil
}
// Resolves the equivalent host path inside the container
// This is required for windows and WSL 2 to translate things like C:\Users\Myproject to /mnt/users/Myproject
// For use in docker volumes and binds
func (config *Config) containerPath(path string) string {
if runtime.GOOS == "windows" && strings.Contains(path, "/") {
log.Error("You cannot specify linux style local paths (/mnt/etc) on Windows as it does not understand them.")
return ""
}
abspath, err := filepath.Abs(path)
if err != nil {
log.Error(err)
return ""
}
// Test if the path is a windows path
windowsPathRegex := regexp.MustCompile(`^([a-zA-Z]):\\(.+)$`)
windowsPathComponents := windowsPathRegex.FindStringSubmatch(abspath)
// Return as-is if no match
if windowsPathComponents == nil {
return abspath
}
// Convert to WSL2-compatible path if it is a windows path
// NOTE: Cannot use filepath because it will use the wrong path separators assuming we want the path to be windows
// based if running on Windows, and because we are feeding this to Docker, GoLang auto-path-translate doesn't work.
driveLetter := strings.ToLower(windowsPathComponents[1])
translatedPath := strings.ReplaceAll(windowsPathComponents[2], `\`, `/`)
// Should make something like /mnt/c/Users/person/My Folder/MyActProject
result := strings.Join([]string{"/mnt", driveLetter, translatedPath}, `/`)
return result
}
// Resolves the equivalent host path inside the container
// This is required for windows and WSL 2 to translate things like C:\Users\Myproject to /mnt/users/Myproject
func (config *Config) ContainerWorkdir() string {
return config.containerPath(config.Workdir)
}
type runnerImpl struct {
config *Config
eventJSON string
@@ -173,11 +129,6 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
if len(matrixes) > 1 {
rc.Name = fmt.Sprintf("%s-%d", rc.Name, i+1)
}
// evaluate environment variables since they can contain
// GitHub's special environment variables.
for k, v := range rc.GetEnv() {
rc.Env[k] = rc.ExprEval.Interpolate(ctx, v)
}
if len(rc.String()) > maxJobNameLen {
maxJobNameLen = len(rc.String())
}

View File

@@ -183,6 +183,9 @@ func TestRunEvent(t *testing.T) {
{workdir, "evalenv", "push", "", platforms},
{workdir, "ensure-post-steps", "push", "Job 'second-post-step-should-fail' failed", platforms},
{workdir, "workflow_dispatch", "workflow_dispatch", "", platforms},
{workdir, "workflow_dispatch_no_inputs_mapping", "workflow_dispatch", "", platforms},
{workdir, "workflow_dispatch-scalar", "workflow_dispatch", "", platforms},
{workdir, "workflow_dispatch-scalar-composite-action", "workflow_dispatch", "", platforms},
{"../model/testdata", "strategy", "push", "", platforms}, // TODO: move all testdata into pkg so we can validate it with planner and runner
// {"testdata", "issue-228", "push", "", platforms, }, // TODO [igni]: Remove this once everything passes
{"../model/testdata", "container-volumes", "push", "", platforms},
@@ -202,6 +205,95 @@ func TestRunEvent(t *testing.T) {
}
}
func TestRunEventHostEnvironment(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := context.Background()
tables := []TestJobFileInfo{}
if runtime.GOOS == "linux" {
platforms := map[string]string{
"ubuntu-latest": "-self-hosted",
}
tables = append(tables, []TestJobFileInfo{
// Shells
{workdir, "shells/defaults", "push", "", platforms},
{workdir, "shells/pwsh", "push", "", platforms},
{workdir, "shells/bash", "push", "", platforms},
{workdir, "shells/python", "push", "", platforms},
{workdir, "shells/sh", "push", "", platforms},
// Local action
{workdir, "local-action-js", "push", "", platforms},
// Uses
{workdir, "uses-composite", "push", "", platforms},
{workdir, "uses-composite-with-error", "push", "Job 'failing-composite-action' failed", platforms},
{workdir, "uses-nested-composite", "push", "", platforms},
{workdir, "act-composite-env-test", "push", "", platforms},
// Eval
{workdir, "evalmatrix", "push", "", platforms},
{workdir, "evalmatrixneeds", "push", "", platforms},
{workdir, "evalmatrixneeds2", "push", "", platforms},
{workdir, "evalmatrix-merge-map", "push", "", platforms},
{workdir, "evalmatrix-merge-array", "push", "", platforms},
{workdir, "issue-1195", "push", "", platforms},
{workdir, "fail", "push", "exit with `FAILURE`: 1", platforms},
{workdir, "runs-on", "push", "", platforms},
{workdir, "checkout", "push", "", platforms},
{workdir, "remote-action-js", "push", "", platforms},
{workdir, "matrix", "push", "", platforms},
{workdir, "matrix-include-exclude", "push", "", platforms},
{workdir, "commands", "push", "", platforms},
{workdir, "defaults-run", "push", "", platforms},
{workdir, "composite-fail-with-output", "push", "", platforms},
{workdir, "issue-597", "push", "", platforms},
{workdir, "issue-598", "push", "", platforms},
{workdir, "if-env-act", "push", "", platforms},
{workdir, "env-and-path", "push", "", platforms},
{workdir, "non-existent-action", "push", "Job 'nopanic' failed", platforms},
{workdir, "outputs", "push", "", platforms},
{workdir, "steps-context/conclusion", "push", "", platforms},
{workdir, "steps-context/outcome", "push", "", platforms},
{workdir, "job-status-check", "push", "job 'fail' failed", platforms},
{workdir, "if-expressions", "push", "Job 'mytest' failed", platforms},
{workdir, "uses-action-with-pre-and-post-step", "push", "", platforms},
{workdir, "evalenv", "push", "", platforms},
{workdir, "ensure-post-steps", "push", "Job 'second-post-step-should-fail' failed", platforms},
}...)
}
if runtime.GOOS == "windows" {
platforms := map[string]string{
"windows-latest": "-self-hosted",
}
tables = append(tables, []TestJobFileInfo{
{workdir, "windows-prepend-path", "push", "", platforms},
{workdir, "windows-add-env", "push", "", platforms},
}...)
} else {
platforms := map[string]string{
"self-hosted": "-self-hosted",
}
tables = append(tables, []TestJobFileInfo{
{workdir, "nix-prepend-path", "push", "", platforms},
}...)
}
for _, table := range tables {
t.Run(table.workflowPath, func(t *testing.T) {
table.runTest(ctx, t, &Config{})
})
}
}
func TestDryrunEvent(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
@@ -317,60 +409,3 @@ func TestRunEventPullRequest(t *testing.T) {
tjfi.runTest(context.Background(), t, &Config{EventPath: filepath.Join(workdir, workflowPath, "event.json")})
}
func TestContainerPath(t *testing.T) {
type containerPathJob struct {
destinationPath string
sourcePath string
workDir string
}
if runtime.GOOS == "windows" {
cwd, err := os.Getwd()
if err != nil {
log.Error(err)
}
rootDrive := os.Getenv("SystemDrive")
rootDriveLetter := strings.ReplaceAll(strings.ToLower(rootDrive), `:`, "")
for _, v := range []containerPathJob{
{"/mnt/c/Users/act/go/src/github.com/nektos/act", "C:\\Users\\act\\go\\src\\github.com\\nektos\\act\\", ""},
{"/mnt/f/work/dir", `F:\work\dir`, ""},
{"/mnt/c/windows/to/unix", "windows\\to\\unix", fmt.Sprintf("%s\\", rootDrive)},
{fmt.Sprintf("/mnt/%v/act", rootDriveLetter), "act", fmt.Sprintf("%s\\", rootDrive)},
} {
if v.workDir != "" {
if err := os.Chdir(v.workDir); err != nil {
log.Error(err)
t.Fail()
}
}
runnerConfig := &Config{
Workdir: v.sourcePath,
}
assert.Equal(t, v.destinationPath, runnerConfig.containerPath(runnerConfig.Workdir))
}
if err := os.Chdir(cwd); err != nil {
log.Error(err)
}
} else {
cwd, err := os.Getwd()
if err != nil {
log.Error(err)
}
for _, v := range []containerPathJob{
{"/home/act/go/src/github.com/nektos/act", "/home/act/go/src/github.com/nektos/act", ""},
{"/home/act", `/home/act/`, ""},
{cwd, ".", ""},
} {
runnerConfig := &Config{
Workdir: v.sourcePath,
}
assert.Equal(t, v.destinationPath, runnerConfig.containerPath(runnerConfig.Workdir))
}
}
}

View File

@@ -3,9 +3,11 @@ package runner
import (
"context"
"fmt"
"path"
"strings"
"github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/container"
"github.com/nektos/act/pkg/exprparser"
"github.com/nektos/act/pkg/model"
)
@@ -88,12 +90,26 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
return nil
}
stepString := stepModel.String()
stepString := rc.ExprEval.Interpolate(ctx, stepModel.String())
if strings.Contains(stepString, "::add-mask::") {
stepString = "add-mask command"
}
logger.Infof("\u2B50 Run %s %s", stage, stepString)
// Prepare and clean Runner File Commands
actPath := rc.JobContainer.GetActPath()
outputFileCommand := path.Join("workflow", "outputcmd.txt")
stateFileCommand := path.Join("workflow", "statecmd.txt")
(*step.getEnv())["GITHUB_OUTPUT"] = path.Join(actPath, outputFileCommand)
(*step.getEnv())["GITHUB_STATE"] = path.Join(actPath, stateFileCommand)
_ = rc.JobContainer.Copy(actPath, &container.FileEntry{
Name: outputFileCommand,
Mode: 0666,
}, &container.FileEntry{
Name: stateFileCommand,
Mode: 0666,
})(ctx)
err = executor(ctx)
if err == nil {
@@ -117,6 +133,27 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
logger.WithField("stepResult", rc.StepResults[rc.CurrentStep].Outcome).Errorf(" \u274C Failure - %s %s", stage, stepString)
}
// Process Runner File Commands
orgerr := err
state := map[string]string{}
err = rc.JobContainer.UpdateFromEnv(path.Join(actPath, stateFileCommand), &state)(ctx)
if err != nil {
return err
}
for k, v := range state {
rc.saveState(ctx, map[string]string{"name": k}, v)
}
output := map[string]string{}
err = rc.JobContainer.UpdateFromEnv(path.Join(actPath, outputFileCommand), &output)(ctx)
if err != nil {
return err
}
for k, v := range output {
rc.setOutput(ctx, map[string]string{"name": k}, v)
}
if orgerr != nil {
return orgerr
}
return err
}
}
@@ -162,13 +199,12 @@ func mergeEnv(ctx context.Context, step step) {
mergeIntoMap(env, rc.GetEnv())
}
if (*env)["PATH"] == "" {
(*env)["PATH"] = `/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`
path := rc.JobContainer.GetPathVariableName()
if (*env)[path] == "" {
(*env)[path] = rc.JobContainer.DefaultPathVariable()
}
if rc.ExtraPath != nil && len(rc.ExtraPath) > 0 {
p := (*env)["PATH"]
(*env)["PATH"] = strings.Join(rc.ExtraPath, `:`)
(*env)["PATH"] += `:` + p
(*env)[path] = rc.JobContainer.JoinPathVariable(append(rc.ExtraPath, (*env)[path])...)
}
rc.withGithubEnv(ctx, step.getGithubContext(ctx), *env)

View File

@@ -2,6 +2,7 @@ package runner
import (
"context"
"path/filepath"
"strings"
"testing"
@@ -63,7 +64,7 @@ func TestStepActionLocalTest(t *testing.T) {
},
}
salm.On("readAction", sal.Step, "/tmp/path/to/action", "", mock.Anything, mock.Anything).
salm.On("readAction", sal.Step, filepath.Clean("/tmp/path/to/action"), "", mock.Anything, mock.Anything).
Return(&model.Action{}, nil)
cm.On("UpdateFromImageEnv", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
@@ -78,7 +79,19 @@ func TestStepActionLocalTest(t *testing.T) {
return nil
})
salm.On("runAction", sal, "/tmp/path/to/action", (*remoteAction)(nil)).Return(func(ctx context.Context) error {
cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(ctx context.Context) error {
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil
})
salm.On("runAction", sal, filepath.Clean("/tmp/path/to/action"), (*remoteAction)(nil)).Return(func(ctx context.Context) error {
return nil
})
@@ -262,6 +275,7 @@ func TestStepActionLocalPost(t *testing.T) {
Step: tt.stepModel,
action: tt.actionModel,
}
sal.RunContext.ExprEval = sal.RunContext.NewExpressionEvaluator(ctx)
if tt.mocks.env {
cm.On("UpdateFromImageEnv", &sal.env).Return(func(ctx context.Context) error { return nil })
@@ -275,6 +289,18 @@ func TestStepActionLocalPost(t *testing.T) {
})
}
cm.On("Exec", suffixMatcher("pkg/runner/local/action/post.js"), sal.env, "", "").Return(func(ctx context.Context) error { return tt.err })
cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(ctx context.Context) error {
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil
})
}
err := sal.post()(ctx)

View File

@@ -115,7 +115,7 @@ func (sar *stepActionRemote) main() common.Executor {
return nil
}
eval := sar.RunContext.NewExpressionEvaluator(ctx)
copyToPath := path.Join(sar.RunContext.Config.ContainerWorkdir(), eval.Interpolate(ctx, sar.Step.With["path"]))
copyToPath := path.Join(sar.RunContext.JobContainer.ToContainerPath(sar.RunContext.Config.Workdir), eval.Interpolate(ctx, sar.Step.With["path"]))
return sar.RunContext.JobContainer.CopyDir(copyToPath, sar.RunContext.Config.Workdir+string(filepath.Separator)+".", sar.RunContext.Config.UseGitIgnore)(ctx)
}

View File

@@ -155,6 +155,7 @@ func TestStepActionRemote(t *testing.T) {
readAction: sarm.readAction,
runAction: sarm.runAction,
}
sar.RunContext.ExprEval = sar.RunContext.NewExpressionEvaluator(ctx)
suffixMatcher := func(suffix string) interface{} {
return mock.MatchedBy(func(actionDir string) bool {
@@ -172,6 +173,18 @@ func TestStepActionRemote(t *testing.T) {
}
if tt.mocks.run {
sarm.On("runAction", sar, suffixMatcher("act/remote-action@v1"), newRemoteAction(sar.Step.Uses)).Return(func(ctx context.Context) error { return tt.runError })
cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(ctx context.Context) error {
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil
})
}
err := sar.pre()(ctx)
@@ -574,6 +587,7 @@ func TestStepActionRemotePost(t *testing.T) {
Step: tt.stepModel,
action: tt.actionModel,
}
sar.RunContext.ExprEval = sar.RunContext.NewExpressionEvaluator(ctx)
if tt.mocks.env {
cm.On("UpdateFromImageEnv", &sar.env).Return(func(ctx context.Context) error { return nil })
@@ -582,6 +596,18 @@ func TestStepActionRemotePost(t *testing.T) {
}
if tt.mocks.exec {
cm.On("Exec", []string{"node", "/var/run/act/actions/remote-action@v1/post.js"}, sar.env, "", "").Return(func(ctx context.Context) error { return tt.err })
cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(ctx context.Context) error {
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil
})
}
err := sar.post()(ctx)

View File

@@ -116,7 +116,7 @@ func (sd *stepDocker) newStepContainer(ctx context.Context, image string, cmd []
stepContainer := ContainerNewContainer(&container.NewContainerInput{
Cmd: cmd,
Entrypoint: entrypoint,
WorkingDir: rc.Config.ContainerWorkdir(),
WorkingDir: rc.JobContainer.ToContainerPath(rc.Config.Workdir),
Image: image,
Username: rc.Config.Secrets["DOCKER_USERNAME"],
Password: rc.Config.Secrets["DOCKER_PASSWORD"],

View File

@@ -17,7 +17,7 @@ func TestStepDockerMain(t *testing.T) {
// mock the new container call
origContainerNewContainer := ContainerNewContainer
ContainerNewContainer = func(containerInput *container.NewContainerInput) container.Container {
ContainerNewContainer = func(containerInput *container.NewContainerInput) container.ExecutionsEnvironment {
input = containerInput
return cm
}
@@ -25,6 +25,8 @@ func TestStepDockerMain(t *testing.T) {
ContainerNewContainer = origContainerNewContainer
})()
ctx := context.Background()
sd := &stepDocker{
RunContext: &RunContext{
StepResults: map[string]*model.StepResult{},
@@ -51,8 +53,7 @@ func TestStepDockerMain(t *testing.T) {
WorkingDirectory: "workdir",
},
}
ctx := context.Background()
sd.RunContext.ExprEval = sd.RunContext.NewExpressionEvaluator(ctx)
cm.On("UpdateFromImageEnv", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil
@@ -86,6 +87,18 @@ func TestStepDockerMain(t *testing.T) {
return nil
})
cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(ctx context.Context) error {
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil
})
err := sd.main()(ctx)
assert.Nil(t, err)

View File

@@ -68,7 +68,8 @@ func (sr *stepRun) setupShellCommandExecutor() common.Executor {
return err
}
return sr.RunContext.JobContainer.Copy(ActPath, &container.FileEntry{
rc := sr.getRunContext()
return rc.JobContainer.Copy(rc.JobContainer.GetActPath(), &container.FileEntry{
Name: scriptName,
Mode: 0755,
Body: script,
@@ -128,7 +129,8 @@ func (sr *stepRun) setupShellCommand(ctx context.Context) (name, script string,
logger.Debugf("Wrote add-mask command to '%s'", name)
}
scriptPath := fmt.Sprintf("%s/%s", ActPath, name)
rc := sr.getRunContext()
scriptPath := fmt.Sprintf("%s/%s", rc.JobContainer.GetActPath(), name)
sr.cmd, err = shellquote.Split(strings.Replace(scCmd, `{0}`, scriptPath, 1))
return name, script, err

View File

@@ -65,6 +65,18 @@ func TestStepRun(t *testing.T) {
return nil
})
cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(ctx context.Context) error {
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
return nil
})
ctx := context.Background()
err := sr.main()(ctx)

View File

@@ -0,0 +1,26 @@
on:
push:
defaults:
run:
shell: sh
jobs:
test:
runs-on: self-hosted
steps:
- run: |
mkdir build
echo '#!/usr/bin/env sh' > build/testtool
echo 'echo Hi' >> build/testtool
chmod +x build/testtool
- run: |
echo '${{ tojson(runner) }}'
ls
echo '${{ github.workspace }}'
working-directory: ${{ github.workspace }}/build
- run: |
echo "$GITHUB_PATH"
echo '${{ github.workspace }}/build' > "$GITHUB_PATH"
cat "$GITHUB_PATH"
- run: |
echo "$PATH"
testtool

View File

@@ -5,6 +5,22 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo '${{github.ref}}'
# test refs from event.json
- run: echo '${{github.ref}}'
- run: echo '${{github.head_ref}}' | grep sample-head-ref
- run: echo '${{github.base_ref}}' | grep sample-base-ref
# test main/composite context equality with data from event.json
- run: |
runs:
using: composite
steps:
- run: |
echo WORKFLOW_GITHUB_CONTEXT="$WORKFLOW_GITHUB_CONTEXT"
echo COMPOSITE_GITHUB_CONTEXT="$COMPOSITE_GITHUB_CONTEXT"
[[ "$WORKFLOW_GITHUB_CONTEXT" = "$COMPOSITE_GITHUB_CONTEXT" ]]
env:
WORKFLOW_GITHUB_CONTEXT: ${{ tojson(tojson(github.event)) }}
COMPOSITE_GITHUB_CONTEXT: ${{ '${{tojson(github.event)}}' }}
shell: bash
shell: cp {0} action.yml
- uses: ./

View File

@@ -0,0 +1,27 @@
on:
push:
defaults:
run:
shell: pwsh
jobs:
test:
runs-on: windows-latest
steps:
- run: |
echo $env:GITHUB_ENV
echo "key=val" > $env:GITHUB_ENV
echo "key2<<EOF" >> $env:GITHUB_ENV
echo "line1" >> $env:GITHUB_ENV
echo "line2" >> $env:GITHUB_ENV
echo "EOF" >> $env:GITHUB_ENV
cat $env:GITHUB_ENV
- run: |
ls env:
if($env:key -ne 'val') {
echo "Unexpected value for `$env:key: $env:key"
exit 1
}
if($env:key2 -ne "line1`nline2") {
echo "Unexpected value for `$env:key2: $env:key2"
exit 1
}

View File

@@ -0,0 +1,25 @@
on:
push:
defaults:
run:
shell: pwsh
jobs:
test:
runs-on: windows-latest
steps:
- run: |
mkdir build
echo '@echo off' > build/test.cmd
echo 'echo Hi' >> build/test.cmd
- run: |
echo '${{ tojson(runner) }}'
ls
echo '${{ github.workspace }}'
working-directory: ${{ github.workspace }}\build
- run: |
echo $env:GITHUB_PATH
echo '${{ github.workspace }}\build' > $env:GITHUB_PATH
cat $env:GITHUB_PATH
- run: |
echo $env:PATH
test

View File

@@ -0,0 +1,17 @@
name: workflow_dispatch
on: workflow_dispatch
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: |
runs:
using: composite
steps:
- run: |
exit 0
shell: bash
shell: cp {0} action.yml
- uses: ./

View File

@@ -0,0 +1,9 @@
name: workflow_dispatch
on: workflow_dispatch
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: exit 0

View File

@@ -0,0 +1,10 @@
name: workflow_dispatch
on:
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: exit 0