diff --git a/act/jobparser/interpeter.go b/act/jobparser/interpeter.go index e82444e4..362e18cc 100644 --- a/act/jobparser/interpeter.go +++ b/act/jobparser/interpeter.go @@ -76,6 +76,36 @@ func NewInterpeter( return exprparser.NewInterpeter(ee, config) } +// Returns an interpeter used in the server in the context of workflow-level templates. Needs github, inputs, and vars +// context only. +func NewWorkflowInterpeter( + gitCtx *model.GithubContext, + vars map[string]string, + inputs map[string]any, +) exprparser.Interpreter { + ee := &exprparser.EvaluationEnvironment{ + Github: gitCtx, + Env: nil, // no need + Job: nil, // no need + Steps: nil, // no need + Runner: nil, // no need + Secrets: nil, // no need + Strategy: nil, // no need + Matrix: nil, // no need + Needs: nil, // no need + Inputs: inputs, + Vars: vars, + } + + config := exprparser.Config{ + Run: nil, + WorkingDir: "", // WorkingDir is used for the function hashFiles, but it's not needed in the server + Context: "workflow", + } + + return exprparser.NewInterpeter(ee, config) +} + // JobResult is the minimum requirement of job results for Interpeter type JobResult struct { Needs []string diff --git a/act/jobparser/model.go b/act/jobparser/model.go index ed6e5ece..e94123e3 100644 --- a/act/jobparser/model.go +++ b/act/jobparser/model.go @@ -1,7 +1,6 @@ package jobparser import ( - "bytes" "fmt" "code.forgejo.org/forgejo/runner/v11/act/model" @@ -193,83 +192,32 @@ func (evt *Event) Schedules() []map[string]string { return evt.schedules } -func ReadWorkflowRawConcurrency(content []byte) (*model.RawConcurrency, error) { - w := new(model.Workflow) - err := yaml.NewDecoder(bytes.NewReader(content)).Decode(w) - return w.RawConcurrency, err -} - -func EvaluateConcurrency(rc *model.RawConcurrency, jobID string, job *Job, gitCtx map[string]any, results map[string]*JobResult, vars map[string]string, inputs map[string]any) (string, bool, error) { - actJob := &model.Job{} - if job != nil { - actJob.Strategy = &model.Strategy{ - FailFastString: job.Strategy.FailFastString, - MaxParallelString: job.Strategy.MaxParallelString, - RawMatrix: job.Strategy.RawMatrix, - } - actJob.Strategy.FailFast = actJob.Strategy.GetFailFast() - actJob.Strategy.MaxParallel = actJob.Strategy.GetMaxParallel() - } - - matrix := make(map[string]any) - matrixes, err := actJob.GetMatrixes() - if err != nil { - return "", false, err - } - if len(matrixes) > 0 { - matrix = matrixes[0] - } - - evaluator := NewExpressionEvaluator(NewInterpeter(jobID, actJob, matrix, toGitContext(gitCtx), results, vars, inputs)) +// Convert the raw YAML from the `concurrency` block on a workflow into the evaluated concurrency group and +// cancel-in-progress value. This implementation only supports workflow-level concurrency definition, where we expect +// expressions to be able to access only the github, inputs and vars contexts. If RawConcurrency is empty, then the +// returned concurrency group will be "" and cancel-in-progress will be nil -- this can be used to distinguish from an +// explicit cancel-in-progress choice even if a group isn't specified. +func EvaluateWorkflowConcurrency(rc *model.RawConcurrency, gitCtx *model.GithubContext, vars map[string]string, inputs map[string]any) (string, *bool, error) { + evaluator := NewExpressionEvaluator(NewWorkflowInterpeter(gitCtx, vars, inputs)) var node yaml.Node if err := node.Encode(rc); err != nil { - return "", false, fmt.Errorf("failed to encode concurrency: %w", err) + return "", nil, fmt.Errorf("failed to encode concurrency: %w", err) } if err := evaluator.EvaluateYamlNode(&node); err != nil { - return "", false, fmt.Errorf("failed to evaluate concurrency: %w", err) + return "", nil, fmt.Errorf("failed to evaluate concurrency: %w", err) } var evaluated model.RawConcurrency if err := node.Decode(&evaluated); err != nil { - return "", false, fmt.Errorf("failed to unmarshal evaluated concurrency: %w", err) + return "", nil, fmt.Errorf("failed to unmarshal evaluated concurrency: %w", err) } if evaluated.RawExpression != "" { - return evaluated.RawExpression, false, nil + return evaluated.RawExpression, nil, nil } - return evaluated.Group, evaluated.CancelInProgress == "true", nil -} - -func toGitContext(input map[string]any) *model.GithubContext { - gitContext := &model.GithubContext{ - EventPath: asString(input["event_path"]), - Workflow: asString(input["workflow"]), - RunID: asString(input["run_id"]), - RunNumber: asString(input["run_number"]), - Actor: asString(input["actor"]), - Repository: asString(input["repository"]), - EventName: asString(input["event_name"]), - Sha: asString(input["sha"]), - Ref: asString(input["ref"]), - RefName: asString(input["ref_name"]), - RefType: asString(input["ref_type"]), - HeadRef: asString(input["head_ref"]), - BaseRef: asString(input["base_ref"]), - Token: asString(input["token"]), - Workspace: asString(input["workspace"]), - Action: asString(input["action"]), - ActionPath: asString(input["action_path"]), - ActionRef: asString(input["action_ref"]), - ActionRepository: asString(input["action_repository"]), - Job: asString(input["job"]), - RepositoryOwner: asString(input["repository_owner"]), - RetentionDays: asString(input["retention_days"]), + if evaluated.CancelInProgress == "" { + return evaluated.Group, nil, nil } - - event, ok := input["event"].(map[string]any) - if ok { - gitContext.Event = event - } - - return gitContext + cancelInProgress := evaluated.CancelInProgress == "true" + return evaluated.Group, &cancelInProgress, nil } func ParseRawOn(rawOn *yaml.Node) ([]*Event, error) { @@ -430,12 +378,3 @@ func parseMappingNode[T any](node *yaml.Node) ([]string, []T, error) { return scalars, datas, nil } - -func asString(v any) string { - if v == nil { - return "" - } else if s, ok := v.(string); ok { - return s - } - return "" -} diff --git a/act/jobparser/model_test.go b/act/jobparser/model_test.go index fd62ef65..45d77428 100644 --- a/act/jobparser/model_test.go +++ b/act/jobparser/model_test.go @@ -342,10 +342,11 @@ func TestParseMappingNode(t *testing.T) { func TestEvaluateConcurrency(t *testing.T) { tests := []struct { - name string - input model.RawConcurrency - group string - cancelInProgress bool + name string + input model.RawConcurrency + group string + cancelInProgressNil bool + cancelInProgress bool }{ { name: "basic", @@ -357,18 +358,18 @@ func TestEvaluateConcurrency(t *testing.T) { cancelInProgress: true, }, { - name: "undefined", - input: model.RawConcurrency{}, - group: "", - cancelInProgress: false, + name: "undefined", + input: model.RawConcurrency{}, + group: "", + cancelInProgressNil: true, }, { name: "group-evaluation", input: model.RawConcurrency{ Group: "${{ github.workflow }}-${{ github.ref }}", }, - group: "test_workflow-main", - cancelInProgress: false, + group: "test_workflow-main", + cancelInProgressNil: true, }, { name: "cancel-evaluation-true", @@ -393,37 +394,44 @@ func TestEvaluateConcurrency(t *testing.T) { input: model.RawConcurrency{ Group: "user-${{ github.event.commits[0].author.username }}", }, - group: "user-someone", - cancelInProgress: false, + group: "user-someone", + cancelInProgressNil: true, }, { name: "arbitrary-var", input: model.RawConcurrency{ Group: "${{ vars.eval_arbitrary_var }}", }, - group: "123", - cancelInProgress: false, + group: "123", + cancelInProgressNil: true, }, { name: "arbitrary-input", input: model.RawConcurrency{ Group: "${{ inputs.eval_arbitrary_input }}", }, - group: "456", - cancelInProgress: false, + group: "456", + cancelInProgressNil: true, + }, + { + name: "cancel-in-progress-only", + input: model.RawConcurrency{ + CancelInProgress: "true", + }, + group: "", + cancelInProgress: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - group, cancelInProgress, err := EvaluateConcurrency( + group, cancelInProgress, err := EvaluateWorkflowConcurrency( &test.input, - "job-id", - nil, // job - map[string]any{ - "workflow": "test_workflow", - "ref": "main", - "event": map[string]any{ + // gitCtx + &model.GithubContext{ + Workflow: "test_workflow", + Ref: "main", + Event: map[string]any{ "commits": []any{ map[string]any{ "author": map[string]any{ @@ -437,20 +445,24 @@ func TestEvaluateConcurrency(t *testing.T) { }, }, }, - }, // gitCtx - map[string]*JobResult{ - "job-id": {}, - }, // results + }, + // vars map[string]string{ "eval_arbitrary_var": "123", - }, // vars + }, + // inputs map[string]any{ "eval_arbitrary_input": "456", - }, // inputs + }, ) assert.NoError(t, err) assert.EqualValues(t, test.group, group) - assert.EqualValues(t, test.cancelInProgress, cancelInProgress) + if test.cancelInProgressNil { + assert.Nil(t, cancelInProgress) + } else { + require.NotNil(t, cancelInProgress) + assert.EqualValues(t, test.cancelInProgress, *cancelInProgress) + } }) } }