mirror of
https://gitea.com/gitea/runner.git
synced 2026-08-28 06:47:45 +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:
@@ -7,12 +7,15 @@ package runner
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
"gitea.com/gitea/runner/act/container"
|
||||
|
||||
"gitea.dev/actionslib/pkg/model"
|
||||
log "github.com/sirupsen/logrus"
|
||||
logrustest "github.com/sirupsen/logrus/hooks/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -404,3 +407,92 @@ func TestRunStepExecutorDoesNotLeakRefusalToNextStep(t *testing.T) {
|
||||
errB := runStepExecutor(stepB, stepStageMain, func(context.Context) error { return nil })(ctx)
|
||||
require.NoError(t, errB)
|
||||
}
|
||||
|
||||
func TestRunStepExecutorParity(t *testing.T) {
|
||||
newStep := func(t *testing.T, stepModel *model.Step) *stepRun {
|
||||
rc := createRunContext(t)
|
||||
rc.JobContainer = &container.HostEnvironment{ActPath: t.TempDir()}
|
||||
rc.ExprEval = rc.NewExpressionEvaluator(context.Background())
|
||||
return &stepRun{RunContext: rc, Step: stepModel, env: map[string]string{}}
|
||||
}
|
||||
badExpression := "${{ 'test' != test }}"
|
||||
for _, test := range []struct {
|
||||
name, wantError string
|
||||
step *model.Step
|
||||
executor common.Executor
|
||||
}{
|
||||
{"condition error", "if-expression", &model.Step{ID: "condition", If: yaml.Node{Value: badExpression}}, noopExecutor},
|
||||
{"continue-on-error expression error", "continue-on-error expression", &model.Step{ID: "continue", RawContinueOnError: badExpression}, common.NewErrorExecutor(assert.AnError)},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
step := newStep(t, test.step)
|
||||
logger, hook := logrustest.NewNullLogger()
|
||||
err := runStepExecutor(step, stepStageMain, test.executor)(common.WithLogger(context.Background(), logger))
|
||||
|
||||
require.ErrorContains(t, err, test.wantError)
|
||||
assert.Equal(t, model.StepStatusFailure, step.RunContext.StepResults[test.step.ID].Conclusion)
|
||||
assert.Equal(t, model.StepStatusFailure, hook.LastEntry().Data["stepResult"])
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("file command error honors continue-on-error", func(t *testing.T) {
|
||||
step := newStep(t, &model.Step{ID: "commands", RawContinueOnError: "true"})
|
||||
logger, hook := logrustest.NewNullLogger()
|
||||
err := runStepExecutor(step, stepStageMain, func(context.Context) error {
|
||||
require.NoError(t, os.WriteFile(step.env["GITHUB_ENV"], []byte("GOOD=1\nmalformed\n"), 0o600))
|
||||
require.NoError(t, os.WriteFile(step.env["GITHUB_OUTPUT"], []byte("kept=value\n"), 0o600))
|
||||
require.NoError(t, os.WriteFile(step.env["GITHUB_STATE"], []byte("saved=value\n"), 0o600))
|
||||
return nil
|
||||
})(common.WithLogger(context.Background(), logger))
|
||||
|
||||
require.NoError(t, err)
|
||||
result := step.RunContext.StepResults[step.Step.ID]
|
||||
assert.Equal(t, model.StepStatusFailure, result.Outcome)
|
||||
assert.Equal(t, model.StepStatusSuccess, result.Conclusion)
|
||||
assert.Equal(t, model.StepStatusSuccess, hook.LastEntry().Data["stepResult"])
|
||||
assert.Equal(t, "1", step.RunContext.Env["GOOD"])
|
||||
assert.Equal(t, "value", result.Outputs["kept"])
|
||||
assert.Equal(t, "value", step.RunContext.IntraActionState[step.Step.ID]["saved"])
|
||||
for _, name := range []string{"GITHUB_ENV", "GITHUB_OUTPUT", "GITHUB_STATE", "GITHUB_PATH"} {
|
||||
contents, readErr := os.ReadFile(step.env[name])
|
||||
require.NoError(t, readErr)
|
||||
assert.Empty(t, contents)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("composite child files do not leak", func(t *testing.T) {
|
||||
outer := newStep(t, &model.Step{ID: "outer"})
|
||||
childRC := &RunContext{
|
||||
Config: outer.RunContext.Config, Run: outer.RunContext.Run, Env: map[string]string{}, StepResults: map[string]*model.StepResult{},
|
||||
JobContainer: outer.RunContext.JobContainer, Parent: outer.RunContext,
|
||||
}
|
||||
childRC.ExprEval = childRC.NewExpressionEvaluator(context.Background())
|
||||
child := &stepRun{RunContext: childRC, Step: &model.Step{ID: "child"}, env: map[string]string{}}
|
||||
|
||||
err := runStepExecutor(outer, stepStageMain, func(ctx context.Context) error {
|
||||
require.NoError(t, runStepExecutor(child, stepStageMain, func(context.Context) error {
|
||||
require.NoError(t, os.WriteFile(child.env["GITHUB_OUTPUT"], []byte("declared=child\nundeclared=leak\n"), 0o600))
|
||||
require.NoError(t, os.WriteFile(child.env["GITHUB_STATE"], []byte("saved=child\n"), 0o600))
|
||||
return nil
|
||||
})(ctx))
|
||||
outer.RunContext.setOutput(ctx, map[string]string{"name": "declared"}, "outer")
|
||||
return nil
|
||||
})(context.Background())
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[string]string{"declared": "outer"}, outer.RunContext.StepResults[outer.Step.ID].Outputs)
|
||||
assert.Equal(t, map[string]string{"declared": "child", "undeclared": "leak"}, childRC.StepResults[child.Step.ID].Outputs)
|
||||
assert.Empty(t, outer.RunContext.IntraActionState)
|
||||
assert.Equal(t, "child", childRC.IntraActionState[child.Step.ID]["saved"])
|
||||
})
|
||||
|
||||
t.Run("timeout must be positive", func(t *testing.T) {
|
||||
exprEval := createRunContext(t).NewExpressionEvaluator(context.Background())
|
||||
for timeout, wantDeadline := range map[string]bool{"-1": false, "0": false, "1": true} {
|
||||
ctx, cancel := evaluateStepTimeout(context.Background(), exprEval, &model.Step{TimeoutMinutes: timeout})
|
||||
_, hasDeadline := ctx.Deadline()
|
||||
cancel()
|
||||
assert.Equal(t, wantDeadline, hasDeadline, timeout)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user