Fix missing outputs for jobs with matrix (#32823)
Fix #32795 If a job uses a matrix, multiple `ActionRunJobs` may have the same `JobID`. We need to merge the outputs of these jobs to make them available to the jobs that need them. (cherry picked from commit 7269130d2878d51dcdf11f7081a591f85bd493e8) Conflicts: models/fixtures/action_run.yml models/fixtures/action_run_job.yml trivial context conflicts
This commit is contained in:
		
					parent
					
						
							
								4be37a986e
							
						
					
				
			
			
				commit
				
					
						96a7f0a3f0
					
				
			
		
					 8 changed files with 233 additions and 18 deletions
				
			
		| 
						 | 
					@ -137,7 +137,7 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return 0, err
 | 
								return 0, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		run.Status = aggregateJobStatus(jobs)
 | 
							run.Status = AggregateJobStatus(jobs)
 | 
				
			||||||
		if run.Started.IsZero() && run.Status.IsRunning() {
 | 
							if run.Started.IsZero() && run.Status.IsRunning() {
 | 
				
			||||||
			run.Started = timeutil.TimeStampNow()
 | 
								run.Started = timeutil.TimeStampNow()
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					@ -152,7 +152,7 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
 | 
				
			||||||
	return affected, nil
 | 
						return affected, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func aggregateJobStatus(jobs []*ActionRunJob) Status {
 | 
					func AggregateJobStatus(jobs []*ActionRunJob) Status {
 | 
				
			||||||
	allDone := true
 | 
						allDone := true
 | 
				
			||||||
	allWaiting := true
 | 
						allWaiting := true
 | 
				
			||||||
	hasFailure := false
 | 
						hasFailure := false
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -413,6 +413,25 @@
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "total_commits": 0
 | 
					      "total_commits": 0
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 793
 | 
				
			||||||
 | 
					  title: "job output"
 | 
				
			||||||
 | 
					  repo_id: 4
 | 
				
			||||||
 | 
					  owner_id: 1
 | 
				
			||||||
 | 
					  workflow_id: "test.yaml"
 | 
				
			||||||
 | 
					  index: 189
 | 
				
			||||||
 | 
					  trigger_user_id: 1
 | 
				
			||||||
 | 
					  ref: "refs/heads/master"
 | 
				
			||||||
 | 
					  commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
 | 
				
			||||||
 | 
					  event: "push"
 | 
				
			||||||
 | 
					  is_fork_pull_request: 0
 | 
				
			||||||
 | 
					  status: 1
 | 
				
			||||||
 | 
					  started: 1683636528
 | 
				
			||||||
 | 
					  stopped: 1683636626
 | 
				
			||||||
 | 
					  created: 1683636108
 | 
				
			||||||
 | 
					  updated: 1683636626
 | 
				
			||||||
 | 
					  need_approval: 0
 | 
				
			||||||
 | 
					  approved_by: 0
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 891
 | 
					  id: 891
 | 
				
			||||||
  title: "update actions"
 | 
					  title: "update actions"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,6 +26,49 @@
 | 
				
			||||||
  status: 1
 | 
					  status: 1
 | 
				
			||||||
  started: 1683636528
 | 
					  started: 1683636528
 | 
				
			||||||
  stopped: 1683636626
 | 
					  stopped: 1683636626
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 194
 | 
				
			||||||
 | 
					  run_id: 793
 | 
				
			||||||
 | 
					  repo_id: 4
 | 
				
			||||||
 | 
					  owner_id: 1
 | 
				
			||||||
 | 
					  commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
 | 
				
			||||||
 | 
					  is_fork_pull_request: 0
 | 
				
			||||||
 | 
					  name: job1 (1)
 | 
				
			||||||
 | 
					  attempt: 1
 | 
				
			||||||
 | 
					  job_id: job1
 | 
				
			||||||
 | 
					  task_id: 49
 | 
				
			||||||
 | 
					  status: 1
 | 
				
			||||||
 | 
					  started: 1683636528
 | 
				
			||||||
 | 
					  stopped: 1683636626
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 195
 | 
				
			||||||
 | 
					  run_id: 793
 | 
				
			||||||
 | 
					  repo_id: 4
 | 
				
			||||||
 | 
					  owner_id: 1
 | 
				
			||||||
 | 
					  commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
 | 
				
			||||||
 | 
					  is_fork_pull_request: 0
 | 
				
			||||||
 | 
					  name: job1 (2)
 | 
				
			||||||
 | 
					  attempt: 1
 | 
				
			||||||
 | 
					  job_id: job1
 | 
				
			||||||
 | 
					  task_id: 50
 | 
				
			||||||
 | 
					  status: 1
 | 
				
			||||||
 | 
					  started: 1683636528
 | 
				
			||||||
 | 
					  stopped: 1683636626
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 196
 | 
				
			||||||
 | 
					  run_id: 793
 | 
				
			||||||
 | 
					  repo_id: 4
 | 
				
			||||||
 | 
					  owner_id: 1
 | 
				
			||||||
 | 
					  commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
 | 
				
			||||||
 | 
					  is_fork_pull_request: 0
 | 
				
			||||||
 | 
					  name: job2
 | 
				
			||||||
 | 
					  attempt: 1
 | 
				
			||||||
 | 
					  job_id: job2
 | 
				
			||||||
 | 
					  needs: [job1]
 | 
				
			||||||
 | 
					  task_id: 51
 | 
				
			||||||
 | 
					  status: 5
 | 
				
			||||||
 | 
					  started: 1683636528
 | 
				
			||||||
 | 
					  stopped: 1683636626
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 292
 | 
					  id: 292
 | 
				
			||||||
  run_id: 891
 | 
					  run_id: 891
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -57,3 +57,63 @@
 | 
				
			||||||
  log_length: 707
 | 
					  log_length: 707
 | 
				
			||||||
  log_size: 90179
 | 
					  log_size: 90179
 | 
				
			||||||
  log_expired: 0
 | 
					  log_expired: 0
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 49
 | 
				
			||||||
 | 
					  job_id: 194
 | 
				
			||||||
 | 
					  attempt: 1
 | 
				
			||||||
 | 
					  runner_id: 1
 | 
				
			||||||
 | 
					  status: 1 # success
 | 
				
			||||||
 | 
					  started: 1683636528
 | 
				
			||||||
 | 
					  stopped: 1683636626
 | 
				
			||||||
 | 
					  repo_id: 4
 | 
				
			||||||
 | 
					  owner_id: 1
 | 
				
			||||||
 | 
					  commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
 | 
				
			||||||
 | 
					  is_fork_pull_request: 0
 | 
				
			||||||
 | 
					  token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784220
 | 
				
			||||||
 | 
					  token_salt: ffffffffff
 | 
				
			||||||
 | 
					  token_last_eight: ffffffff
 | 
				
			||||||
 | 
					  log_filename: artifact-test2/2f/47.log
 | 
				
			||||||
 | 
					  log_in_storage: 1
 | 
				
			||||||
 | 
					  log_length: 707
 | 
				
			||||||
 | 
					  log_size: 90179
 | 
				
			||||||
 | 
					  log_expired: 0
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 50
 | 
				
			||||||
 | 
					  job_id: 195
 | 
				
			||||||
 | 
					  attempt: 1
 | 
				
			||||||
 | 
					  runner_id: 1
 | 
				
			||||||
 | 
					  status: 1 # success
 | 
				
			||||||
 | 
					  started: 1683636528
 | 
				
			||||||
 | 
					  stopped: 1683636626
 | 
				
			||||||
 | 
					  repo_id: 4
 | 
				
			||||||
 | 
					  owner_id: 1
 | 
				
			||||||
 | 
					  commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
 | 
				
			||||||
 | 
					  is_fork_pull_request: 0
 | 
				
			||||||
 | 
					  token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784221
 | 
				
			||||||
 | 
					  token_salt: ffffffffff
 | 
				
			||||||
 | 
					  token_last_eight: ffffffff
 | 
				
			||||||
 | 
					  log_filename: artifact-test2/2f/47.log
 | 
				
			||||||
 | 
					  log_in_storage: 1
 | 
				
			||||||
 | 
					  log_length: 707
 | 
				
			||||||
 | 
					  log_size: 90179
 | 
				
			||||||
 | 
					  log_expired: 0
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 51
 | 
				
			||||||
 | 
					  job_id: 196
 | 
				
			||||||
 | 
					  attempt: 1
 | 
				
			||||||
 | 
					  runner_id: 1
 | 
				
			||||||
 | 
					  status: 6 # running
 | 
				
			||||||
 | 
					  started: 1683636528
 | 
				
			||||||
 | 
					  stopped: 1683636626
 | 
				
			||||||
 | 
					  repo_id: 4
 | 
				
			||||||
 | 
					  owner_id: 1
 | 
				
			||||||
 | 
					  commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
 | 
				
			||||||
 | 
					  is_fork_pull_request: 0
 | 
				
			||||||
 | 
					  token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784222
 | 
				
			||||||
 | 
					  token_salt: ffffffffff
 | 
				
			||||||
 | 
					  token_last_eight: ffffffff
 | 
				
			||||||
 | 
					  log_filename: artifact-test2/2f/47.log
 | 
				
			||||||
 | 
					  log_in_storage: 1
 | 
				
			||||||
 | 
					  log_length: 707
 | 
				
			||||||
 | 
					  log_size: 90179
 | 
				
			||||||
 | 
					  log_expired: 0
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										20
									
								
								models/fixtures/action_task_output.yml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								models/fixtures/action_task_output.yml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,20 @@
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 1
 | 
				
			||||||
 | 
					  task_id: 49
 | 
				
			||||||
 | 
					  output_key: output_a
 | 
				
			||||||
 | 
					  output_value: abc
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 2
 | 
				
			||||||
 | 
					  task_id: 49
 | 
				
			||||||
 | 
					  output_key: output_b
 | 
				
			||||||
 | 
					  output_value: ''
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 3
 | 
				
			||||||
 | 
					  task_id: 50
 | 
				
			||||||
 | 
					  output_key: output_a
 | 
				
			||||||
 | 
					  output_value: ''
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 4
 | 
				
			||||||
 | 
					  task_id: 50
 | 
				
			||||||
 | 
					  output_key: output_b
 | 
				
			||||||
 | 
					  output_value: bbb
 | 
				
			||||||
							
								
								
									
										16
									
								
								routers/api/actions/runner/main_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								routers/api/actions/runner/main_test.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,16 @@
 | 
				
			||||||
 | 
					// Copyright 2024 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package runner
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/unittest"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_ "code.gitea.io/gitea/models/forgefed"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestMain(m *testing.M) {
 | 
				
			||||||
 | 
						unittest.MainTest(m)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -162,28 +162,56 @@ func findTaskNeeds(ctx context.Context, task *actions_model.ActionTask) (map[str
 | 
				
			||||||
		return nil, fmt.Errorf("FindRunJobs: %w", err)
 | 
							return nil, fmt.Errorf("FindRunJobs: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ret := make(map[string]*runnerv1.TaskNeed, len(needs))
 | 
						jobIDJobs := make(map[string][]*actions_model.ActionRunJob)
 | 
				
			||||||
	for _, job := range jobs {
 | 
						for _, job := range jobs {
 | 
				
			||||||
		if !needs.Contains(job.JobID) {
 | 
							jobIDJobs[job.JobID] = append(jobIDJobs[job.JobID], job)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ret := make(map[string]*runnerv1.TaskNeed, len(needs))
 | 
				
			||||||
 | 
						for jobID, jobsWithSameID := range jobIDJobs {
 | 
				
			||||||
 | 
							if !needs.Contains(jobID) {
 | 
				
			||||||
			continue
 | 
								continue
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							var jobOutputs map[string]string
 | 
				
			||||||
 | 
							for _, job := range jobsWithSameID {
 | 
				
			||||||
			if job.TaskID == 0 || !job.Status.IsDone() {
 | 
								if job.TaskID == 0 || !job.Status.IsDone() {
 | 
				
			||||||
				// it shouldn't happen, or the job has been rerun
 | 
									// it shouldn't happen, or the job has been rerun
 | 
				
			||||||
				continue
 | 
									continue
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		outputs := make(map[string]string)
 | 
					 | 
				
			||||||
			got, err := actions_model.FindTaskOutputByTaskID(ctx, job.TaskID)
 | 
								got, err := actions_model.FindTaskOutputByTaskID(ctx, job.TaskID)
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				return nil, fmt.Errorf("FindTaskOutputByTaskID: %w", err)
 | 
									return nil, fmt.Errorf("FindTaskOutputByTaskID: %w", err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
								outputs := make(map[string]string, len(got))
 | 
				
			||||||
			for _, v := range got {
 | 
								for _, v := range got {
 | 
				
			||||||
				outputs[v.OutputKey] = v.OutputValue
 | 
									outputs[v.OutputKey] = v.OutputValue
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		ret[job.JobID] = &runnerv1.TaskNeed{
 | 
								if len(jobOutputs) == 0 {
 | 
				
			||||||
			Outputs: outputs,
 | 
									jobOutputs = outputs
 | 
				
			||||||
			Result:  runnerv1.Result(job.Status),
 | 
								} else {
 | 
				
			||||||
 | 
									jobOutputs = mergeTwoOutputs(outputs, jobOutputs)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							ret[jobID] = &runnerv1.TaskNeed{
 | 
				
			||||||
 | 
								Outputs: jobOutputs,
 | 
				
			||||||
 | 
								Result:  runnerv1.Result(actions_model.AggregateJobStatus(jobsWithSameID)),
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return ret, nil
 | 
						return ret, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// mergeTwoOutputs merges two outputs from two different ActionRunJobs
 | 
				
			||||||
 | 
					// Values with the same output name may be overridden. The user should ensure the output names are unique.
 | 
				
			||||||
 | 
					// See https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#using-job-outputs-in-a-matrix-job
 | 
				
			||||||
 | 
					func mergeTwoOutputs(o1, o2 map[string]string) map[string]string {
 | 
				
			||||||
 | 
						ret := make(map[string]string, len(o1))
 | 
				
			||||||
 | 
						for k1, v1 := range o1 {
 | 
				
			||||||
 | 
							if len(v1) > 0 {
 | 
				
			||||||
 | 
								ret[k1] = v1
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								ret[k1] = o2[k1]
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ret
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										29
									
								
								routers/api/actions/runner/utils_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								routers/api/actions/runner/utils_test.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,29 @@
 | 
				
			||||||
 | 
					// Copyright 2024 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package runner
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						actions_model "code.gitea.io/gitea/models/actions"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/unittest"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/require"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Test_findTaskNeeds(t *testing.T) {
 | 
				
			||||||
 | 
						require.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 51})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ret, err := findTaskNeeds(context.Background(), task)
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
						assert.Len(t, ret, 1)
 | 
				
			||||||
 | 
						assert.Contains(t, ret, "job1")
 | 
				
			||||||
 | 
						assert.Len(t, ret["job1"].Outputs, 2)
 | 
				
			||||||
 | 
						assert.Equal(t, "abc", ret["job1"].Outputs["output_a"])
 | 
				
			||||||
 | 
						assert.Equal(t, "bbb", ret["job1"].Outputs["output_b"])
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue