refactor: remove composite action runcontext workaround (#1085)

* refactor: remove composite action runcontext workaround

The RunContext is cloned to execute a composite action with all its
steps in a similar context. This required some workaround, since
the command handler has kept a reference to the original RunContext.

This is solved now, by replacing the docker LogWriter with a proper
scoped LogWriter.

This prepares for a simpler setup of composite actions to be able
to create and re-create the composite RunContext for pre/main/post
action steps.

* test: check env-vars for local js and docker actions

* test: test remote docker and js actions

* fix: merge github context into env when read and setup

* refacotr: simplify composite context setup

* test: use a map matcher to test input setup

* fix: restore composite log output

Since we create a new line writer, we need to log the raw_output as well.
Otherwise no output will be available from the log-writer

* fix: add RunContext JobName to fill GITHUB_JOBNAME

* test: use nektos/act-test-actions

* fix: allow masking values in composite actions

To allow masking of values from composite actions, we need
to use a custom job logger with a reference to the masked
values for the composite run context.

* refactor: keep existing logger for composite actions

To not introduce another new logger while still be able to use
the masking from the composite action, we add the masks to
the go context. To leverage that context, we also add the context
to the log entries where the valueMasker then could get the actual
mask values.

With this way to 'inject' the masked values into the logger, we do
- keep the logger
- keep the coloring
- stay away from inconsistencies due to parallel jobs

* fix: re-add removed color increase

This one should have never removed :-)

* fix: add missing ExtraPath attribute

* fix: merge run context env into composite run context env

This adds a test and fix for the parent environment. It should be
inherited by the composite environment.

* test: add missing test case

* fix: store github token next to secrets

We must not expose the secrets to composite actions, but the
`github.token` is available inside composite actions.
To provide this we store the token in the config and create it in
the GithubContext from there.

The token can be used with `github.token` but is not available as
`secrets.GITHUB_TOKEN`.

This implements the same behavior as on GitHub.

Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se>
Co-authored-by: Marcus Noll <markus.noll@new-work.se>

* fixup! fix: allow masking values in composite actions

* style: use tabs instead of spaces to fix linter errors

Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se>
Co-authored-by: Marcus Noll <markus.noll@new-work.se>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
Markus Wolf 2022-05-11 21:06:05 +02:00 committed by GitHub
parent a76c349872
commit e360811570
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 396 additions and 167 deletions

View file

@ -366,6 +366,7 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
JSONLogger: input.jsonLogger, JSONLogger: input.jsonLogger,
Env: envs, Env: envs,
Secrets: secrets, Secrets: secrets,
Token: secrets["GITHUB_TOKEN"],
InsecureSecrets: input.insecureSecrets, InsecureSecrets: input.insecureSecrets,
Platforms: input.newPlatforms(), Platforms: input.newPlatforms(),
Privileged: input.privileged, Privileged: input.privileged,

View file

@ -77,6 +77,7 @@ type Container interface {
UpdateFromPath(env *map[string]string) common.Executor UpdateFromPath(env *map[string]string) common.Executor
Remove() common.Executor Remove() common.Executor
Close() common.Executor Close() common.Executor
ReplaceLogWriter(io.Writer, io.Writer) (io.Writer, io.Writer)
} }
// NewContainer creates a reference to a container // NewContainer creates a reference to a container
@ -195,6 +196,16 @@ func (cr *containerReference) Remove() common.Executor {
).IfNot(common.Dryrun) ).IfNot(common.Dryrun)
} }
func (cr *containerReference) ReplaceLogWriter(stdout io.Writer, stderr io.Writer) (io.Writer, io.Writer) {
out := cr.input.Stdout
err := cr.input.Stderr
cr.input.Stdout = stdout
cr.input.Stderr = stderr
return out, err
}
type containerReference struct { type containerReference struct {
cli *client.Client cli *client.Client
id string id string

View file

@ -102,30 +102,31 @@ func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction
rc := step.getRunContext() rc := step.getRunContext()
stepModel := step.getStepModel() stepModel := step.getStepModel()
return func(ctx context.Context) error { return func(ctx context.Context) error {
// Backup the parent composite action path and restore it on continue
parentActionPath := rc.ActionPath
parentActionRepository := rc.ActionRepository
parentActionRef := rc.ActionRef
defer func() {
rc.ActionPath = parentActionPath
rc.ActionRef = parentActionRef
rc.ActionRepository = parentActionRepository
}()
actionPath := "" actionPath := ""
if remoteAction != nil { if remoteAction != nil && remoteAction.Path != "" {
rc.ActionRef = remoteAction.Ref actionPath = remoteAction.Path
rc.ActionRepository = remoteAction.Repo
if remoteAction.Path != "" {
actionPath = remoteAction.Path
}
} else {
rc.ActionRef = ""
rc.ActionRepository = ""
} }
action := step.getActionModel() action := step.getActionModel()
log.Debugf("About to run action %v", action) log.Debugf("About to run action %v", action)
if remoteAction != nil {
rc.ActionRepository = fmt.Sprintf("%s/%s", remoteAction.Org, remoteAction.Repo)
rc.ActionRef = remoteAction.Ref
} else {
rc.ActionRepository = ""
rc.ActionRef = ""
}
defer (func() {
// cleanup after the action is done, to avoid side-effects in
// the next step/action
rc.ActionRepository = ""
rc.ActionRef = ""
})()
// we need to merge with github-env again, since at the step setup
// time, we don't have all environment prepared
mergeIntoMap(step.getEnv(), rc.withGithubEnv(map[string]string{}))
populateEnvsFromInput(step.getEnv(), action, rc) populateEnvsFromInput(step.getEnv(), action, rc)
actionLocation := path.Join(actionDir, actionPath) actionLocation := path.Join(actionDir, actionPath)
@ -134,7 +135,6 @@ func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction
log.Debugf("type=%v actionDir=%s actionPath=%s workdir=%s actionCacheDir=%s actionName=%s containerActionDir=%s", stepModel.Type(), actionDir, actionPath, rc.Config.Workdir, rc.ActionCacheDir(), actionName, containerActionDir) log.Debugf("type=%v actionDir=%s actionPath=%s workdir=%s actionCacheDir=%s actionName=%s containerActionDir=%s", stepModel.Type(), actionDir, actionPath, rc.Config.Workdir, rc.ActionCacheDir(), actionName, containerActionDir)
maybeCopyToActionDir := func() error { maybeCopyToActionDir := func() error {
rc.ActionPath = containerActionDir
if stepModel.Type() != model.StepTypeUsesActionRemote { if stepModel.Type() != model.StepTypeUsesActionRemote {
return nil return nil
} }
@ -170,7 +170,7 @@ func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction
if err := maybeCopyToActionDir(); err != nil { if err := maybeCopyToActionDir(); err != nil {
return err return err
} }
return execAsComposite(step)(ctx) return execAsComposite(step, containerActionDir)(ctx)
default: default:
return fmt.Errorf(fmt.Sprintf("The runs.using key must be one of: %v, got %s", []string{ return fmt.Errorf(fmt.Sprintf("The runs.using key must be one of: %v, got %s", []string{
model.ActionRunsUsingDocker, model.ActionRunsUsingDocker,
@ -359,7 +359,7 @@ func newStepContainer(ctx context.Context, step step, image string, cmd []string
return stepContainer return stepContainer
} }
func execAsComposite(step actionStep) common.Executor { func execAsComposite(step actionStep, containerActionDir string) common.Executor {
rc := step.getRunContext() rc := step.getRunContext()
action := step.getActionModel() action := step.getActionModel()
@ -370,9 +370,10 @@ func execAsComposite(step actionStep) common.Executor {
return err return err
} }
} }
eval := rc.NewExpressionEvaluator()
inputs := make(map[string]interface{}) inputs := make(map[string]interface{})
eval := step.getRunContext().NewExpressionEvaluator()
// Set Defaults
for k, input := range action.Inputs { for k, input := range action.Inputs {
inputs[k] = eval.Interpolate(input.Default) inputs[k] = eval.Interpolate(input.Default)
} }
@ -381,63 +382,128 @@ func execAsComposite(step actionStep) common.Executor {
inputs[k] = eval.Interpolate(v) inputs[k] = eval.Interpolate(v)
} }
} }
// Doesn't work with the command processor has a pointer to the original rc
// compositerc := rc.Clone()
// Workaround start
backup := *rc
defer func() { *rc = backup }()
*rc = *rc.Clone()
scriptName := backup.CurrentStep
for rcs := &backup; rcs.Parent != nil; rcs = rcs.Parent {
scriptName = fmt.Sprintf("%s-composite-%s", rcs.Parent.CurrentStep, scriptName)
}
compositerc := rc
compositerc.Parent = &RunContext{
CurrentStep: scriptName,
}
// Workaround end
compositerc.Composite = action
envToEvaluate := mergeMaps(compositerc.Env, step.getStepModel().Environment())
compositerc.Env = make(map[string]string)
// origEnvMap: is used to pass env changes back to parent runcontext
origEnvMap := make(map[string]string)
for k, v := range envToEvaluate {
ev := eval.Interpolate(v)
origEnvMap[k] = ev
compositerc.Env[k] = ev
}
compositerc.Inputs = inputs
compositerc.ExprEval = compositerc.NewExpressionEvaluator()
err := compositerc.compositeExecutor()(ctx) env := make(map[string]string)
for k, v := range rc.Env {
env[k] = eval.Interpolate(v)
}
for k, v := range step.getStepModel().Environment() {
env[k] = eval.Interpolate(v)
}
// Map outputs to parent rc // run with the global config but without secrets
eval = compositerc.NewStepExpressionEvaluator(step) configCopy := *rc.Config
configCopy.Secrets = nil
// create a run context for the composite action to run in
compositerc := &RunContext{
Name: rc.Name,
JobName: rc.JobName,
Run: &model.Run{
JobID: "composite-job",
Workflow: &model.Workflow{
Name: rc.Run.Workflow.Name,
Jobs: map[string]*model.Job{
"composite-job": {},
},
},
},
Config: &configCopy,
StepResults: map[string]*model.StepResult{},
JobContainer: rc.JobContainer,
Inputs: inputs,
ActionPath: containerActionDir,
ActionRepository: rc.ActionRepository,
ActionRef: rc.ActionRef,
Env: env,
Masks: rc.Masks,
ExtraPath: rc.ExtraPath,
}
ctx = WithCompositeLogger(ctx, &compositerc.Masks)
// We need to inject a composite RunContext related command
// handler into the current running job container
// We need this, to support scoping commands to the composite action
// executing.
rawLogger := common.Logger(ctx).WithField("raw_output", true)
logWriter := common.NewLineWriter(compositerc.commandHandler(ctx), func(s string) bool {
if rc.Config.LogOutput {
rawLogger.Infof("%s", s)
} else {
rawLogger.Debugf("%s", s)
}
return true
})
oldout, olderr := compositerc.JobContainer.ReplaceLogWriter(logWriter, logWriter)
defer (func() {
rc.JobContainer.ReplaceLogWriter(oldout, olderr)
})()
err := compositerc.compositeExecutor(action)(ctx)
// Map outputs from composite RunContext to job RunContext
eval = compositerc.NewExpressionEvaluator()
for outputName, output := range action.Outputs { for outputName, output := range action.Outputs {
backup.setOutput(ctx, map[string]string{ rc.setOutput(ctx, map[string]string{
"name": outputName, "name": outputName,
}, eval.Interpolate(output.Value)) }, eval.Interpolate(output.Value))
} }
backup.Masks = append(backup.Masks, compositerc.Masks...) rc.Masks = compositerc.Masks
// Test if evaluated parent env was altered by this composite step rc.ExtraPath = compositerc.ExtraPath
// Known Issues:
// - you try to set an env variable to the same value as a scoped step env, will be discared
for k, v := range compositerc.Env {
if ov, ok := origEnvMap[k]; !ok || ov != v {
backup.Env[k] = v
}
}
return err return err
} }
} }
// Executor returns a pipeline executor for all the steps in the job
func (rc *RunContext) compositeExecutor(action *model.Action) common.Executor {
steps := make([]common.Executor, 0)
sf := &stepFactoryImpl{}
for i, step := range action.Runs.Steps {
if step.ID == "" {
step.ID = fmt.Sprintf("%d", i)
}
// create a copy of the step, since this composite action could
// run multiple times and we might modify the instance
stepcopy := step
step, err := sf.newStep(&stepcopy, rc)
if err != nil {
return common.NewErrorExecutor(err)
}
stepExec := common.NewPipelineExecutor(step.pre(), step.main(), step.post())
steps = append(steps, func(ctx context.Context) error {
err := stepExec(ctx)
if err != nil {
common.Logger(ctx).Errorf("%v", err)
common.SetJobError(ctx, err)
} else if ctx.Err() != nil {
common.Logger(ctx).Errorf("%v", ctx.Err())
common.SetJobError(ctx, ctx.Err())
}
return nil
})
}
steps = append(steps, common.JobError)
return func(ctx context.Context) error {
return common.NewPipelineExecutor(steps...)(common.WithJobErrorContainer(ctx))
}
}
func populateEnvsFromInput(env *map[string]string, action *model.Action, rc *RunContext) { func populateEnvsFromInput(env *map[string]string, action *model.Action, rc *RunContext) {
eval := rc.NewExpressionEvaluator()
for inputID, input := range action.Inputs { for inputID, input := range action.Inputs {
envKey := regexp.MustCompile("[^A-Z0-9-]").ReplaceAllString(strings.ToUpper(inputID), "_") envKey := regexp.MustCompile("[^A-Z0-9-]").ReplaceAllString(strings.ToUpper(inputID), "_")
envKey = fmt.Sprintf("INPUT_%s", envKey) envKey = fmt.Sprintf("INPUT_%s", envKey)
if _, ok := (*env)[envKey]; !ok { if _, ok := (*env)[envKey]; !ok {
(*env)[envKey] = rc.ExprEval.Interpolate(input.Default) (*env)[envKey] = eval.Interpolate(input.Default)
} }
} }
} }

View file

@ -25,8 +25,8 @@ func TestActionReader(t *testing.T) {
yaml := strings.ReplaceAll(` yaml := strings.ReplaceAll(`
name: 'name' name: 'name'
runs: runs:
using: 'node16' using: 'node16'
main: 'main.js' main: 'main.js'
`, "\t", " ") `, "\t", " ")
table := []struct { table := []struct {
@ -136,16 +136,6 @@ runs:
} }
} }
type exprEvalMock struct {
ExpressionEvaluator
mock.Mock
}
func (e *exprEvalMock) Interpolate(expr string) string {
args := e.Called(expr)
return args.String(0)
}
func TestActionRunner(t *testing.T) { func TestActionRunner(t *testing.T) {
table := []struct { table := []struct {
name string name string
@ -158,10 +148,7 @@ func TestActionRunner(t *testing.T) {
Uses: "repo@ref", Uses: "repo@ref",
}, },
RunContext: &RunContext{ RunContext: &RunContext{
ActionRepository: "repo", Config: &Config{},
ActionPath: "path",
ActionRef: "ref",
Config: &Config{},
Run: &model.Run{ Run: &model.Run{
JobID: "job", JobID: "job",
Workflow: &model.Workflow{ Workflow: &model.Workflow{
@ -194,17 +181,17 @@ func TestActionRunner(t *testing.T) {
cm := &containerMock{} cm := &containerMock{}
cm.On("CopyDir", "/var/run/act/actions/dir/", "dir/", false).Return(func(ctx context.Context) error { return nil }) cm.On("CopyDir", "/var/run/act/actions/dir/", "dir/", false).Return(func(ctx context.Context) error { return nil })
cm.On("Exec", []string{"node", "/var/run/act/actions/dir/path"}, map[string]string{"INPUT_KEY": "default value"}, "", "").Return(func(ctx context.Context) error { return nil })
tt.step.getRunContext().JobContainer = cm
ee := &exprEvalMock{} envMatcher := mock.MatchedBy(func(env map[string]string) bool {
ee.On("Interpolate", "default value").Return("default value") return env["INPUT_KEY"] == "default value"
tt.step.getRunContext().ExprEval = ee })
cm.On("Exec", []string{"node", "/var/run/act/actions/dir/path"}, envMatcher, "", "").Return(func(ctx context.Context) error { return nil })
tt.step.getRunContext().JobContainer = cm
err := runActionImpl(tt.step, "dir", newRemoteAction("org/repo/path@ref"))(ctx) err := runActionImpl(tt.step, "dir", newRemoteAction("org/repo/path@ref"))(ctx)
assert.Nil(t, err) assert.Nil(t, err)
ee.AssertExpectations(t)
cm.AssertExpectations(t) cm.AssertExpectations(t)
}) })
} }

View file

@ -37,11 +37,6 @@ func (rc *RunContext) NewExpressionEvaluator() ExpressionEvaluator {
} }
} }
secrets := rc.Config.Secrets
if rc.Composite != nil {
secrets = nil
}
ee := &exprparser.EvaluationEnvironment{ ee := &exprparser.EvaluationEnvironment{
Github: rc.getGithubContext(), Github: rc.getGithubContext(),
Env: rc.GetEnv(), Env: rc.GetEnv(),
@ -54,7 +49,7 @@ func (rc *RunContext) NewExpressionEvaluator() ExpressionEvaluator {
"temp": "/tmp", "temp": "/tmp",
"tool_cache": "/opt/hostedtoolcache", "tool_cache": "/opt/hostedtoolcache",
}, },
Secrets: secrets, Secrets: rc.Config.Secrets,
Strategy: strategy, Strategy: strategy,
Matrix: rc.Matrix, Matrix: rc.Matrix,
Needs: using, Needs: using,
@ -89,11 +84,6 @@ func (rc *RunContext) NewStepExpressionEvaluator(step step) ExpressionEvaluator
} }
} }
secrets := rc.Config.Secrets
if rc.Composite != nil {
secrets = nil
}
ee := &exprparser.EvaluationEnvironment{ ee := &exprparser.EvaluationEnvironment{
Github: rc.getGithubContext(), Github: rc.getGithubContext(),
Env: *step.getEnv(), Env: *step.getEnv(),
@ -104,7 +94,7 @@ func (rc *RunContext) NewStepExpressionEvaluator(step step) ExpressionEvaluator
"temp": "/tmp", "temp": "/tmp",
"tool_cache": "/opt/hostedtoolcache", "tool_cache": "/opt/hostedtoolcache",
}, },
Secrets: secrets, Secrets: rc.Config.Secrets,
Strategy: strategy, Strategy: strategy,
Matrix: rc.Matrix, Matrix: rc.Matrix,
Needs: using, Needs: using,

View file

@ -37,6 +37,26 @@ func init() {
} }
} }
type masksContextKey string
const masksContextKeyVal = masksContextKey("logrus.FieldLogger")
// Logger returns the appropriate logger for current context
func Masks(ctx context.Context) *[]string {
val := ctx.Value(masksContextKeyVal)
if val != nil {
if masks, ok := val.(*[]string); ok {
return masks
}
}
return &[]string{}
}
// WithLogger adds a value to the context for the logger
func WithMasks(ctx context.Context, masks *[]string) context.Context {
return context.WithValue(ctx, masksContextKeyVal, masks)
}
// WithJobLogger attaches a new logger to context that is aware of steps // WithJobLogger attaches a new logger to context that is aware of steps
func WithJobLogger(ctx context.Context, jobName string, config *Config, masks *[]string) context.Context { func WithJobLogger(ctx context.Context, jobName string, config *Config, masks *[]string) context.Context {
mux.Lock() mux.Lock()
@ -46,26 +66,32 @@ func WithJobLogger(ctx context.Context, jobName string, config *Config, masks *[
if config.JSONLogger { if config.JSONLogger {
formatter = &jobLogJSONFormatter{ formatter = &jobLogJSONFormatter{
formatter: &logrus.JSONFormatter{}, formatter: &logrus.JSONFormatter{},
masker: valueMasker(config.InsecureSecrets, config.Secrets, masks), masker: valueMasker(config.InsecureSecrets, config.Secrets),
} }
} else { } else {
formatter = &jobLogFormatter{ formatter = &jobLogFormatter{
color: colors[nextColor%len(colors)], color: colors[nextColor%len(colors)],
masker: valueMasker(config.InsecureSecrets, config.Secrets, masks), masker: valueMasker(config.InsecureSecrets, config.Secrets),
} }
} }
nextColor++ nextColor++
ctx = WithMasks(ctx, masks)
logger := logrus.New() logger := logrus.New()
logger.SetFormatter(formatter) logger.SetFormatter(formatter)
logger.SetOutput(os.Stdout) logger.SetOutput(os.Stdout)
logger.SetLevel(logrus.GetLevel()) logger.SetLevel(logrus.GetLevel())
rtn := logger.WithFields(logrus.Fields{"job": jobName, "dryrun": common.Dryrun(ctx)}) rtn := logger.WithFields(logrus.Fields{"job": jobName, "dryrun": common.Dryrun(ctx)}).WithContext(ctx)
return common.WithLogger(ctx, rtn) return common.WithLogger(ctx, rtn)
} }
func WithCompositeLogger(ctx context.Context, masks *[]string) context.Context {
ctx = WithMasks(ctx, masks)
return common.WithLogger(ctx, common.Logger(ctx).WithFields(logrus.Fields{}).WithContext(ctx))
}
func withStepLogger(ctx context.Context, stepName string) context.Context { func withStepLogger(ctx context.Context, stepName string) context.Context {
rtn := common.Logger(ctx).WithFields(logrus.Fields{"step": stepName}) rtn := common.Logger(ctx).WithFields(logrus.Fields{"step": stepName})
return common.WithLogger(ctx, rtn) return common.WithLogger(ctx, rtn)
@ -73,12 +99,14 @@ func withStepLogger(ctx context.Context, stepName string) context.Context {
type entryProcessor func(entry *logrus.Entry) *logrus.Entry type entryProcessor func(entry *logrus.Entry) *logrus.Entry
func valueMasker(insecureSecrets bool, secrets map[string]string, masks *[]string) entryProcessor { func valueMasker(insecureSecrets bool, secrets map[string]string) entryProcessor {
return func(entry *logrus.Entry) *logrus.Entry { return func(entry *logrus.Entry) *logrus.Entry {
if insecureSecrets { if insecureSecrets {
return entry return entry
} }
masks := Masks(entry.Context)
for _, v := range secrets { for _, v := range secrets {
if v != "" { if v != "" {
entry.Message = strings.ReplaceAll(entry.Message, v, "***") entry.Message = strings.ReplaceAll(entry.Message, v, "***")

View file

@ -43,7 +43,6 @@ type RunContext struct {
ActionPath string ActionPath string
ActionRef string ActionRef string
ActionRepository string ActionRepository string
Composite *model.Action
Inputs map[string]interface{} Inputs map[string]interface{}
Parent *RunContext Parent *RunContext
Masks []string Masks []string
@ -53,16 +52,6 @@ func (rc *RunContext) AddMask(mask string) {
rc.Masks = append(rc.Masks, mask) rc.Masks = append(rc.Masks, mask)
} }
func (rc *RunContext) Clone() *RunContext {
clone := *rc
clone.CurrentStep = ""
clone.Composite = nil
clone.Inputs = nil
clone.StepResults = make(map[string]*model.StepResult)
clone.Parent = rc
return &clone
}
type MappableOutput struct { type MappableOutput struct {
StepID string StepID string
OutputName string OutputName string
@ -309,46 +298,6 @@ func (rc *RunContext) Executor() common.Executor {
} }
} }
// Executor returns a pipeline executor for all the steps in the job
func (rc *RunContext) compositeExecutor() common.Executor {
steps := make([]common.Executor, 0)
sf := &stepFactoryImpl{}
for i, step := range rc.Composite.Runs.Steps {
if step.ID == "" {
step.ID = fmt.Sprintf("%d", i)
}
// create a copy of the step, since this composite action could
// run multiple times and we might modify the instance
stepcopy := step
step, err := sf.newStep(&stepcopy, rc)
if err != nil {
return common.NewErrorExecutor(err)
}
stepExec := common.NewPipelineExecutor(step.pre(), step.main(), step.post())
steps = append(steps, func(ctx context.Context) error {
err := stepExec(ctx)
if err != nil {
common.Logger(ctx).Errorf("%v", err)
common.SetJobError(ctx, err)
} else if ctx.Err() != nil {
common.Logger(ctx).Errorf("%v", ctx.Err())
common.SetJobError(ctx, ctx.Err())
}
return nil
})
}
steps = append(steps, common.JobError)
return func(ctx context.Context) error {
return common.NewPipelineExecutor(steps...)(common.WithJobErrorContainer(ctx))
}
}
func (rc *RunContext) platformImage() string { func (rc *RunContext) platformImage() string {
job := rc.Run.Job() job := rc.Run.Job()
@ -492,7 +441,7 @@ func (rc *RunContext) getGithubContext() *model.GithubContext {
EventName: rc.Config.EventName, EventName: rc.Config.EventName,
Workspace: rc.Config.ContainerWorkdir(), Workspace: rc.Config.ContainerWorkdir(),
Action: rc.CurrentStep, Action: rc.CurrentStep,
Token: rc.Config.Secrets["GITHUB_TOKEN"], Token: rc.Config.Token,
ActionPath: rc.ActionPath, ActionPath: rc.ActionPath,
ActionRef: rc.ActionRef, ActionRef: rc.ActionRef,
ActionRepository: rc.ActionRepository, ActionRepository: rc.ActionRepository,
@ -639,8 +588,6 @@ func (rc *RunContext) withGithubEnv(env map[string]string) map[string]string {
env["GITHUB_SERVER_URL"] = "https://github.com" env["GITHUB_SERVER_URL"] = "https://github.com"
env["GITHUB_API_URL"] = "https://api.github.com" env["GITHUB_API_URL"] = "https://api.github.com"
env["GITHUB_GRAPHQL_URL"] = "https://api.github.com/graphql" env["GITHUB_GRAPHQL_URL"] = "https://api.github.com/graphql"
env["GITHUB_ACTION_REF"] = github.ActionRef
env["GITHUB_ACTION_REPOSITORY"] = github.ActionRepository
env["GITHUB_BASE_REF"] = github.BaseRef env["GITHUB_BASE_REF"] = github.BaseRef
env["GITHUB_HEAD_REF"] = github.HeadRef env["GITHUB_HEAD_REF"] = github.HeadRef
env["GITHUB_JOB"] = rc.JobName env["GITHUB_JOB"] = rc.JobName

View file

@ -310,7 +310,17 @@ func TestRunContext_GetBindsAndMounts(t *testing.T) {
}) })
assert.NoError(t, err) assert.NoError(t, err)
rc := rctemplate.Clone() rc := &RunContext{
Name: "TestRCName",
Run: &model.Run{
Workflow: &model.Workflow{
Name: "TestWorkflowName",
},
},
Config: &Config{
BindWorkdir: false,
},
}
rc.Run.JobID = "job1" rc.Run.JobID = "job1"
rc.Run.Workflow.Jobs = map[string]*model.Job{"job1": job} rc.Run.Workflow.Jobs = map[string]*model.Job{"job1": job}

View file

@ -36,6 +36,7 @@ type Config struct {
JSONLogger bool // use json or text logger JSONLogger bool // use json or text logger
Env map[string]string // env for containers Env map[string]string // env for containers
Secrets map[string]string // list of secrets Secrets map[string]string // list of secrets
Token string // GitHub token
InsecureSecrets bool // switch hiding output when printing to terminal InsecureSecrets bool // switch hiding output when printing to terminal
Platforms map[string]string // list of platforms Platforms map[string]string // list of platforms
Privileged bool // use privileged mode Privileged bool // use privileged mode

View file

@ -138,6 +138,7 @@ func TestRunEvent(t *testing.T) {
{workdir, "uses-nested-composite", "push", "", platforms}, {workdir, "uses-nested-composite", "push", "", platforms},
{workdir, "uses-workflow", "push", "reusable workflows are currently not supported (see https://github.com/nektos/act/issues/826 for updates)", platforms}, {workdir, "uses-workflow", "push", "reusable workflows are currently not supported (see https://github.com/nektos/act/issues/826 for updates)", platforms},
{workdir, "uses-docker-url", "push", "", platforms}, {workdir, "uses-docker-url", "push", "", platforms},
{workdir, "act-composite-env-test", "push", "", platforms},
// Eval // Eval
{workdir, "evalmatrix", "push", "", platforms}, {workdir, "evalmatrix", "push", "", platforms},
@ -171,6 +172,7 @@ func TestRunEvent(t *testing.T) {
{workdir, "steps-context/outcome", "push", "", platforms}, {workdir, "steps-context/outcome", "push", "", platforms},
{workdir, "job-status-check", "push", "job 'fail' failed", platforms}, {workdir, "job-status-check", "push", "job 'fail' failed", platforms},
{workdir, "if-expressions", "push", "Job 'mytest' failed", platforms}, {workdir, "if-expressions", "push", "Job 'mytest' failed", platforms},
{workdir, "actions-environment-and-context-tests", "push", "", platforms},
{"../model/testdata", "strategy", "push", "", platforms}, // TODO: move all testdata into pkg so we can validate it with planner and runner {"../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 // {"testdata", "issue-228", "push", "", platforms, }, // TODO [igni]: Remove this once everything passes
{"../model/testdata", "container-volumes", "push", "", platforms}, {"../model/testdata", "container-volumes", "push", "", platforms},
@ -199,6 +201,37 @@ func TestRunDifferentArchitecture(t *testing.T) {
tjfi.runTest(context.Background(), t, &Config{ContainerArchitecture: "linux/arm64"}) tjfi.runTest(context.Background(), t, &Config{ContainerArchitecture: "linux/arm64"})
} }
func TestMaskValues(t *testing.T) {
assertNoSecret := func(text string, secret string) {
index := strings.Index(text, "composite secret")
if index > -1 {
fmt.Printf("\nFound Secret in the given text:\n%s\n", text)
}
assert.False(t, strings.Contains(text, "composite secret"))
}
if testing.Short() {
t.Skip("skipping integration test")
}
log.SetLevel(log.DebugLevel)
tjfi := TestJobFileInfo{
workdir: workdir,
workflowPath: "mask-values",
eventName: "push",
errorMessage: "",
platforms: platforms,
}
output := captureOutput(t, func() {
tjfi.runTest(context.Background(), t, &Config{})
})
assertNoSecret(output, "secret value")
assertNoSecret(output, "composite secret")
}
func TestRunEventSecrets(t *testing.T) { func TestRunEventSecrets(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skipping integration test") t.Skip("skipping integration test")

View file

@ -51,14 +51,18 @@ func runStepExecutor(step step, executor common.Executor) common.Executor {
return nil return nil
} }
common.Logger(ctx).Infof("\u2B50 Run %s", stepModel) stepString := stepModel.String()
if strings.Contains(stepString, "::add-mask::") {
stepString = "add-mask command"
}
common.Logger(ctx).Infof("\u2B50 Run %s", stepString)
err = executor(ctx) err = executor(ctx)
if err == nil { if err == nil {
common.Logger(ctx).Infof(" \u2705 Success - %s", stepModel) common.Logger(ctx).Infof(" \u2705 Success - %s", stepString)
} else { } else {
common.Logger(ctx).Errorf(" \u274C Failure - %s", stepModel) common.Logger(ctx).Errorf(" \u274C Failure - %s", stepString)
rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusFailure rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusFailure
if stepModel.ContinueOnError { if stepModel.ContinueOnError {

View file

@ -9,7 +9,6 @@ import (
"github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/container" "github.com/nektos/act/pkg/container"
"github.com/nektos/act/pkg/model" "github.com/nektos/act/pkg/model"
log "github.com/sirupsen/logrus"
) )
type stepRun struct { type stepRun struct {
@ -56,7 +55,7 @@ func (sr *stepRun) getEnv() *map[string]string {
func (sr *stepRun) setupShellCommandExecutor() common.Executor { func (sr *stepRun) setupShellCommandExecutor() common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
scriptName, script, err := sr.setupShellCommand() scriptName, script, err := sr.setupShellCommand(ctx)
if err != nil { if err != nil {
return err return err
} }
@ -82,7 +81,7 @@ func getScriptName(rc *RunContext, step *model.Step) string {
// so we return proper errors before any execution or spawning containers // so we return proper errors before any execution or spawning containers
// it will error anyway with: // it will error anyway with:
// OCI runtime exec failed: exec failed: container_linux.go:380: starting container process caused: exec: "${{": executable file not found in $PATH: unknown // OCI runtime exec failed: exec failed: container_linux.go:380: starting container process caused: exec: "${{": executable file not found in $PATH: unknown
func (sr *stepRun) setupShellCommand() (name, script string, err error) { func (sr *stepRun) setupShellCommand(ctx context.Context) (name, script string, err error) {
sr.setupShell() sr.setupShell()
sr.setupWorkingDirectory() sr.setupWorkingDirectory()
@ -114,7 +113,11 @@ func (sr *stepRun) setupShellCommand() (name, script string, err error) {
script = fmt.Sprintf("%s\n%s\n%s", runPrepend, script, runAppend) script = fmt.Sprintf("%s\n%s\n%s", runPrepend, script, runAppend)
log.Debugf("Wrote command \n%s\n to '%s'", script, name) if !strings.Contains(script, "::add-mask::") && !sr.RunContext.Config.InsecureSecrets {
common.Logger(ctx).Debugf("Wrote command \n%s\n to '%s'", script, name)
} else {
common.Logger(ctx).Debugf("Wrote add-mask command to '%s'", name)
}
scriptPath := fmt.Sprintf("%s/%s", ActPath, name) scriptPath := fmt.Sprintf("%s/%s", ActPath, name)
sr.cmd, err = shellquote.Split(strings.Replace(scCmd, `{0}`, scriptPath, 1)) sr.cmd, err = shellquote.Split(strings.Replace(scCmd, `{0}`, scriptPath, 1))
@ -130,7 +133,7 @@ func (sr *stepRun) setupShell() {
step.Shell = rc.Run.Job().Defaults.Run.Shell step.Shell = rc.Run.Job().Defaults.Run.Shell
} }
step.Shell = rc.ExprEval.Interpolate(step.Shell) step.Shell = rc.NewExpressionEvaluator().Interpolate(step.Shell)
if step.Shell == "" { if step.Shell == "" {
step.Shell = rc.Run.Workflow.Defaults.Run.Shell step.Shell = rc.Run.Workflow.Defaults.Run.Shell
@ -157,7 +160,7 @@ func (sr *stepRun) setupWorkingDirectory() {
} }
// jobs can receive context values, so we interpolate // jobs can receive context values, so we interpolate
step.WorkingDirectory = rc.ExprEval.Interpolate(step.WorkingDirectory) step.WorkingDirectory = rc.NewExpressionEvaluator().Interpolate(step.WorkingDirectory)
// but top level keys in workflow file like `defaults` or `env` can't // but top level keys in workflow file like `defaults` or `env` can't
if step.WorkingDirectory == "" { if step.WorkingDirectory == "" {

View file

@ -0,0 +1,21 @@
name: action1
description: action1
runs:
using: composite
steps:
- name: env.COMPOSITE_OVERRIDE != '1'
run: exit 1
if: env.COMPOSITE_OVERRIDE != '1'
shell: bash
- name: env.JOB != '1'
run: exit 1
if: env.JOB != '1'
shell: bash
- name: env.GLOBAL != '1'
run: exit 1
if: env.GLOBAL != '1'
shell: bash
- uses: ./act-composite-env-test/action2
env:
COMPOSITE_OVERRIDE: "2"
COMPOSITE: "1"

View file

@ -0,0 +1,21 @@
name: action2
description: actions2
runs:
using: composite
steps:
- name: env.COMPOSITE_OVERRIDE != '2'
run: exit 1
if: env.COMPOSITE_OVERRIDE != '2'
shell: bash
- name: env.COMPOSITE != '1'
run: exit 1
if: env.COMPOSITE != '1'
shell: bash
- name: env.JOB != '1'
run: exit 1
if: env.JOB != '1'
shell: bash
- name: env.GLOBAL != '1'
run: exit 1
if: env.GLOBAL != '1'
shell: bash

View file

@ -0,0 +1,13 @@
on: push
env:
GLOBAL: "1"
jobs:
test:
runs-on: ubuntu-latest
env:
JOB: "1"
steps:
- uses: actions/checkout@v2
- uses: ./act-composite-env-test/action1
env:
COMPOSITE_OVERRIDE: "1"

View file

@ -0,0 +1,5 @@
FROM alpine:3
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT [ "/entrypoint.sh" ]

View file

@ -0,0 +1,5 @@
name: 'Test'
description: 'Test'
runs:
using: 'docker'
image: 'Dockerfile'

View file

@ -0,0 +1,26 @@
#!/bin/sh
checkEnvVar() {
name=$1
value=$2
allowEmpty=$3
if [ -z "$value" ]; then
echo "Missing environment variable: $name"
exit 1
fi
if [ "$allowEmpty" != "true" ]; then
test=$(echo "$value" |cut -f 2 -d "=")
if [ -z "$test" ]; then
echo "Environment variable is empty: $name"
exit 1
fi
fi
echo "$value"
}
checkEnvVar "GITHUB_ACTION" "$(env |grep "GITHUB_ACTION=")" false
checkEnvVar "GITHUB_ACTION_REPOSITORY" "$(env |grep "GITHUB_ACTION_REPOSITORY=")" true
checkEnvVar "GITHUB_ACTION_REF" "$(env |grep "GITHUB_ACTION_REF=")" true

View file

@ -0,0 +1,5 @@
name: 'Test'
description: 'Test'
runs:
using: 'node12'
main: 'index.js'

View file

@ -0,0 +1,15 @@
function checkEnvVar({ name, allowEmpty }) {
if (
process.env[name] === undefined ||
(allowEmpty === false && process.env[name] === "")
) {
throw new Error(
`${name} is undefined` + (allowEmpty === false ? " or empty" : "")
);
}
console.log(`${name}=${process.env[name]}`);
}
checkEnvVar({ name: "GITHUB_ACTION", allowEmpty: false });
checkEnvVar({ name: "GITHUB_ACTION_REPOSITORY", allowEmpty: true /* allows to be empty for local actions */ });
checkEnvVar({ name: "GITHUB_ACTION_REF", allowEmpty: true /* allows to be empty for local actions */ });

View file

@ -0,0 +1,13 @@
name: actions-with-environment-and-context-tests
description: "Actions with environment (env vars) and context (expression) tests"
on: push
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: './actions-environment-and-context-tests/js'
- uses: './actions-environment-and-context-tests/docker'
- uses: 'nektos/act-test-actions/js@main'
- uses: 'nektos/act-test-actions/docker@main'

View file

@ -0,0 +1,12 @@
name: composite
description: composite
runs:
using: composite
steps:
- run: echo "secret value"
shell: bash
- run: echo "::add-mask::composite secret"
shell: bash
- run: echo "composite secret"
shell: bash

View file

@ -0,0 +1,12 @@
name: mask-values
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: echo "::add-mask::secret value"
- run: echo "secret value"
- uses: ./mask-values/composite
- run: echo "composite secret"