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.

Merge pull request '[gitea] week 2024-53 cherry pick (gitea/main -> forgejo)' (#6391) from earl-warren/wcp/2024-53 into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6391
Reviewed-by: Gusted <gusted@noreply.codeberg.org>

+806 -11
+11
models/fixtures/label.yml
··· 96 96 num_issues: 0 97 97 num_closed_issues: 0 98 98 archived_unix: 0 99 + 100 + - 101 + id: 10 102 + repo_id: 3 103 + org_id: 0 104 + name: repo3label1 105 + color: '#112233' 106 + exclusive: false 107 + num_issues: 0 108 + num_closed_issues: 0 109 + archived_unix: 0
+11
models/issues/label.go
··· 353 353 Find(&labelIDs) 354 354 } 355 355 356 + // GetLabelIDsInOrgByNames returns a list of labelIDs by names in a given org. 357 + func GetLabelIDsInOrgByNames(ctx context.Context, orgID int64, labelNames []string) ([]int64, error) { 358 + labelIDs := make([]int64, 0, len(labelNames)) 359 + return labelIDs, db.GetEngine(ctx).Table("label"). 360 + Where("org_id = ?", orgID). 361 + In("name", labelNames). 362 + Asc("name"). 363 + Cols("id"). 364 + Find(&labelIDs) 365 + } 366 + 356 367 // BuildLabelNamesIssueIDsCondition returns a builder where get issue ids match label names 357 368 func BuildLabelNamesIssueIDsCondition(labelNames []string) *builder.Builder { 358 369 return builder.Select("issue_label.issue_id").
+1 -1
modules/references/references.go
··· 32 32 // issueNumericPattern matches string that references to a numeric issue, e.g. #1287 33 33 issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\'|\")([#!][0-9]+)(?:\s|$|\)|\]|\'|\"|[:;,.?!]\s|[:;,.?!]$)`) 34 34 // issueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234 35 - issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\"|\')([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$)|\"|\')`) 35 + issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\"|\')([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$)|\"|\'|,)`) 36 36 // crossReferenceIssueNumericPattern matches string that references a numeric issue in a different repository 37 37 // e.g. org/repo#12345 38 38 crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+[#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)
+1
modules/references/references_test.go
··· 466 466 "ABC-123:", 467 467 "\"ABC-123\"", 468 468 "'ABC-123'", 469 + "ABC-123, unknown PR", 469 470 } 470 471 falseTestCases := []string{ 471 472 "RC-08",
+13 -1
routers/api/v1/repo/issue_label.go
··· 350 350 labelIDs = append(labelIDs, int64(rv.Float())) 351 351 case reflect.String: 352 352 labelNames = append(labelNames, rv.String()) 353 + default: 354 + ctx.Error(http.StatusBadRequest, "InvalidLabel", "a label must be an integer or a string") 355 + return nil, nil, fmt.Errorf("invalid label") 353 356 } 354 357 } 355 358 if len(labelIDs) > 0 && len(labelNames) > 0 { ··· 357 360 return nil, nil, fmt.Errorf("invalid labels") 358 361 } 359 362 if len(labelNames) > 0 { 360 - labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, labelNames) 363 + repoLabelIDs, err := issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, labelNames) 361 364 if err != nil { 362 365 ctx.Error(http.StatusInternalServerError, "GetLabelIDsInRepoByNames", err) 363 366 return nil, nil, err 367 + } 368 + labelIDs = append(labelIDs, repoLabelIDs...) 369 + if ctx.Repo.Owner.IsOrganization() { 370 + orgLabelIDs, err := issues_model.GetLabelIDsInOrgByNames(ctx, ctx.Repo.Owner.ID, labelNames) 371 + if err != nil { 372 + ctx.Error(http.StatusInternalServerError, "GetLabelIDsInOrgByNames", err) 373 + return nil, nil, err 374 + } 375 + labelIDs = append(labelIDs, orgLabelIDs...) 364 376 } 365 377 } 366 378
+4
services/issue/milestone.go
··· 85 85 } 86 86 } 87 87 88 + if issue.MilestoneID == 0 { 89 + issue.Milestone = nil 90 + } 91 + 88 92 return nil 89 93 } 90 94
+8
services/issue/milestone_test.go
··· 24 24 25 25 oldMilestoneID := issue.MilestoneID 26 26 issue.MilestoneID = 2 27 + require.NoError(t, issue.LoadMilestone(db.DefaultContext)) 27 28 require.NoError(t, ChangeMilestoneAssign(db.DefaultContext, issue, doer, oldMilestoneID)) 28 29 unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ 29 30 IssueID: issue.ID, ··· 32 33 OldMilestoneID: oldMilestoneID, 33 34 }) 34 35 unittest.CheckConsistencyFor(t, &issues_model.Milestone{}, &issues_model.Issue{}) 36 + assert.NotNil(t, issue.Milestone) 37 + 38 + oldMilestoneID = issue.MilestoneID 39 + issue.MilestoneID = 0 40 + require.NoError(t, ChangeMilestoneAssign(db.DefaultContext, issue, doer, oldMilestoneID)) 41 + assert.EqualValues(t, 0, issue.MilestoneID) 42 + assert.Nil(t, issue.Milestone) 35 43 }
+417
tests/integration/actions_job_test.go
··· 1 + // Copyright 2024 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package integration 5 + 6 + import ( 7 + "encoding/base64" 8 + "fmt" 9 + "net/http" 10 + "net/url" 11 + "testing" 12 + "time" 13 + 14 + actions_model "code.gitea.io/gitea/models/actions" 15 + auth_model "code.gitea.io/gitea/models/auth" 16 + "code.gitea.io/gitea/models/unittest" 17 + user_model "code.gitea.io/gitea/models/user" 18 + "code.gitea.io/gitea/modules/setting" 19 + api "code.gitea.io/gitea/modules/structs" 20 + 21 + runnerv1 "code.gitea.io/actions-proto-go/runner/v1" 22 + "github.com/stretchr/testify/assert" 23 + ) 24 + 25 + func TestJobWithNeeds(t *testing.T) { 26 + if !setting.Database.Type.IsSQLite3() { 27 + t.Skip() 28 + } 29 + testCases := []struct { 30 + treePath string 31 + fileContent string 32 + outcomes map[string]*mockTaskOutcome 33 + expectedStatuses map[string]string 34 + }{ 35 + { 36 + treePath: ".gitea/workflows/job-with-needs.yml", 37 + fileContent: `name: job-with-needs 38 + on: 39 + push: 40 + paths: 41 + - '.gitea/workflows/job-with-needs.yml' 42 + jobs: 43 + job1: 44 + runs-on: ubuntu-latest 45 + steps: 46 + - run: echo job1 47 + job2: 48 + runs-on: ubuntu-latest 49 + needs: [job1] 50 + steps: 51 + - run: echo job2 52 + `, 53 + outcomes: map[string]*mockTaskOutcome{ 54 + "job1": { 55 + result: runnerv1.Result_RESULT_SUCCESS, 56 + }, 57 + "job2": { 58 + result: runnerv1.Result_RESULT_SUCCESS, 59 + }, 60 + }, 61 + expectedStatuses: map[string]string{ 62 + "job1": actions_model.StatusSuccess.String(), 63 + "job2": actions_model.StatusSuccess.String(), 64 + }, 65 + }, 66 + { 67 + treePath: ".gitea/workflows/job-with-needs-fail.yml", 68 + fileContent: `name: job-with-needs-fail 69 + on: 70 + push: 71 + paths: 72 + - '.gitea/workflows/job-with-needs-fail.yml' 73 + jobs: 74 + job1: 75 + runs-on: ubuntu-latest 76 + steps: 77 + - run: echo job1 78 + job2: 79 + runs-on: ubuntu-latest 80 + needs: [job1] 81 + steps: 82 + - run: echo job2 83 + `, 84 + outcomes: map[string]*mockTaskOutcome{ 85 + "job1": { 86 + result: runnerv1.Result_RESULT_FAILURE, 87 + }, 88 + }, 89 + expectedStatuses: map[string]string{ 90 + "job1": actions_model.StatusFailure.String(), 91 + "job2": actions_model.StatusSkipped.String(), 92 + }, 93 + }, 94 + { 95 + treePath: ".gitea/workflows/job-with-needs-fail-if.yml", 96 + fileContent: `name: job-with-needs-fail-if 97 + on: 98 + push: 99 + paths: 100 + - '.gitea/workflows/job-with-needs-fail-if.yml' 101 + jobs: 102 + job1: 103 + runs-on: ubuntu-latest 104 + steps: 105 + - run: echo job1 106 + job2: 107 + runs-on: ubuntu-latest 108 + if: ${{ always() }} 109 + needs: [job1] 110 + steps: 111 + - run: echo job2 112 + `, 113 + outcomes: map[string]*mockTaskOutcome{ 114 + "job1": { 115 + result: runnerv1.Result_RESULT_FAILURE, 116 + }, 117 + "job2": { 118 + result: runnerv1.Result_RESULT_SUCCESS, 119 + }, 120 + }, 121 + expectedStatuses: map[string]string{ 122 + "job1": actions_model.StatusFailure.String(), 123 + "job2": actions_model.StatusSuccess.String(), 124 + }, 125 + }, 126 + } 127 + onGiteaRun(t, func(t *testing.T, u *url.URL) { 128 + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) 129 + session := loginUser(t, user2.Name) 130 + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 131 + 132 + apiRepo := createActionsTestRepo(t, token, "actions-jobs-with-needs", false) 133 + runner := newMockRunner() 134 + runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"}) 135 + 136 + for _, tc := range testCases { 137 + t.Run(fmt.Sprintf("test %s", tc.treePath), func(t *testing.T) { 138 + // create the workflow file 139 + opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, fmt.Sprintf("create %s", tc.treePath), tc.fileContent) 140 + fileResp := createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, opts) 141 + 142 + // fetch and execute task 143 + for i := 0; i < len(tc.outcomes); i++ { 144 + task := runner.fetchTask(t) 145 + jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id) 146 + outcome := tc.outcomes[jobName] 147 + assert.NotNil(t, outcome) 148 + runner.execTask(t, task, outcome) 149 + } 150 + 151 + // check result 152 + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", user2.Name, apiRepo.Name)). 153 + AddTokenAuth(token) 154 + resp := MakeRequest(t, req, http.StatusOK) 155 + var actionTaskRespAfter api.ActionTaskResponse 156 + DecodeJSON(t, resp, &actionTaskRespAfter) 157 + for _, apiTask := range actionTaskRespAfter.Entries { 158 + if apiTask.HeadSHA != fileResp.Commit.SHA { 159 + continue 160 + } 161 + status := apiTask.Status 162 + assert.Equal(t, status, tc.expectedStatuses[apiTask.Name]) 163 + } 164 + }) 165 + } 166 + 167 + httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteRepository) 168 + doAPIDeleteRepository(httpContext)(t) 169 + }) 170 + } 171 + 172 + func TestJobNeedsMatrix(t *testing.T) { 173 + if !setting.Database.Type.IsSQLite3() { 174 + t.Skip() 175 + } 176 + testCases := []struct { 177 + treePath string 178 + fileContent string 179 + outcomes map[string]*mockTaskOutcome 180 + expectedTaskNeeds map[string]*runnerv1.TaskNeed // jobID => TaskNeed 181 + }{ 182 + { 183 + treePath: ".gitea/workflows/jobs-outputs-with-matrix.yml", 184 + fileContent: `name: jobs-outputs-with-matrix 185 + on: 186 + push: 187 + paths: 188 + - '.gitea/workflows/jobs-outputs-with-matrix.yml' 189 + jobs: 190 + job1: 191 + runs-on: ubuntu-latest 192 + outputs: 193 + output_1: ${{ steps.gen_output.outputs.output_1 }} 194 + output_2: ${{ steps.gen_output.outputs.output_2 }} 195 + output_3: ${{ steps.gen_output.outputs.output_3 }} 196 + strategy: 197 + matrix: 198 + version: [1, 2, 3] 199 + steps: 200 + - name: Generate output 201 + id: gen_output 202 + run: | 203 + version="${{ matrix.version }}" 204 + echo "output_${version}=${version}" >> "$GITHUB_OUTPUT" 205 + job2: 206 + runs-on: ubuntu-latest 207 + needs: [job1] 208 + steps: 209 + - run: echo '${{ toJSON(needs.job1.outputs) }}' 210 + `, 211 + outcomes: map[string]*mockTaskOutcome{ 212 + "job1 (1)": { 213 + result: runnerv1.Result_RESULT_SUCCESS, 214 + outputs: map[string]string{ 215 + "output_1": "1", 216 + "output_2": "", 217 + "output_3": "", 218 + }, 219 + }, 220 + "job1 (2)": { 221 + result: runnerv1.Result_RESULT_SUCCESS, 222 + outputs: map[string]string{ 223 + "output_1": "", 224 + "output_2": "2", 225 + "output_3": "", 226 + }, 227 + }, 228 + "job1 (3)": { 229 + result: runnerv1.Result_RESULT_SUCCESS, 230 + outputs: map[string]string{ 231 + "output_1": "", 232 + "output_2": "", 233 + "output_3": "3", 234 + }, 235 + }, 236 + }, 237 + expectedTaskNeeds: map[string]*runnerv1.TaskNeed{ 238 + "job1": { 239 + Result: runnerv1.Result_RESULT_SUCCESS, 240 + Outputs: map[string]string{ 241 + "output_1": "1", 242 + "output_2": "2", 243 + "output_3": "3", 244 + }, 245 + }, 246 + }, 247 + }, 248 + { 249 + treePath: ".gitea/workflows/jobs-outputs-with-matrix-failure.yml", 250 + fileContent: `name: jobs-outputs-with-matrix-failure 251 + on: 252 + push: 253 + paths: 254 + - '.gitea/workflows/jobs-outputs-with-matrix-failure.yml' 255 + jobs: 256 + job1: 257 + runs-on: ubuntu-latest 258 + outputs: 259 + output_1: ${{ steps.gen_output.outputs.output_1 }} 260 + output_2: ${{ steps.gen_output.outputs.output_2 }} 261 + output_3: ${{ steps.gen_output.outputs.output_3 }} 262 + strategy: 263 + matrix: 264 + version: [1, 2, 3] 265 + steps: 266 + - name: Generate output 267 + id: gen_output 268 + run: | 269 + version="${{ matrix.version }}" 270 + echo "output_${version}=${version}" >> "$GITHUB_OUTPUT" 271 + job2: 272 + runs-on: ubuntu-latest 273 + if: ${{ always() }} 274 + needs: [job1] 275 + steps: 276 + - run: echo '${{ toJSON(needs.job1.outputs) }}' 277 + `, 278 + outcomes: map[string]*mockTaskOutcome{ 279 + "job1 (1)": { 280 + result: runnerv1.Result_RESULT_SUCCESS, 281 + outputs: map[string]string{ 282 + "output_1": "1", 283 + "output_2": "", 284 + "output_3": "", 285 + }, 286 + }, 287 + "job1 (2)": { 288 + result: runnerv1.Result_RESULT_FAILURE, 289 + outputs: map[string]string{ 290 + "output_1": "", 291 + "output_2": "", 292 + "output_3": "", 293 + }, 294 + }, 295 + "job1 (3)": { 296 + result: runnerv1.Result_RESULT_SUCCESS, 297 + outputs: map[string]string{ 298 + "output_1": "", 299 + "output_2": "", 300 + "output_3": "3", 301 + }, 302 + }, 303 + }, 304 + expectedTaskNeeds: map[string]*runnerv1.TaskNeed{ 305 + "job1": { 306 + Result: runnerv1.Result_RESULT_FAILURE, 307 + Outputs: map[string]string{ 308 + "output_1": "1", 309 + "output_2": "", 310 + "output_3": "3", 311 + }, 312 + }, 313 + }, 314 + }, 315 + } 316 + onGiteaRun(t, func(t *testing.T, u *url.URL) { 317 + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) 318 + session := loginUser(t, user2.Name) 319 + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 320 + 321 + apiRepo := createActionsTestRepo(t, token, "actions-jobs-outputs-with-matrix", false) 322 + runner := newMockRunner() 323 + runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"}) 324 + 325 + for _, tc := range testCases { 326 + t.Run(fmt.Sprintf("test %s", tc.treePath), func(t *testing.T) { 327 + opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, fmt.Sprintf("create %s", tc.treePath), tc.fileContent) 328 + createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, opts) 329 + 330 + for i := 0; i < len(tc.outcomes); i++ { 331 + task := runner.fetchTask(t) 332 + jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id) 333 + outcome := tc.outcomes[jobName] 334 + assert.NotNil(t, outcome) 335 + runner.execTask(t, task, outcome) 336 + } 337 + 338 + task := runner.fetchTask(t) 339 + actualTaskNeeds := task.Needs 340 + assert.Len(t, actualTaskNeeds, len(tc.expectedTaskNeeds)) 341 + for jobID, tn := range tc.expectedTaskNeeds { 342 + actualNeed := actualTaskNeeds[jobID] 343 + assert.Equal(t, tn.Result, actualNeed.Result) 344 + assert.Len(t, actualNeed.Outputs, len(tn.Outputs)) 345 + for outputKey, outputValue := range tn.Outputs { 346 + assert.Equal(t, outputValue, actualNeed.Outputs[outputKey]) 347 + } 348 + } 349 + }) 350 + } 351 + 352 + httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteRepository) 353 + doAPIDeleteRepository(httpContext)(t) 354 + }) 355 + } 356 + 357 + func createActionsTestRepo(t *testing.T, authToken, repoName string, isPrivate bool) *api.Repository { 358 + req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{ 359 + Name: repoName, 360 + Private: isPrivate, 361 + Readme: "Default", 362 + AutoInit: true, 363 + DefaultBranch: "main", 364 + }).AddTokenAuth(authToken) 365 + resp := MakeRequest(t, req, http.StatusCreated) 366 + var apiRepo api.Repository 367 + DecodeJSON(t, resp, &apiRepo) 368 + return &apiRepo 369 + } 370 + 371 + func getWorkflowCreateFileOptions(u *user_model.User, branch, msg, content string) *api.CreateFileOptions { 372 + return &api.CreateFileOptions{ 373 + FileOptions: api.FileOptions{ 374 + BranchName: branch, 375 + Message: msg, 376 + Author: api.Identity{ 377 + Name: u.Name, 378 + Email: u.Email, 379 + }, 380 + Committer: api.Identity{ 381 + Name: u.Name, 382 + Email: u.Email, 383 + }, 384 + Dates: api.CommitDateOptions{ 385 + Author: time.Now(), 386 + Committer: time.Now(), 387 + }, 388 + }, 389 + ContentBase64: base64.StdEncoding.EncodeToString([]byte(content)), 390 + } 391 + } 392 + 393 + func createWorkflowFile(t *testing.T, authToken, ownerName, repoName, treePath string, opts *api.CreateFileOptions) *api.FileResponse { 394 + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", ownerName, repoName, treePath), opts). 395 + AddTokenAuth(authToken) 396 + resp := MakeRequest(t, req, http.StatusCreated) 397 + var fileResponse api.FileResponse 398 + DecodeJSON(t, resp, &fileResponse) 399 + return &fileResponse 400 + } 401 + 402 + // getTaskJobNameByTaskID get the job name of the task by task ID 403 + // there is currently not an API for querying a task by ID so we have to list all the tasks 404 + func getTaskJobNameByTaskID(t *testing.T, authToken, ownerName, repoName string, taskID int64) string { 405 + // FIXME: we may need to query several pages 406 + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", ownerName, repoName)). 407 + AddTokenAuth(authToken) 408 + resp := MakeRequest(t, req, http.StatusOK) 409 + var taskRespBefore api.ActionTaskResponse 410 + DecodeJSON(t, resp, &taskRespBefore) 411 + for _, apiTask := range taskRespBefore.Entries { 412 + if apiTask.ID == taskID { 413 + return apiTask.Name 414 + } 415 + } 416 + return "" 417 + }
+163
tests/integration/actions_log_test.go
··· 1 + // Copyright 2024 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package integration 5 + 6 + import ( 7 + "fmt" 8 + "net/http" 9 + "net/url" 10 + "strings" 11 + "testing" 12 + "time" 13 + 14 + auth_model "code.gitea.io/gitea/models/auth" 15 + repo_model "code.gitea.io/gitea/models/repo" 16 + "code.gitea.io/gitea/models/unittest" 17 + user_model "code.gitea.io/gitea/models/user" 18 + "code.gitea.io/gitea/modules/setting" 19 + "code.gitea.io/gitea/modules/storage" 20 + "code.gitea.io/gitea/modules/test" 21 + 22 + runnerv1 "code.gitea.io/actions-proto-go/runner/v1" 23 + "github.com/stretchr/testify/assert" 24 + "github.com/stretchr/testify/require" 25 + "google.golang.org/protobuf/types/known/timestamppb" 26 + ) 27 + 28 + func TestDownloadTaskLogs(t *testing.T) { 29 + if !setting.Database.Type.IsSQLite3() { 30 + t.Skip() 31 + } 32 + now := time.Now() 33 + testCases := []struct { 34 + treePath string 35 + fileContent string 36 + outcome *mockTaskOutcome 37 + zstdEnabled bool 38 + }{ 39 + { 40 + treePath: ".gitea/workflows/download-task-logs-zstd.yml", 41 + fileContent: `name: download-task-logs-zstd 42 + on: 43 + push: 44 + paths: 45 + - '.gitea/workflows/download-task-logs-zstd.yml' 46 + jobs: 47 + job1: 48 + runs-on: ubuntu-latest 49 + steps: 50 + - run: echo job1 with zstd enabled 51 + `, 52 + outcome: &mockTaskOutcome{ 53 + result: runnerv1.Result_RESULT_SUCCESS, 54 + logRows: []*runnerv1.LogRow{ 55 + { 56 + Time: timestamppb.New(now.Add(1 * time.Second)), 57 + Content: " \U0001F433 docker create image", 58 + }, 59 + { 60 + Time: timestamppb.New(now.Add(2 * time.Second)), 61 + Content: "job1 zstd enabled", 62 + }, 63 + { 64 + Time: timestamppb.New(now.Add(3 * time.Second)), 65 + Content: "\U0001F3C1 Job succeeded", 66 + }, 67 + }, 68 + }, 69 + zstdEnabled: true, 70 + }, 71 + { 72 + treePath: ".gitea/workflows/download-task-logs-no-zstd.yml", 73 + fileContent: `name: download-task-logs-no-zstd 74 + on: 75 + push: 76 + paths: 77 + - '.gitea/workflows/download-task-logs-no-zstd.yml' 78 + jobs: 79 + job1: 80 + runs-on: ubuntu-latest 81 + steps: 82 + - run: echo job1 with zstd disabled 83 + `, 84 + outcome: &mockTaskOutcome{ 85 + result: runnerv1.Result_RESULT_SUCCESS, 86 + logRows: []*runnerv1.LogRow{ 87 + { 88 + Time: timestamppb.New(now.Add(4 * time.Second)), 89 + Content: " \U0001F433 docker create image", 90 + }, 91 + { 92 + Time: timestamppb.New(now.Add(5 * time.Second)), 93 + Content: "job1 zstd disabled", 94 + }, 95 + { 96 + Time: timestamppb.New(now.Add(6 * time.Second)), 97 + Content: "\U0001F3C1 Job succeeded", 98 + }, 99 + }, 100 + }, 101 + zstdEnabled: false, 102 + }, 103 + } 104 + onGiteaRun(t, func(t *testing.T, u *url.URL) { 105 + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) 106 + session := loginUser(t, user2.Name) 107 + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 108 + 109 + apiRepo := createActionsTestRepo(t, token, "actions-download-task-logs", false) 110 + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID}) 111 + runner := newMockRunner() 112 + runner.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-runner", []string{"ubuntu-latest"}) 113 + 114 + for _, tc := range testCases { 115 + t.Run(fmt.Sprintf("test %s", tc.treePath), func(t *testing.T) { 116 + var resetFunc func() 117 + if tc.zstdEnabled { 118 + resetFunc = test.MockVariableValue(&setting.Actions.LogCompression, "zstd") 119 + assert.True(t, setting.Actions.LogCompression.IsZstd()) 120 + } else { 121 + resetFunc = test.MockVariableValue(&setting.Actions.LogCompression, "none") 122 + assert.False(t, setting.Actions.LogCompression.IsZstd()) 123 + } 124 + 125 + // create the workflow file 126 + opts := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, fmt.Sprintf("create %s", tc.treePath), tc.fileContent) 127 + createWorkflowFile(t, token, user2.Name, repo.Name, tc.treePath, opts) 128 + 129 + // fetch and execute task 130 + task := runner.fetchTask(t) 131 + runner.execTask(t, task, tc.outcome) 132 + 133 + // check whether the log file exists 134 + logFileName := fmt.Sprintf("%s/%02x/%d.log", repo.FullName(), task.Id%256, task.Id) 135 + if setting.Actions.LogCompression.IsZstd() { 136 + logFileName += ".zst" 137 + } 138 + _, err := storage.Actions.Stat(logFileName) 139 + require.NoError(t, err) 140 + 141 + // download task logs and check content 142 + runIndex := task.Context.GetFields()["run_number"].GetStringValue() 143 + req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/0/logs", user2.Name, repo.Name, runIndex)). 144 + AddTokenAuth(token) 145 + resp := MakeRequest(t, req, http.StatusOK) 146 + logTextLines := strings.Split(strings.TrimSpace(resp.Body.String()), "\n") 147 + assert.Len(t, logTextLines, len(tc.outcome.logRows)) 148 + for idx, lr := range tc.outcome.logRows { 149 + assert.Equal( 150 + t, 151 + fmt.Sprintf("%s %s", lr.Time.AsTime().Format("2006-01-02T15:04:05.0000000Z07:00"), lr.Content), 152 + logTextLines[idx], 153 + ) 154 + } 155 + 156 + resetFunc() 157 + }) 158 + } 159 + 160 + httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository) 161 + doAPIDeleteRepository(httpContext)(t) 162 + }) 163 + }
+162
tests/integration/actions_runner_test.go
··· 1 + // Copyright 2024 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package integration 5 + 6 + import ( 7 + "context" 8 + "fmt" 9 + "net/http" 10 + "testing" 11 + "time" 12 + 13 + auth_model "code.gitea.io/gitea/models/auth" 14 + "code.gitea.io/gitea/modules/setting" 15 + 16 + pingv1 "code.gitea.io/actions-proto-go/ping/v1" 17 + "code.gitea.io/actions-proto-go/ping/v1/pingv1connect" 18 + runnerv1 "code.gitea.io/actions-proto-go/runner/v1" 19 + "code.gitea.io/actions-proto-go/runner/v1/runnerv1connect" 20 + "connectrpc.com/connect" 21 + "github.com/stretchr/testify/assert" 22 + "github.com/stretchr/testify/require" 23 + "google.golang.org/protobuf/types/known/timestamppb" 24 + ) 25 + 26 + type mockRunner struct { 27 + client *mockRunnerClient 28 + } 29 + 30 + type mockRunnerClient struct { 31 + pingServiceClient pingv1connect.PingServiceClient 32 + runnerServiceClient runnerv1connect.RunnerServiceClient 33 + } 34 + 35 + func newMockRunner() *mockRunner { 36 + client := newMockRunnerClient("", "") 37 + return &mockRunner{client: client} 38 + } 39 + 40 + func newMockRunnerClient(uuid, token string) *mockRunnerClient { 41 + baseURL := fmt.Sprintf("%sapi/actions", setting.AppURL) 42 + 43 + opt := connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc { 44 + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { 45 + if uuid != "" { 46 + req.Header().Set("x-runner-uuid", uuid) 47 + } 48 + if token != "" { 49 + req.Header().Set("x-runner-token", token) 50 + } 51 + return next(ctx, req) 52 + } 53 + })) 54 + 55 + client := &mockRunnerClient{ 56 + pingServiceClient: pingv1connect.NewPingServiceClient(http.DefaultClient, baseURL, opt), 57 + runnerServiceClient: runnerv1connect.NewRunnerServiceClient(http.DefaultClient, baseURL, opt), 58 + } 59 + 60 + return client 61 + } 62 + 63 + func (r *mockRunner) doPing(t *testing.T) { 64 + resp, err := r.client.pingServiceClient.Ping(context.Background(), connect.NewRequest(&pingv1.PingRequest{ 65 + Data: "mock-runner", 66 + })) 67 + require.NoError(t, err) 68 + require.Equal(t, "Hello, mock-runner!", resp.Msg.Data) 69 + } 70 + 71 + func (r *mockRunner) doRegister(t *testing.T, name, token string, labels []string) { 72 + r.doPing(t) 73 + resp, err := r.client.runnerServiceClient.Register(context.Background(), connect.NewRequest(&runnerv1.RegisterRequest{ 74 + Name: name, 75 + Token: token, 76 + Version: "mock-runner-version", 77 + Labels: labels, 78 + })) 79 + require.NoError(t, err) 80 + r.client = newMockRunnerClient(resp.Msg.Runner.Uuid, resp.Msg.Runner.Token) 81 + } 82 + 83 + func (r *mockRunner) registerAsRepoRunner(t *testing.T, ownerName, repoName, runnerName string, labels []string) { 84 + if !setting.Database.Type.IsSQLite3() { 85 + // registering a mock runner when using a database other than SQLite leaves leftovers 86 + t.FailNow() 87 + } 88 + session := loginUser(t, ownerName) 89 + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) 90 + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/registration-token", ownerName, repoName)).AddTokenAuth(token) 91 + resp := MakeRequest(t, req, http.StatusOK) 92 + var registrationToken struct { 93 + Token string `json:"token"` 94 + } 95 + DecodeJSON(t, resp, &registrationToken) 96 + r.doRegister(t, runnerName, registrationToken.Token, labels) 97 + } 98 + 99 + func (r *mockRunner) fetchTask(t *testing.T, timeout ...time.Duration) *runnerv1.Task { 100 + fetchTimeout := 10 * time.Second 101 + if len(timeout) > 0 { 102 + fetchTimeout = timeout[0] 103 + } 104 + ddl := time.Now().Add(fetchTimeout) 105 + var task *runnerv1.Task 106 + for time.Now().Before(ddl) { 107 + resp, err := r.client.runnerServiceClient.FetchTask(context.Background(), connect.NewRequest(&runnerv1.FetchTaskRequest{ 108 + TasksVersion: 0, 109 + })) 110 + require.NoError(t, err) 111 + if resp.Msg.Task != nil { 112 + task = resp.Msg.Task 113 + break 114 + } 115 + time.Sleep(time.Second) 116 + } 117 + assert.NotNil(t, task, "failed to fetch a task") 118 + return task 119 + } 120 + 121 + type mockTaskOutcome struct { 122 + result runnerv1.Result 123 + outputs map[string]string 124 + logRows []*runnerv1.LogRow 125 + execTime time.Duration 126 + } 127 + 128 + func (r *mockRunner) execTask(t *testing.T, task *runnerv1.Task, outcome *mockTaskOutcome) { 129 + for idx, lr := range outcome.logRows { 130 + resp, err := r.client.runnerServiceClient.UpdateLog(context.Background(), connect.NewRequest(&runnerv1.UpdateLogRequest{ 131 + TaskId: task.Id, 132 + Index: int64(idx), 133 + Rows: []*runnerv1.LogRow{lr}, 134 + NoMore: idx == len(outcome.logRows)-1, 135 + })) 136 + require.NoError(t, err) 137 + assert.EqualValues(t, idx+1, resp.Msg.AckIndex) 138 + } 139 + sentOutputKeys := make([]string, 0, len(outcome.outputs)) 140 + for outputKey, outputValue := range outcome.outputs { 141 + resp, err := r.client.runnerServiceClient.UpdateTask(context.Background(), connect.NewRequest(&runnerv1.UpdateTaskRequest{ 142 + State: &runnerv1.TaskState{ 143 + Id: task.Id, 144 + Result: runnerv1.Result_RESULT_UNSPECIFIED, 145 + }, 146 + Outputs: map[string]string{outputKey: outputValue}, 147 + })) 148 + require.NoError(t, err) 149 + sentOutputKeys = append(sentOutputKeys, outputKey) 150 + assert.ElementsMatch(t, sentOutputKeys, resp.Msg.SentOutputs) 151 + } 152 + time.Sleep(outcome.execTime) 153 + resp, err := r.client.runnerServiceClient.UpdateTask(context.Background(), connect.NewRequest(&runnerv1.UpdateTaskRequest{ 154 + State: &runnerv1.TaskState{ 155 + Id: task.Id, 156 + Result: outcome.result, 157 + StoppedAt: timestamppb.Now(), 158 + }, 159 + })) 160 + require.NoError(t, err) 161 + assert.Equal(t, outcome.result, resp.Msg.State.Result) 162 + }
+15 -9
tests/integration/api_issue_label_test.go
··· 120 120 func TestAPIAddIssueLabelsWithLabelNames(t *testing.T) { 121 121 require.NoError(t, unittest.LoadFixtures()) 122 122 123 - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) 124 - issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID}) 123 + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) 124 + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 6, RepoID: repo.ID}) 125 125 owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) 126 + repoLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 10, RepoID: repo.ID}) 127 + orgLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 4, OrgID: owner.ID}) 126 128 127 - session := loginUser(t, owner.Name) 128 - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) 129 - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels", 130 - repo.OwnerName, repo.Name, issue.Index) 129 + user1Session := loginUser(t, "user1") 130 + token := getTokenForLoggedInUser(t, user1Session, auth_model.AccessTokenScopeWriteIssue) 131 + 132 + // add the org label and the repo label to the issue 133 + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels", owner.Name, repo.Name, issue.Index) 131 134 req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueLabelsOption{ 132 - Labels: []any{"label1", "label2"}, 135 + Labels: []any{repoLabel.Name, orgLabel.Name}, 133 136 }).AddTokenAuth(token) 134 137 resp := MakeRequest(t, req, http.StatusOK) 135 138 var apiLabels []*api.Label 136 139 DecodeJSON(t, resp, &apiLabels) 137 140 assert.Len(t, apiLabels, unittest.GetCount(t, &issues_model.IssueLabel{IssueID: issue.ID})) 138 - 139 141 var apiLabelNames []string 140 142 for _, label := range apiLabels { 141 143 apiLabelNames = append(apiLabelNames, label.Name) 142 144 } 143 - assert.ElementsMatch(t, apiLabelNames, []string{"label1", "label2"}) 145 + assert.ElementsMatch(t, apiLabelNames, []string{repoLabel.Name, orgLabel.Name}) 146 + 147 + // delete labels 148 + req = NewRequest(t, "DELETE", urlStr).AddTokenAuth(token) 149 + MakeRequest(t, req, http.StatusNoContent) 144 150 } 145 151 146 152 func TestAPIAddIssueLabelsAutoDate(t *testing.T) {