diff --git a/act/runner/action_composite_test.go b/act/runner/action_composite_test.go index 29c8c36f..e71d30bb 100644 --- a/act/runner/action_composite_test.go +++ b/act/runner/action_composite_test.go @@ -17,7 +17,7 @@ import ( func TestCompositeActionParity(t *testing.T) { t.Run("inherits contexts without leaking inputs", func(t *testing.T) { ctx := t.Context() - strategy := &model.Strategy{MaxParallel: 3} + strategy := &model.Strategy{MaxParallelString: "3"} parent := &RunContext{ Config: &Config{}, Matrix: map[string]any{"os": "linux"}, diff --git a/act/runner/expression.go b/act/runner/expression.go index 7502fcf9..d2106235 100644 --- a/act/runner/expression.go +++ b/act/runner/expression.go @@ -27,6 +27,29 @@ import ( "go.yaml.in/yaml/v4" ) +// setStrategyContext leaves `max-parallel` unset when the job declares none, as GitHub does. +func setStrategyContext(strategy map[string]any, jobStrategy *model.Strategy) { + strategy["fail-fast"] = jobStrategy.GetFailFast() + if limit, declared, err := jobStrategy.ParseMaxParallel(); declared && err == nil { + strategy["max-parallel"] = limit + } +} + +// decodeDeferred decodes a whole-value `${{ }}` parked in a Raw* field, evaluating a clone since the node is shared across matrix combinations. +func decodeDeferred[T any](ctx context.Context, eval *expressionEvaluator, name string, raw yaml.Node, out *T) error { + if raw.Kind != yaml.ScalarNode { + return nil + } + node := model.CloneYamlNode(raw) + if err := eval.EvaluateYamlNode(ctx, &node); err != nil { + return fmt.Errorf("unable to evaluate %s: %w", name, err) + } + if err := node.Decode(out); err != nil { + return fmt.Errorf("unable to decode %s: %w", name, err) + } + return nil +} + // NewExpressionEvaluator creates a new evaluator func (rc *RunContext) NewExpressionEvaluator(ctx context.Context) *ExpressionEvaluator { return rc.NewExpressionEvaluatorWithEnv(ctx, rc.GetEnv()) @@ -41,8 +64,7 @@ func (rc *RunContext) NewExpressionEvaluatorWithEnv(ctx context.Context, env map if rc.Run != nil { job := rc.Run.Job() if job != nil && job.Strategy != nil { - strategy["fail-fast"] = job.Strategy.FailFast - strategy["max-parallel"] = job.Strategy.MaxParallel + setStrategyContext(strategy, job.Strategy) } jobs := rc.Run.Workflow.Jobs @@ -118,8 +140,7 @@ func (rc *RunContext) newStepExpressionEvaluator(ctx context.Context, step step, job := rc.Run.Job() strategy := make(map[string]any) if job.Strategy != nil { - strategy["fail-fast"] = job.Strategy.FailFast - strategy["max-parallel"] = job.Strategy.MaxParallel + setStrategyContext(strategy, job.Strategy) } jobs := rc.Run.Workflow.Jobs @@ -333,12 +354,16 @@ func (rc *RunContext) resolveWorkflowCall(ctx context.Context) error { } callerJob := rc.caller.runContext.Run.Job() callerEval := rc.caller.runContext.ExprEval + callerWith := callerJob.With + if err := decodeDeferred(ctx, callerEval, "workflow inputs", callerJob.RawWith, &callerWith); err != nil { + return err + } calleeEval := sync.OnceValue(func() *expressionEvaluator { return rc.NewExpressionEvaluator(ctx) }) config := rc.Run.Workflow.WorkflowCallConfig() rc.workflowCallInputs = make(map[string]any, len(config.Inputs)) for name, input := range config.Inputs { - value, eval, label := callerJob.With[name], callerEval, "input" + value, eval, label := callerWith[name], callerEval, "input" if value == nil { value, eval, label = input.Default, calleeEval(), "the default of input" } diff --git a/act/runner/expression_test.go b/act/runner/expression_test.go index 56c82c66..31f003a0 100644 --- a/act/runner/expression_test.go +++ b/act/runner/expression_test.go @@ -89,7 +89,7 @@ func TestEvaluateRunContext(t *testing.T) { out any errMesg string }{ - {" 1 ", 1, ""}, + {" 1 ", 1.0, ""}, // {"1 + 3", "4", ""}, // {"(1 + 3) * -2", "-8", ""}, {"'my text'", "my text", ""}, @@ -266,7 +266,6 @@ func TestInterpolate(t *testing.T) { {"${{ null }}", ""}, {"${{ fromJSON('[1,2]') }}", "Array"}, {"${{ fromJSON('{\"a\":1}') }}", "Object"}, - {"${{ 1", "${{ 1"}, } for _, table := range tables { @@ -277,7 +276,7 @@ func TestInterpolate(t *testing.T) { }) } - for _, in := range []string{"${{ 1) && (2 }}", "run ${{ 1) && (2 }} now"} { + for _, in := range []string{"${{ 1) && (2 }}", "run ${{ 1) && (2 }} now", "${{ 1"} { _, err := ee.Interpolate(context.Background(), in) assert.Error(t, err, in) } @@ -360,6 +359,32 @@ on: }) } } + + t.Run("deferred workflow call inputs", func(t *testing.T) { + workflow, err := model.ReadWorkflow(strings.NewReader(workflows["workflow_call"] + ` +jobs: + test: {} + call: + uses: ./reuse.yml + with: ${{ fromJSON(matrix.args) }} +`)) + require.NoError(t, err) + workflow.GetJob("test") + job := workflow.GetJob("call") + rawWith := model.CloneYamlNode(job.RawWith) + for _, value := range []string{"true", "false"} { + t.Run(value, func(t *testing.T) { + t.Parallel() + parent, err := (&runnerImpl{config: &Config{}}).newRunContext(t.Context(), &model.Run{Workflow: workflow, JobID: "call"}, map[string]any{"args": `{"flag":` + value + `,"name":"runner"}`}) + require.NoError(t, err) + child, err := (&runnerImpl{config: &Config{}, caller: &caller{runContext: parent}}).newRunContext(t.Context(), &model.Run{Workflow: workflow, JobID: "test"}, nil) + require.NoError(t, err) + assert.Equal(t, map[string]any{"flag": value == "true", "name": "runner"}, child.workflowCallInputs) + assert.Equal(t, rawWith, job.RawWith) + assert.Nil(t, job.With) + }) + } + }) } func TestJobNameMasksSecrets(t *testing.T) { diff --git a/act/runner/job_executor.go b/act/runner/job_executor.go index 268ffd96..78a5cd2e 100644 --- a/act/runner/job_executor.go +++ b/act/runner/job_executor.go @@ -417,6 +417,14 @@ func setJobResult(ctx context.Context, info jobInfo, rc *RunContext, success boo // evaluateJobEnvAndDefaults resolves the job's env and defaults.run once, as GitHub does at job setup. func evaluateJobEnvAndDefaults(ctx context.Context, rc *RunContext) error { rc.ExprEval = rc.NewExpressionEvaluator(ctx) + var workflowEnv map[string]string + if err := decodeDeferred(ctx, rc.ExprEval, "workflow env", rc.Run.Workflow.RawEnv, &workflowEnv); err != nil { + return err + } + if workflowEnv != nil { + rc.Env = mergeMaps(workflowEnv, rc.GetEnv()) + rc.ExprEval = rc.NewExpressionEvaluator(ctx) + } var err error for k, v := range rc.GetEnv() { if rc.Env[k], err = rc.ExprEval.Interpolate(ctx, v); err != nil { diff --git a/act/runner/max_parallel_test.go b/act/runner/max_parallel_test.go index afa4b455..821fd802 100644 --- a/act/runner/max_parallel_test.go +++ b/act/runner/max_parallel_test.go @@ -4,6 +4,7 @@ package runner import ( + "strings" "testing" "gitea.dev/actionslib/pkg/model" @@ -34,9 +35,14 @@ func TestMaxParallelStrategy(t *testing.T) { expectedMaxParallel: 4, }, { - name: "max-parallel-10", + name: "max-parallel-10-clamped-to-combinations", maxParallelString: "10", - expectedMaxParallel: 10, + expectedMaxParallel: 5, + }, + { + name: "max-parallel-invalid-falls-back-to-default", + maxParallelString: "tow", + expectedMaxParallel: 4, }, } @@ -61,9 +67,29 @@ func TestMaxParallelStrategy(t *testing.T) { assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act assert.NotNil(t, matrixes) assert.Len(t, matrixes, 5) - assert.Equal(t, tt.expectedMaxParallel, job.Strategy.MaxParallel) + assert.Equal(t, tt.expectedMaxParallel, maxParallelFor(job.Strategy, len(matrixes))) }) } + + t.Run("deferred strategy", func(t *testing.T) { + workflow, err := model.ReadWorkflow(strings.NewReader(` +jobs: + test: + if: false + strategy: ${{ fromJSON('{"max-parallel":2,"fail-fast":false,"matrix":{"os":["ubuntu","windows"]}}') }} +`)) + require.NoError(t, err) + job := workflow.Jobs["test"] + rawStrategy := model.CloneYamlNode(job.RawStrategy) + require.NoError(t, (&runnerImpl{config: &Config{}}).NewPlanExecutor(&model.Plan{Stages: []*model.Stage{{Runs: []*model.Run{{Workflow: workflow, JobID: "test"}}}}})(t.Context())) + require.NotNil(t, job.Strategy) + assert.False(t, job.Strategy.GetFailFast()) + assert.Equal(t, 2, maxParallelFor(job.Strategy, 5)) + matrixes, err := job.GetMatrixes() + require.NoError(t, err) + assert.Equal(t, []map[string]any{{"os": "ubuntu"}, {"os": "windows"}}, matrixes) + assert.Equal(t, rawStrategy, job.RawStrategy) + }) } func TestNewPlanExecutorInvalidMatrix(t *testing.T) { @@ -81,4 +107,15 @@ func TestNewPlanExecutorInvalidMatrix(t *testing.T) { runner := &runnerImpl{config: &Config{}} require.ErrorContains(t, runner.NewPlanExecutor(plan)(t.Context()), "could not get job matrix:") + + for _, strategy := range []string{ + `${{ fromJSON('invalid') }}`, + `${{ '${{ inputs.unresolved }}' }}`, + } { + t.Run(strategy, func(t *testing.T) { + workflow, err := model.ReadWorkflow(strings.NewReader("jobs:\n test:\n strategy: " + strategy)) + require.NoError(t, err) + require.Error(t, runner.NewPlanExecutor(&model.Plan{Stages: []*model.Stage{{Runs: []*model.Run{{Workflow: workflow, JobID: "test"}}}}})(t.Context())) + }) + } } diff --git a/act/runner/run_context.go b/act/runner/run_context.go index b059d81d..d4db782a 100644 --- a/act/runner/run_context.go +++ b/act/runner/run_context.go @@ -495,8 +495,11 @@ func (rc *RunContext) startJobContainer() common.Executor { networkName, createAndDeleteNetwork := rc.networkNameForGitea() rc.cleanUpJobContainer = rc.cleanupJobResources(networkName, createAndDeleteNetwork, false) - // add service containers - for serviceID, spec := range rc.Run.Job().Services { + services := rc.Run.Job().Services + if err := decodeDeferred(ctx, rc.ExprEval, "job services", rc.Run.Job().RawServices, &services); err != nil { + return err + } + for serviceID, spec := range services { // GitHub compatibility: skip services whose image evaluates to an // empty string, enabling conditional services via expressions serviceImage, err := rc.ExprEval.Interpolate(ctx, spec.Image) @@ -1022,6 +1025,15 @@ func (rc *RunContext) interpolateOutputs() common.Executor { // with its own resolved values (last wins, as on GitHub) instead of the first combo's // resolved values freezing the shared template against later combos. // Resolved up front so one failure publishes none of them, as GitHub does. + var deferredOutputs map[string]string + if err := decodeDeferred(ctx, ee, "job outputs", job.RawOutputs, &deferredOutputs); err != nil { + return err + } + if deferredOutputs != nil { + defer lockJob(job)() + job.Outputs = deferredOutputs + return nil + } outputs := make(map[string]string, len(rc.outputTemplate)) var err error for k, v := range rc.outputTemplate { diff --git a/act/runner/run_context_test.go b/act/runner/run_context_test.go index 7d559b89..509824af 100644 --- a/act/runner/run_context_test.go +++ b/act/runner/run_context_test.go @@ -372,6 +372,19 @@ jobs: require.Empty(t, redis.WorkingDir) } +// A whole-value `services:` expression only reaches the typed field through DecodeRaw. +func TestStartJobContainerGivesServicesTheirVolumesFromExpression(t *testing.T) { + redis := startJobContainerInputs(t, ` +jobs: + job: + services: ${{ fromJSON('{"redis":{"image":"redis:latest","volumes":["data:/data"]}}') }} +`, &Config{ValidVolumes: []string{"data"}})[0] + + require.Equal(t, "redis:latest", redis.Image) + require.Equal(t, []string{"data"}, redis.ValidVolumes) + require.Equal(t, map[string]string{"data": "/data"}, redis.Mounts) +} + // Only the workflow's options may be stripped later, so the two sources have to reach the // container apart from each other. func TestStartJobContainerKeepsRunnerOptionsApartFromWorkflowOptions(t *testing.T) { @@ -827,6 +840,20 @@ func TestInterpolateOutputsIsPerMatrixCombo(t *testing.T) { require.Equal(t, "b", job.Outputs["o"]) } +// A whole-value `outputs:` expression only reaches the typed field through DecodeRaw. +func TestInterpolateOutputsFromExpression(t *testing.T) { + var rawOutputs yaml.Node + require.NoError(t, rawOutputs.Encode(`${{ fromJSON('{"o":"resolved"}') }}`)) + + job := &model.Job{RawOutputs: rawOutputs} + run := &model.Run{JobID: "j", Workflow: &model.Workflow{Name: "w", Jobs: map[string]*model.Job{"j": job}}} + rc, err := (&runnerImpl{config: &Config{}}).newRunContext(t.Context(), run, nil) + require.NoError(t, err) + + require.NoError(t, rc.interpolateOutputs()(t.Context())) + require.Equal(t, "resolved", job.Outputs["o"]) +} + func TestGetGitHubContext(t *testing.T) { log.SetLevel(log.DebugLevel) diff --git a/act/runner/runner.go b/act/runner/runner.go index 91102930..aaec6a93 100644 --- a/act/runner/runner.go +++ b/act/runner/runner.go @@ -19,6 +19,7 @@ import ( "gitea.dev/actionslib/pkg/model" docker_container "github.com/moby/moby/api/types/container" log "github.com/sirupsen/logrus" + "go.yaml.in/yaml/v4" ) // Config contains the config for a new runner @@ -155,6 +156,18 @@ func (runner *runnerImpl) configure() (*runnerImpl, error) { return runner, nil } +func maxParallelFor(strategy *model.Strategy, combinations int) int { + maxParallel := 4 // actionslib has no default for an undeclared max-parallel + if strategy != nil { + if limit, declared, err := strategy.ParseMaxParallel(); err != nil { + log.Errorf("Ignoring invalid max-parallel: %v", err) + } else if declared { + maxParallel = limit + } + } + return min(maxParallel, combinations) +} + // NewPlanExecutor ... func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor { maxJobNameLen := 0 @@ -191,21 +204,24 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor { log.Debugf("Job.With: %v", job.With) log.Debugf("Job.Result: %v", job.Result) - if job.Strategy != nil { - log.Debugf("Job.Strategy.FailFast: %v", job.Strategy.FailFast) - log.Debugf("Job.Strategy.MaxParallel: %v", job.Strategy.MaxParallel) - log.Debugf("Job.Strategy.FailFastString: %v", job.Strategy.FailFastString) - log.Debugf("Job.Strategy.MaxParallelString: %v", job.Strategy.MaxParallelString) - log.Debugf("Job.Strategy.RawMatrix: %v", job.Strategy.RawMatrix) - + if job.Strategy != nil || job.RawStrategy.Kind == yaml.ScalarNode { strategyRc, err := runner.newRunContext(ctx, run, nil) if err != nil { return err } - // Resolve template expressions in the matrix node before Matrix() is called. - // On failure the literal string is kept and normalizeMatrixValue wraps it as a fallback. - if err := strategyRc.NewExpressionEvaluator(ctx).EvaluateYamlNode(ctx, &job.Strategy.RawMatrix); err != nil { - log.Errorf("Error while evaluating matrix: %v", err) + if job.RawStrategy.Kind == yaml.ScalarNode { + if err := decodeDeferred(ctx, strategyRc.ExprEval, "job strategy", job.RawStrategy, &job.Strategy); err != nil { + return err + } + } else { + log.Debugf("Job.Strategy.FailFast: %v", job.Strategy.GetFailFast()) + log.Debugf("Job.Strategy.FailFastString: %v", job.Strategy.FailFastString) + log.Debugf("Job.Strategy.MaxParallelString: %v", job.Strategy.MaxParallelString) + log.Debugf("Job.Strategy.RawMatrix: %v", job.Strategy.RawMatrix) + // An unevaluated expression is left in place, which GetMatrixes below rejects. + if err := strategyRc.NewExpressionEvaluator(ctx).EvaluateYamlNode(ctx, &job.Strategy.RawMatrix); err != nil { + log.Errorf("Error while evaluating matrix: %v", err) + } } } @@ -215,20 +231,7 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor { } log.Debugf("Job Matrices: %v", matrixes) - maxParallel := 4 - if job.Strategy != nil { - // Ensure GetMaxParallel() is called if MaxParallel is still 0 - if job.Strategy.MaxParallel == 0 { - job.Strategy.MaxParallel = job.Strategy.GetMaxParallel() - } - maxParallel = job.Strategy.MaxParallel - log.Debugf("Using job.Strategy.MaxParallel: %d", maxParallel) - } - - if len(matrixes) < maxParallel { - log.Debugf("Adjusting maxParallel from %d to %d (number of matrix combinations)", maxParallel, len(matrixes)) - maxParallel = len(matrixes) - } + maxParallel := maxParallelFor(job.Strategy, len(matrixes)) log.Infof("Running job with maxParallel=%d for %d matrix combinations", maxParallel, len(matrixes)) diff --git a/act/runner/step.go b/act/runner/step.go index 962679e0..9daf0d87 100644 --- a/act/runner/step.go +++ b/act/runner/step.go @@ -19,6 +19,7 @@ import ( "gitea.dev/actionslib/pkg/exprparser" "gitea.dev/actionslib/pkg/model" + "go.yaml.in/yaml/v4" ) type step interface { @@ -261,6 +262,14 @@ func setupEnv(ctx context.Context, step step) error { } } } + if step.getStepModel().RawWith.Kind == yaml.ScalarNode { + decoded := &model.Step{} + if err := decodeDeferred(ctx, inputEval(), "with", step.getStepModel().RawWith, &decoded.With); err != nil { + return err + } + step.getStepModel().With = decoded.With + mergeIntoMap(step, step.getEnv(), decoded.GetEnv()) + } return nil } diff --git a/act/runner/step_test.go b/act/runner/step_test.go index 0659ac19..1d9c4a27 100644 --- a/act/runner/step_test.go +++ b/act/runner/step_test.go @@ -8,6 +8,7 @@ import ( "context" "errors" "os" + "strings" "testing" "gitea.com/gitea/runner/act/common" @@ -205,6 +206,55 @@ func TestSetupEnv(t *testing.T) { }, env) cm.AssertExpectations(t) + + for _, expression := range []string{"inputs.args", "env.ARGS"} { + t.Run("deferred inputs from "+expression, func(t *testing.T) { + workflow, err := model.ReadWorkflow(strings.NewReader(` +env: ${{ fromJSON(matrix.env) }} +jobs: + test: + env: + PRIORITY: job + steps: + - uses: ./act + env: + ARGS: ${{ inputs.args }} + with: ${{ fromJSON(` + expression + `) }} +`)) + require.NoError(t, err) + job := workflow.GetJob("test") + rawEnv := model.CloneYamlNode(workflow.RawEnv) + rawWith := model.CloneYamlNode(job.Steps[0].RawWith) + for _, value := range []string{"first", "second"} { + t.Run(value, func(t *testing.T) { + t.Parallel() + rc, err := (&runnerImpl{config: &Config{}}).newRunContext(t.Context(), &model.Run{Workflow: workflow, JobID: "test"}, map[string]any{"env": `{"WORKFLOW":"` + value + `","PRIORITY":"workflow"}`}) + require.NoError(t, err) + rc.workflowCallInputs = map[string]any{"args": `{"name":"` + value + `","fetch-depth":2}`} + require.NoError(t, evaluateJobEnvAndDefaults(t.Context(), rc)) + step := &stepRun{RunContext: rc, Step: job.Steps[0].Clone(), env: map[string]string{}} + require.NoError(t, setupEnv(t.Context(), step)) + assert.Equal(t, map[string]string{"ARGS": `${{ inputs.args }}`, "INPUT_NAME": value, "INPUT_FETCH-DEPTH": "2"}, step.Step.GetEnv()) + assert.Equal(t, value, step.env["INPUT_NAME"]) + assert.Equal(t, value, step.env["WORKFLOW"]) + assert.Equal(t, "job", step.env["PRIORITY"]) + assert.Equal(t, rawEnv, workflow.RawEnv) + assert.Equal(t, rawWith, job.Steps[0].RawWith) + assert.Nil(t, workflow.Env) + assert.Nil(t, job.Steps[0].With) + }) + } + }) + } + + t.Run("rejects inputs that still contain an expression", func(t *testing.T) { + workflow, err := model.ReadWorkflow(strings.NewReader("jobs:\n test:\n steps:\n - uses: ./act\n with: ${{ inputs.args }}\n")) + require.NoError(t, err) + rc, err := (&runnerImpl{config: &Config{}}).newRunContext(t.Context(), &model.Run{Workflow: workflow, JobID: "test"}, nil) + require.NoError(t, err) + rc.workflowCallInputs = map[string]any{"args": "${{ inputs.unresolved }}"} + require.ErrorContains(t, setupEnv(t.Context(), &stepRun{RunContext: rc, Step: workflow.Jobs["test"].Steps[0].Clone(), env: map[string]string{}}), "with:") + }) } func TestIsStepEnabled(t *testing.T) { diff --git a/go.mod b/go.mod index 42c087b5..54b90f68 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.27.1 require ( connectrpc.com/connect v1.21.0 dario.cat/mergo v1.0.2 - gitea.dev/actionslib v0.7.0 + gitea.dev/actionslib v1.0.0 github.com/avast/retry-go/v5 v5.0.0 github.com/containerd/errdefs v1.0.0 github.com/creack/pty v1.1.24 @@ -37,7 +37,7 @@ require ( github.com/stretchr/testify v1.12.1 github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928 go.etcd.io/bbolt v1.5.0 - go.yaml.in/yaml/v4 v4.0.0-rc.3 + go.yaml.in/yaml/v4 v4.0.0-rc.6 golang.org/x/net v0.59.0 golang.org/x/sync v0.23.0 golang.org/x/sys v0.48.0 @@ -55,14 +55,12 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cloudflare/circl v1.6.5 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/cyphar/filepath-securejoin v0.7.0 // indirect github.com/docker/docker-credential-helpers v0.9.8 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/fatih/color v1.19.0 // indirect github.com/felixge/httpsnoop v1.1.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-logr/logr v1.4.4 // indirect @@ -75,9 +73,6 @@ require ( github.com/kevinburke/ssh_config v1.6.0 // indirect github.com/klauspost/compress v1.19.2 // indirect github.com/klauspost/cpuid/v2 v2.4.0 // indirect - github.com/mattn/go-colorable v0.1.15 // indirect - github.com/mattn/go-runewidth v0.0.28 // indirect - github.com/mattn/go-shellwords v1.0.14 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/sys/sequential v0.7.0 // indirect github.com/moby/sys/user v0.4.1 // indirect @@ -87,8 +82,6 @@ require ( github.com/pjbgf/sha1cd v0.6.0 // indirect github.com/prometheus/common v0.70.1 // indirect github.com/prometheus/procfs v0.21.1 // indirect - github.com/rhysd/actionlint v1.7.12 // indirect - github.com/robfig/cron/v3 v3.0.1 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect github.com/sergi/go-diff v1.4.0 // indirect github.com/skeema/knownhosts v1.3.2 // indirect diff --git a/go.sum b/go.sum index d1c40bcc..2bf640b4 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ cyphar.com/go-pathrs v0.2.5 h1:SnX9FBvnoyn3lUs1dkMgZ52bAETpirNu3FTRh5HlRik= cyphar.com/go-pathrs v0.2.5/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= -gitea.dev/actionslib v0.7.0 h1:JCV8eeIGwjlXcuSr7ojEdQC22VoE2466+K+D9vuQWKQ= -gitea.dev/actionslib v0.7.0/go.mod h1:DI3Lqp+8TrycM7/semMdqsDQOHaBskjIAuMn+SXfZJ0= +gitea.dev/actionslib v1.0.0 h1:l0oFJP+P4Ds1rlCI5zk618dYkuBc2mU7Gz5wPeG0lZY= +gitea.dev/actionslib v1.0.0/go.mod h1:6O8YHkqVTKSR0LL2e5VhIDePYzGTZCbfmSVqJWEhk9g= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= @@ -25,8 +25,6 @@ github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6 github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= -github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA= github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -58,8 +56,6 @@ github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= -github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= @@ -114,14 +110,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= -github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= -github.com/mattn/go-runewidth v0.0.28 h1:rPyg2ybwEKPebvpzVWe1gKBkH8EQFkxO4Y0hjBeLaBU= -github.com/mattn/go-runewidth v0.0.28/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= -github.com/mattn/go-shellwords v1.0.14 h1:yUKzIgsCnosndOASY6/enly1EAuaXeFSQ7cdyA3OuYg= -github.com/mattn/go-shellwords v1.0.14/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/go-archive v0.3.3 h1:OxxR9paxsluYi+zDUEXTTaIxtkK3viymW+Ka7vRhhME= @@ -165,10 +155,6 @@ github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc= github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= -github.com/rhysd/actionlint v1.7.12 h1:vQ4GeJN86C0QH+gTUQcs8McmK62OLT3kmakPMtEWYnY= -github.com/rhysd/actionlint v1.7.12/go.mod h1:krOUhujIsJusovkaYzQ/VNH8PFexjNKqU0q5XI/4w+g= -github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= -github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -227,8 +213,8 @@ go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= -go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go= -go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4= +go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 94c5439a..c6aab9fa 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -391,10 +391,10 @@ func warnUnknownKeys(file string, content []byte) { decoder := yaml.NewDecoder(bytes.NewReader(content)) decoder.KnownFields(true) - var typeErr *yaml.TypeError - if err := decoder.Decode(&Config{}); errors.As(err, &typeErr) { - for _, message := range typeErr.Errors { - log.Warnf("config file %q: %s, it will be ignored", file, message) + var loadErrs *yaml.LoadErrors + if err := decoder.Decode(&Config{}); errors.As(err, &loadErrs) { + for _, loadErr := range loadErrs.Errors { + log.Warnf("config file %q: %s, it will be ignored", file, loadErr) } } } diff --git a/internal/pkg/config/edit.go b/internal/pkg/config/edit.go index f3310f3e..0b38ccf1 100644 --- a/internal/pkg/config/edit.go +++ b/internal/pkg/config/edit.go @@ -312,7 +312,19 @@ func allScalars(nodes []*yaml.Node) bool { return true } +// quoteLeadingNewlines keeps a scalar starting with a newline out of block style, whose indentation indicator the encoder drops. +// TODO: remove once https://github.com/yaml/go-yaml/pull/396 is released. +func quoteLeadingNewlines(node *yaml.Node) { + if node.Kind == yaml.ScalarNode && strings.HasPrefix(node.Value, "\n") { + node.Style = yaml.DoubleQuotedStyle + } + for _, child := range node.Content { + quoteLeadingNewlines(child) + } +} + func encodeYAML(node *yaml.Node) ([]byte, error) { + quoteLeadingNewlines(node) var buf bytes.Buffer encoder := yaml.NewEncoder(&buf) encoder.SetIndent(2) diff --git a/internal/pkg/config/edit_test.go b/internal/pkg/config/edit_test.go index 9c33b302..e1284884 100644 --- a/internal/pkg/config/edit_test.go +++ b/internal/pkg/config/edit_test.go @@ -74,6 +74,13 @@ func TestEditValues(t *testing.T) { assert.Equal(t, map[string]string{"EXISTING": "value", "ADDED": "yes"}, cfg.Runner.Envs) }, }, + { + name: "set map entry starting with a newline before an indented line", + edit: func(file string) error { return SetValue(file, "runner.envs.ADDED", "\n indented\nplain\n") }, + assert: func(t *testing.T, cfg *Config, _ string) { + assert.Equal(t, "\n indented\nplain\n", cfg.Runner.Envs["ADDED"]) + }, + }, { name: "set replaces a list", edit: func(file string) error { return SetValue(file, "runner.labels", "one", "two") }, diff --git a/renovate.json5 b/renovate.json5 index 6ff4eaae..f4feb828 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -3,10 +3,4 @@ "extends": [ "local>gitea/renovate-config" ], - "packageRules": [ - { - "matchPackageNames": ["go.yaml.in/yaml/v4"], - "allowedVersions": "<4.0.0-rc.4", // rc.4 removes the error types actionlint builds against - }, - ], }