gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  1. // Copyright 2024 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package integration
  4. import (
  5. "encoding/base64"
  6. "fmt"
  7. "net/http"
  8. "net/url"
  9. "reflect"
  10. "strconv"
  11. "testing"
  12. "time"
  13. actions_model "code.gitea.io/gitea/models/actions"
  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/git"
  19. "code.gitea.io/gitea/modules/json"
  20. "code.gitea.io/gitea/modules/setting"
  21. api "code.gitea.io/gitea/modules/structs"
  22. actions_service "code.gitea.io/gitea/services/actions"
  23. runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
  24. "connectrpc.com/connect"
  25. "github.com/stretchr/testify/assert"
  26. )
  27. func TestJobWithNeeds(t *testing.T) {
  28. testCases := []struct {
  29. treePath string
  30. fileContent string
  31. outcomes map[string]*mockTaskOutcome
  32. expectedStatuses map[string]string
  33. }{
  34. {
  35. treePath: ".gitea/workflows/job-with-needs.yml",
  36. fileContent: `name: job-with-needs
  37. on:
  38. push:
  39. paths:
  40. - '.gitea/workflows/job-with-needs.yml'
  41. jobs:
  42. job1:
  43. runs-on: ubuntu-latest
  44. steps:
  45. - run: echo job1
  46. job2:
  47. runs-on: ubuntu-latest
  48. needs: [job1]
  49. steps:
  50. - run: echo job2
  51. `,
  52. outcomes: map[string]*mockTaskOutcome{
  53. "job1": {
  54. result: runnerv1.Result_RESULT_SUCCESS,
  55. },
  56. "job2": {
  57. result: runnerv1.Result_RESULT_SUCCESS,
  58. },
  59. },
  60. expectedStatuses: map[string]string{
  61. "job1": actions_model.StatusSuccess.String(),
  62. "job2": actions_model.StatusSuccess.String(),
  63. },
  64. },
  65. {
  66. treePath: ".gitea/workflows/job-with-needs-fail.yml",
  67. fileContent: `name: job-with-needs-fail
  68. on:
  69. push:
  70. paths:
  71. - '.gitea/workflows/job-with-needs-fail.yml'
  72. jobs:
  73. job1:
  74. runs-on: ubuntu-latest
  75. steps:
  76. - run: echo job1
  77. job2:
  78. runs-on: ubuntu-latest
  79. needs: [job1]
  80. steps:
  81. - run: echo job2
  82. `,
  83. outcomes: map[string]*mockTaskOutcome{
  84. "job1": {
  85. result: runnerv1.Result_RESULT_FAILURE,
  86. },
  87. },
  88. expectedStatuses: map[string]string{
  89. "job1": actions_model.StatusFailure.String(),
  90. "job2": actions_model.StatusSkipped.String(),
  91. },
  92. },
  93. {
  94. treePath: ".gitea/workflows/job-with-needs-fail-if.yml",
  95. fileContent: `name: job-with-needs-fail-if
  96. on:
  97. push:
  98. paths:
  99. - '.gitea/workflows/job-with-needs-fail-if.yml'
  100. jobs:
  101. job1:
  102. runs-on: ubuntu-latest
  103. steps:
  104. - run: echo job1
  105. job2:
  106. runs-on: ubuntu-latest
  107. if: ${{ always() }}
  108. needs: [job1]
  109. steps:
  110. - run: echo job2
  111. `,
  112. outcomes: map[string]*mockTaskOutcome{
  113. "job1": {
  114. result: runnerv1.Result_RESULT_FAILURE,
  115. },
  116. "job2": {
  117. result: runnerv1.Result_RESULT_SUCCESS,
  118. },
  119. },
  120. expectedStatuses: map[string]string{
  121. "job1": actions_model.StatusFailure.String(),
  122. "job2": actions_model.StatusSuccess.String(),
  123. },
  124. },
  125. }
  126. onGiteaRun(t, func(t *testing.T, u *url.URL) {
  127. user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
  128. session := loginUser(t, user2.Name)
  129. token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
  130. apiRepo := createActionsTestRepo(t, token, "actions-jobs-with-needs", false)
  131. runner := newMockRunner()
  132. runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
  133. for _, tc := range testCases {
  134. t.Run("test "+tc.treePath, func(t *testing.T) {
  135. // create the workflow file
  136. opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, "create "+tc.treePath, tc.fileContent)
  137. fileResp := createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, opts)
  138. // fetch and execute task
  139. for i := 0; i < len(tc.outcomes); i++ {
  140. task := runner.fetchTask(t)
  141. jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id)
  142. outcome := tc.outcomes[jobName]
  143. assert.NotNil(t, outcome)
  144. runner.execTask(t, task, outcome)
  145. }
  146. // check result
  147. req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", user2.Name, apiRepo.Name)).
  148. AddTokenAuth(token)
  149. resp := MakeRequest(t, req, http.StatusOK)
  150. var actionTaskRespAfter api.ActionTaskResponse
  151. DecodeJSON(t, resp, &actionTaskRespAfter)
  152. for _, apiTask := range actionTaskRespAfter.Entries {
  153. if apiTask.HeadSHA != fileResp.Commit.SHA {
  154. continue
  155. }
  156. status := apiTask.Status
  157. assert.Equal(t, status, tc.expectedStatuses[apiTask.Name])
  158. }
  159. })
  160. }
  161. })
  162. }
  163. func TestJobNeedsMatrix(t *testing.T) {
  164. testCases := []struct {
  165. treePath string
  166. fileContent string
  167. outcomes map[string]*mockTaskOutcome
  168. expectedTaskNeeds map[string]*runnerv1.TaskNeed // jobID => TaskNeed
  169. }{
  170. {
  171. treePath: ".gitea/workflows/jobs-outputs-with-matrix.yml",
  172. fileContent: `name: jobs-outputs-with-matrix
  173. on:
  174. push:
  175. paths:
  176. - '.gitea/workflows/jobs-outputs-with-matrix.yml'
  177. jobs:
  178. job1:
  179. runs-on: ubuntu-latest
  180. outputs:
  181. output_1: ${{ steps.gen_output.outputs.output_1 }}
  182. output_2: ${{ steps.gen_output.outputs.output_2 }}
  183. output_3: ${{ steps.gen_output.outputs.output_3 }}
  184. strategy:
  185. matrix:
  186. version: [1, 2, 3]
  187. steps:
  188. - name: Generate output
  189. id: gen_output
  190. run: |
  191. version="${{ matrix.version }}"
  192. echo "output_${version}=${version}" >> "$GITHUB_OUTPUT"
  193. job2:
  194. runs-on: ubuntu-latest
  195. needs: [job1]
  196. steps:
  197. - run: echo '${{ toJSON(needs.job1.outputs) }}'
  198. `,
  199. outcomes: map[string]*mockTaskOutcome{
  200. "job1 (1)": {
  201. result: runnerv1.Result_RESULT_SUCCESS,
  202. outputs: map[string]string{
  203. "output_1": "1",
  204. "output_2": "",
  205. "output_3": "",
  206. },
  207. },
  208. "job1 (2)": {
  209. result: runnerv1.Result_RESULT_SUCCESS,
  210. outputs: map[string]string{
  211. "output_1": "",
  212. "output_2": "2",
  213. "output_3": "",
  214. },
  215. },
  216. "job1 (3)": {
  217. result: runnerv1.Result_RESULT_SUCCESS,
  218. outputs: map[string]string{
  219. "output_1": "",
  220. "output_2": "",
  221. "output_3": "3",
  222. },
  223. },
  224. },
  225. expectedTaskNeeds: map[string]*runnerv1.TaskNeed{
  226. "job1": {
  227. Result: runnerv1.Result_RESULT_SUCCESS,
  228. Outputs: map[string]string{
  229. "output_1": "1",
  230. "output_2": "2",
  231. "output_3": "3",
  232. },
  233. },
  234. },
  235. },
  236. {
  237. treePath: ".gitea/workflows/jobs-outputs-with-matrix-failure.yml",
  238. fileContent: `name: jobs-outputs-with-matrix-failure
  239. on:
  240. push:
  241. paths:
  242. - '.gitea/workflows/jobs-outputs-with-matrix-failure.yml'
  243. jobs:
  244. job1:
  245. runs-on: ubuntu-latest
  246. outputs:
  247. output_1: ${{ steps.gen_output.outputs.output_1 }}
  248. output_2: ${{ steps.gen_output.outputs.output_2 }}
  249. output_3: ${{ steps.gen_output.outputs.output_3 }}
  250. strategy:
  251. matrix:
  252. version: [1, 2, 3]
  253. steps:
  254. - name: Generate output
  255. id: gen_output
  256. run: |
  257. version="${{ matrix.version }}"
  258. echo "output_${version}=${version}" >> "$GITHUB_OUTPUT"
  259. job2:
  260. runs-on: ubuntu-latest
  261. if: ${{ always() }}
  262. needs: [job1]
  263. steps:
  264. - run: echo '${{ toJSON(needs.job1.outputs) }}'
  265. `,
  266. outcomes: map[string]*mockTaskOutcome{
  267. "job1 (1)": {
  268. result: runnerv1.Result_RESULT_SUCCESS,
  269. outputs: map[string]string{
  270. "output_1": "1",
  271. "output_2": "",
  272. "output_3": "",
  273. },
  274. },
  275. "job1 (2)": {
  276. result: runnerv1.Result_RESULT_FAILURE,
  277. outputs: map[string]string{
  278. "output_1": "",
  279. "output_2": "",
  280. "output_3": "",
  281. },
  282. },
  283. "job1 (3)": {
  284. result: runnerv1.Result_RESULT_SUCCESS,
  285. outputs: map[string]string{
  286. "output_1": "",
  287. "output_2": "",
  288. "output_3": "3",
  289. },
  290. },
  291. },
  292. expectedTaskNeeds: map[string]*runnerv1.TaskNeed{
  293. "job1": {
  294. Result: runnerv1.Result_RESULT_FAILURE,
  295. Outputs: map[string]string{
  296. "output_1": "1",
  297. "output_2": "",
  298. "output_3": "3",
  299. },
  300. },
  301. },
  302. },
  303. }
  304. onGiteaRun(t, func(t *testing.T, u *url.URL) {
  305. user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
  306. session := loginUser(t, user2.Name)
  307. token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
  308. apiRepo := createActionsTestRepo(t, token, "actions-jobs-outputs-with-matrix", false)
  309. runner := newMockRunner()
  310. runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
  311. for _, tc := range testCases {
  312. t.Run("test "+tc.treePath, func(t *testing.T) {
  313. opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, "create "+tc.treePath, tc.fileContent)
  314. createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, opts)
  315. for i := 0; i < len(tc.outcomes); i++ {
  316. task := runner.fetchTask(t)
  317. jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id)
  318. outcome := tc.outcomes[jobName]
  319. assert.NotNil(t, outcome)
  320. runner.execTask(t, task, outcome)
  321. }
  322. task := runner.fetchTask(t)
  323. actualTaskNeeds := task.Needs
  324. assert.Len(t, actualTaskNeeds, len(tc.expectedTaskNeeds))
  325. for jobID, tn := range tc.expectedTaskNeeds {
  326. actualNeed := actualTaskNeeds[jobID]
  327. assert.Equal(t, tn.Result, actualNeed.Result)
  328. assert.Len(t, actualNeed.Outputs, len(tn.Outputs))
  329. for outputKey, outputValue := range tn.Outputs {
  330. assert.Equal(t, outputValue, actualNeed.Outputs[outputKey])
  331. }
  332. }
  333. })
  334. }
  335. })
  336. }
  337. func TestActionsGiteaContext(t *testing.T) {
  338. onGiteaRun(t, func(t *testing.T, u *url.URL) {
  339. user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
  340. user2Session := loginUser(t, user2.Name)
  341. user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
  342. apiBaseRepo := createActionsTestRepo(t, user2Token, "actions-gitea-context", false)
  343. baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiBaseRepo.ID})
  344. user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository)
  345. runner := newMockRunner()
  346. runner.registerAsRepoRunner(t, baseRepo.OwnerName, baseRepo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
  347. // init the workflow
  348. wfTreePath := ".gitea/workflows/pull.yml"
  349. wfFileContent := `name: Pull Request
  350. on: pull_request
  351. jobs:
  352. wf1-job:
  353. runs-on: ubuntu-latest
  354. steps:
  355. - run: echo 'test the pull'
  356. `
  357. opts := getWorkflowCreateFileOptions(user2, baseRepo.DefaultBranch, "create "+wfTreePath, wfFileContent)
  358. createWorkflowFile(t, user2Token, baseRepo.OwnerName, baseRepo.Name, wfTreePath, opts)
  359. // user2 creates a pull request
  360. doAPICreateFile(user2APICtx, "user2-patch.txt", &api.CreateFileOptions{
  361. FileOptions: api.FileOptions{
  362. NewBranchName: "user2/patch-1",
  363. Message: "create user2-patch.txt",
  364. Author: api.Identity{
  365. Name: user2.Name,
  366. Email: user2.Email,
  367. },
  368. Committer: api.Identity{
  369. Name: user2.Name,
  370. Email: user2.Email,
  371. },
  372. Dates: api.CommitDateOptions{
  373. Author: time.Now(),
  374. Committer: time.Now(),
  375. },
  376. },
  377. ContentBase64: base64.StdEncoding.EncodeToString([]byte("user2-fix")),
  378. })(t)
  379. apiPull, err := doAPICreatePullRequest(user2APICtx, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, "user2/patch-1")(t)
  380. assert.NoError(t, err)
  381. task := runner.fetchTask(t)
  382. gtCtx := task.Context.GetFields()
  383. actionTask := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: task.Id})
  384. actionRunJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: actionTask.JobID})
  385. actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: actionRunJob.RunID})
  386. assert.NoError(t, actionRun.LoadAttributes(t.Context()))
  387. assert.Equal(t, user2.Name, gtCtx["actor"].GetStringValue())
  388. assert.Equal(t, setting.AppURL+"api/v1", gtCtx["api_url"].GetStringValue())
  389. assert.Equal(t, apiPull.Base.Ref, gtCtx["base_ref"].GetStringValue())
  390. runEvent := map[string]any{}
  391. assert.NoError(t, json.Unmarshal([]byte(actionRun.EventPayload), &runEvent))
  392. assert.True(t, reflect.DeepEqual(gtCtx["event"].GetStructValue().AsMap(), runEvent))
  393. assert.Equal(t, actionRun.TriggerEvent, gtCtx["event_name"].GetStringValue())
  394. assert.Equal(t, apiPull.Head.Ref, gtCtx["head_ref"].GetStringValue())
  395. assert.Equal(t, actionRunJob.JobID, gtCtx["job"].GetStringValue())
  396. assert.Equal(t, actionRun.Ref, gtCtx["ref"].GetStringValue())
  397. assert.Equal(t, (git.RefName(actionRun.Ref)).ShortName(), gtCtx["ref_name"].GetStringValue())
  398. assert.False(t, gtCtx["ref_protected"].GetBoolValue())
  399. assert.Equal(t, string((git.RefName(actionRun.Ref)).RefType()), gtCtx["ref_type"].GetStringValue())
  400. assert.Equal(t, actionRun.Repo.OwnerName+"/"+actionRun.Repo.Name, gtCtx["repository"].GetStringValue())
  401. assert.Equal(t, actionRun.Repo.OwnerName, gtCtx["repository_owner"].GetStringValue())
  402. assert.Equal(t, actionRun.Repo.HTMLURL(), gtCtx["repositoryUrl"].GetStringValue())
  403. assert.Equal(t, strconv.FormatInt(actionRunJob.RunID, 10), gtCtx["run_id"].GetStringValue())
  404. assert.Equal(t, strconv.FormatInt(actionRun.Index, 10), gtCtx["run_number"].GetStringValue())
  405. assert.Equal(t, strconv.FormatInt(actionRunJob.Attempt, 10), gtCtx["run_attempt"].GetStringValue())
  406. assert.Equal(t, "Actions", gtCtx["secret_source"].GetStringValue())
  407. assert.Equal(t, setting.AppURL, gtCtx["server_url"].GetStringValue())
  408. assert.Equal(t, actionRun.CommitSHA, gtCtx["sha"].GetStringValue())
  409. assert.Equal(t, actionRun.WorkflowID, gtCtx["workflow"].GetStringValue())
  410. assert.Equal(t, setting.Actions.DefaultActionsURL.URL(), gtCtx["gitea_default_actions_url"].GetStringValue())
  411. token := gtCtx["token"].GetStringValue()
  412. assert.Equal(t, actionTask.TokenLastEight, token[len(token)-8:])
  413. })
  414. }
  415. // Ephemeral
  416. func TestActionsGiteaContextEphemeral(t *testing.T) {
  417. onGiteaRun(t, func(t *testing.T, u *url.URL) {
  418. user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
  419. user2Session := loginUser(t, user2.Name)
  420. user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
  421. apiBaseRepo := createActionsTestRepo(t, user2Token, "actions-gitea-context", false)
  422. baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiBaseRepo.ID})
  423. user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository)
  424. runner := newMockRunner()
  425. runner.registerAsRepoRunner(t, baseRepo.OwnerName, baseRepo.Name, "mock-runner", []string{"ubuntu-latest"}, true)
  426. // verify CleanupEphemeralRunners does not remove this runner
  427. err := actions_service.CleanupEphemeralRunners(t.Context())
  428. assert.NoError(t, err)
  429. // init the workflow
  430. wfTreePath := ".gitea/workflows/pull.yml"
  431. wfFileContent := `name: Pull Request
  432. on: pull_request
  433. jobs:
  434. wf1-job:
  435. runs-on: ubuntu-latest
  436. steps:
  437. - run: echo 'test the pull'
  438. wf2-job:
  439. runs-on: ubuntu-latest
  440. steps:
  441. - run: echo 'test the pull'
  442. `
  443. opts := getWorkflowCreateFileOptions(user2, baseRepo.DefaultBranch, "create "+wfTreePath, wfFileContent)
  444. createWorkflowFile(t, user2Token, baseRepo.OwnerName, baseRepo.Name, wfTreePath, opts)
  445. // user2 creates a pull request
  446. doAPICreateFile(user2APICtx, "user2-patch.txt", &api.CreateFileOptions{
  447. FileOptions: api.FileOptions{
  448. NewBranchName: "user2/patch-1",
  449. Message: "create user2-patch.txt",
  450. Author: api.Identity{
  451. Name: user2.Name,
  452. Email: user2.Email,
  453. },
  454. Committer: api.Identity{
  455. Name: user2.Name,
  456. Email: user2.Email,
  457. },
  458. Dates: api.CommitDateOptions{
  459. Author: time.Now(),
  460. Committer: time.Now(),
  461. },
  462. },
  463. ContentBase64: base64.StdEncoding.EncodeToString([]byte("user2-fix")),
  464. })(t)
  465. apiPull, err := doAPICreatePullRequest(user2APICtx, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, "user2/patch-1")(t)
  466. assert.NoError(t, err)
  467. task := runner.fetchTask(t)
  468. gtCtx := task.Context.GetFields()
  469. actionTask := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: task.Id})
  470. actionRunJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: actionTask.JobID})
  471. actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: actionRunJob.RunID})
  472. assert.NoError(t, actionRun.LoadAttributes(t.Context()))
  473. assert.Equal(t, user2.Name, gtCtx["actor"].GetStringValue())
  474. assert.Equal(t, setting.AppURL+"api/v1", gtCtx["api_url"].GetStringValue())
  475. assert.Equal(t, apiPull.Base.Ref, gtCtx["base_ref"].GetStringValue())
  476. runEvent := map[string]any{}
  477. assert.NoError(t, json.Unmarshal([]byte(actionRun.EventPayload), &runEvent))
  478. assert.True(t, reflect.DeepEqual(gtCtx["event"].GetStructValue().AsMap(), runEvent))
  479. assert.Equal(t, actionRun.TriggerEvent, gtCtx["event_name"].GetStringValue())
  480. assert.Equal(t, apiPull.Head.Ref, gtCtx["head_ref"].GetStringValue())
  481. assert.Equal(t, actionRunJob.JobID, gtCtx["job"].GetStringValue())
  482. assert.Equal(t, actionRun.Ref, gtCtx["ref"].GetStringValue())
  483. assert.Equal(t, (git.RefName(actionRun.Ref)).ShortName(), gtCtx["ref_name"].GetStringValue())
  484. assert.False(t, gtCtx["ref_protected"].GetBoolValue())
  485. assert.Equal(t, string((git.RefName(actionRun.Ref)).RefType()), gtCtx["ref_type"].GetStringValue())
  486. assert.Equal(t, actionRun.Repo.OwnerName+"/"+actionRun.Repo.Name, gtCtx["repository"].GetStringValue())
  487. assert.Equal(t, actionRun.Repo.OwnerName, gtCtx["repository_owner"].GetStringValue())
  488. assert.Equal(t, actionRun.Repo.HTMLURL(), gtCtx["repositoryUrl"].GetStringValue())
  489. assert.Equal(t, strconv.FormatInt(actionRunJob.RunID, 10), gtCtx["run_id"].GetStringValue())
  490. assert.Equal(t, strconv.FormatInt(actionRun.Index, 10), gtCtx["run_number"].GetStringValue())
  491. assert.Equal(t, strconv.FormatInt(actionRunJob.Attempt, 10), gtCtx["run_attempt"].GetStringValue())
  492. assert.Equal(t, "Actions", gtCtx["secret_source"].GetStringValue())
  493. assert.Equal(t, setting.AppURL, gtCtx["server_url"].GetStringValue())
  494. assert.Equal(t, actionRun.CommitSHA, gtCtx["sha"].GetStringValue())
  495. assert.Equal(t, actionRun.WorkflowID, gtCtx["workflow"].GetStringValue())
  496. assert.Equal(t, setting.Actions.DefaultActionsURL.URL(), gtCtx["gitea_default_actions_url"].GetStringValue())
  497. token := gtCtx["token"].GetStringValue()
  498. assert.Equal(t, actionTask.TokenLastEight, token[len(token)-8:])
  499. // verify CleanupEphemeralRunners does not remove this runner
  500. err = actions_service.CleanupEphemeralRunners(t.Context())
  501. assert.NoError(t, err)
  502. resp, err := runner.client.runnerServiceClient.FetchTask(t.Context(), connect.NewRequest(&runnerv1.FetchTaskRequest{
  503. TasksVersion: 0,
  504. }))
  505. assert.NoError(t, err)
  506. assert.Nil(t, resp.Msg.Task)
  507. // verify CleanupEphemeralRunners does not remove this runner
  508. err = actions_service.CleanupEphemeralRunners(t.Context())
  509. assert.NoError(t, err)
  510. _, err = runner.client.runnerServiceClient.UpdateTask(t.Context(), connect.NewRequest(&runnerv1.UpdateTaskRequest{
  511. State: &runnerv1.TaskState{
  512. Id: actionTask.ID,
  513. Result: runnerv1.Result_RESULT_SUCCESS,
  514. },
  515. }))
  516. assert.NoError(t, err)
  517. resp, err = runner.client.runnerServiceClient.FetchTask(t.Context(), connect.NewRequest(&runnerv1.FetchTaskRequest{
  518. TasksVersion: 0,
  519. }))
  520. assert.Error(t, err)
  521. assert.Nil(t, resp)
  522. resp, err = runner.client.runnerServiceClient.FetchTask(t.Context(), connect.NewRequest(&runnerv1.FetchTaskRequest{
  523. TasksVersion: 0,
  524. }))
  525. assert.Error(t, err)
  526. assert.Nil(t, resp)
  527. // create a runner that picks a job and get force cancelled
  528. runnerToBeRemoved := newMockRunner()
  529. runnerToBeRemoved.registerAsRepoRunner(t, baseRepo.OwnerName, baseRepo.Name, "mock-runner-to-be-removed", []string{"ubuntu-latest"}, true)
  530. taskToStopAPIObj := runnerToBeRemoved.fetchTask(t)
  531. taskToStop := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: taskToStopAPIObj.Id})
  532. // verify CleanupEphemeralRunners does not remove the custom crafted runner
  533. err = actions_service.CleanupEphemeralRunners(t.Context())
  534. assert.NoError(t, err)
  535. runnerToRemove := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: taskToStop.RunnerID})
  536. err = actions_model.StopTask(t.Context(), taskToStop.ID, actions_model.StatusFailure)
  537. assert.NoError(t, err)
  538. // verify CleanupEphemeralRunners does remove the custom crafted runner
  539. err = actions_service.CleanupEphemeralRunners(t.Context())
  540. assert.NoError(t, err)
  541. unittest.AssertNotExistsBean(t, &actions_model.ActionRunner{ID: runnerToRemove.ID})
  542. })
  543. }
  544. func createActionsTestRepo(t *testing.T, authToken, repoName string, isPrivate bool) *api.Repository {
  545. req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
  546. Name: repoName,
  547. Private: isPrivate,
  548. Readme: "Default",
  549. AutoInit: true,
  550. DefaultBranch: "main",
  551. }).AddTokenAuth(authToken)
  552. resp := MakeRequest(t, req, http.StatusCreated)
  553. var apiRepo api.Repository
  554. DecodeJSON(t, resp, &apiRepo)
  555. return &apiRepo
  556. }
  557. func getWorkflowCreateFileOptions(u *user_model.User, branch, msg, content string) *api.CreateFileOptions {
  558. return &api.CreateFileOptions{
  559. FileOptions: api.FileOptions{
  560. BranchName: branch,
  561. Message: msg,
  562. Author: api.Identity{
  563. Name: u.Name,
  564. Email: u.Email,
  565. },
  566. Committer: api.Identity{
  567. Name: u.Name,
  568. Email: u.Email,
  569. },
  570. Dates: api.CommitDateOptions{
  571. Author: time.Now(),
  572. Committer: time.Now(),
  573. },
  574. },
  575. ContentBase64: base64.StdEncoding.EncodeToString([]byte(content)),
  576. }
  577. }
  578. func createWorkflowFile(t *testing.T, authToken, ownerName, repoName, treePath string, opts *api.CreateFileOptions) *api.FileResponse {
  579. req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", ownerName, repoName, treePath), opts).
  580. AddTokenAuth(authToken)
  581. resp := MakeRequest(t, req, http.StatusCreated)
  582. var fileResponse api.FileResponse
  583. DecodeJSON(t, resp, &fileResponse)
  584. return &fileResponse
  585. }
  586. // getTaskJobNameByTaskID get the job name of the task by task ID
  587. // there is currently not an API for querying a task by ID so we have to list all the tasks
  588. func getTaskJobNameByTaskID(t *testing.T, authToken, ownerName, repoName string, taskID int64) string {
  589. // FIXME: we may need to query several pages
  590. req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", ownerName, repoName)).
  591. AddTokenAuth(authToken)
  592. resp := MakeRequest(t, req, http.StatusOK)
  593. var taskRespBefore api.ActionTaskResponse
  594. DecodeJSON(t, resp, &taskRespBefore)
  595. for _, apiTask := range taskRespBefore.Entries {
  596. if apiTask.ID == taskID {
  597. return apiTask.Name
  598. }
  599. }
  600. return ""
  601. }