mirror of
https://gitea.com/gitea/act_runner
synced 2026-09-21 19:37:07 +02:00
fix(deps): update to actionslib v1.0.0 (#1221)
Updates `gitea.dev/actionslib` to v1.0.0, see https://gitea.com/gitea/actionslib/pulls/17: 1. First-party expression parser passing GitHub's full conformance suite, replacing actionlint 1. Expression results match GitHub, including truthiness, numbers, `fromJSON`, `toJSON` and `hashFiles` cache keys 1. Whole-value `${{ }}` works for `strategy`, `env`, `with`, `services` and `outputs` 1. Matrix validation matches GitHub --------- Co-authored-by: bircni <bircni@icloud.com> Reviewed-on: https://gitea.com/gitea/runner/pulls/1221 Reviewed-by: bircni <bircni@icloud.com> Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
committed by
bircni
co-authored by
bircni
parent
e0ce6776c7
commit
3d116eb0c2
@@ -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"},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+26
-23
@@ -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,23 +204,26 @@ 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 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matrixes, err := job.GetMatrixes()
|
||||
if err != nil {
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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") },
|
||||
|
||||
@@ -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
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user