loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Support upload `outputs` and use `needs` context on Actions (#24230)

See [Defining outputs for
jobs](https://docs.github.com/en/actions/using-jobs/defining-outputs-for-jobs)
and [Example usage of the needs
context](https://docs.github.com/en/actions/learn-github-actions/contexts#example-usage-of-the-needs-context).

Related to:
- [actions-proto-def
#5](https://gitea.com/gitea/actions-proto-def/pulls/5)
- [act_runner #133](https://gitea.com/gitea/act_runner/pulls/133)

<details>
<summary>Tests & screenshots</summary>

Test workflow file:
```yaml
name: outputs
on: push

jobs:
job1:
runs-on: ubuntu-latest
outputs:
output1: ${{ steps.step1.outputs.output1 }}
output2: ${{ steps.step2.outputs.output2 }}
steps:
- name: step1
id: step1
run: |
date -Is > output1
cat output1
echo "output1=$(cat output1)" >> $GITHUB_OUTPUT
- name: step2
id: step2
run: |
cat /proc/sys/kernel/random/uuid > output2
cat output2
echo "output2=$(cat output2)" >> $GITHUB_OUTPUT
job2:
needs: job1
runs-on: ubuntu-latest
steps:
- run: echo ${{ needs.job1.outputs.output1 }}
- run: echo ${{ needs.job1.outputs.output2 }}
- run: echo ${{ needs.job1.result }}
```

<img width="397" alt="image"
src="https://user-images.githubusercontent.com/9418365/233313322-903e7ebf-49a7-48e2-8c17-95a4581b3284.png">
<img width="385" alt="image"
src="https://user-images.githubusercontent.com/9418365/233313442-30909135-1711-4b78-a5c6-133fcc79f47c.png">



</details>

---------

Co-authored-by: Giteabot <teabot@gitea.io>

authored by

Jason Song
Giteabot
and committed by
GitHub
ac384c4e 8dc6eabb

+155 -4
+1 -1
go.mod
··· 3 3 go 1.19 4 4 5 5 require ( 6 - code.gitea.io/actions-proto-go v0.2.0 6 + code.gitea.io/actions-proto-go v0.2.1 7 7 code.gitea.io/gitea-vet v0.2.2 8 8 code.gitea.io/sdk/gitea v0.15.1 9 9 codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
+2 -2
go.sum
··· 40 40 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 41 41 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 42 42 cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= 43 - code.gitea.io/actions-proto-go v0.2.0 h1:nYh9nhhfk67YA4wVNLsCzd//RCvXnljwXClJ33+HPVk= 44 - code.gitea.io/actions-proto-go v0.2.0/go.mod h1:00ys5QDo1iHN1tHNvvddAcy2W/g+425hQya1cCSvq9A= 43 + code.gitea.io/actions-proto-go v0.2.1 h1:ToMN/8thz2q10TuCq8dL2d8mI+/pWpJcHCvG+TELwa0= 44 + code.gitea.io/actions-proto-go v0.2.1/go.mod h1:00ys5QDo1iHN1tHNvvddAcy2W/g+425hQya1cCSvq9A= 45 45 code.gitea.io/gitea-vet v0.2.1/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= 46 46 code.gitea.io/gitea-vet v0.2.2 h1:TEOV/Glf38iGmKzKP0EB++Z5OSL4zGg3RrAvlwaMuvk= 47 47 code.gitea.io/gitea-vet v0.2.2/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
+51
models/actions/task_output.go
··· 1 + // Copyright 2023 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package actions 5 + 6 + import ( 7 + "context" 8 + 9 + "code.gitea.io/gitea/models/db" 10 + ) 11 + 12 + // ActionTaskOutput represents an output of ActionTask. 13 + // So the outputs are bound to a task, that means when a completed job has been rerun, 14 + // the outputs of the job will be reset because the task is new. 15 + // It's by design, to avoid the outputs of the old task to be mixed with the new task. 16 + type ActionTaskOutput struct { 17 + ID int64 18 + TaskID int64 `xorm:"INDEX UNIQUE(task_id_output_key)"` 19 + OutputKey string `xorm:"VARCHAR(255) UNIQUE(task_id_output_key)"` 20 + OutputValue string `xorm:"MEDIUMTEXT"` 21 + } 22 + 23 + // FindTaskOutputByTaskID returns the outputs of the task. 24 + func FindTaskOutputByTaskID(ctx context.Context, taskID int64) ([]*ActionTaskOutput, error) { 25 + var outputs []*ActionTaskOutput 26 + return outputs, db.GetEngine(ctx).Where("task_id=?", taskID).Find(&outputs) 27 + } 28 + 29 + // FindTaskOutputKeyByTaskID returns the keys of the outputs of the task. 30 + func FindTaskOutputKeyByTaskID(ctx context.Context, taskID int64) ([]string, error) { 31 + var keys []string 32 + return keys, db.GetEngine(ctx).Table(ActionTaskOutput{}).Where("task_id=?", taskID).Cols("output_key").Find(&keys) 33 + } 34 + 35 + // InsertTaskOutputIfNotExist inserts a new task output if it does not exist. 36 + func InsertTaskOutputIfNotExist(ctx context.Context, taskID int64, key, value string) error { 37 + return db.WithTx(ctx, func(ctx context.Context) error { 38 + sess := db.GetEngine(ctx) 39 + if exist, err := sess.Exist(&ActionTaskOutput{TaskID: taskID, OutputKey: key}); err != nil { 40 + return err 41 + } else if exist { 42 + return nil 43 + } 44 + _, err := sess.Insert(&ActionTaskOutput{ 45 + TaskID: taskID, 46 + OutputKey: key, 47 + OutputValue: value, 48 + }) 49 + return err 50 + }) 51 + }
+2
models/migrations/migrations.go
··· 485 485 NewMigration("Fix incorrect admin team unit access mode", v1_20.FixIncorrectAdminTeamUnitAccessMode), 486 486 // v253 -> v254 487 487 NewMigration("Fix ExternalTracker and ExternalWiki accessMode in owner and admin team", v1_20.FixExternalTrackerAndExternalWikiAccessModeInOwnerAndAdminTeam), 488 + // v254 -> v255 489 + NewMigration("Add ActionTaskOutput table", v1_20.AddActionTaskOutputTable), 488 490 } 489 491 490 492 // GetCurrentDBVersion returns the current db version
+18
models/migrations/v1_20/v254.go
··· 1 + // Copyright 2023 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package v1_20 //nolint 5 + 6 + import ( 7 + "xorm.io/xorm" 8 + ) 9 + 10 + func AddActionTaskOutputTable(x *xorm.Engine) error { 11 + type ActionTaskOutput struct { 12 + ID int64 13 + TaskID int64 `xorm:"INDEX UNIQUE(task_id_output_key)"` 14 + OutputKey string `xorm:"VARCHAR(255) UNIQUE(task_id_output_key)"` 15 + OutputValue string `xorm:"MEDIUMTEXT"` 16 + } 17 + return x.Sync(new(ActionTaskOutput)) 18 + }
+27 -1
routers/api/actions/runner/runner.go
··· 97 97 // FetchTask assigns a task to the runner 98 98 func (s *Service) FetchTask( 99 99 ctx context.Context, 100 - req *connect.Request[runnerv1.FetchTaskRequest], 100 + _ *connect.Request[runnerv1.FetchTaskRequest], 101 101 ) (*connect.Response[runnerv1.FetchTaskResponse], error) { 102 102 runner := GetRunner(ctx) 103 103 ··· 145 145 return nil, status.Errorf(codes.Internal, "update task: %v", err) 146 146 } 147 147 148 + for k, v := range req.Msg.Outputs { 149 + if len(k) > 255 { 150 + log.Warn("Ignore the output of task %d because the key is too long: %q", task.ID, k) 151 + continue 152 + } 153 + // The value can be a maximum of 1 MB 154 + if l := len(v); l > 1024*1024 { 155 + log.Warn("Ignore the output %q of task %d because the value is too long: %v", k, task.ID, l) 156 + continue 157 + } 158 + // There's another limitation on GitHub that the total of all outputs in a workflow run can be a maximum of 50 MB. 159 + // We don't check the total size here because it's not easy to do, and it doesn't really worth it. 160 + // See https://docs.github.com/en/actions/using-jobs/defining-outputs-for-jobs 161 + 162 + if err := actions_model.InsertTaskOutputIfNotExist(ctx, task.ID, k, v); err != nil { 163 + log.Warn("Failed to insert the output %q of task %d: %v", k, task.ID, err) 164 + // It's ok not to return errors, the runner will resend the outputs. 165 + } 166 + } 167 + sentOutputs, err := actions_model.FindTaskOutputKeyByTaskID(ctx, task.ID) 168 + if err != nil { 169 + log.Warn("Failed to find the sent outputs of task %d: %v", task.ID, err) 170 + // It's not to return errors, it can be handled when the runner resends sent outputs. 171 + } 172 + 148 173 if err := task.LoadJob(ctx); err != nil { 149 174 return nil, status.Errorf(codes.Internal, "load job: %v", err) 150 175 } ··· 162 187 Id: req.Msg.State.Id, 163 188 Result: task.Status.AsResult(), 164 189 }, 190 + SentOutputs: sentOutputs, 165 191 }), nil 166 192 } 167 193
+54
routers/api/actions/runner/utils.go
··· 37 37 Context: generateTaskContext(t), 38 38 Secrets: getSecretsOfTask(ctx, t), 39 39 } 40 + 41 + if needs, err := findTaskNeeds(ctx, t); err != nil { 42 + log.Error("Cannot find needs for task %v: %v", t.ID, err) 43 + // Go on with empty needs. 44 + // If return error, the task will be wild, which means the runner will never get it when it has been assigned to the runner. 45 + // In contrast, missing needs is less serious. 46 + // And the task will fail and the runner will report the error in the logs. 47 + } else { 48 + task.Needs = needs 49 + } 50 + 40 51 return task, true, nil 41 52 } 42 53 ··· 124 135 125 136 return taskContext 126 137 } 138 + 139 + func findTaskNeeds(ctx context.Context, task *actions_model.ActionTask) (map[string]*runnerv1.TaskNeed, error) { 140 + if err := task.LoadAttributes(ctx); err != nil { 141 + return nil, fmt.Errorf("LoadAttributes: %w", err) 142 + } 143 + if len(task.Job.Needs) == 0 { 144 + return nil, nil 145 + } 146 + needs := map[string]struct{}{} 147 + for _, v := range task.Job.Needs { 148 + needs[v] = struct{}{} 149 + } 150 + 151 + jobs, _, err := actions_model.FindRunJobs(ctx, actions_model.FindRunJobOptions{RunID: task.Job.RunID}) 152 + if err != nil { 153 + return nil, fmt.Errorf("FindRunJobs: %w", err) 154 + } 155 + 156 + ret := make(map[string]*runnerv1.TaskNeed, len(needs)) 157 + for _, job := range jobs { 158 + if _, ok := needs[job.JobID]; !ok { 159 + continue 160 + } 161 + if job.TaskID == 0 || !job.Status.IsDone() { 162 + // it shouldn't happen, or the job has been rerun 163 + continue 164 + } 165 + outputs := make(map[string]string) 166 + got, err := actions_model.FindTaskOutputByTaskID(ctx, job.TaskID) 167 + if err != nil { 168 + return nil, fmt.Errorf("FindTaskOutputByTaskID: %w", err) 169 + } 170 + for _, v := range got { 171 + outputs[v.OutputKey] = v.OutputValue 172 + } 173 + ret[job.JobID] = &runnerv1.TaskNeed{ 174 + Outputs: outputs, 175 + Result: runnerv1.Result(job.Status), 176 + } 177 + } 178 + 179 + return ret, nil 180 + }