diff --git a/models/actions/run_job.go b/models/actions/run_job.go index 94a9f11ed6..e3e3f4f85f 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -234,7 +234,9 @@ func AggregateJobStatus(jobs []*ActionRunJob) Status { } } -func (job *ActionRunJob) decodeWorkflowPayload() (*jobparser.SingleWorkflow, error) { +// Retrieves the parsed workflow for this specific job. This field is often accessed multiple times in succession, so +// the parsed content is cached in-memory on the `ActionRunJob` instance. +func (job *ActionRunJob) DecodeWorkflowPayload() (*jobparser.SingleWorkflow, error) { if job.workflowPayloadDecoded != nil { return job.workflowPayloadDecoded, nil } @@ -259,7 +261,7 @@ func (job *ActionRunJob) ClearCachedWorkflowPayload() { // then regenerated and deleted. If it is incomplete, and if the information is available, the specific job and/or // output that causes it to be incomplete will be returned as well. func (job *ActionRunJob) IsIncompleteMatrix() (bool, *jobparser.IncompleteNeeds, error) { - jobWorkflow, err := job.decodeWorkflowPayload() + jobWorkflow, err := job.DecodeWorkflowPayload() if err != nil { return false, nil, fmt.Errorf("failure decoding workflow payload: %w", err) } @@ -269,7 +271,7 @@ func (job *ActionRunJob) IsIncompleteMatrix() (bool, *jobparser.IncompleteNeeds, // Checks whether the target job has a `runs-on` field with an expression that requires an input from another job. The // job will be blocked until the other job is complete, and then regenerated and deleted. func (job *ActionRunJob) IsIncompleteRunsOn() (bool, *jobparser.IncompleteNeeds, *jobparser.IncompleteMatrix, error) { - jobWorkflow, err := job.decodeWorkflowPayload() + jobWorkflow, err := job.DecodeWorkflowPayload() if err != nil { return false, nil, nil, fmt.Errorf("failure decoding workflow payload: %w", err) } @@ -279,7 +281,7 @@ func (job *ActionRunJob) IsIncompleteRunsOn() (bool, *jobparser.IncompleteNeeds, // Check whether this job is a caller of a reusable workflow -- in other words, the real work done in this job is in // spawned child jobs, not this job. func (job *ActionRunJob) IsWorkflowCallOuterJob() (bool, error) { - jobWorkflow, err := job.decodeWorkflowPayload() + jobWorkflow, err := job.DecodeWorkflowPayload() if err != nil { return false, fmt.Errorf("failure decoding workflow payload: %w", err) } diff --git a/services/actions/Test_tryHandleWorkflowCallOuterJob/action_task_output.yml b/services/actions/Test_tryHandleWorkflowCallOuterJob/action_task_output.yml new file mode 100644 index 0000000000..9bf82649c1 --- /dev/null +++ b/services/actions/Test_tryHandleWorkflowCallOuterJob/action_task_output.yml @@ -0,0 +1,6 @@ +# Case w/ action_run_job.id = 601 +- + id: 100 + task_id: 100 + output_key: my_output + output_value: 'abcdefghijklmnopqrstuvwxyz' diff --git a/services/actions/Test_tryHandleWorkflowCallOuterJob/action_variable.yml b/services/actions/Test_tryHandleWorkflowCallOuterJob/action_variable.yml new file mode 100644 index 0000000000..859f90ae34 --- /dev/null +++ b/services/actions/Test_tryHandleWorkflowCallOuterJob/action_variable.yml @@ -0,0 +1,22 @@ +# Case w/ action_run_job.id = 601 +- + id: 1001 + name: REPO_VAR + owner_id: 0 + repo_id: 63 + data: "this is a repo variable" + created_unix: 1737000000 +- + id: 1002 + name: ORG_VAR + owner_id: 2 + repo_id: 0 + data: "this is an org variable" + created_unix: 1737000000 +- + id: 1003 + name: GLOBAL_VAR + owner_id: 0 + repo_id: 0 + data: "this is a global variable" + created_unix: 1737000000 diff --git a/services/actions/job_emitter.go b/services/actions/job_emitter.go index 55e05aa712..4e61029878 100644 --- a/services/actions/job_emitter.go +++ b/services/actions/job_emitter.go @@ -391,8 +391,49 @@ func tryHandleWorkflowCallOuterJob(ctx context.Context, job *actions_model.Actio return nil, nil } - // Insert a placeholder task; this will be used in the future to store computed outputs - actionTask, err := actions_model.CreatePlaceholderTask(ctx, job, map[string]string{}) + // Gather all the data that is needed to perform an expression evaluation of the job's outputs: + singleWorkflow, err := job.DecodeWorkflowPayload() + if err != nil { + return nil, fmt.Errorf("failure to decode workflow payload: %w", err) + } + err = job.LoadRun(ctx) + if err != nil { + return nil, fmt.Errorf("failure to load job's run: %w", err) + } + err = job.Run.LoadRepo(ctx) + if err != nil { + return nil, fmt.Errorf("failure to load run's repo: %w", err) + } + githubContext := generateGiteaContextForRun(job.Run) + taskNeeds, err := FindTaskNeeds(ctx, job) + if err != nil { + return nil, fmt.Errorf("failure to 'needs' for job: %w", err) + } + needs := make([]string, 0, len(taskNeeds)) + jobResults := make(map[string]string, len(taskNeeds)) + jobOutputs := make(map[string]map[string]string, len(taskNeeds)) + for jobID, n := range taskNeeds { + needs = append(needs, jobID) + jobResults[jobID] = n.Result.String() + jobOutputs[jobID] = n.Outputs + } + vars, err := actions_model.GetVariablesOfRun(ctx, job.Run) + if err != nil { + return nil, fmt.Errorf("failure to 'var' for run: %w", err) + } + + // With all the required contexts, we can calculate the outputs. + outputs := jobparser.EvaluateWorkflowCallOutputs( + singleWorkflow, + githubContext, + vars, + needs, + jobResults, + jobOutputs, + ) + + // Insert a placeholder task with all the computed outputs + actionTask, err := actions_model.CreatePlaceholderTask(ctx, job, outputs) if err != nil { return nil, fmt.Errorf("failure to insert placeholder task: %w", err) } diff --git a/services/actions/job_emitter_test.go b/services/actions/job_emitter_test.go index 5e1885c709..0bfa10d7ea 100644 --- a/services/actions/job_emitter_test.go +++ b/services/actions/job_emitter_test.go @@ -440,7 +440,15 @@ func Test_tryHandleWorkflowCallOuterJob(t *testing.T) { name: "outputs for every context", runJobID: 601, updateFields: []string{"task_id"}, - outputs: map[string]string{}, + outputs: map[string]string{ + "from_inner_job": "abcdefghijklmnopqrstuvwxyz", + "from_inner_job_result": "success", + "from_forgejo_ctx": "refs/heads/main", + "from_input_ctx": "hello, world!", + "from_vars_repo": "this is a repo variable", + "from_vars_org": "this is an org variable", + "from_vars_global": "this is a global variable", + }, }, } for _, tt := range tests {