gitea源码

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694
  1. // Copyright 2023 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package repo
  4. import (
  5. go_context "context"
  6. "crypto/hmac"
  7. "crypto/sha256"
  8. "encoding/base64"
  9. "errors"
  10. "fmt"
  11. "net/http"
  12. "net/url"
  13. "strconv"
  14. "strings"
  15. "time"
  16. actions_model "code.gitea.io/gitea/models/actions"
  17. "code.gitea.io/gitea/models/db"
  18. repo_model "code.gitea.io/gitea/models/repo"
  19. secret_model "code.gitea.io/gitea/models/secret"
  20. "code.gitea.io/gitea/modules/actions"
  21. "code.gitea.io/gitea/modules/httplib"
  22. "code.gitea.io/gitea/modules/setting"
  23. api "code.gitea.io/gitea/modules/structs"
  24. "code.gitea.io/gitea/modules/util"
  25. "code.gitea.io/gitea/modules/web"
  26. "code.gitea.io/gitea/routers/api/v1/shared"
  27. "code.gitea.io/gitea/routers/api/v1/utils"
  28. actions_service "code.gitea.io/gitea/services/actions"
  29. "code.gitea.io/gitea/services/context"
  30. "code.gitea.io/gitea/services/convert"
  31. secret_service "code.gitea.io/gitea/services/secrets"
  32. "github.com/nektos/act/pkg/model"
  33. )
  34. // ListActionsSecrets list an repo's actions secrets
  35. func (Action) ListActionsSecrets(ctx *context.APIContext) {
  36. // swagger:operation GET /repos/{owner}/{repo}/actions/secrets repository repoListActionsSecrets
  37. // ---
  38. // summary: List an repo's actions secrets
  39. // produces:
  40. // - application/json
  41. // parameters:
  42. // - name: owner
  43. // in: path
  44. // description: owner of the repo
  45. // type: string
  46. // required: true
  47. // - name: repo
  48. // in: path
  49. // description: name of the repository
  50. // type: string
  51. // required: true
  52. // - name: page
  53. // in: query
  54. // description: page number of results to return (1-based)
  55. // type: integer
  56. // - name: limit
  57. // in: query
  58. // description: page size of results
  59. // type: integer
  60. // responses:
  61. // "200":
  62. // "$ref": "#/responses/SecretList"
  63. // "404":
  64. // "$ref": "#/responses/notFound"
  65. repo := ctx.Repo.Repository
  66. opts := &secret_model.FindSecretsOptions{
  67. RepoID: repo.ID,
  68. ListOptions: utils.GetListOptions(ctx),
  69. }
  70. secrets, count, err := db.FindAndCount[secret_model.Secret](ctx, opts)
  71. if err != nil {
  72. ctx.APIErrorInternal(err)
  73. return
  74. }
  75. apiSecrets := make([]*api.Secret, len(secrets))
  76. for k, v := range secrets {
  77. apiSecrets[k] = &api.Secret{
  78. Name: v.Name,
  79. Description: v.Description,
  80. Created: v.CreatedUnix.AsTime(),
  81. }
  82. }
  83. ctx.SetTotalCountHeader(count)
  84. ctx.JSON(http.StatusOK, apiSecrets)
  85. }
  86. // create or update one secret of the repository
  87. func (Action) CreateOrUpdateSecret(ctx *context.APIContext) {
  88. // swagger:operation PUT /repos/{owner}/{repo}/actions/secrets/{secretname} repository updateRepoSecret
  89. // ---
  90. // summary: Create or Update a secret value in a repository
  91. // consumes:
  92. // - application/json
  93. // produces:
  94. // - application/json
  95. // parameters:
  96. // - name: owner
  97. // in: path
  98. // description: owner of the repository
  99. // type: string
  100. // required: true
  101. // - name: repo
  102. // in: path
  103. // description: name of the repository
  104. // type: string
  105. // required: true
  106. // - name: secretname
  107. // in: path
  108. // description: name of the secret
  109. // type: string
  110. // required: true
  111. // - name: body
  112. // in: body
  113. // schema:
  114. // "$ref": "#/definitions/CreateOrUpdateSecretOption"
  115. // responses:
  116. // "201":
  117. // description: response when creating a secret
  118. // "204":
  119. // description: response when updating a secret
  120. // "400":
  121. // "$ref": "#/responses/error"
  122. // "404":
  123. // "$ref": "#/responses/notFound"
  124. repo := ctx.Repo.Repository
  125. opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption)
  126. _, created, err := secret_service.CreateOrUpdateSecret(ctx, 0, repo.ID, ctx.PathParam("secretname"), opt.Data, opt.Description)
  127. if err != nil {
  128. if errors.Is(err, util.ErrInvalidArgument) {
  129. ctx.APIError(http.StatusBadRequest, err)
  130. } else if errors.Is(err, util.ErrNotExist) {
  131. ctx.APIError(http.StatusNotFound, err)
  132. } else {
  133. ctx.APIErrorInternal(err)
  134. }
  135. return
  136. }
  137. if created {
  138. ctx.Status(http.StatusCreated)
  139. } else {
  140. ctx.Status(http.StatusNoContent)
  141. }
  142. }
  143. // DeleteSecret delete one secret of the repository
  144. func (Action) DeleteSecret(ctx *context.APIContext) {
  145. // swagger:operation DELETE /repos/{owner}/{repo}/actions/secrets/{secretname} repository deleteRepoSecret
  146. // ---
  147. // summary: Delete a secret in a repository
  148. // consumes:
  149. // - application/json
  150. // produces:
  151. // - application/json
  152. // parameters:
  153. // - name: owner
  154. // in: path
  155. // description: owner of the repository
  156. // type: string
  157. // required: true
  158. // - name: repo
  159. // in: path
  160. // description: name of the repository
  161. // type: string
  162. // required: true
  163. // - name: secretname
  164. // in: path
  165. // description: name of the secret
  166. // type: string
  167. // required: true
  168. // responses:
  169. // "204":
  170. // description: delete one secret of the repository
  171. // "400":
  172. // "$ref": "#/responses/error"
  173. // "404":
  174. // "$ref": "#/responses/notFound"
  175. repo := ctx.Repo.Repository
  176. err := secret_service.DeleteSecretByName(ctx, 0, repo.ID, ctx.PathParam("secretname"))
  177. if err != nil {
  178. if errors.Is(err, util.ErrInvalidArgument) {
  179. ctx.APIError(http.StatusBadRequest, err)
  180. } else if errors.Is(err, util.ErrNotExist) {
  181. ctx.APIError(http.StatusNotFound, err)
  182. } else {
  183. ctx.APIErrorInternal(err)
  184. }
  185. return
  186. }
  187. ctx.Status(http.StatusNoContent)
  188. }
  189. // GetVariable get a repo-level variable
  190. func (Action) GetVariable(ctx *context.APIContext) {
  191. // swagger:operation GET /repos/{owner}/{repo}/actions/variables/{variablename} repository getRepoVariable
  192. // ---
  193. // summary: Get a repo-level variable
  194. // produces:
  195. // - application/json
  196. // parameters:
  197. // - name: owner
  198. // in: path
  199. // description: owner of the repo
  200. // type: string
  201. // required: true
  202. // - name: repo
  203. // in: path
  204. // description: name of the repository
  205. // type: string
  206. // required: true
  207. // - name: variablename
  208. // in: path
  209. // description: name of the variable
  210. // type: string
  211. // required: true
  212. // responses:
  213. // "200":
  214. // "$ref": "#/responses/ActionVariable"
  215. // "400":
  216. // "$ref": "#/responses/error"
  217. // "404":
  218. // "$ref": "#/responses/notFound"
  219. v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
  220. RepoID: ctx.Repo.Repository.ID,
  221. Name: ctx.PathParam("variablename"),
  222. })
  223. if err != nil {
  224. if errors.Is(err, util.ErrNotExist) {
  225. ctx.APIError(http.StatusNotFound, err)
  226. } else {
  227. ctx.APIErrorInternal(err)
  228. }
  229. return
  230. }
  231. variable := &api.ActionVariable{
  232. OwnerID: v.OwnerID,
  233. RepoID: v.RepoID,
  234. Name: v.Name,
  235. Data: v.Data,
  236. Description: v.Description,
  237. }
  238. ctx.JSON(http.StatusOK, variable)
  239. }
  240. // DeleteVariable delete a repo-level variable
  241. func (Action) DeleteVariable(ctx *context.APIContext) {
  242. // swagger:operation DELETE /repos/{owner}/{repo}/actions/variables/{variablename} repository deleteRepoVariable
  243. // ---
  244. // summary: Delete a repo-level variable
  245. // produces:
  246. // - application/json
  247. // parameters:
  248. // - name: owner
  249. // in: path
  250. // description: owner of the repo
  251. // type: string
  252. // required: true
  253. // - name: repo
  254. // in: path
  255. // description: name of the repository
  256. // type: string
  257. // required: true
  258. // - name: variablename
  259. // in: path
  260. // description: name of the variable
  261. // type: string
  262. // required: true
  263. // responses:
  264. // "200":
  265. // "$ref": "#/responses/ActionVariable"
  266. // "201":
  267. // description: response when deleting a variable
  268. // "204":
  269. // description: response when deleting a variable
  270. // "400":
  271. // "$ref": "#/responses/error"
  272. // "404":
  273. // "$ref": "#/responses/notFound"
  274. if err := actions_service.DeleteVariableByName(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParam("variablename")); err != nil {
  275. if errors.Is(err, util.ErrInvalidArgument) {
  276. ctx.APIError(http.StatusBadRequest, err)
  277. } else if errors.Is(err, util.ErrNotExist) {
  278. ctx.APIError(http.StatusNotFound, err)
  279. } else {
  280. ctx.APIErrorInternal(err)
  281. }
  282. return
  283. }
  284. ctx.Status(http.StatusNoContent)
  285. }
  286. // CreateVariable create a repo-level variable
  287. func (Action) CreateVariable(ctx *context.APIContext) {
  288. // swagger:operation POST /repos/{owner}/{repo}/actions/variables/{variablename} repository createRepoVariable
  289. // ---
  290. // summary: Create a repo-level variable
  291. // produces:
  292. // - application/json
  293. // parameters:
  294. // - name: owner
  295. // in: path
  296. // description: owner of the repo
  297. // type: string
  298. // required: true
  299. // - name: repo
  300. // in: path
  301. // description: name of the repository
  302. // type: string
  303. // required: true
  304. // - name: variablename
  305. // in: path
  306. // description: name of the variable
  307. // type: string
  308. // required: true
  309. // - name: body
  310. // in: body
  311. // schema:
  312. // "$ref": "#/definitions/CreateVariableOption"
  313. // responses:
  314. // "201":
  315. // description: response when creating a repo-level variable
  316. // "400":
  317. // "$ref": "#/responses/error"
  318. // "409":
  319. // description: variable name already exists.
  320. // "500":
  321. // "$ref": "#/responses/error"
  322. opt := web.GetForm(ctx).(*api.CreateVariableOption)
  323. repoID := ctx.Repo.Repository.ID
  324. variableName := ctx.PathParam("variablename")
  325. v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
  326. RepoID: repoID,
  327. Name: variableName,
  328. })
  329. if err != nil && !errors.Is(err, util.ErrNotExist) {
  330. ctx.APIErrorInternal(err)
  331. return
  332. }
  333. if v != nil && v.ID > 0 {
  334. ctx.APIError(http.StatusConflict, util.NewAlreadyExistErrorf("variable name %s already exists", variableName))
  335. return
  336. }
  337. if _, err := actions_service.CreateVariable(ctx, 0, repoID, variableName, opt.Value, opt.Description); err != nil {
  338. if errors.Is(err, util.ErrInvalidArgument) {
  339. ctx.APIError(http.StatusBadRequest, err)
  340. } else {
  341. ctx.APIErrorInternal(err)
  342. }
  343. return
  344. }
  345. ctx.Status(http.StatusCreated)
  346. }
  347. // UpdateVariable update a repo-level variable
  348. func (Action) UpdateVariable(ctx *context.APIContext) {
  349. // swagger:operation PUT /repos/{owner}/{repo}/actions/variables/{variablename} repository updateRepoVariable
  350. // ---
  351. // summary: Update a repo-level variable
  352. // produces:
  353. // - application/json
  354. // parameters:
  355. // - name: owner
  356. // in: path
  357. // description: owner of the repo
  358. // type: string
  359. // required: true
  360. // - name: repo
  361. // in: path
  362. // description: name of the repository
  363. // type: string
  364. // required: true
  365. // - name: variablename
  366. // in: path
  367. // description: name of the variable
  368. // type: string
  369. // required: true
  370. // - name: body
  371. // in: body
  372. // schema:
  373. // "$ref": "#/definitions/UpdateVariableOption"
  374. // responses:
  375. // "201":
  376. // description: response when updating a repo-level variable
  377. // "204":
  378. // description: response when updating a repo-level variable
  379. // "400":
  380. // "$ref": "#/responses/error"
  381. // "404":
  382. // "$ref": "#/responses/notFound"
  383. opt := web.GetForm(ctx).(*api.UpdateVariableOption)
  384. v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
  385. RepoID: ctx.Repo.Repository.ID,
  386. Name: ctx.PathParam("variablename"),
  387. })
  388. if err != nil {
  389. if errors.Is(err, util.ErrNotExist) {
  390. ctx.APIError(http.StatusNotFound, err)
  391. } else {
  392. ctx.APIErrorInternal(err)
  393. }
  394. return
  395. }
  396. if opt.Name == "" {
  397. opt.Name = ctx.PathParam("variablename")
  398. }
  399. v.Name = opt.Name
  400. v.Data = opt.Value
  401. v.Description = opt.Description
  402. if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil {
  403. if errors.Is(err, util.ErrInvalidArgument) {
  404. ctx.APIError(http.StatusBadRequest, err)
  405. } else {
  406. ctx.APIErrorInternal(err)
  407. }
  408. return
  409. }
  410. ctx.Status(http.StatusNoContent)
  411. }
  412. // ListVariables list repo-level variables
  413. func (Action) ListVariables(ctx *context.APIContext) {
  414. // swagger:operation GET /repos/{owner}/{repo}/actions/variables repository getRepoVariablesList
  415. // ---
  416. // summary: Get repo-level variables list
  417. // produces:
  418. // - application/json
  419. // parameters:
  420. // - name: owner
  421. // in: path
  422. // description: owner of the repo
  423. // type: string
  424. // required: true
  425. // - name: repo
  426. // in: path
  427. // description: name of the repository
  428. // type: string
  429. // required: true
  430. // - name: page
  431. // in: query
  432. // description: page number of results to return (1-based)
  433. // type: integer
  434. // - name: limit
  435. // in: query
  436. // description: page size of results
  437. // type: integer
  438. // responses:
  439. // "200":
  440. // "$ref": "#/responses/VariableList"
  441. // "400":
  442. // "$ref": "#/responses/error"
  443. // "404":
  444. // "$ref": "#/responses/notFound"
  445. vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{
  446. RepoID: ctx.Repo.Repository.ID,
  447. ListOptions: utils.GetListOptions(ctx),
  448. })
  449. if err != nil {
  450. ctx.APIErrorInternal(err)
  451. return
  452. }
  453. variables := make([]*api.ActionVariable, len(vars))
  454. for i, v := range vars {
  455. variables[i] = &api.ActionVariable{
  456. OwnerID: v.OwnerID,
  457. RepoID: v.RepoID,
  458. Name: v.Name,
  459. Data: v.Data,
  460. Description: v.Description,
  461. }
  462. }
  463. ctx.SetTotalCountHeader(count)
  464. ctx.JSON(http.StatusOK, variables)
  465. }
  466. // GetRegistrationToken returns the token to register repo runners
  467. func (Action) GetRegistrationToken(ctx *context.APIContext) {
  468. // swagger:operation GET /repos/{owner}/{repo}/actions/runners/registration-token repository repoGetRunnerRegistrationToken
  469. // ---
  470. // summary: Get a repository's actions runner registration token
  471. // produces:
  472. // - application/json
  473. // parameters:
  474. // - name: owner
  475. // in: path
  476. // description: owner of the repo
  477. // type: string
  478. // required: true
  479. // - name: repo
  480. // in: path
  481. // description: name of the repo
  482. // type: string
  483. // required: true
  484. // responses:
  485. // "200":
  486. // "$ref": "#/responses/RegistrationToken"
  487. shared.GetRegistrationToken(ctx, 0, ctx.Repo.Repository.ID)
  488. }
  489. // CreateRegistrationToken returns the token to register repo runners
  490. func (Action) CreateRegistrationToken(ctx *context.APIContext) {
  491. // swagger:operation POST /repos/{owner}/{repo}/actions/runners/registration-token repository repoCreateRunnerRegistrationToken
  492. // ---
  493. // summary: Get a repository's actions runner registration token
  494. // produces:
  495. // - application/json
  496. // parameters:
  497. // - name: owner
  498. // in: path
  499. // description: owner of the repo
  500. // type: string
  501. // required: true
  502. // - name: repo
  503. // in: path
  504. // description: name of the repo
  505. // type: string
  506. // required: true
  507. // responses:
  508. // "200":
  509. // "$ref": "#/responses/RegistrationToken"
  510. shared.GetRegistrationToken(ctx, 0, ctx.Repo.Repository.ID)
  511. }
  512. // ListRunners get repo-level runners
  513. func (Action) ListRunners(ctx *context.APIContext) {
  514. // swagger:operation GET /repos/{owner}/{repo}/actions/runners repository getRepoRunners
  515. // ---
  516. // summary: Get repo-level runners
  517. // produces:
  518. // - application/json
  519. // parameters:
  520. // - name: owner
  521. // in: path
  522. // description: owner of the repo
  523. // type: string
  524. // required: true
  525. // - name: repo
  526. // in: path
  527. // description: name of the repo
  528. // type: string
  529. // required: true
  530. // responses:
  531. // "200":
  532. // "$ref": "#/definitions/ActionRunnersResponse"
  533. // "400":
  534. // "$ref": "#/responses/error"
  535. // "404":
  536. // "$ref": "#/responses/notFound"
  537. shared.ListRunners(ctx, 0, ctx.Repo.Repository.ID)
  538. }
  539. // GetRunner get an repo-level runner
  540. func (Action) GetRunner(ctx *context.APIContext) {
  541. // swagger:operation GET /repos/{owner}/{repo}/actions/runners/{runner_id} repository getRepoRunner
  542. // ---
  543. // summary: Get an repo-level runner
  544. // produces:
  545. // - application/json
  546. // parameters:
  547. // - name: owner
  548. // in: path
  549. // description: owner of the repo
  550. // type: string
  551. // required: true
  552. // - name: repo
  553. // in: path
  554. // description: name of the repo
  555. // type: string
  556. // required: true
  557. // - name: runner_id
  558. // in: path
  559. // description: id of the runner
  560. // type: string
  561. // required: true
  562. // responses:
  563. // "200":
  564. // "$ref": "#/definitions/ActionRunner"
  565. // "400":
  566. // "$ref": "#/responses/error"
  567. // "404":
  568. // "$ref": "#/responses/notFound"
  569. shared.GetRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id"))
  570. }
  571. // DeleteRunner delete an repo-level runner
  572. func (Action) DeleteRunner(ctx *context.APIContext) {
  573. // swagger:operation DELETE /repos/{owner}/{repo}/actions/runners/{runner_id} repository deleteRepoRunner
  574. // ---
  575. // summary: Delete an repo-level runner
  576. // produces:
  577. // - application/json
  578. // parameters:
  579. // - name: owner
  580. // in: path
  581. // description: owner of the repo
  582. // type: string
  583. // required: true
  584. // - name: repo
  585. // in: path
  586. // description: name of the repo
  587. // type: string
  588. // required: true
  589. // - name: runner_id
  590. // in: path
  591. // description: id of the runner
  592. // type: string
  593. // required: true
  594. // responses:
  595. // "204":
  596. // description: runner has been deleted
  597. // "400":
  598. // "$ref": "#/responses/error"
  599. // "404":
  600. // "$ref": "#/responses/notFound"
  601. shared.DeleteRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id"))
  602. }
  603. // GetWorkflowRunJobs Lists all jobs for a workflow run.
  604. func (Action) ListWorkflowJobs(ctx *context.APIContext) {
  605. // swagger:operation GET /repos/{owner}/{repo}/actions/jobs repository listWorkflowJobs
  606. // ---
  607. // summary: Lists all jobs for a repository
  608. // produces:
  609. // - application/json
  610. // parameters:
  611. // - name: owner
  612. // in: path
  613. // description: owner of the repo
  614. // type: string
  615. // required: true
  616. // - name: repo
  617. // in: path
  618. // description: name of the repository
  619. // type: string
  620. // required: true
  621. // - name: status
  622. // in: query
  623. // description: workflow status (pending, queued, in_progress, failure, success, skipped)
  624. // type: string
  625. // required: false
  626. // - name: page
  627. // in: query
  628. // description: page number of results to return (1-based)
  629. // type: integer
  630. // - name: limit
  631. // in: query
  632. // description: page size of results
  633. // type: integer
  634. // responses:
  635. // "200":
  636. // "$ref": "#/responses/WorkflowJobsList"
  637. // "400":
  638. // "$ref": "#/responses/error"
  639. // "404":
  640. // "$ref": "#/responses/notFound"
  641. repoID := ctx.Repo.Repository.ID
  642. shared.ListJobs(ctx, 0, repoID, 0)
  643. }
  644. // ListWorkflowRuns Lists all runs for a repository run.
  645. func (Action) ListWorkflowRuns(ctx *context.APIContext) {
  646. // swagger:operation GET /repos/{owner}/{repo}/actions/runs repository getWorkflowRuns
  647. // ---
  648. // summary: Lists all runs for a repository run
  649. // produces:
  650. // - application/json
  651. // parameters:
  652. // - name: owner
  653. // in: path
  654. // description: owner of the repo
  655. // type: string
  656. // required: true
  657. // - name: repo
  658. // in: path
  659. // description: name of the repository
  660. // type: string
  661. // required: true
  662. // - name: event
  663. // in: query
  664. // description: workflow event name
  665. // type: string
  666. // required: false
  667. // - name: branch
  668. // in: query
  669. // description: workflow branch
  670. // type: string
  671. // required: false
  672. // - name: status
  673. // in: query
  674. // description: workflow status (pending, queued, in_progress, failure, success, skipped)
  675. // type: string
  676. // required: false
  677. // - name: actor
  678. // in: query
  679. // description: triggered by user
  680. // type: string
  681. // required: false
  682. // - name: head_sha
  683. // in: query
  684. // description: triggering sha of the workflow run
  685. // type: string
  686. // required: false
  687. // - name: page
  688. // in: query
  689. // description: page number of results to return (1-based)
  690. // type: integer
  691. // - name: limit
  692. // in: query
  693. // description: page size of results
  694. // type: integer
  695. // responses:
  696. // "200":
  697. // "$ref": "#/responses/WorkflowRunsList"
  698. // "400":
  699. // "$ref": "#/responses/error"
  700. // "404":
  701. // "$ref": "#/responses/notFound"
  702. repoID := ctx.Repo.Repository.ID
  703. shared.ListRuns(ctx, 0, repoID)
  704. }
  705. var _ actions_service.API = new(Action)
  706. // Action implements actions_service.API
  707. type Action struct{}
  708. // NewAction creates a new Action service
  709. func NewAction() actions_service.API {
  710. return Action{}
  711. }
  712. // ListActionTasks list all the actions of a repository
  713. func ListActionTasks(ctx *context.APIContext) {
  714. // swagger:operation GET /repos/{owner}/{repo}/actions/tasks repository ListActionTasks
  715. // ---
  716. // summary: List a repository's action tasks
  717. // produces:
  718. // - application/json
  719. // parameters:
  720. // - name: owner
  721. // in: path
  722. // description: owner of the repo
  723. // type: string
  724. // required: true
  725. // - name: repo
  726. // in: path
  727. // description: name of the repo
  728. // type: string
  729. // required: true
  730. // - name: page
  731. // in: query
  732. // description: page number of results to return (1-based)
  733. // type: integer
  734. // - name: limit
  735. // in: query
  736. // description: page size of results, default maximum page size is 50
  737. // type: integer
  738. // responses:
  739. // "200":
  740. // "$ref": "#/responses/TasksList"
  741. // "400":
  742. // "$ref": "#/responses/error"
  743. // "403":
  744. // "$ref": "#/responses/forbidden"
  745. // "404":
  746. // "$ref": "#/responses/notFound"
  747. // "409":
  748. // "$ref": "#/responses/conflict"
  749. // "422":
  750. // "$ref": "#/responses/validationError"
  751. tasks, total, err := db.FindAndCount[actions_model.ActionTask](ctx, &actions_model.FindTaskOptions{
  752. ListOptions: utils.GetListOptions(ctx),
  753. RepoID: ctx.Repo.Repository.ID,
  754. })
  755. if err != nil {
  756. ctx.APIErrorInternal(err)
  757. return
  758. }
  759. res := new(api.ActionTaskResponse)
  760. res.TotalCount = total
  761. res.Entries = make([]*api.ActionTask, len(tasks))
  762. for i := range tasks {
  763. convertedTask, err := convert.ToActionTask(ctx, tasks[i])
  764. if err != nil {
  765. ctx.APIErrorInternal(err)
  766. return
  767. }
  768. res.Entries[i] = convertedTask
  769. }
  770. ctx.JSON(http.StatusOK, &res)
  771. }
  772. func ActionsListRepositoryWorkflows(ctx *context.APIContext) {
  773. // swagger:operation GET /repos/{owner}/{repo}/actions/workflows repository ActionsListRepositoryWorkflows
  774. // ---
  775. // summary: List repository workflows
  776. // produces:
  777. // - application/json
  778. // parameters:
  779. // - name: owner
  780. // in: path
  781. // description: owner of the repo
  782. // type: string
  783. // required: true
  784. // - name: repo
  785. // in: path
  786. // description: name of the repo
  787. // type: string
  788. // required: true
  789. // responses:
  790. // "200":
  791. // "$ref": "#/responses/ActionWorkflowList"
  792. // "400":
  793. // "$ref": "#/responses/error"
  794. // "403":
  795. // "$ref": "#/responses/forbidden"
  796. // "404":
  797. // "$ref": "#/responses/notFound"
  798. // "422":
  799. // "$ref": "#/responses/validationError"
  800. // "500":
  801. // "$ref": "#/responses/error"
  802. workflows, err := convert.ListActionWorkflows(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository)
  803. if err != nil {
  804. ctx.APIErrorInternal(err)
  805. return
  806. }
  807. ctx.JSON(http.StatusOK, &api.ActionWorkflowResponse{Workflows: workflows, TotalCount: int64(len(workflows))})
  808. }
  809. func ActionsGetWorkflow(ctx *context.APIContext) {
  810. // swagger:operation GET /repos/{owner}/{repo}/actions/workflows/{workflow_id} repository ActionsGetWorkflow
  811. // ---
  812. // summary: Get a workflow
  813. // produces:
  814. // - application/json
  815. // parameters:
  816. // - name: owner
  817. // in: path
  818. // description: owner of the repo
  819. // type: string
  820. // required: true
  821. // - name: repo
  822. // in: path
  823. // description: name of the repo
  824. // type: string
  825. // required: true
  826. // - name: workflow_id
  827. // in: path
  828. // description: id of the workflow
  829. // type: string
  830. // required: true
  831. // responses:
  832. // "200":
  833. // "$ref": "#/responses/ActionWorkflow"
  834. // "400":
  835. // "$ref": "#/responses/error"
  836. // "403":
  837. // "$ref": "#/responses/forbidden"
  838. // "404":
  839. // "$ref": "#/responses/notFound"
  840. // "422":
  841. // "$ref": "#/responses/validationError"
  842. // "500":
  843. // "$ref": "#/responses/error"
  844. workflowID := ctx.PathParam("workflow_id")
  845. workflow, err := convert.GetActionWorkflow(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository, workflowID)
  846. if err != nil {
  847. if errors.Is(err, util.ErrNotExist) {
  848. ctx.APIError(http.StatusNotFound, err)
  849. } else {
  850. ctx.APIErrorInternal(err)
  851. }
  852. return
  853. }
  854. ctx.JSON(http.StatusOK, workflow)
  855. }
  856. func ActionsDisableWorkflow(ctx *context.APIContext) {
  857. // swagger:operation PUT /repos/{owner}/{repo}/actions/workflows/{workflow_id}/disable repository ActionsDisableWorkflow
  858. // ---
  859. // summary: Disable a workflow
  860. // produces:
  861. // - application/json
  862. // parameters:
  863. // - name: owner
  864. // in: path
  865. // description: owner of the repo
  866. // type: string
  867. // required: true
  868. // - name: repo
  869. // in: path
  870. // description: name of the repo
  871. // type: string
  872. // required: true
  873. // - name: workflow_id
  874. // in: path
  875. // description: id of the workflow
  876. // type: string
  877. // required: true
  878. // responses:
  879. // "204":
  880. // description: No Content
  881. // "400":
  882. // "$ref": "#/responses/error"
  883. // "403":
  884. // "$ref": "#/responses/forbidden"
  885. // "404":
  886. // "$ref": "#/responses/notFound"
  887. // "422":
  888. // "$ref": "#/responses/validationError"
  889. workflowID := ctx.PathParam("workflow_id")
  890. err := actions_service.EnableOrDisableWorkflow(ctx, workflowID, false)
  891. if err != nil {
  892. if errors.Is(err, util.ErrNotExist) {
  893. ctx.APIError(http.StatusNotFound, err)
  894. } else {
  895. ctx.APIErrorInternal(err)
  896. }
  897. return
  898. }
  899. ctx.Status(http.StatusNoContent)
  900. }
  901. func ActionsDispatchWorkflow(ctx *context.APIContext) {
  902. // swagger:operation POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches repository ActionsDispatchWorkflow
  903. // ---
  904. // summary: Create a workflow dispatch event
  905. // produces:
  906. // - application/json
  907. // parameters:
  908. // - name: owner
  909. // in: path
  910. // description: owner of the repo
  911. // type: string
  912. // required: true
  913. // - name: repo
  914. // in: path
  915. // description: name of the repo
  916. // type: string
  917. // required: true
  918. // - name: workflow_id
  919. // in: path
  920. // description: id of the workflow
  921. // type: string
  922. // required: true
  923. // - name: body
  924. // in: body
  925. // schema:
  926. // "$ref": "#/definitions/CreateActionWorkflowDispatch"
  927. // responses:
  928. // "204":
  929. // description: No Content
  930. // "400":
  931. // "$ref": "#/responses/error"
  932. // "403":
  933. // "$ref": "#/responses/forbidden"
  934. // "404":
  935. // "$ref": "#/responses/notFound"
  936. // "422":
  937. // "$ref": "#/responses/validationError"
  938. workflowID := ctx.PathParam("workflow_id")
  939. opt := web.GetForm(ctx).(*api.CreateActionWorkflowDispatch)
  940. if opt.Ref == "" {
  941. ctx.APIError(http.StatusUnprocessableEntity, util.NewInvalidArgumentErrorf("ref is required parameter"))
  942. return
  943. }
  944. err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, opt.Ref, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error {
  945. if strings.Contains(ctx.Req.Header.Get("Content-Type"), "form-urlencoded") {
  946. // The chi framework's "Binding" doesn't support to bind the form map values into a map[string]string
  947. // So we have to manually read the `inputs[key]` from the form
  948. for name, config := range workflowDispatch.Inputs {
  949. value := ctx.FormString("inputs["+name+"]", config.Default)
  950. inputs[name] = value
  951. }
  952. } else {
  953. for name, config := range workflowDispatch.Inputs {
  954. value, ok := opt.Inputs[name]
  955. if ok {
  956. inputs[name] = value
  957. } else {
  958. inputs[name] = config.Default
  959. }
  960. }
  961. }
  962. return nil
  963. })
  964. if err != nil {
  965. if errors.Is(err, util.ErrNotExist) {
  966. ctx.APIError(http.StatusNotFound, err)
  967. } else if errors.Is(err, util.ErrPermissionDenied) {
  968. ctx.APIError(http.StatusForbidden, err)
  969. } else {
  970. ctx.APIErrorInternal(err)
  971. }
  972. return
  973. }
  974. ctx.Status(http.StatusNoContent)
  975. }
  976. func ActionsEnableWorkflow(ctx *context.APIContext) {
  977. // swagger:operation PUT /repos/{owner}/{repo}/actions/workflows/{workflow_id}/enable repository ActionsEnableWorkflow
  978. // ---
  979. // summary: Enable a workflow
  980. // produces:
  981. // - application/json
  982. // parameters:
  983. // - name: owner
  984. // in: path
  985. // description: owner of the repo
  986. // type: string
  987. // required: true
  988. // - name: repo
  989. // in: path
  990. // description: name of the repo
  991. // type: string
  992. // required: true
  993. // - name: workflow_id
  994. // in: path
  995. // description: id of the workflow
  996. // type: string
  997. // required: true
  998. // responses:
  999. // "204":
  1000. // description: No Content
  1001. // "400":
  1002. // "$ref": "#/responses/error"
  1003. // "403":
  1004. // "$ref": "#/responses/forbidden"
  1005. // "404":
  1006. // "$ref": "#/responses/notFound"
  1007. // "409":
  1008. // "$ref": "#/responses/conflict"
  1009. // "422":
  1010. // "$ref": "#/responses/validationError"
  1011. workflowID := ctx.PathParam("workflow_id")
  1012. err := actions_service.EnableOrDisableWorkflow(ctx, workflowID, true)
  1013. if err != nil {
  1014. if errors.Is(err, util.ErrNotExist) {
  1015. ctx.APIError(http.StatusNotFound, err)
  1016. } else {
  1017. ctx.APIErrorInternal(err)
  1018. }
  1019. return
  1020. }
  1021. ctx.Status(http.StatusNoContent)
  1022. }
  1023. // GetWorkflowRun Gets a specific workflow run.
  1024. func GetWorkflowRun(ctx *context.APIContext) {
  1025. // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run} repository GetWorkflowRun
  1026. // ---
  1027. // summary: Gets a specific workflow run
  1028. // produces:
  1029. // - application/json
  1030. // parameters:
  1031. // - name: owner
  1032. // in: path
  1033. // description: owner of the repo
  1034. // type: string
  1035. // required: true
  1036. // - name: repo
  1037. // in: path
  1038. // description: name of the repository
  1039. // type: string
  1040. // required: true
  1041. // - name: run
  1042. // in: path
  1043. // description: id of the run
  1044. // type: string
  1045. // required: true
  1046. // responses:
  1047. // "200":
  1048. // "$ref": "#/responses/WorkflowRun"
  1049. // "400":
  1050. // "$ref": "#/responses/error"
  1051. // "404":
  1052. // "$ref": "#/responses/notFound"
  1053. runID := ctx.PathParamInt64("run")
  1054. job, has, err := db.GetByID[actions_model.ActionRun](ctx, runID)
  1055. if err != nil {
  1056. ctx.APIErrorInternal(err)
  1057. return
  1058. }
  1059. if !has || job.RepoID != ctx.Repo.Repository.ID {
  1060. ctx.APIErrorNotFound(util.ErrNotExist)
  1061. return
  1062. }
  1063. convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, job)
  1064. if err != nil {
  1065. ctx.APIErrorInternal(err)
  1066. return
  1067. }
  1068. ctx.JSON(http.StatusOK, convertedRun)
  1069. }
  1070. // ListWorkflowRunJobs Lists all jobs for a workflow run.
  1071. func ListWorkflowRunJobs(ctx *context.APIContext) {
  1072. // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/jobs repository listWorkflowRunJobs
  1073. // ---
  1074. // summary: Lists all jobs for a workflow run
  1075. // produces:
  1076. // - application/json
  1077. // parameters:
  1078. // - name: owner
  1079. // in: path
  1080. // description: owner of the repo
  1081. // type: string
  1082. // required: true
  1083. // - name: repo
  1084. // in: path
  1085. // description: name of the repository
  1086. // type: string
  1087. // required: true
  1088. // - name: run
  1089. // in: path
  1090. // description: runid of the workflow run
  1091. // type: integer
  1092. // required: true
  1093. // - name: status
  1094. // in: query
  1095. // description: workflow status (pending, queued, in_progress, failure, success, skipped)
  1096. // type: string
  1097. // required: false
  1098. // - name: page
  1099. // in: query
  1100. // description: page number of results to return (1-based)
  1101. // type: integer
  1102. // - name: limit
  1103. // in: query
  1104. // description: page size of results
  1105. // type: integer
  1106. // responses:
  1107. // "200":
  1108. // "$ref": "#/responses/WorkflowJobsList"
  1109. // "400":
  1110. // "$ref": "#/responses/error"
  1111. // "404":
  1112. // "$ref": "#/responses/notFound"
  1113. repoID := ctx.Repo.Repository.ID
  1114. runID := ctx.PathParamInt64("run")
  1115. // Avoid the list all jobs functionality for this api route to be used with a runID == 0.
  1116. if runID <= 0 {
  1117. ctx.APIError(http.StatusBadRequest, util.NewInvalidArgumentErrorf("runID must be a positive integer"))
  1118. return
  1119. }
  1120. // runID is used as an additional filter next to repoID to ensure that we only list jobs for the specified repoID and runID.
  1121. // no additional checks for runID are needed here
  1122. shared.ListJobs(ctx, 0, repoID, runID)
  1123. }
  1124. // GetWorkflowJob Gets a specific workflow job for a workflow run.
  1125. func GetWorkflowJob(ctx *context.APIContext) {
  1126. // swagger:operation GET /repos/{owner}/{repo}/actions/jobs/{job_id} repository getWorkflowJob
  1127. // ---
  1128. // summary: Gets a specific workflow job for a workflow run
  1129. // produces:
  1130. // - application/json
  1131. // parameters:
  1132. // - name: owner
  1133. // in: path
  1134. // description: owner of the repo
  1135. // type: string
  1136. // required: true
  1137. // - name: repo
  1138. // in: path
  1139. // description: name of the repository
  1140. // type: string
  1141. // required: true
  1142. // - name: job_id
  1143. // in: path
  1144. // description: id of the job
  1145. // type: string
  1146. // required: true
  1147. // responses:
  1148. // "200":
  1149. // "$ref": "#/responses/WorkflowJob"
  1150. // "400":
  1151. // "$ref": "#/responses/error"
  1152. // "404":
  1153. // "$ref": "#/responses/notFound"
  1154. jobID := ctx.PathParamInt64("job_id")
  1155. job, has, err := db.GetByID[actions_model.ActionRunJob](ctx, jobID)
  1156. if err != nil {
  1157. ctx.APIErrorInternal(err)
  1158. return
  1159. }
  1160. if !has || job.RepoID != ctx.Repo.Repository.ID {
  1161. ctx.APIErrorNotFound(util.ErrNotExist)
  1162. return
  1163. }
  1164. convertedWorkflowJob, err := convert.ToActionWorkflowJob(ctx, ctx.Repo.Repository, nil, job)
  1165. if err != nil {
  1166. ctx.APIErrorInternal(err)
  1167. return
  1168. }
  1169. ctx.JSON(http.StatusOK, convertedWorkflowJob)
  1170. }
  1171. // GetArtifactsOfRun Lists all artifacts for a repository.
  1172. func GetArtifactsOfRun(ctx *context.APIContext) {
  1173. // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/artifacts repository getArtifactsOfRun
  1174. // ---
  1175. // summary: Lists all artifacts for a repository run
  1176. // produces:
  1177. // - application/json
  1178. // parameters:
  1179. // - name: owner
  1180. // in: path
  1181. // description: owner of the repo
  1182. // type: string
  1183. // required: true
  1184. // - name: repo
  1185. // in: path
  1186. // description: name of the repository
  1187. // type: string
  1188. // required: true
  1189. // - name: run
  1190. // in: path
  1191. // description: runid of the workflow run
  1192. // type: integer
  1193. // required: true
  1194. // - name: name
  1195. // in: query
  1196. // description: name of the artifact
  1197. // type: string
  1198. // required: false
  1199. // responses:
  1200. // "200":
  1201. // "$ref": "#/responses/ArtifactsList"
  1202. // "400":
  1203. // "$ref": "#/responses/error"
  1204. // "404":
  1205. // "$ref": "#/responses/notFound"
  1206. repoID := ctx.Repo.Repository.ID
  1207. artifactName := ctx.Req.URL.Query().Get("name")
  1208. runID := ctx.PathParamInt64("run")
  1209. artifacts, total, err := db.FindAndCount[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{
  1210. RepoID: repoID,
  1211. RunID: runID,
  1212. ArtifactName: artifactName,
  1213. FinalizedArtifactsV4: true,
  1214. ListOptions: utils.GetListOptions(ctx),
  1215. })
  1216. if err != nil {
  1217. ctx.APIErrorInternal(err)
  1218. return
  1219. }
  1220. res := new(api.ActionArtifactsResponse)
  1221. res.TotalCount = total
  1222. res.Entries = make([]*api.ActionArtifact, len(artifacts))
  1223. for i := range artifacts {
  1224. convertedArtifact, err := convert.ToActionArtifact(ctx.Repo.Repository, artifacts[i])
  1225. if err != nil {
  1226. ctx.APIErrorInternal(err)
  1227. return
  1228. }
  1229. res.Entries[i] = convertedArtifact
  1230. }
  1231. ctx.JSON(http.StatusOK, &res)
  1232. }
  1233. // DeleteActionRun Delete a workflow run
  1234. func DeleteActionRun(ctx *context.APIContext) {
  1235. // swagger:operation DELETE /repos/{owner}/{repo}/actions/runs/{run} repository deleteActionRun
  1236. // ---
  1237. // summary: Delete a workflow run
  1238. // produces:
  1239. // - application/json
  1240. // parameters:
  1241. // - name: owner
  1242. // in: path
  1243. // description: owner of the repo
  1244. // type: string
  1245. // required: true
  1246. // - name: repo
  1247. // in: path
  1248. // description: name of the repository
  1249. // type: string
  1250. // required: true
  1251. // - name: run
  1252. // in: path
  1253. // description: runid of the workflow run
  1254. // type: integer
  1255. // required: true
  1256. // responses:
  1257. // "204":
  1258. // description: "No Content"
  1259. // "400":
  1260. // "$ref": "#/responses/error"
  1261. // "404":
  1262. // "$ref": "#/responses/notFound"
  1263. runID := ctx.PathParamInt64("run")
  1264. run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID)
  1265. if errors.Is(err, util.ErrNotExist) {
  1266. ctx.APIErrorNotFound(err)
  1267. return
  1268. } else if err != nil {
  1269. ctx.APIErrorInternal(err)
  1270. return
  1271. }
  1272. if !run.Status.IsDone() {
  1273. ctx.APIError(http.StatusBadRequest, "this workflow run is not done")
  1274. return
  1275. }
  1276. if err := actions_service.DeleteRun(ctx, run); err != nil {
  1277. ctx.APIErrorInternal(err)
  1278. return
  1279. }
  1280. ctx.Status(http.StatusNoContent)
  1281. }
  1282. // GetArtifacts Lists all artifacts for a repository.
  1283. func GetArtifacts(ctx *context.APIContext) {
  1284. // swagger:operation GET /repos/{owner}/{repo}/actions/artifacts repository getArtifacts
  1285. // ---
  1286. // summary: Lists all artifacts for a repository
  1287. // produces:
  1288. // - application/json
  1289. // parameters:
  1290. // - name: owner
  1291. // in: path
  1292. // description: owner of the repo
  1293. // type: string
  1294. // required: true
  1295. // - name: repo
  1296. // in: path
  1297. // description: name of the repository
  1298. // type: string
  1299. // required: true
  1300. // - name: name
  1301. // in: query
  1302. // description: name of the artifact
  1303. // type: string
  1304. // required: false
  1305. // responses:
  1306. // "200":
  1307. // "$ref": "#/responses/ArtifactsList"
  1308. // "400":
  1309. // "$ref": "#/responses/error"
  1310. // "404":
  1311. // "$ref": "#/responses/notFound"
  1312. repoID := ctx.Repo.Repository.ID
  1313. artifactName := ctx.Req.URL.Query().Get("name")
  1314. artifacts, total, err := db.FindAndCount[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{
  1315. RepoID: repoID,
  1316. ArtifactName: artifactName,
  1317. FinalizedArtifactsV4: true,
  1318. ListOptions: utils.GetListOptions(ctx),
  1319. })
  1320. if err != nil {
  1321. ctx.APIErrorInternal(err)
  1322. return
  1323. }
  1324. res := new(api.ActionArtifactsResponse)
  1325. res.TotalCount = total
  1326. res.Entries = make([]*api.ActionArtifact, len(artifacts))
  1327. for i := range artifacts {
  1328. convertedArtifact, err := convert.ToActionArtifact(ctx.Repo.Repository, artifacts[i])
  1329. if err != nil {
  1330. ctx.APIErrorInternal(err)
  1331. return
  1332. }
  1333. res.Entries[i] = convertedArtifact
  1334. }
  1335. ctx.JSON(http.StatusOK, &res)
  1336. }
  1337. // GetArtifact Gets a specific artifact for a workflow run.
  1338. func GetArtifact(ctx *context.APIContext) {
  1339. // swagger:operation GET /repos/{owner}/{repo}/actions/artifacts/{artifact_id} repository getArtifact
  1340. // ---
  1341. // summary: Gets a specific artifact for a workflow run
  1342. // produces:
  1343. // - application/json
  1344. // parameters:
  1345. // - name: owner
  1346. // in: path
  1347. // description: owner of the repo
  1348. // type: string
  1349. // required: true
  1350. // - name: repo
  1351. // in: path
  1352. // description: name of the repository
  1353. // type: string
  1354. // required: true
  1355. // - name: artifact_id
  1356. // in: path
  1357. // description: id of the artifact
  1358. // type: string
  1359. // required: true
  1360. // responses:
  1361. // "200":
  1362. // "$ref": "#/responses/Artifact"
  1363. // "400":
  1364. // "$ref": "#/responses/error"
  1365. // "404":
  1366. // "$ref": "#/responses/notFound"
  1367. art := getArtifactByPathParam(ctx, ctx.Repo.Repository)
  1368. if ctx.Written() {
  1369. return
  1370. }
  1371. if actions.IsArtifactV4(art) {
  1372. convertedArtifact, err := convert.ToActionArtifact(ctx.Repo.Repository, art)
  1373. if err != nil {
  1374. ctx.APIErrorInternal(err)
  1375. return
  1376. }
  1377. ctx.JSON(http.StatusOK, convertedArtifact)
  1378. return
  1379. }
  1380. // v3 not supported due to not having one unique id
  1381. ctx.APIError(http.StatusNotFound, "Artifact not found")
  1382. }
  1383. // DeleteArtifact Deletes a specific artifact for a workflow run.
  1384. func DeleteArtifact(ctx *context.APIContext) {
  1385. // swagger:operation DELETE /repos/{owner}/{repo}/actions/artifacts/{artifact_id} repository deleteArtifact
  1386. // ---
  1387. // summary: Deletes a specific artifact for a workflow run
  1388. // produces:
  1389. // - application/json
  1390. // parameters:
  1391. // - name: owner
  1392. // in: path
  1393. // description: owner of the repo
  1394. // type: string
  1395. // required: true
  1396. // - name: repo
  1397. // in: path
  1398. // description: name of the repository
  1399. // type: string
  1400. // required: true
  1401. // - name: artifact_id
  1402. // in: path
  1403. // description: id of the artifact
  1404. // type: string
  1405. // required: true
  1406. // responses:
  1407. // "204":
  1408. // description: "No Content"
  1409. // "400":
  1410. // "$ref": "#/responses/error"
  1411. // "404":
  1412. // "$ref": "#/responses/notFound"
  1413. art := getArtifactByPathParam(ctx, ctx.Repo.Repository)
  1414. if ctx.Written() {
  1415. return
  1416. }
  1417. if actions.IsArtifactV4(art) {
  1418. if err := actions_model.SetArtifactNeedDelete(ctx, art.RunID, art.ArtifactName); err != nil {
  1419. ctx.APIErrorInternal(err)
  1420. return
  1421. }
  1422. ctx.Status(http.StatusNoContent)
  1423. return
  1424. }
  1425. // v3 not supported due to not having one unique id
  1426. ctx.APIError(http.StatusNotFound, "Artifact not found")
  1427. }
  1428. func buildSignature(endp string, expires, artifactID int64) []byte {
  1429. mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret())
  1430. mac.Write([]byte(endp))
  1431. fmt.Fprint(mac, expires)
  1432. fmt.Fprint(mac, artifactID)
  1433. return mac.Sum(nil)
  1434. }
  1435. func buildDownloadRawEndpoint(repo *repo_model.Repository, artifactID int64) string {
  1436. return fmt.Sprintf("api/v1/repos/%s/%s/actions/artifacts/%d/zip/raw", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name), artifactID)
  1437. }
  1438. func buildSigURL(ctx go_context.Context, endPoint string, artifactID int64) string {
  1439. // endPoint is a path like "api/v1/repos/owner/repo/actions/artifacts/1/zip/raw"
  1440. expires := time.Now().Add(60 * time.Minute).Unix()
  1441. uploadURL := httplib.GuessCurrentAppURL(ctx) + endPoint + "?sig=" + base64.URLEncoding.EncodeToString(buildSignature(endPoint, expires, artifactID)) + "&expires=" + strconv.FormatInt(expires, 10)
  1442. return uploadURL
  1443. }
  1444. // DownloadArtifact Downloads a specific artifact for a workflow run redirects to blob url.
  1445. func DownloadArtifact(ctx *context.APIContext) {
  1446. // swagger:operation GET /repos/{owner}/{repo}/actions/artifacts/{artifact_id}/zip repository downloadArtifact
  1447. // ---
  1448. // summary: Downloads a specific artifact for a workflow run redirects to blob url
  1449. // produces:
  1450. // - application/json
  1451. // parameters:
  1452. // - name: owner
  1453. // in: path
  1454. // description: owner of the repo
  1455. // type: string
  1456. // required: true
  1457. // - name: repo
  1458. // in: path
  1459. // description: name of the repository
  1460. // type: string
  1461. // required: true
  1462. // - name: artifact_id
  1463. // in: path
  1464. // description: id of the artifact
  1465. // type: string
  1466. // required: true
  1467. // responses:
  1468. // "302":
  1469. // description: redirect to the blob download
  1470. // "400":
  1471. // "$ref": "#/responses/error"
  1472. // "404":
  1473. // "$ref": "#/responses/notFound"
  1474. art := getArtifactByPathParam(ctx, ctx.Repo.Repository)
  1475. if ctx.Written() {
  1476. return
  1477. }
  1478. // if artifacts status is not uploaded-confirmed, treat it as not found
  1479. if art.Status == actions_model.ArtifactStatusExpired {
  1480. ctx.APIError(http.StatusNotFound, "Artifact has expired")
  1481. return
  1482. }
  1483. ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(art.ArtifactName), art.ArtifactName))
  1484. if actions.IsArtifactV4(art) {
  1485. ok, err := actions.DownloadArtifactV4ServeDirectOnly(ctx.Base, art)
  1486. if ok {
  1487. return
  1488. }
  1489. if err != nil {
  1490. ctx.APIErrorInternal(err)
  1491. return
  1492. }
  1493. redirectURL := buildSigURL(ctx, buildDownloadRawEndpoint(ctx.Repo.Repository, art.ID), art.ID)
  1494. ctx.Redirect(redirectURL, http.StatusFound)
  1495. return
  1496. }
  1497. // v3 not supported due to not having one unique id
  1498. ctx.APIError(http.StatusNotFound, "Artifact not found")
  1499. }
  1500. // DownloadArtifactRaw Downloads a specific artifact for a workflow run directly.
  1501. func DownloadArtifactRaw(ctx *context.APIContext) {
  1502. // it doesn't use repoAssignment middleware, so it needs to prepare the repo and check permission (sig) by itself
  1503. repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ctx.PathParam("username"), ctx.PathParam("reponame"))
  1504. if err != nil {
  1505. if errors.Is(err, util.ErrNotExist) {
  1506. ctx.APIErrorNotFound()
  1507. } else {
  1508. ctx.APIErrorInternal(err)
  1509. }
  1510. return
  1511. }
  1512. art := getArtifactByPathParam(ctx, repo)
  1513. if ctx.Written() {
  1514. return
  1515. }
  1516. sigStr := ctx.Req.URL.Query().Get("sig")
  1517. expiresStr := ctx.Req.URL.Query().Get("expires")
  1518. sigBytes, _ := base64.URLEncoding.DecodeString(sigStr)
  1519. expires, _ := strconv.ParseInt(expiresStr, 10, 64)
  1520. expectedSig := buildSignature(buildDownloadRawEndpoint(repo, art.ID), expires, art.ID)
  1521. if !hmac.Equal(sigBytes, expectedSig) {
  1522. ctx.APIError(http.StatusUnauthorized, "Error unauthorized")
  1523. return
  1524. }
  1525. t := time.Unix(expires, 0)
  1526. if t.Before(time.Now()) {
  1527. ctx.APIError(http.StatusUnauthorized, "Error link expired")
  1528. return
  1529. }
  1530. // if artifacts status is not uploaded-confirmed, treat it as not found
  1531. if art.Status == actions_model.ArtifactStatusExpired {
  1532. ctx.APIError(http.StatusNotFound, "Artifact has expired")
  1533. return
  1534. }
  1535. ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(art.ArtifactName), art.ArtifactName))
  1536. if actions.IsArtifactV4(art) {
  1537. err := actions.DownloadArtifactV4(ctx.Base, art)
  1538. if err != nil {
  1539. ctx.APIErrorInternal(err)
  1540. return
  1541. }
  1542. return
  1543. }
  1544. // v3 not supported due to not having one unique id
  1545. ctx.APIError(http.StatusNotFound, "artifact not found")
  1546. }
  1547. // Try to get the artifact by ID and check access
  1548. func getArtifactByPathParam(ctx *context.APIContext, repo *repo_model.Repository) *actions_model.ActionArtifact {
  1549. artifactID := ctx.PathParamInt64("artifact_id")
  1550. art, ok, err := db.GetByID[actions_model.ActionArtifact](ctx, artifactID)
  1551. if err != nil {
  1552. ctx.APIErrorInternal(err)
  1553. return nil
  1554. }
  1555. // if artifacts status is not uploaded-confirmed, treat it as not found
  1556. // only check RepoID here, because the repository owner may change over the time
  1557. if !ok ||
  1558. art.RepoID != repo.ID ||
  1559. art.Status != actions_model.ArtifactStatusUploadConfirmed && art.Status != actions_model.ArtifactStatusExpired {
  1560. ctx.APIError(http.StatusNotFound, "artifact not found")
  1561. return nil
  1562. }
  1563. return art
  1564. }