Skip to content

Commit

Permalink
feat: add post step to actions and add state command
Browse files Browse the repository at this point in the history
This commit includes requried changes for running post steps
for local and remote actions.
This allows general cleanup work to be done after executing
an action.

Communication is allowed between this steps, by using the
action state.
  • Loading branch information
KnisterPeter authored and github-actions committed May 18, 2022
1 parent aab5f8f commit 7f669b5
Show file tree
Hide file tree
Showing 14 changed files with 804 additions and 87 deletions.
12 changes: 12 additions & 0 deletions pkg/model/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ type ActionRuns struct {
Using ActionRunsUsing `yaml:"using"`
Env map[string]string `yaml:"env"`
Main string `yaml:"main"`
Pre string `yaml:"pre"`
PreIf string `yaml:"pre-if"`
Post string `yaml:"post"`
PostIf string `yaml:"post-if"`
Image string `yaml:"image"`
Entrypoint string `yaml:"entrypoint"`
Args []string `yaml:"args"`
Expand Down Expand Up @@ -90,5 +94,13 @@ func ReadAction(in io.Reader) (*Action, error) {
return nil, err
}

// set defaults
if a.Runs.Pre != "" && a.Runs.PreIf == "" {
a.Runs.PreIf = "always()"
}
if a.Runs.Post != "" && a.Runs.PostIf == "" {
a.Runs.PostIf = "always()"
}

return a, nil
}
1 change: 1 addition & 0 deletions pkg/model/step_result.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,5 @@ type StepResult struct {
Outputs map[string]string `json:"outputs"`
Conclusion stepStatus `json:"conclusion"`
Outcome stepStatus `json:"outcome"`
State map[string]string
}
76 changes: 76 additions & 0 deletions pkg/runner/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction
// time, we don't have all environment prepared
mergeIntoMap(step.getEnv(), rc.withGithubEnv(map[string]string{}))

populateEnvsFromSavedState(step.getEnv(), step, rc)
populateEnvsFromInput(step.getEnv(), action, rc)

actionLocation := path.Join(actionDir, actionPath)
Expand Down Expand Up @@ -497,6 +498,16 @@ func (rc *RunContext) compositeExecutor(action *model.Action) common.Executor {
}
}

func populateEnvsFromSavedState(env *map[string]string, step actionStep, rc *RunContext) {
stepResult := rc.StepResults[step.getStepModel().ID]
if stepResult != nil {
for name, value := range stepResult.State {
envName := fmt.Sprintf("STATE_%s", name)
(*env)[envName] = value
}
}
}

func populateEnvsFromInput(env *map[string]string, action *model.Action, rc *RunContext) {
eval := rc.NewExpressionEvaluator()
for inputID, input := range action.Inputs {
Expand Down Expand Up @@ -538,3 +549,68 @@ func getOsSafeRelativePath(s, prefix string) string {

return actionName
}

func shouldRunPostStep(step actionStep) common.Conditional {
return func(ctx context.Context) bool {
stepResults := step.getRunContext().getStepsContext()

if stepResults[step.getStepModel().ID].Conclusion == model.StepStatusSkipped {
return false
}

if step.getActionModel() == nil {
return false
}

return true
}
}

func hasPostStep(step actionStep) common.Conditional {
return func(ctx context.Context) bool {
action := step.getActionModel()
return (action.Runs.Using == model.ActionRunsUsingNode12 ||
action.Runs.Using == model.ActionRunsUsingNode16) &&
action.Runs.Post != ""
}
}

func runPostStep(step actionStep) common.Executor {
return func(ctx context.Context) error {
rc := step.getRunContext()
stepModel := step.getStepModel()
action := step.getActionModel()

switch action.Runs.Using {
case model.ActionRunsUsingNode12, model.ActionRunsUsingNode16:

populateEnvsFromSavedState(step.getEnv(), step, rc)

var actionDir string
var actionPath string
if _, ok := step.(*stepActionRemote); ok {
actionPath = newRemoteAction(stepModel.Uses).Path
actionDir = fmt.Sprintf("%s/%s", rc.ActionCacheDir(), strings.ReplaceAll(stepModel.Uses, "/", "-"))
} else {
actionDir = filepath.Join(rc.Config.Workdir, stepModel.Uses)
actionPath = ""
}

actionLocation := ""
if actionPath != "" {
actionLocation = path.Join(actionDir, actionPath)
} else {
actionLocation = actionDir
}

_, containerActionDir := getContainerActionPaths(stepModel, actionLocation, rc)

containerArgs := []string{"node", path.Join(containerActionDir, action.Runs.Post)}
log.Debugf("executing remote job container: %s", containerArgs)

return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx)
default:
return nil
}
}
}
56 changes: 52 additions & 4 deletions pkg/runner/action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,12 @@ runs:

func TestActionRunner(t *testing.T) {
table := []struct {
name string
step actionStep
name string
step actionStep
expectedEnv map[string]string
}{
{
name: "Test",
name: "with-input",
step: &stepActionRemote{
Step: &model.Step{
Uses: "repo@ref",
Expand Down Expand Up @@ -172,6 +173,47 @@ func TestActionRunner(t *testing.T) {
},
env: map[string]string{},
},
expectedEnv: map[string]string{"INPUT_KEY": "default value"},
},
{
name: "restore-saved-state",
step: &stepActionRemote{
Step: &model.Step{
ID: "step",
Uses: "repo@ref",
},
RunContext: &RunContext{
ActionRepository: "repo",
ActionPath: "path",
ActionRef: "ref",
Config: &Config{},
Run: &model.Run{
JobID: "job",
Workflow: &model.Workflow{
Jobs: map[string]*model.Job{
"job": {
Name: "job",
},
},
},
},
CurrentStep: "post-step",
StepResults: map[string]*model.StepResult{
"step": {
State: map[string]string{
"name": "state value",
},
},
},
},
action: &model.Action{
Runs: model.ActionRuns{
Using: "node16",
},
},
env: map[string]string{},
},
expectedEnv: map[string]string{"STATE_name": "state value"},
},
}

Expand All @@ -183,8 +225,14 @@ func TestActionRunner(t *testing.T) {
cm.On("CopyDir", "/var/run/act/actions/dir/", "dir/", false).Return(func(ctx context.Context) error { return nil })

envMatcher := mock.MatchedBy(func(env map[string]string) bool {
return env["INPUT_KEY"] == "default value"
for k, v := range tt.expectedEnv {
if env[k] != v {
return false
}
}
return true
})

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
Expand Down
15 changes: 15 additions & 0 deletions pkg/runner/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ func (rc *RunContext) commandHandler(ctx context.Context) common.LineHandler {
case resumeCommand:
resumeCommand = ""
logger.Infof(" \U00002699 %s", line)
case "save-state":
logger.Infof(" \U0001f4be %s", line)
rc.saveState(ctx, kvPairs, arg)
default:
logger.Infof(" \U00002753 %s", line)
}
Expand Down Expand Up @@ -141,3 +144,15 @@ func unescapeKvPairs(kvPairs map[string]string) map[string]string {
}
return kvPairs
}

func (rc *RunContext) saveState(ctx context.Context, kvPairs map[string]string, arg string) {
if rc.CurrentStep != "" {
stepResult := rc.StepResults[rc.CurrentStep]
if stepResult != nil {
if stepResult.State == nil {
stepResult.State = map[string]string{}
}
stepResult.State[kvPairs["name"]] = arg
}
}
}
18 changes: 18 additions & 0 deletions pkg/runner/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,21 @@ func TestAddmaskUsemask(t *testing.T) {

a.Equal("[testjob] \U00002699 ***\n[testjob] \U00002699 ::set-output:: = token=***\n", re)
}

func TestSaveState(t *testing.T) {
rc := &RunContext{
CurrentStep: "step",
StepResults: map[string]*model.StepResult{
"step": {
State: map[string]string{},
},
},
}

ctx := context.Background()

handler := rc.commandHandler(ctx)
handler("::save-state name=state-name::state-value\n")

assert.Equal(t, "state-value", rc.StepResults["step"].State["state-name"])
}
49 changes: 42 additions & 7 deletions pkg/runner/step.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,49 @@ type step interface {
getRunContext() *RunContext
getStepModel() *model.Step
getEnv() *map[string]string
getIfExpression(stage stepStage) string
}

func runStepExecutor(step step, executor common.Executor) common.Executor {
type stepStage int

const (
stepStagePre stepStage = iota
stepStageMain
stepStagePost
)

func (s stepStage) String() string {
switch s {
case stepStagePre:
return "Pre"
case stepStageMain:
return "Run"
case stepStagePost:
return "Post"
}
return "Unknown"
}

func (s stepStage) getStepName(stepModel *model.Step) string {
switch s {
case stepStagePre:
return fmt.Sprintf("pre-%s", stepModel.ID)
case stepStageMain:
return stepModel.ID
case stepStagePost:
return fmt.Sprintf("post-%s", stepModel.ID)
}
return "unknown"
}

func runStepExecutor(step step, stage stepStage, executor common.Executor) common.Executor {
return func(ctx context.Context) error {
rc := step.getRunContext()
stepModel := step.getStepModel()

rc.CurrentStep = stepModel.ID
ifExpression := step.getIfExpression(stage)
rc.CurrentStep = stage.getStepName(stepModel)

rc.StepResults[rc.CurrentStep] = &model.StepResult{
Outcome: model.StepStatusSuccess,
Conclusion: model.StepStatusSuccess,
Expand All @@ -37,15 +72,15 @@ func runStepExecutor(step step, executor common.Executor) common.Executor {
return err
}

runStep, err := isStepEnabled(ctx, step)
runStep, err := isStepEnabled(ctx, ifExpression, step)
if err != nil {
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusFailure
rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusFailure
return err
}

if !runStep {
log.Debugf("Skipping step '%s' due to '%s'", stepModel.String(), stepModel.If.Value)
log.Debugf("Skipping step '%s' due to '%s'", stepModel, stepModel.If.Value)
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusSkipped
rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusSkipped
return nil
Expand All @@ -55,7 +90,7 @@ func runStepExecutor(step step, executor common.Executor) common.Executor {
if strings.Contains(stepString, "::add-mask::") {
stepString = "add-mask command"
}
common.Logger(ctx).Infof("\u2B50 Run %s", stepString)
common.Logger(ctx).Infof("\u2B50 %s %s", stage, stepString)

err = executor(ctx)

Expand Down Expand Up @@ -129,10 +164,10 @@ func mergeEnv(step step) {
mergeIntoMap(env, rc.withGithubEnv(*env))
}

func isStepEnabled(ctx context.Context, step step) (bool, error) {
func isStepEnabled(ctx context.Context, expr string, step step) (bool, error) {
rc := step.getRunContext()

runStep, err := EvalBool(rc.NewStepExpressionEvaluator(step), step.getStepModel().If.Value)
runStep, err := EvalBool(rc.NewStepExpressionEvaluator(step), expr)
if err != nil {
return false, fmt.Errorf(" \u274C Error in if-expression: \"if: %s\" (%s)", step.getStepModel().If.Value, err)
}
Expand Down
16 changes: 12 additions & 4 deletions pkg/runner/step_action_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func (sal *stepActionLocal) pre() common.Executor {
func (sal *stepActionLocal) main() common.Executor {
sal.env = map[string]string{}

return runStepExecutor(sal, func(ctx context.Context) error {
return runStepExecutor(sal, stepStageMain, func(ctx context.Context) error {
actionDir := filepath.Join(sal.getRunContext().Config.Workdir, sal.Step.Uses)

localReader := func(ctx context.Context) actionYamlReader {
Expand Down Expand Up @@ -63,9 +63,7 @@ func (sal *stepActionLocal) main() common.Executor {
}

func (sal *stepActionLocal) post() common.Executor {
return func(ctx context.Context) error {
return nil
}
return runStepExecutor(sal, stepStagePost, runPostStep(sal)).If(hasPostStep(sal)).If(shouldRunPostStep(sal))
}

func (sal *stepActionLocal) getRunContext() *RunContext {
Expand All @@ -80,6 +78,16 @@ func (sal *stepActionLocal) getEnv() *map[string]string {
return &sal.env
}

func (sal *stepActionLocal) getIfExpression(stage stepStage) string {
switch stage {
case stepStageMain:
return sal.Step.If.Value
case stepStagePost:
return sal.action.Runs.PostIf
}
return ""
}

func (sal *stepActionLocal) getActionModel() *model.Action {
return sal.action
}
Loading

0 comments on commit 7f669b5

Please sign in to comment.