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:
parent
014b4ba5f6
commit
56ef60060b
3 changed files with 87 additions and 106 deletions
|
@ -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
|
||||||
|
|
|
@ -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 ""
|
|
||||||
}
|
|
||||||
|
|
|
@ -342,10 +342,11 @@ func TestParseMappingNode(t *testing.T) {
|
||||||
|
|
||||||
func TestEvaluateConcurrency(t *testing.T) {
|
func TestEvaluateConcurrency(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
input model.RawConcurrency
|
input model.RawConcurrency
|
||||||
group string
|
group string
|
||||||
cancelInProgress bool
|
cancelInProgressNil bool
|
||||||
|
cancelInProgress bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "basic",
|
name: "basic",
|
||||||
|
@ -357,18 +358,18 @@ func TestEvaluateConcurrency(t *testing.T) {
|
||||||
cancelInProgress: true,
|
cancelInProgress: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "undefined",
|
name: "undefined",
|
||||||
input: model.RawConcurrency{},
|
input: model.RawConcurrency{},
|
||||||
group: "",
|
group: "",
|
||||||
cancelInProgress: false,
|
cancelInProgressNil: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "group-evaluation",
|
name: "group-evaluation",
|
||||||
input: model.RawConcurrency{
|
input: model.RawConcurrency{
|
||||||
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",
|
||||||
|
@ -393,37 +394,44 @@ func TestEvaluateConcurrency(t *testing.T) {
|
||||||
input: model.RawConcurrency{
|
input: model.RawConcurrency{
|
||||||
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",
|
||||||
input: model.RawConcurrency{
|
input: model.RawConcurrency{
|
||||||
Group: "${{ vars.eval_arbitrary_var }}",
|
Group: "${{ vars.eval_arbitrary_var }}",
|
||||||
},
|
},
|
||||||
group: "123",
|
group: "123",
|
||||||
cancelInProgress: false,
|
cancelInProgressNil: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "arbitrary-input",
|
name: "arbitrary-input",
|
||||||
input: model.RawConcurrency{
|
input: model.RawConcurrency{
|
||||||
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)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue