123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625 |
- // Copyright 2022 The Gitea Authors. All rights reserved.
- // SPDX-License-Identifier: MIT
-
- package actions
-
- import (
- "archive/zip"
- "compress/gzip"
- "context"
- "errors"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "strings"
- "time"
-
- actions_model "code.gitea.io/gitea/models/actions"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/actions"
- "code.gitea.io/gitea/modules/base"
- context_module "code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/storage"
- "code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- actions_service "code.gitea.io/gitea/services/actions"
-
- "xorm.io/builder"
- )
-
- func View(ctx *context_module.Context) {
- ctx.Data["PageIsActions"] = true
- runIndex := ctx.ParamsInt64("run")
- jobIndex := ctx.ParamsInt64("job")
- ctx.Data["RunIndex"] = runIndex
- ctx.Data["JobIndex"] = jobIndex
- ctx.Data["ActionsURL"] = ctx.Repo.RepoLink + "/actions"
-
- if getRunJobs(ctx, runIndex, jobIndex); ctx.Written() {
- return
- }
-
- ctx.HTML(http.StatusOK, tplViewActions)
- }
-
- type ViewRequest struct {
- LogCursors []struct {
- Step int `json:"step"`
- Cursor int64 `json:"cursor"`
- Expanded bool `json:"expanded"`
- } `json:"logCursors"`
- }
-
- type ViewResponse struct {
- State struct {
- Run struct {
- Link string `json:"link"`
- Title string `json:"title"`
- Status string `json:"status"`
- CanCancel bool `json:"canCancel"`
- CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve
- CanRerun bool `json:"canRerun"`
- Done bool `json:"done"`
- Jobs []*ViewJob `json:"jobs"`
- Commit ViewCommit `json:"commit"`
- } `json:"run"`
- CurrentJob struct {
- Title string `json:"title"`
- Detail string `json:"detail"`
- Steps []*ViewJobStep `json:"steps"`
- } `json:"currentJob"`
- } `json:"state"`
- Logs struct {
- StepsLog []*ViewStepLog `json:"stepsLog"`
- } `json:"logs"`
- }
-
- type ViewJob struct {
- ID int64 `json:"id"`
- Name string `json:"name"`
- Status string `json:"status"`
- CanRerun bool `json:"canRerun"`
- Duration string `json:"duration"`
- }
-
- type ViewCommit struct {
- LocaleCommit string `json:"localeCommit"`
- LocalePushedBy string `json:"localePushedBy"`
- ShortSha string `json:"shortSHA"`
- Link string `json:"link"`
- Pusher ViewUser `json:"pusher"`
- Branch ViewBranch `json:"branch"`
- }
-
- type ViewUser struct {
- DisplayName string `json:"displayName"`
- Link string `json:"link"`
- }
-
- type ViewBranch struct {
- Name string `json:"name"`
- Link string `json:"link"`
- }
-
- type ViewJobStep struct {
- Summary string `json:"summary"`
- Duration string `json:"duration"`
- Status string `json:"status"`
- }
-
- type ViewStepLog struct {
- Step int `json:"step"`
- Cursor int64 `json:"cursor"`
- Lines []*ViewStepLogLine `json:"lines"`
- Started int64 `json:"started"`
- }
-
- type ViewStepLogLine struct {
- Index int64 `json:"index"`
- Message string `json:"message"`
- Timestamp float64 `json:"timestamp"`
- }
-
- func ViewPost(ctx *context_module.Context) {
- req := web.GetForm(ctx).(*ViewRequest)
- runIndex := ctx.ParamsInt64("run")
- jobIndex := ctx.ParamsInt64("job")
-
- current, jobs := getRunJobs(ctx, runIndex, jobIndex)
- if ctx.Written() {
- return
- }
- run := current.Run
- if err := run.LoadAttributes(ctx); err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
-
- resp := &ViewResponse{}
-
- resp.State.Run.Title = run.Title
- resp.State.Run.Link = run.Link()
- resp.State.Run.CanCancel = !run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
- resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions)
- resp.State.Run.CanRerun = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
- resp.State.Run.Done = run.Status.IsDone()
- resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead fo 'null' in json
- resp.State.Run.Status = run.Status.String()
- for _, v := range jobs {
- resp.State.Run.Jobs = append(resp.State.Run.Jobs, &ViewJob{
- ID: v.ID,
- Name: v.Name,
- Status: v.Status.String(),
- CanRerun: v.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions),
- Duration: v.Duration().String(),
- })
- }
-
- pusher := ViewUser{
- DisplayName: run.TriggerUser.GetDisplayName(),
- Link: run.TriggerUser.HomeLink(),
- }
- branch := ViewBranch{
- Name: run.PrettyRef(),
- Link: run.RefLink(),
- }
- resp.State.Run.Commit = ViewCommit{
- LocaleCommit: ctx.Tr("actions.runs.commit"),
- LocalePushedBy: ctx.Tr("actions.runs.pushed_by"),
- ShortSha: base.ShortSha(run.CommitSHA),
- Link: fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA),
- Pusher: pusher,
- Branch: branch,
- }
-
- var task *actions_model.ActionTask
- if current.TaskID > 0 {
- var err error
- task, err = actions_model.GetTaskByID(ctx, current.TaskID)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
- task.Job = current
- if err := task.LoadAttributes(ctx); err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
- }
-
- resp.State.CurrentJob.Title = current.Name
- resp.State.CurrentJob.Detail = current.Status.LocaleString(ctx.Locale)
- if run.NeedApproval {
- resp.State.CurrentJob.Detail = ctx.Locale.Tr("actions.need_approval_desc")
- }
- resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json
- resp.Logs.StepsLog = make([]*ViewStepLog, 0) // marshal to '[]' instead fo 'null' in json
- if task != nil {
- steps := actions.FullSteps(task)
-
- for _, v := range steps {
- resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, &ViewJobStep{
- Summary: v.Name,
- Duration: v.Duration().String(),
- Status: v.Status.String(),
- })
- }
-
- for _, cursor := range req.LogCursors {
- if !cursor.Expanded {
- continue
- }
-
- step := steps[cursor.Step]
-
- logLines := make([]*ViewStepLogLine, 0) // marshal to '[]' instead fo 'null' in json
-
- index := step.LogIndex + cursor.Cursor
- validCursor := cursor.Cursor >= 0 &&
- // !(cursor.Cursor < step.LogLength) when the frontend tries to fetch next line before it's ready.
- // So return the same cursor and empty lines to let the frontend retry.
- cursor.Cursor < step.LogLength &&
- // !(index < task.LogIndexes[index]) when task data is older than step data.
- // It can be fixed by making sure write/read tasks and steps in the same transaction,
- // but it's easier to just treat it as fetching the next line before it's ready.
- index < int64(len(task.LogIndexes))
-
- if validCursor {
- length := step.LogLength - cursor.Cursor
- offset := task.LogIndexes[index]
- var err error
- logRows, err := actions.ReadLogs(ctx, task.LogInStorage, task.LogFilename, offset, length)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
-
- for i, row := range logRows {
- logLines = append(logLines, &ViewStepLogLine{
- Index: cursor.Cursor + int64(i) + 1, // start at 1
- Message: row.Content,
- Timestamp: float64(row.Time.AsTime().UnixNano()) / float64(time.Second),
- })
- }
- }
-
- resp.Logs.StepsLog = append(resp.Logs.StepsLog, &ViewStepLog{
- Step: cursor.Step,
- Cursor: cursor.Cursor + int64(len(logLines)),
- Lines: logLines,
- Started: int64(step.Started),
- })
- }
- }
-
- ctx.JSON(http.StatusOK, resp)
- }
-
- // Rerun will rerun jobs in the given run
- // jobIndex = 0 means rerun all jobs
- func Rerun(ctx *context_module.Context) {
- runIndex := ctx.ParamsInt64("run")
- jobIndex := ctx.ParamsInt64("job")
-
- run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
-
- // can not rerun job when workflow is disabled
- cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
- cfg := cfgUnit.ActionsConfig()
- if cfg.IsWorkflowDisabled(run.WorkflowID) {
- ctx.JSONError(ctx.Locale.Tr("actions.workflow.disabled"))
- return
- }
-
- job, jobs := getRunJobs(ctx, runIndex, jobIndex)
- if ctx.Written() {
- return
- }
-
- if jobIndex != 0 {
- jobs = []*actions_model.ActionRunJob{job}
- }
-
- for _, j := range jobs {
- if err := rerunJob(ctx, j); err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
- }
-
- ctx.JSON(http.StatusOK, struct{}{})
- }
-
- func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob) error {
- status := job.Status
- if !status.IsDone() {
- return nil
- }
-
- job.TaskID = 0
- job.Status = actions_model.StatusWaiting
- job.Started = 0
- job.Stopped = 0
-
- if err := db.WithTx(ctx, func(ctx context.Context) error {
- _, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": status}, "task_id", "status", "started", "stopped")
- return err
- }); err != nil {
- return err
- }
-
- actions_service.CreateCommitStatus(ctx, job)
- return nil
- }
-
- func Logs(ctx *context_module.Context) {
- runIndex := ctx.ParamsInt64("run")
- jobIndex := ctx.ParamsInt64("job")
-
- job, _ := getRunJobs(ctx, runIndex, jobIndex)
- if ctx.Written() {
- return
- }
- if job.TaskID == 0 {
- ctx.Error(http.StatusNotFound, "job is not started")
- return
- }
-
- err := job.LoadRun(ctx)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
-
- task, err := actions_model.GetTaskByID(ctx, job.TaskID)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
- if task.LogExpired {
- ctx.Error(http.StatusNotFound, "logs have been cleaned up")
- return
- }
-
- reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
- defer reader.Close()
-
- workflowName := job.Run.WorkflowID
- if p := strings.Index(workflowName, "."); p > 0 {
- workflowName = workflowName[0:p]
- }
- ctx.ServeContent(reader, &context_module.ServeHeaderOptions{
- Filename: fmt.Sprintf("%v-%v-%v.log", workflowName, job.Name, task.ID),
- ContentLength: &task.LogSize,
- ContentType: "text/plain",
- ContentTypeCharset: "utf-8",
- Disposition: "attachment",
- })
- }
-
- func Cancel(ctx *context_module.Context) {
- runIndex := ctx.ParamsInt64("run")
-
- _, jobs := getRunJobs(ctx, runIndex, -1)
- if ctx.Written() {
- return
- }
-
- if err := db.WithTx(ctx, func(ctx context.Context) error {
- for _, job := range jobs {
- status := job.Status
- if status.IsDone() {
- continue
- }
- if job.TaskID == 0 {
- job.Status = actions_model.StatusCancelled
- job.Stopped = timeutil.TimeStampNow()
- n, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped")
- if err != nil {
- return err
- }
- if n == 0 {
- return fmt.Errorf("job has changed, try again")
- }
- continue
- }
- if err := actions_model.StopTask(ctx, job.TaskID, actions_model.StatusCancelled); err != nil {
- return err
- }
- }
- return nil
- }); err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
-
- actions_service.CreateCommitStatus(ctx, jobs...)
-
- ctx.JSON(http.StatusOK, struct{}{})
- }
-
- func Approve(ctx *context_module.Context) {
- runIndex := ctx.ParamsInt64("run")
-
- current, jobs := getRunJobs(ctx, runIndex, -1)
- if ctx.Written() {
- return
- }
- run := current.Run
- doer := ctx.Doer
-
- if err := db.WithTx(ctx, func(ctx context.Context) error {
- run.NeedApproval = false
- run.ApprovedBy = doer.ID
- if err := actions_model.UpdateRun(ctx, run, "need_approval", "approved_by"); err != nil {
- return err
- }
- for _, job := range jobs {
- if len(job.Needs) == 0 && job.Status.IsBlocked() {
- job.Status = actions_model.StatusWaiting
- _, err := actions_model.UpdateRunJob(ctx, job, nil, "status")
- if err != nil {
- return err
- }
- }
- }
- return nil
- }); err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
-
- actions_service.CreateCommitStatus(ctx, jobs...)
-
- ctx.JSON(http.StatusOK, struct{}{})
- }
-
- // getRunJobs gets the jobs of runIndex, and returns jobs[jobIndex], jobs.
- // Any error will be written to the ctx.
- // It never returns a nil job of an empty jobs, if the jobIndex is out of range, it will be treated as 0.
- func getRunJobs(ctx *context_module.Context, runIndex, jobIndex int64) (*actions_model.ActionRunJob, []*actions_model.ActionRunJob) {
- run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
- if err != nil {
- if errors.Is(err, util.ErrNotExist) {
- ctx.Error(http.StatusNotFound, err.Error())
- return nil, nil
- }
- ctx.Error(http.StatusInternalServerError, err.Error())
- return nil, nil
- }
- run.Repo = ctx.Repo.Repository
-
- jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return nil, nil
- }
- if len(jobs) == 0 {
- ctx.Error(http.StatusNotFound, err.Error())
- return nil, nil
- }
-
- for _, v := range jobs {
- v.Run = run
- }
-
- if jobIndex >= 0 && jobIndex < int64(len(jobs)) {
- return jobs[jobIndex], jobs
- }
- return jobs[0], jobs
- }
-
- type ArtifactsViewResponse struct {
- Artifacts []*ArtifactsViewItem `json:"artifacts"`
- }
-
- type ArtifactsViewItem struct {
- Name string `json:"name"`
- Size int64 `json:"size"`
- Status string `json:"status"`
- }
-
- func ArtifactsView(ctx *context_module.Context) {
- runIndex := ctx.ParamsInt64("run")
- run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
- if err != nil {
- if errors.Is(err, util.ErrNotExist) {
- ctx.Error(http.StatusNotFound, err.Error())
- return
- }
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
- artifacts, err := actions_model.ListUploadedArtifactsMeta(ctx, run.ID)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
- artifactsResponse := ArtifactsViewResponse{
- Artifacts: make([]*ArtifactsViewItem, 0, len(artifacts)),
- }
- for _, art := range artifacts {
- status := "completed"
- if art.Status == int64(actions_model.ArtifactStatusExpired) {
- status = "expired"
- }
- artifactsResponse.Artifacts = append(artifactsResponse.Artifacts, &ArtifactsViewItem{
- Name: art.ArtifactName,
- Size: art.FileSize,
- Status: status,
- })
- }
- ctx.JSON(http.StatusOK, artifactsResponse)
- }
-
- func ArtifactsDownloadView(ctx *context_module.Context) {
- runIndex := ctx.ParamsInt64("run")
- artifactName := ctx.Params("artifact_name")
-
- run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
- if err != nil {
- if errors.Is(err, util.ErrNotExist) {
- ctx.Error(http.StatusNotFound, err.Error())
- return
- }
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
-
- artifacts, err := actions_model.ListArtifactsByRunIDAndName(ctx, run.ID, artifactName)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
- if len(artifacts) == 0 {
- ctx.Error(http.StatusNotFound, "artifact not found")
- return
- }
-
- ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName))
-
- writer := zip.NewWriter(ctx.Resp)
- defer writer.Close()
- for _, art := range artifacts {
-
- f, err := storage.ActionsArtifacts.Open(art.StoragePath)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
-
- var r io.ReadCloser
- if art.ContentEncoding == "gzip" {
- r, err = gzip.NewReader(f)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
- } else {
- r = f
- }
- defer r.Close()
-
- w, err := writer.Create(art.ArtifactPath)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
- if _, err := io.Copy(w, r); err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
- }
- }
-
- func DisableWorkflowFile(ctx *context_module.Context) {
- disableOrEnableWorkflowFile(ctx, false)
- }
-
- func EnableWorkflowFile(ctx *context_module.Context) {
- disableOrEnableWorkflowFile(ctx, true)
- }
-
- func disableOrEnableWorkflowFile(ctx *context_module.Context, isEnable bool) {
- workflow := ctx.FormString("workflow")
- if len(workflow) == 0 {
- ctx.ServerError("workflow", nil)
- return
- }
-
- cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
- cfg := cfgUnit.ActionsConfig()
-
- if isEnable {
- cfg.EnableWorkflow(workflow)
- } else {
- cfg.DisableWorkflow(workflow)
- }
-
- if err := repo_model.UpdateRepoUnit(cfgUnit); err != nil {
- ctx.ServerError("UpdateRepoUnit", err)
- return
- }
-
- if isEnable {
- ctx.Flash.Success(ctx.Tr("actions.workflow.enable_success", workflow))
- } else {
- ctx.Flash.Success(ctx.Tr("actions.workflow.disable_success", workflow))
- }
-
- redirectURL := fmt.Sprintf("%s/actions?workflow=%s&actor=%s&status=%s", ctx.Repo.RepoLink, url.QueryEscape(workflow),
- url.QueryEscape(ctx.FormString("actor")), url.QueryEscape(ctx.FormString("status")))
- ctx.JSONRedirect(redirectURL)
- }
|