codecatalyst-runner/pkg/actions/action_output_handler.go (117 lines of code) (raw):
package actions
import (
"context"
"encoding/json"
"errors"
"fmt"
"regexp"
"slices"
"strings"
"github.com/aws/codecatalyst-runner-cli/command-runner/pkg/runner"
"github.com/rs/zerolog/log"
"golang.org/x/exp/maps"
)
// ActionOutputHandler collects the output from an action and checks for failures in the ACTION_RUN_SUMMARY output
func ActionOutputHandler(outputs map[string]string, suppressOutput bool) runner.Feature {
return func(ctx context.Context, plan runner.Plan, e runner.PlanExecutor) error {
log.Ctx(ctx).Debug().Msg("ENTER ActionOutputHandler")
var action *Action
if ap, ok := plan.(ActionProvider); ok {
action = ap.Action()
} else {
return fmt.Errorf("plan must implement ActionProvider for ActionOutputHandler")
}
lineHandlers := []lineHandler{
actionOutputLineHandler(outputs, maps.Keys(action.Outputs.Variables)),
}
if suppressOutput {
log.Ctx(ctx).Debug().Msgf("suppressing action output")
} else {
rawLogger := log.Ctx(ctx).With().Logger()
lineHandlers = append(lineHandlers, func(s string) bool {
s = strings.TrimRight(s, "\r\n")
rawLogger.Info().Msg(s)
return true
})
}
logWriter := newLineWriter(lineHandlers...)
log.Ctx(ctx).Debug().Msgf("Setting stdout/stderr to %+v", logWriter)
plan.EnvironmentConfiguration().Stdout = logWriter
plan.EnvironmentConfiguration().Stderr = logWriter
if err := e(ctx); err != nil {
maps.Clear(outputs)
return err
}
for k, v := range outputs {
if k == "ACTION_RUN_SUMMARY" {
delete(outputs, k)
// handle ACTION_RUN_SUMMARY output
actionRunSummaries := make([]ActionRunSummaryMessage, 0)
err := json.Unmarshal([]byte(v), &actionRunSummaries)
if err != nil {
return fmt.Errorf("unable to unmarshal ACTION_RUN_SUMMARY: %w\n%s", err, v)
}
var actionRunErrors error
for _, actionRunSummary := range actionRunSummaries {
if actionRunSummary.Level == ActionRunSummaryLevelError {
actionRunErrors = errors.Join(
actionRunErrors,
fmt.Errorf("[%s] %s", actionRunSummary.Text, actionRunSummary.Message),
)
}
}
if actionRunErrors != nil {
return actionRunErrors
}
}
}
log.Ctx(ctx).Debug().Msgf("action outputs: %+v", outputs)
if len(outputs) > 0 {
log.Ctx(ctx).Info().Msgf("")
log.Ctx(ctx).Info().Msgf("💬 OUTPUTS:")
for k, v := range outputs {
log.Ctx(ctx).Info().Msgf(" %s = %s", k, v)
}
log.Ctx(ctx).Info().Msgf("")
}
log.Ctx(ctx).Debug().Msg("EXIT ActionOutputHandler")
return nil
}
}
var actionCommandPattern = regexp.MustCompile("^::([^ ]+)( (.+))?::([^\r\n]*)[\r\n]+$")
func actionOutputLineHandler(outputs map[string]string, filter []string) lineHandler {
return func(line string) bool {
if m := actionCommandPattern.FindStringSubmatch(line); m != nil {
command := m[1]
kvPairs := make(map[string]string)
kvPairList := strings.Split(m[3], ",")
for _, kvPair := range kvPairList {
kv := strings.Split(kvPair, "=")
if len(kv) == 2 {
kvPairs[kv[0]] = kv[1]
}
}
arg := m[4]
if command == "set-output" {
if slices.Contains(filter, kvPairs["name"]) || kvPairs["name"] == "ACTION_RUN_SUMMARY" {
log.Debug().Msgf("Setting output %s = %s", kvPairs["name"], arg)
outputs[kvPairs["name"]] = arg
}
return false
}
}
return true
}
}
// ActionRunSummaryMessage describes a messages that was returned from the action
type ActionRunSummaryMessage struct {
Text string // text of the message
Level ActionRunSummaryLevel // level of the message
Message string // template to be used with TemplateVariables
TemplateVariables []ActionRunTemplateVariable // variables to apply in the message template
}
// ActionRunSummaryLevel of an ActionRunSummaryMessage
type ActionRunSummaryLevel string
const (
// ActionRunSummaryLevelError represents the error level
ActionRunSummaryLevelError ActionRunSummaryLevel = "Error"
)
// ActionRunTemplateVariable describes a variable for a message template
type ActionRunTemplateVariable struct {
Name string
Value string
}