262 lines
7 KiB
Go
262 lines
7 KiB
Go
|
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::<p>
|
||
|
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::<p>
|
||
|
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=<o>::
|
||
|
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::<t>
|
||
|
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.fields>::<msg, args>
|
||
|
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.fields>::<msg, args>
|
||
|
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.fields>::<msg, args>
|
||
|
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.fields>::<msg, args>
|
||
|
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),
|
||
|
})
|
||
|
}
|