package sdk import ( "fmt" "io" "os" "strings" ) const ( addMaskCmd = "add-mask" envCmd = "env" outputCmd = "output" pathCmd = "path" stateCmd = "state" // https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings multiLineFileDelim = "234baa68-d26f-4bf9-996d-45ec3520cb95" multilineFileCmd = "%s<<" + multiLineFileDelim + "\n%s\n" + multiLineFileDelim // ${name}<<${delimiter}${os.EOL}${convertedVal}${os.EOL}${delimiter} addMatcherCmd = "add-matcher" removeMatcherCmd = "remove-matcher" groupCmd = "group" endGroupCmd = "endgroup" stepSummaryCmd = "step-summary" debugCmd = "debug" noticeCmd = "notice" warningCmd = "warning" errorCmd = "error" errFileCmdFmt = "unable to write command to the environment file: %s" ) type Action struct { w io.Writer env func(string) string fields CommandProperties } func New() *Action { return &Action{w: os.Stdout, env: os.Getenv} } // WithFieldsSlice includes the provided fields in log output. "f" must be a // slice of k=v pairs. The given slice will be sorted. It panics if any of the // string in the given slice does not construct a valid 'key=value' pair. func (c *Action) WithFieldsSlice(f ...string) *Action { m := make(CommandProperties) for _, s := range f { pair := strings.SplitN(s, "=", 2) if len(pair) < 2 { panic(fmt.Sprintf("%q is not a proper k=v pair!", s)) } m[pair[0]] = pair[1] } return c.WithFieldsMap(m) } // WithFieldsMap includes the provided fields in log output. The fields in "m" // are automatically converted to k=v pairs and sorted. func (c *Action) WithFieldsMap(m map[string]string) *Action { return &Action{ w: c.w, fields: m, } } // GetInput gets the input by the given name. It returns the empty string if the // input is not defined. func (c *Action) GetInput(i string) string { e := strings.ReplaceAll(i, " ", "_") e = strings.ToUpper(e) e = "INPUT_" + e return strings.TrimSpace(c.env(e)) } // IssueCommand issues a new GitHub actions Command. // It panics if it cannot write to the output stream. func (c *Action) IssueCommand(cmd *Command) { if _, err := fmt.Fprintln(c.w, cmd.String()); err != nil { panic(fmt.Errorf("failed to issue command: %w", err)) } } // IssueFileCommand issues a new GitHub actions Command using environment files. // It panics if writing to the file fails. func (c *Action) IssueFileCommand(cmd *Command) { e := strings.ReplaceAll(cmd.Name, "-", "_") e = strings.ToUpper(e) e = "GITHUB_" + e filepath := c.env(e) msg := []byte(cmd.Message) f, err := os.OpenFile(filepath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { panic(fmt.Errorf(errFileCmdFmt, err)) } defer func() { if err := f.Close(); err != nil { panic(err) } }() if _, err := f.Write(msg); err != nil { panic(fmt.Errorf(errFileCmdFmt, err)) } } // AddMask adds a new field mask for the given string "p". After called, future // attempts to log "p" will be replaced with "***" in log output. It panics if // it cannot write to the output stream. func (c *Action) AddMask(p string) { // ::add-mask::

c.IssueCommand(&Command{ Name: addMaskCmd, Message: p, }) } // AddMatcher adds a new matcher with the given file path. It panics if it // cannot write to the output stream. func (c *Action) AddMatcher(p string) { // ::add-matcher::

c.IssueCommand(&Command{ Name: addMatcherCmd, Message: p, }) } // RemoveMatcher removes a matcher with the given owner name. It panics if it // cannot write to the output stream. func (c *Action) RemoveMatcher(o string) { // ::remove-matcher owner=:: c.IssueCommand(&Command{ Name: removeMatcherCmd, Properties: CommandProperties{ "owner": o, }, }) } // Group starts a new collapsable region up to the next ungroup invocation. It // panics if it cannot write to the output stream. func (c *Action) Group(t string) { // ::group:: c.IssueCommand(&Command{ Name: groupCmd, Message: t, }) } // EndGroup ends the current group. It panics if it cannot write to the output // stream. func (c *Action) EndGroup() { // ::endgroup:: c.IssueCommand(&Command{ Name: endGroupCmd, }) } // Debugf prints a debug-level message. It follows the standard fmt.Printf // arguments, appending an OS-specific line break to the end of the message. // It panics if it cannot write to the output stream. func (c *Action) Debugf(msg string, args ...any) { // ::debug :: c.IssueCommand(&Command{ Name: debugCmd, Message: fmt.Sprintf(msg, args...), Properties: c.fields, }) } // Noticef prints a notice-level message. It follows the standard fmt.Printf // arguments, appending an OS-specific line break to the end of the message. // It panics if it cannot write to the output stream. func (c *Action) Noticef(msg string, args ...any) { // ::notice :: c.IssueCommand(&Command{ Name: noticeCmd, Message: fmt.Sprintf(msg, args...), Properties: c.fields, }) } // Warningf prints a warning-level message. It follows the standard fmt.Printf // arguments, appending an OS-specific line break to the end of the message. // It panics if it cannot write to the output stream. func (c *Action) Warningf(msg string, args ...any) { // ::warning :: c.IssueCommand(&Command{ Name: warningCmd, Message: fmt.Sprintf(msg, args...), Properties: c.fields, }) } // Errorf prints a error-level message. It follows the standard fmt.Printf // arguments, appending an OS-specific line break to the end of the message. // It panics if it cannot write to the output stream. func (c *Action) Errorf(msg string, args ...any) { // ::error :: c.IssueCommand(&Command{ Name: errorCmd, Message: fmt.Sprintf(msg, args...), Properties: c.fields, }) } // AddPath adds the string "p" to the path for the invocation. // It panics if it cannot write to the output file. func (c *Action) AddPath(p string) { c.IssueFileCommand(&Command{ Name: pathCmd, Message: p, }) } // SaveState saves state to be used in the "finally" post job entry point. // It panics if it cannot write to the output stream. func (c *Action) SaveState(k, v string) { c.IssueFileCommand(&Command{ Name: stateCmd, Message: fmt.Sprintf(multilineFileCmd, k, v), }) } // AddStepSummary writes the given markdown to the job summary. If a job summary // already exists, this value is appended. // It panics if it cannot write to the output file. func (c *Action) AddStepSummary(markdown string) { c.IssueFileCommand(&Command{ Name: stepSummaryCmd, Message: markdown, }) } // SetEnv sets an environment variable. // It panics if it cannot write to the output file. func (c *Action) SetEnv(k, v string) { c.IssueFileCommand(&Command{ Name: envCmd, Message: fmt.Sprintf(multilineFileCmd, k, v), }) } // SetOutput sets an output parameter. // It panics if it cannot write to the output file. func (c *Action) SetOutput(k, v string) { c.IssueFileCommand(&Command{ Name: outputCmd, Message: fmt.Sprintf(multilineFileCmd, k, v), }) }