github-actions/internal/toolkit/toolkit.go

288 lines
7.0 KiB
Go

/*
* Copyright 2018-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package toolkit
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"strconv"
"strings"
"sync"
)
//go:generate mockery --all --inpackage --case=underscore
type Toolkit interface {
AddPath(paths ...string) error
ExportVariable(name string, value string) error
GetInput(name string) (string, bool)
GetInputList(name string) ([]string, bool)
SetOutput(name string, value string)
GetState(name string) (string, bool)
SetState(name string, value string)
AddMask(mask string)
StartGroup(title string)
EndGroup()
IsDebug() bool
Debug(a ...interface{})
Debugf(format string, a ...interface{})
Warning(a ...interface{})
Warningc(context MessageContext)
Warningf(format string, a ...interface{})
Error(a ...interface{})
Errorc(context MessageContext)
Errorf(format string, a ...interface{})
}
type MessageContext struct {
File string
Line string
Column string
Message string
}
func (m *MessageContext) String() string {
var s []string
if m.File != "" {
s = append(s, fmt.Sprintf("file=%s", m.File))
}
if m.Line != "" {
s = append(s, fmt.Sprintf("line=%s", m.Line))
}
if m.Column != "" {
s = append(s, fmt.Sprintf("col=%s", m.Column))
}
return fmt.Sprintf("%s::%s", strings.Join(s, ","), escape(m.Message))
}
func FailedError(a ...interface{}) error {
return errors.New(errorString(a...))
}
func FailedErrorc(context MessageContext) error {
return errors.New(errorStringc(context))
}
func FailedErrorf(format string, a ...interface{}) error {
return errors.New(errorStringf(format, a...))
}
type DefaultToolkit struct {
once sync.Once
Environment map[string]string
Writer io.Writer
Delemiter string
}
func (d *DefaultToolkit) AddPath(paths ...string) error {
d.once.Do(d.init)
path, ok := d.Environment["GITHUB_PATH"]
if !ok {
return FailedError("$GITHUB_PATH must be set")
}
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return FailedErrorf("unable to open %s\n%w", path, err)
}
defer f.Close()
for _, p := range paths {
_, _ = fmt.Fprintln(f, p)
}
return nil
}
func (d *DefaultToolkit) ExportVariable(name string, value string) error {
return d.export("GITHUB_ENV", name, value)
}
func (d *DefaultToolkit) export(env string, name string, value string) error {
d.once.Do(d.init)
path, ok := d.Environment[env]
if !ok {
return FailedErrorf("$%s must be set", env)
}
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return FailedErrorf("unable to open %s\n%w", path, err)
}
defer f.Close()
if strings.ContainsRune(value, '\n') {
if _, err := fmt.Fprintln(f, fmt.Sprintf("%s<<%s\n%s\n%s", name, d.Delemiter, value, d.Delemiter)); err != nil {
return FailedError("unable to write variable")
}
} else {
_, _ = fmt.Fprintln(f, fmt.Sprintf("%s=%s", name, value))
}
return nil
}
func (d *DefaultToolkit) GetInput(name string) (string, bool) {
d.once.Do(d.init)
s, ok := d.Environment[fmt.Sprintf("INPUT_%s", strings.ToUpper(name))]
return s, ok
}
func (d *DefaultToolkit) GetInputList(name string) ([]string, bool) {
d.once.Do(d.init)
s, ok := d.Environment[fmt.Sprintf("INPUT_%s", strings.ToUpper(name))]
ss := strings.Split(s, ",")
return ss, ok
}
func (d *DefaultToolkit) SetOutput(name string, value string) {
err := d.export("GITHUB_OUTPUT", name, value)
if err != nil {
panic(err)
}
}
func (d *DefaultToolkit) GetState(name string) (string, bool) {
d.once.Do(d.init)
s, ok := d.Environment[fmt.Sprintf("STATE_%s", strings.ToUpper(name))]
return s, ok
}
func (d *DefaultToolkit) SetState(name string, value string) {
err := d.export("GITHUB_STATE", name, value)
if err != nil {
panic(err)
}
}
func (d *DefaultToolkit) AddMask(mask string) {
d.once.Do(d.init)
_, _ = fmt.Fprintf(d.Writer, "::add-mask::%s\n", escape(mask))
}
func (d *DefaultToolkit) StartGroup(title string) {
d.once.Do(d.init)
_, _ = fmt.Fprintf(d.Writer, "::group::%s\n", title)
}
func (d *DefaultToolkit) EndGroup() {
d.once.Do(d.init)
_, _ = fmt.Fprintln(d.Writer, "::endgroup::")
}
func (d *DefaultToolkit) IsDebug() bool {
d.once.Do(d.init)
t, err := strconv.ParseBool(d.Environment["RUNNER_DEBUG"])
if err != nil {
return false
}
return t
}
func (d *DefaultToolkit) Debug(a ...interface{}) {
d.once.Do(d.init)
_, _ = fmt.Fprintf(d.Writer, "::debug::%s\n", escape(fmt.Sprint(a...)))
}
func (d *DefaultToolkit) Debugf(format string, a ...interface{}) {
d.once.Do(d.init)
_, _ = fmt.Fprintf(d.Writer, "::debug::%s\n", escape(fmt.Sprintf(format, a...)))
}
func (d *DefaultToolkit) Error(a ...interface{}) {
d.once.Do(d.init)
_, _ = fmt.Fprintln(d.Writer, errorString(a...))
}
func (d *DefaultToolkit) Errorc(context MessageContext) {
d.once.Do(d.init)
_, _ = fmt.Fprintln(d.Writer, errorStringc(context))
}
func (d *DefaultToolkit) Errorf(format string, a ...interface{}) {
d.once.Do(d.init)
_, _ = fmt.Fprintln(d.Writer, errorStringf(format, a...))
}
func (d *DefaultToolkit) Warning(a ...interface{}) {
d.once.Do(d.init)
_, _ = fmt.Fprintf(d.Writer, "::warning ::%s\n", escape(fmt.Sprint(a...)))
}
func (d *DefaultToolkit) Warningc(context MessageContext) {
d.once.Do(d.init)
_, _ = fmt.Fprintln(d.Writer, "::warning", escape(context.String()))
}
func (d *DefaultToolkit) Warningf(format string, a ...interface{}) {
d.once.Do(d.init)
_, _ = fmt.Fprintf(d.Writer, "::warning ::%s\n", escape(fmt.Sprintf(format, a...)))
}
func (d *DefaultToolkit) init() {
if d.Environment == nil {
d.Environment = make(map[string]string)
for _, s := range os.Environ() {
t := strings.SplitN(s, "=", 2)
d.Environment[t[0]] = t[1]
}
}
if d.Writer == nil {
d.Writer = os.Stdout
}
if d.Delemiter == "" {
data := make([]byte, 16) // roughly the same entropy as uuid v4 used in https://github.com/actions/toolkit/blob/b36e70495fbee083eb20f600eafa9091d832577d/packages/core/src/file-command.ts#L28
_, err := rand.Read(data)
if err != nil {
panic(fmt.Errorf("could not generate random delimiter: %w", err))
}
d.Delemiter = hex.EncodeToString(data)
}
}
func errorString(a ...interface{}) string {
return fmt.Sprintf("::error ::%s", escape(fmt.Sprint(a...)))
}
func errorStringc(context MessageContext) string {
return fmt.Sprintf("::error %s", context.String())
}
func errorStringf(format string, a ...interface{}) string {
return fmt.Sprintf("::error ::%s", escape(fmt.Errorf(format, a...).Error()))
}
func escape(s string) string {
return strings.ReplaceAll(s, "\n", "%0A")
}