mirror of
https://gitea.com/gitea/runner.git
synced 2026-08-27 22:37:46 +00:00
fix: improve behaviour across masking, commands and status (#1194)
Fixes 51 bugs discovered via comparison with `actions/runner`. Every fix has test coverage.
### Secrets
- A short secret registered no shifted-base64 form, so `base64("user:$TOKEN")` printed in the clear
- Encoded forms came only from the whole trimmed value, missing padded and per-line spellings
- Masks split only on `\n`, so `::add-mask::a%0Db` registered neither half
- Adds XML, expression-string and quote-trimming encoders
### Workflow commands
- Split at the last `::` or `]` rather than the first, so `::add-mask::a::b` registered no mask
- A command on the last line without a newline was ignored, and `::ADD-MASK::` did nothing
- `##[...]` did not decode `%3B`/`%5D`, properties lost anything after a second `=`
- `$GITHUB_ENV` and `::set-env::` now refuse `NODE_OPTIONS`
### Status
- `continue-on-error` reported failed, a cancelled job reported success, an `if:` error reported cancelled
- File commands ran after `continue-on-error`, failing the job while the step stayed green
- A bad job output aborted the whole run instead of that job
### Steps and actions
- `${{ matrix.* }}` and `${{ strategy.* }}` were empty inside composite actions
- Composite inputs leaked into nested actions as `INPUT_*`, `with:` matched case-sensitively, `pre` failures were dropped
- Docker actions dropped `runs.env` when the caller passed `with: args:`, and caller `args`/`entrypoint` beat the manifest
- An implicit shell ran with `pipefail`, a `shell:` without `{0}` passed without running
- `container.env` overrode job env and every `$GITHUB_ENV` write, heredocs lost leading blank lines, `$GITHUB_PATH` was not BOM-decoded
Written by Claude Opus 5.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1194
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
+43
-12
@@ -18,11 +18,17 @@ var commandPatternGA *regexp.Regexp
|
||||
var commandPatternADO *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
commandPatternGA = regexp.MustCompile("^::([^ ]+)( (.+))?::([^\r\n]*)[\r\n]+$")
|
||||
commandPatternADO = regexp.MustCompile("^##\\[([^ ]+)( (.+))?]([^\r\n]*)[\r\n]+$")
|
||||
commandPatternGA = regexp.MustCompile("^::([^ ]+?)( (.+?))?::([^\r\n]*)[\r\n]*$")
|
||||
// excluding ']' ends the command info at the first bracket, as GitHub does
|
||||
commandPatternADO = regexp.MustCompile("^##\\[([^ \\]]+)( ([^\\]]*))?]([^\r\n]*)[\r\n]*$")
|
||||
}
|
||||
|
||||
func tryParseRawActionCommand(line string) (command string, kvPairs map[string]string, arg string, ok bool) {
|
||||
command, kvPairs, arg, _, ok = tryParseActionCommand(line)
|
||||
return command, kvPairs, arg, ok
|
||||
}
|
||||
|
||||
func tryParseActionCommand(line string) (command string, kvPairs map[string]string, arg string, legacy, ok bool) {
|
||||
if m := commandPatternGA.FindStringSubmatch(line); m != nil {
|
||||
command = m[1]
|
||||
kvPairs = parseKeyValuePairs(m[3], ",")
|
||||
@@ -32,19 +38,21 @@ func tryParseRawActionCommand(line string) (command string, kvPairs map[string]s
|
||||
command = m[1]
|
||||
kvPairs = parseKeyValuePairs(m[3], ";")
|
||||
arg = m[4]
|
||||
legacy = true
|
||||
ok = true
|
||||
}
|
||||
return command, kvPairs, arg, ok
|
||||
return command, kvPairs, arg, legacy, ok
|
||||
}
|
||||
|
||||
func (rc *RunContext) commandHandler(ctx context.Context) common.LineHandler {
|
||||
logger := common.Logger(ctx)
|
||||
resumeCommand := ""
|
||||
return func(line string) bool {
|
||||
command, kvPairs, arg, ok := tryParseRawActionCommand(line)
|
||||
command, kvPairs, arg, legacy, ok := tryParseActionCommand(line)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
command = strings.ToLower(command)
|
||||
|
||||
if resumeCommand != "" {
|
||||
// There should not be any emojis in the log output for Gitea.
|
||||
@@ -54,19 +62,24 @@ func (rc *RunContext) commandHandler(ctx context.Context) common.LineHandler {
|
||||
logger.Infof("%s", line)
|
||||
// Resumed here rather than from the switch, because the end token is arbitrary
|
||||
// and a token naming a real command would otherwise never resume.
|
||||
if command == resumeCommand {
|
||||
if strings.EqualFold(command, resumeCommand) {
|
||||
resumeCommand = ""
|
||||
}
|
||||
return true
|
||||
}
|
||||
arg = UnescapeCommandData(arg)
|
||||
kvPairs = unescapeKvPairs(kvPairs)
|
||||
if legacy {
|
||||
arg = UnescapeLegacyCommand(arg)
|
||||
kvPairs = unescapeKvPairs(kvPairs, UnescapeLegacyCommand)
|
||||
} else {
|
||||
arg = UnescapeCommandData(arg)
|
||||
kvPairs = unescapeKvPairs(kvPairs, unescapeCommandProperty)
|
||||
}
|
||||
if (command == "set-env" || command == "add-path") && rc.refuseUnsecureCommand(ctx, command) {
|
||||
return true
|
||||
}
|
||||
switch command {
|
||||
case "set-env":
|
||||
rc.setEnv(ctx, kvPairs, arg)
|
||||
rc.setEnv(ctx, kvPairs, arg, true)
|
||||
case "set-output":
|
||||
rc.setOutput(ctx, kvPairs, arg)
|
||||
case "add-path":
|
||||
@@ -139,8 +152,16 @@ func (rc *RunContext) takeUnsecureCommandError() error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (rc *RunContext) setEnv(ctx context.Context, kvPairs map[string]string, arg string) {
|
||||
func (rc *RunContext) setEnv(ctx context.Context, kvPairs map[string]string, arg string, fromCommand bool) {
|
||||
name := kvPairs["name"]
|
||||
if strings.EqualFold(name, "NODE_OPTIONS") {
|
||||
message := "Can't store NODE_OPTIONS output parameter using '$GITHUB_ENV' command."
|
||||
if fromCommand {
|
||||
message = "Can't update NODE_OPTIONS environment variable using ::set-env:: command."
|
||||
}
|
||||
common.Logger(ctx).WithField(rawOutputField, true).Errorf("##[error]%s", EscapeCommandData(message))
|
||||
return
|
||||
}
|
||||
common.Logger(ctx).Infof("::set-env:: %s=%s", name, arg)
|
||||
if rc.Env == nil {
|
||||
rc.Env = make(map[string]string)
|
||||
@@ -159,6 +180,10 @@ func (rc *RunContext) setEnv(ctx context.Context, kvPairs map[string]string, arg
|
||||
mergeIntoMap(rc.GlobalEnv, newenv)
|
||||
}
|
||||
|
||||
func (rc *RunContext) setEnvFile(ctx context.Context, kvPairs map[string]string, arg string) {
|
||||
rc.setEnv(ctx, kvPairs, arg, false)
|
||||
}
|
||||
|
||||
func (rc *RunContext) setOutput(ctx context.Context, kvPairs map[string]string, arg string) {
|
||||
logger := common.Logger(ctx)
|
||||
stepID := rc.CurrentStep
|
||||
@@ -189,7 +214,7 @@ func parseKeyValuePairs(kvPairs, separator string) map[string]string {
|
||||
rtn := make(map[string]string)
|
||||
kvPairList := strings.SplitSeq(kvPairs, separator)
|
||||
for kvPair := range kvPairList {
|
||||
kv := strings.Split(kvPair, "=")
|
||||
kv := strings.SplitN(kvPair, "=", 2)
|
||||
if len(kv) == 2 {
|
||||
rtn[kv[0]] = kv[1]
|
||||
}
|
||||
@@ -202,6 +227,7 @@ var (
|
||||
commandDataEscaper = strings.NewReplacer("%", "%25", "\r", "%0D", "\n", "%0A")
|
||||
commandDataUnescaper = strings.NewReplacer("%25", "%", "%0D", "\r", "%0A", "\n")
|
||||
commandPropertyUnescaper = strings.NewReplacer("%25", "%", "%0D", "\r", "%0A", "\n", "%3A", ":", "%2C", ",")
|
||||
legacyCommandUnescaper = strings.NewReplacer("%3B", ";", "%0D", "\r", "%0A", "\n", "%5D", "]", "%25", "%")
|
||||
)
|
||||
|
||||
// EscapeCommandData encodes the data part of a "::cmd::" or "##[cmd]" line the runner writes itself,
|
||||
@@ -218,9 +244,14 @@ func unescapeCommandProperty(arg string) string {
|
||||
return commandPropertyUnescaper.Replace(arg)
|
||||
}
|
||||
|
||||
func unescapeKvPairs(kvPairs map[string]string) map[string]string {
|
||||
// UnescapeLegacyCommand decodes a "##[cmd]" line, which also spells ";" and "]" escaped.
|
||||
func UnescapeLegacyCommand(arg string) string {
|
||||
return legacyCommandUnescaper.Replace(arg)
|
||||
}
|
||||
|
||||
func unescapeKvPairs(kvPairs map[string]string, unescape func(string) string) map[string]string {
|
||||
for k, v := range kvPairs {
|
||||
kvPairs[k] = unescapeCommandProperty(v)
|
||||
kvPairs[k] = unescape(v)
|
||||
}
|
||||
return kvPairs
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user