1
0
Fork 0
mirror of https://code.forgejo.org/forgejo/runner.git synced 2025-09-30 19:22:09 +00:00

feat: support evaluating workflow-level concurrency blocks in jobparser (#1026)

- Changes `EvaluateConcurrency` to `EvaluateWorkflowConcurrency`, which has no job-related arguments
- Changes gitContext to be sent as an object rather than a map
- Allows `nil` to be returned for `cancelInProgress`, which indicates that the value wasn't specified in the input yaml -- required for distinguishing the `cancel-in-progress: false` case from not being specified at all.

ReadWorkflowRawConcurrency & EvaluateWorkflowConcurrency were never used in forgejo yet, so this shouldn't break the forgejo build.

Prerequisite for https://codeberg.org/forgejo/forgejo/pulls/9434.

<!--start release-notes-assistant-->
<!--URL:https://code.forgejo.org/forgejo/runner-->
- features
  - [PR](https://code.forgejo.org/forgejo/runner/pulls/1026): <!--number 1026 --><!--line 0 --><!--description ZmVhdDogc3VwcG9ydCBldmFsdWF0aW5nIHdvcmtmbG93LWxldmVsIGNvbmN1cnJlbmN5IGJsb2NrcyBpbiBqb2JwYXJzZXI=-->feat: support evaluating workflow-level concurrency blocks in jobparser<!--description-->
<!--end release-notes-assistant-->

Reviewed-on: https://code.forgejo.org/forgejo/runner/pulls/1026
Reviewed-by: earl-warren <earl-warren@noreply.code.forgejo.org>
Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net>
Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
This commit is contained in:
Mathieu Fenniak 2025-09-27 04:37:36 +00:00 committed by earl-warren
parent 014b4ba5f6
commit 56ef60060b
No known key found for this signature in database
GPG key ID: F128CBE6AB3A7201
3 changed files with 87 additions and 106 deletions

View file

@ -76,6 +76,36 @@ func NewInterpeter(
return exprparser.NewInterpeter(ee, config) 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 // JobResult is the minimum requirement of job results for Interpeter
type JobResult struct { type JobResult struct {
Needs []string Needs []string

View file

@ -1,7 +1,6 @@
package jobparser package jobparser
import ( import (
"bytes"
"fmt" "fmt"
"code.forgejo.org/forgejo/runner/v11/act/model" "code.forgejo.org/forgejo/runner/v11/act/model"
@ -193,83 +192,32 @@ func (evt *Event) Schedules() []map[string]string {
return evt.schedules return evt.schedules
} }
func ReadWorkflowRawConcurrency(content []byte) (*model.RawConcurrency, error) { // Convert the raw YAML from the `concurrency` block on a workflow into the evaluated concurrency group and
w := new(model.Workflow) // cancel-in-progress value. This implementation only supports workflow-level concurrency definition, where we expect
err := yaml.NewDecoder(bytes.NewReader(content)).Decode(w) // expressions to be able to access only the github, inputs and vars contexts. If RawConcurrency is empty, then the
return w.RawConcurrency, err // 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) {
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) { evaluator := NewExpressionEvaluator(NewWorkflowInterpeter(gitCtx, vars, inputs))
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))
var node yaml.Node var node yaml.Node
if err := node.Encode(rc); err != nil { 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 { 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 var evaluated model.RawConcurrency
if err := node.Decode(&evaluated); err != nil { 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 != "" { if evaluated.RawExpression != "" {
return evaluated.RawExpression, false, nil return evaluated.RawExpression, nil, nil
} }
return evaluated.Group, evaluated.CancelInProgress == "true", nil if evaluated.CancelInProgress == "" {
} return evaluated.Group, nil, 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"]),
} }
cancelInProgress := evaluated.CancelInProgress == "true"
event, ok := input["event"].(map[string]any) return evaluated.Group, &cancelInProgress, nil
if ok {
gitContext.Event = event
}
return gitContext
} }
func ParseRawOn(rawOn *yaml.Node) ([]*Event, error) { 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 return scalars, datas, nil
} }
func asString(v any) string {
if v == nil {
return ""
} else if s, ok := v.(string); ok {
return s
}
return ""
}

View file

@ -345,6 +345,7 @@ func TestEvaluateConcurrency(t *testing.T) {
name string name string
input model.RawConcurrency input model.RawConcurrency
group string group string
cancelInProgressNil bool
cancelInProgress bool cancelInProgress bool
}{ }{
{ {
@ -360,7 +361,7 @@ func TestEvaluateConcurrency(t *testing.T) {
name: "undefined", name: "undefined",
input: model.RawConcurrency{}, input: model.RawConcurrency{},
group: "", group: "",
cancelInProgress: false, cancelInProgressNil: true,
}, },
{ {
name: "group-evaluation", name: "group-evaluation",
@ -368,7 +369,7 @@ func TestEvaluateConcurrency(t *testing.T) {
Group: "${{ github.workflow }}-${{ github.ref }}", Group: "${{ github.workflow }}-${{ github.ref }}",
}, },
group: "test_workflow-main", group: "test_workflow-main",
cancelInProgress: false, cancelInProgressNil: true,
}, },
{ {
name: "cancel-evaluation-true", name: "cancel-evaluation-true",
@ -394,7 +395,7 @@ func TestEvaluateConcurrency(t *testing.T) {
Group: "user-${{ github.event.commits[0].author.username }}", Group: "user-${{ github.event.commits[0].author.username }}",
}, },
group: "user-someone", group: "user-someone",
cancelInProgress: false, cancelInProgressNil: true,
}, },
{ {
name: "arbitrary-var", name: "arbitrary-var",
@ -402,7 +403,7 @@ func TestEvaluateConcurrency(t *testing.T) {
Group: "${{ vars.eval_arbitrary_var }}", Group: "${{ vars.eval_arbitrary_var }}",
}, },
group: "123", group: "123",
cancelInProgress: false, cancelInProgressNil: true,
}, },
{ {
name: "arbitrary-input", name: "arbitrary-input",
@ -410,20 +411,27 @@ func TestEvaluateConcurrency(t *testing.T) {
Group: "${{ inputs.eval_arbitrary_input }}", Group: "${{ inputs.eval_arbitrary_input }}",
}, },
group: "456", group: "456",
cancelInProgress: false, cancelInProgressNil: true,
},
{
name: "cancel-in-progress-only",
input: model.RawConcurrency{
CancelInProgress: "true",
},
group: "",
cancelInProgress: true,
}, },
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
group, cancelInProgress, err := EvaluateConcurrency( group, cancelInProgress, err := EvaluateWorkflowConcurrency(
&test.input, &test.input,
"job-id", // gitCtx
nil, // job &model.GithubContext{
map[string]any{ Workflow: "test_workflow",
"workflow": "test_workflow", Ref: "main",
"ref": "main", Event: map[string]any{
"event": map[string]any{
"commits": []any{ "commits": []any{
map[string]any{ map[string]any{
"author": map[string]any{ "author": map[string]any{
@ -437,20 +445,24 @@ func TestEvaluateConcurrency(t *testing.T) {
}, },
}, },
}, },
}, // gitCtx },
map[string]*JobResult{ // vars
"job-id": {},
}, // results
map[string]string{ map[string]string{
"eval_arbitrary_var": "123", "eval_arbitrary_var": "123",
}, // vars },
// inputs
map[string]any{ map[string]any{
"eval_arbitrary_input": "456", "eval_arbitrary_input": "456",
}, // inputs },
) )
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, test.group, group) 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)
}
}) })
} }
} }