gitea源码

actions.go 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. // Copyright 2022 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package actions
  4. import (
  5. "bytes"
  6. stdCtx "context"
  7. "errors"
  8. "net/http"
  9. "slices"
  10. "strings"
  11. actions_model "code.gitea.io/gitea/models/actions"
  12. "code.gitea.io/gitea/models/db"
  13. git_model "code.gitea.io/gitea/models/git"
  14. repo_model "code.gitea.io/gitea/models/repo"
  15. "code.gitea.io/gitea/models/unit"
  16. "code.gitea.io/gitea/modules/actions"
  17. "code.gitea.io/gitea/modules/container"
  18. "code.gitea.io/gitea/modules/git"
  19. "code.gitea.io/gitea/modules/log"
  20. "code.gitea.io/gitea/modules/optional"
  21. "code.gitea.io/gitea/modules/setting"
  22. "code.gitea.io/gitea/modules/templates"
  23. "code.gitea.io/gitea/modules/util"
  24. shared_user "code.gitea.io/gitea/routers/web/shared/user"
  25. "code.gitea.io/gitea/services/context"
  26. "code.gitea.io/gitea/services/convert"
  27. "github.com/nektos/act/pkg/model"
  28. "gopkg.in/yaml.v3"
  29. )
  30. const (
  31. tplListActions templates.TplName = "repo/actions/list"
  32. tplDispatchInputsActions templates.TplName = "repo/actions/workflow_dispatch_inputs"
  33. tplViewActions templates.TplName = "repo/actions/view"
  34. )
  35. type Workflow struct {
  36. Entry git.TreeEntry
  37. ErrMsg string
  38. }
  39. // MustEnableActions check if actions are enabled in settings
  40. func MustEnableActions(ctx *context.Context) {
  41. if !setting.Actions.Enabled {
  42. ctx.NotFound(nil)
  43. return
  44. }
  45. if unit.TypeActions.UnitGlobalDisabled() {
  46. ctx.NotFound(nil)
  47. return
  48. }
  49. if ctx.Repo.Repository != nil {
  50. if !ctx.Repo.CanRead(unit.TypeActions) {
  51. ctx.NotFound(nil)
  52. return
  53. }
  54. }
  55. }
  56. func List(ctx *context.Context) {
  57. ctx.Data["Title"] = ctx.Tr("actions.actions")
  58. ctx.Data["PageIsActions"] = true
  59. commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
  60. if errors.Is(err, util.ErrNotExist) {
  61. ctx.Data["NotFoundPrompt"] = ctx.Tr("repo.branch.default_branch_not_exist", ctx.Repo.Repository.DefaultBranch)
  62. ctx.NotFound(nil)
  63. return
  64. } else if err != nil {
  65. ctx.ServerError("GetBranchCommit", err)
  66. return
  67. }
  68. workflows := prepareWorkflowDispatchTemplate(ctx, commit)
  69. if ctx.Written() {
  70. return
  71. }
  72. prepareWorkflowList(ctx, workflows)
  73. if ctx.Written() {
  74. return
  75. }
  76. ctx.HTML(http.StatusOK, tplListActions)
  77. }
  78. func WorkflowDispatchInputs(ctx *context.Context) {
  79. ref := ctx.FormString("ref")
  80. if ref == "" {
  81. ctx.NotFound(nil)
  82. return
  83. }
  84. // get target commit of run from specified ref
  85. refName := git.RefName(ref)
  86. var commit *git.Commit
  87. var err error
  88. if refName.IsTag() {
  89. commit, err = ctx.Repo.GitRepo.GetTagCommit(refName.TagName())
  90. } else if refName.IsBranch() {
  91. commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName.BranchName())
  92. } else {
  93. ctx.ServerError("UnsupportedRefType", nil)
  94. return
  95. }
  96. if err != nil {
  97. ctx.ServerError("GetTagCommit/GetBranchCommit", err)
  98. return
  99. }
  100. prepareWorkflowDispatchTemplate(ctx, commit)
  101. if ctx.Written() {
  102. return
  103. }
  104. ctx.HTML(http.StatusOK, tplDispatchInputsActions)
  105. }
  106. func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) (workflows []Workflow) {
  107. workflowID := ctx.FormString("workflow")
  108. ctx.Data["CurWorkflow"] = workflowID
  109. ctx.Data["CurWorkflowExists"] = false
  110. var curWorkflow *model.Workflow
  111. _, entries, err := actions.ListWorkflows(commit)
  112. if err != nil {
  113. ctx.ServerError("ListWorkflows", err)
  114. return nil
  115. }
  116. // Get all runner labels
  117. runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{
  118. RepoID: ctx.Repo.Repository.ID,
  119. IsOnline: optional.Some(true),
  120. WithAvailable: true,
  121. })
  122. if err != nil {
  123. ctx.ServerError("FindRunners", err)
  124. return nil
  125. }
  126. allRunnerLabels := make(container.Set[string])
  127. for _, r := range runners {
  128. allRunnerLabels.AddMultiple(r.AgentLabels...)
  129. }
  130. workflows = make([]Workflow, 0, len(entries))
  131. for _, entry := range entries {
  132. workflow := Workflow{Entry: *entry}
  133. content, err := actions.GetContentFromEntry(entry)
  134. if err != nil {
  135. ctx.ServerError("GetContentFromEntry", err)
  136. return nil
  137. }
  138. wf, err := model.ReadWorkflow(bytes.NewReader(content))
  139. if err != nil {
  140. workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error())
  141. workflows = append(workflows, workflow)
  142. continue
  143. }
  144. // The workflow must contain at least one job without "needs". Otherwise, a deadlock will occur and no jobs will be able to run.
  145. hasJobWithoutNeeds := false
  146. // Check whether you have matching runner and a job without "needs"
  147. emptyJobsNumber := 0
  148. for _, j := range wf.Jobs {
  149. if j == nil {
  150. emptyJobsNumber++
  151. continue
  152. }
  153. if !hasJobWithoutNeeds && len(j.Needs()) == 0 {
  154. hasJobWithoutNeeds = true
  155. }
  156. runsOnList := j.RunsOn()
  157. for _, ro := range runsOnList {
  158. if strings.Contains(ro, "${{") {
  159. // Skip if it contains expressions.
  160. // The expressions could be very complex and could not be evaluated here,
  161. // so just skip it, it's OK since it's just a tooltip message.
  162. continue
  163. }
  164. if !allRunnerLabels.Contains(ro) {
  165. workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", ro)
  166. break
  167. }
  168. }
  169. if workflow.ErrMsg != "" {
  170. break
  171. }
  172. }
  173. if !hasJobWithoutNeeds {
  174. workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs")
  175. }
  176. if emptyJobsNumber == len(wf.Jobs) {
  177. workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job")
  178. }
  179. workflows = append(workflows, workflow)
  180. if workflow.Entry.Name() == workflowID {
  181. curWorkflow = wf
  182. ctx.Data["CurWorkflowExists"] = true
  183. }
  184. }
  185. ctx.Data["workflows"] = workflows
  186. ctx.Data["RepoLink"] = ctx.Repo.Repository.Link()
  187. actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
  188. ctx.Data["ActionsConfig"] = actionsConfig
  189. if len(workflowID) > 0 && ctx.Repo.CanWrite(unit.TypeActions) {
  190. ctx.Data["AllowDisableOrEnableWorkflow"] = ctx.Repo.IsAdmin()
  191. isWorkflowDisabled := actionsConfig.IsWorkflowDisabled(workflowID)
  192. ctx.Data["CurWorkflowDisabled"] = isWorkflowDisabled
  193. if !isWorkflowDisabled && curWorkflow != nil {
  194. workflowDispatchConfig := workflowDispatchConfig(curWorkflow)
  195. if workflowDispatchConfig != nil {
  196. ctx.Data["WorkflowDispatchConfig"] = workflowDispatchConfig
  197. branchOpts := git_model.FindBranchOptions{
  198. RepoID: ctx.Repo.Repository.ID,
  199. IsDeletedBranch: optional.Some(false),
  200. ListOptions: db.ListOptions{
  201. ListAll: true,
  202. },
  203. }
  204. branches, err := git_model.FindBranchNames(ctx, branchOpts)
  205. if err != nil {
  206. ctx.ServerError("FindBranchNames", err)
  207. return nil
  208. }
  209. // always put default branch on the top if it exists
  210. if slices.Contains(branches, ctx.Repo.Repository.DefaultBranch) {
  211. branches = util.SliceRemoveAll(branches, ctx.Repo.Repository.DefaultBranch)
  212. branches = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
  213. }
  214. ctx.Data["Branches"] = branches
  215. tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
  216. if err != nil {
  217. ctx.ServerError("GetTagNamesByRepoID", err)
  218. return nil
  219. }
  220. ctx.Data["Tags"] = tags
  221. }
  222. }
  223. }
  224. return workflows
  225. }
  226. func prepareWorkflowList(ctx *context.Context, workflows []Workflow) {
  227. actorID := ctx.FormInt64("actor")
  228. status := ctx.FormInt("status")
  229. workflowID := ctx.FormString("workflow")
  230. page := ctx.FormInt("page")
  231. if page <= 0 {
  232. page = 1
  233. }
  234. // if status or actor query param is not given to frontend href, (href="/<repoLink>/actions")
  235. // they will be 0 by default, which indicates get all status or actors
  236. ctx.Data["CurActor"] = actorID
  237. ctx.Data["CurStatus"] = status
  238. if actorID > 0 || status > int(actions_model.StatusUnknown) {
  239. ctx.Data["IsFiltered"] = true
  240. }
  241. opts := actions_model.FindRunOptions{
  242. ListOptions: db.ListOptions{
  243. Page: page,
  244. PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
  245. },
  246. RepoID: ctx.Repo.Repository.ID,
  247. WorkflowID: workflowID,
  248. TriggerUserID: actorID,
  249. }
  250. // if status is not StatusUnknown, it means user has selected a status filter
  251. if actions_model.Status(status) != actions_model.StatusUnknown {
  252. opts.Status = []actions_model.Status{actions_model.Status(status)}
  253. }
  254. runs, total, err := db.FindAndCount[actions_model.ActionRun](ctx, opts)
  255. if err != nil {
  256. ctx.ServerError("FindAndCount", err)
  257. return
  258. }
  259. for _, run := range runs {
  260. run.Repo = ctx.Repo.Repository
  261. }
  262. if err := actions_model.RunList(runs).LoadTriggerUser(ctx); err != nil {
  263. ctx.ServerError("LoadTriggerUser", err)
  264. return
  265. }
  266. if err := loadIsRefDeleted(ctx, ctx.Repo.Repository.ID, runs); err != nil {
  267. log.Error("LoadIsRefDeleted", err)
  268. }
  269. ctx.Data["Runs"] = runs
  270. actors, err := actions_model.GetActors(ctx, ctx.Repo.Repository.ID)
  271. if err != nil {
  272. ctx.ServerError("GetActors", err)
  273. return
  274. }
  275. ctx.Data["Actors"] = shared_user.MakeSelfOnTop(ctx.Doer, actors)
  276. ctx.Data["StatusInfoList"] = actions_model.GetStatusInfoList(ctx, ctx.Locale)
  277. pager := context.NewPagination(int(total), opts.PageSize, opts.Page, 5)
  278. pager.AddParamFromRequest(ctx.Req)
  279. ctx.Data["Page"] = pager
  280. ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0
  281. ctx.Data["CanWriteRepoUnitActions"] = ctx.Repo.CanWrite(unit.TypeActions)
  282. }
  283. // loadIsRefDeleted loads the IsRefDeleted field for each run in the list.
  284. // TODO: move this function to models/actions/run_list.go but now it will result in a circular import.
  285. func loadIsRefDeleted(ctx stdCtx.Context, repoID int64, runs actions_model.RunList) error {
  286. branches := make(container.Set[string], len(runs))
  287. for _, run := range runs {
  288. refName := git.RefName(run.Ref)
  289. if refName.IsBranch() {
  290. branches.Add(refName.ShortName())
  291. }
  292. }
  293. if len(branches) == 0 {
  294. return nil
  295. }
  296. branchInfos, err := git_model.GetBranches(ctx, repoID, branches.Values(), false)
  297. if err != nil {
  298. return err
  299. }
  300. branchSet := git_model.BranchesToNamesSet(branchInfos)
  301. for _, run := range runs {
  302. refName := git.RefName(run.Ref)
  303. if refName.IsBranch() && !branchSet.Contains(refName.ShortName()) {
  304. run.IsRefDeleted = true
  305. }
  306. }
  307. return nil
  308. }
  309. type WorkflowDispatchInput struct {
  310. Name string `yaml:"name"`
  311. Description string `yaml:"description"`
  312. Required bool `yaml:"required"`
  313. Default string `yaml:"default"`
  314. Type string `yaml:"type"`
  315. Options []string `yaml:"options"`
  316. }
  317. type WorkflowDispatch struct {
  318. Inputs []WorkflowDispatchInput
  319. }
  320. func workflowDispatchConfig(w *model.Workflow) *WorkflowDispatch {
  321. switch w.RawOn.Kind {
  322. case yaml.ScalarNode:
  323. var val string
  324. if !decodeNode(w.RawOn, &val) {
  325. return nil
  326. }
  327. if val == "workflow_dispatch" {
  328. return &WorkflowDispatch{}
  329. }
  330. case yaml.SequenceNode:
  331. var val []string
  332. if !decodeNode(w.RawOn, &val) {
  333. return nil
  334. }
  335. if slices.Contains(val, "workflow_dispatch") {
  336. return &WorkflowDispatch{}
  337. }
  338. case yaml.MappingNode:
  339. var val map[string]yaml.Node
  340. if !decodeNode(w.RawOn, &val) {
  341. return nil
  342. }
  343. workflowDispatchNode, found := val["workflow_dispatch"]
  344. if !found {
  345. return nil
  346. }
  347. var workflowDispatch WorkflowDispatch
  348. var workflowDispatchVal map[string]yaml.Node
  349. if !decodeNode(workflowDispatchNode, &workflowDispatchVal) {
  350. return &workflowDispatch
  351. }
  352. inputsNode, found := workflowDispatchVal["inputs"]
  353. if !found || inputsNode.Kind != yaml.MappingNode {
  354. return &workflowDispatch
  355. }
  356. i := 0
  357. for {
  358. if i+1 >= len(inputsNode.Content) {
  359. break
  360. }
  361. var input WorkflowDispatchInput
  362. if decodeNode(*inputsNode.Content[i+1], &input) {
  363. input.Name = inputsNode.Content[i].Value
  364. workflowDispatch.Inputs = append(workflowDispatch.Inputs, input)
  365. }
  366. i += 2
  367. }
  368. return &workflowDispatch
  369. default:
  370. return nil
  371. }
  372. return nil
  373. }
  374. func decodeNode(node yaml.Node, out any) bool {
  375. if err := node.Decode(out); err != nil {
  376. log.Warn("Failed to decode node %v into %T: %v", node, out, err)
  377. return false
  378. }
  379. return true
  380. }