Files

1021 lines
30 KiB
Go
Raw Permalink Normal View History

2023-01-31 09:45:19 +08:00
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"archive/zip"
"compress/gzip"
2023-01-31 09:45:19 +08:00
"context"
"errors"
"fmt"
"html/template"
"io"
2023-01-31 09:45:19 +08:00
"net/http"
"net/url"
"strconv"
2023-01-31 09:45:19 +08:00
"time"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
2023-08-14 23:14:30 +08:00
repo_model "code.gitea.io/gitea/models/repo"
2023-01-31 09:45:19 +08:00
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/actions"
2023-03-27 19:34:09 +09:00
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
2023-05-19 21:37:57 +08:00
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/translation"
2023-01-31 09:45:19 +08:00
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
2025-03-26 11:30:52 -07:00
"code.gitea.io/gitea/routers/common"
2023-01-31 09:45:19 +08:00
actions_service "code.gitea.io/gitea/services/actions"
context_module "code.gitea.io/gitea/services/context"
2025-03-11 18:40:38 +01:00
notify_service "code.gitea.io/gitea/services/notify"
2023-01-31 09:45:19 +08:00
"github.com/nektos/act/pkg/model"
2023-01-31 09:45:19 +08:00
)
2026-03-22 02:04:39 +01:00
func findCurrentJobByPathParam(ctx *context_module.Context, jobs []*actions_model.ActionRunJob) (job *actions_model.ActionRunJob, hasPathParam bool) {
selectedJobID := ctx.PathParamInt64("job")
if selectedJobID <= 0 {
return nil, false
}
for _, job = range jobs {
if job.ID == selectedJobID {
return job, true
}
}
return nil, true
}
func getCurrentRunByPathParam(ctx *context_module.Context) (run *actions_model.ActionRun) {
var err error
// if run param is "latest", get the latest run id
if ctx.PathParam("run") == "latest" {
2026-03-22 02:04:39 +01:00
run, err = actions_model.GetLatestRun(ctx, ctx.Repo.Repository.ID)
} else {
run, err = actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("run"))
}
if errors.Is(err, util.ErrNotExist) {
ctx.NotFound(nil)
} else if err != nil {
ctx.ServerError("GetRun:"+ctx.PathParam("run"), err)
}
2026-03-22 02:04:39 +01:00
return run
}
// resolveCurrentRunForView resolves GET Actions page URLs and supports both ID-based and legacy index-based forms.
//
// By default, run summary pages (/actions/runs/{run}) use a best-effort ID-first fallback,
// and job pages (/actions/runs/{run}/jobs/{job}) try to confirm an ID-based URL first and prefer the ID-based interpretation when both are valid.
//
// `by_id=1` param explicitly forces the ID-based path, and `by_index=1` explicitly forces the legacy index-based path.
// If both are present, `by_id` takes precedence.
func resolveCurrentRunForView(ctx *context_module.Context) *actions_model.ActionRun {
// `by_id` explicitly requests ID-based resolution, so the request skips the legacy index-based disambiguation logic and resolves the run by ID directly.
// It takes precedence over `by_index` when both query parameters are present.
if ctx.PathParam("run") == "latest" || ctx.FormBool("by_id") {
return getCurrentRunByPathParam(ctx)
}
runNum := ctx.PathParamInt64("run")
if runNum <= 0 {
ctx.NotFound(nil)
return nil
}
byIndex := ctx.FormBool("by_index")
if ctx.PathParam("job") == "" {
// The URL does not contain a {job} path parameter, so it cannot use the
// job-specific rules to disambiguate ID-based URLs from legacy index-based URLs.
// Because of that, this path is handled with a best-effort ID-first fallback by default.
//
// When the same repository contains:
// - a run whose ID matches runNum, and
// - a different run whose repo-scope index also matches runNum
// this path prefers the ID match and may show a different run than the old legacy URL originally intended,
// unless `by_index=1` explicitly forces the legacy index-based interpretation.
if !byIndex {
runByID, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runNum)
if err == nil {
return runByID
}
if !errors.Is(err, util.ErrNotExist) {
ctx.ServerError("GetRun:"+ctx.PathParam("run"), err)
return nil
}
}
runByIndex, err := actions_model.GetRunByRepoAndIndex(ctx, ctx.Repo.Repository.ID, runNum)
if err == nil {
ctx.Redirect(fmt.Sprintf("%s/actions/runs/%d", ctx.Repo.RepoLink, runByIndex.ID), http.StatusFound)
return nil
}
if !errors.Is(err, util.ErrNotExist) {
ctx.ServerError("GetRunByRepoAndIndex", err)
return nil
}
ctx.NotFound(nil)
return nil
}
jobNum := ctx.PathParamInt64("job")
if jobNum < 0 {
ctx.NotFound(nil)
return nil
}
// A job index should not be larger than MaxJobNumPerRun, so larger values can skip the legacy index-based path and be treated as job IDs directly.
if !byIndex && jobNum >= actions_model.MaxJobNumPerRun {
return getCurrentRunByPathParam(ctx)
}
var runByID, runByIndex *actions_model.ActionRun
var targetJobByIndex *actions_model.ActionRunJob
if !byIndex {
// Probe the repo-scoped job ID first and only accept it when the job exists and belongs to the same runNum.
job, err := actions_model.GetRunJobByRepoAndID(ctx, ctx.Repo.Repository.ID, jobNum)
if err != nil && !errors.Is(err, util.ErrNotExist) {
ctx.ServerError("GetRunJobByRepoAndID", err)
return nil
}
if job != nil {
if err := job.LoadRun(ctx); err != nil {
ctx.ServerError("LoadRun", err)
return nil
}
if job.Run.ID == runNum {
runByID = job.Run
}
}
}
// Try to resolve the request as a legacy run-index/job-index URL.
{
run, err := actions_model.GetRunByRepoAndIndex(ctx, ctx.Repo.Repository.ID, runNum)
if err != nil && !errors.Is(err, util.ErrNotExist) {
ctx.ServerError("GetRunByRepoAndIndex", err)
return nil
}
if run != nil {
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
if err != nil {
ctx.ServerError("GetRunJobsByRunID", err)
return nil
}
if jobNum < int64(len(jobs)) {
runByIndex = run
targetJobByIndex = jobs[jobNum]
}
}
}
if runByID == nil && runByIndex == nil {
ctx.NotFound(nil)
return nil
}
if runByID != nil && runByIndex == nil {
return runByID
}
if runByID == nil && runByIndex != nil {
ctx.Redirect(fmt.Sprintf("%s/actions/runs/%d/jobs/%d", ctx.Repo.RepoLink, runByIndex.ID, targetJobByIndex.ID), http.StatusFound)
return nil
}
// Reaching this point means both ID-based and legacy index-based interpretations are valid. Prefer the ID-based interpretation by default.
// Use `by_index=1` query parameter to access the legacy index-based interpretation when necessary.
return runByID
}
2023-01-31 09:45:19 +08:00
func View(ctx *context_module.Context) {
ctx.Data["PageIsActions"] = true
run := resolveCurrentRunForView(ctx)
if ctx.Written() {
2023-01-31 09:45:19 +08:00
return
}
2026-03-22 02:04:39 +01:00
ctx.Data["RunID"] = run.ID
ctx.Data["JobID"] = ctx.PathParamInt64("job") // it can be 0 when no job (e.g.: run summary view)
ctx.Data["ActionsURL"] = ctx.Repo.RepoLink + "/actions"
2023-01-31 09:45:19 +08:00
ctx.HTML(http.StatusOK, tplViewActions)
}
func ViewWorkflowFile(ctx *context_module.Context) {
2026-03-22 02:04:39 +01:00
run := getCurrentRunByPathParam(ctx)
if ctx.Written() {
return
}
2026-03-22 02:04:39 +01:00
commit, err := ctx.Repo.GitRepo.GetCommit(run.CommitSHA)
if err != nil {
ctx.NotFoundOrServerError("GetCommit", func(err error) bool {
return errors.Is(err, util.ErrNotExist)
}, err)
return
}
rpath, entries, err := actions.ListWorkflows(commit)
if err != nil {
ctx.ServerError("ListWorkflows", err)
return
}
for _, entry := range entries {
if entry.Name() == run.WorkflowID {
ctx.Redirect(fmt.Sprintf("%s/src/commit/%s/%s/%s", ctx.Repo.RepoLink, url.PathEscape(run.CommitSHA), util.PathEscapeSegments(rpath), util.PathEscapeSegments(run.WorkflowID)))
return
}
}
ctx.NotFound(nil)
}
type LogCursor struct {
Step int `json:"step"`
Cursor int64 `json:"cursor"`
Expanded bool `json:"expanded"`
}
2023-01-31 09:45:19 +08:00
type ViewRequest struct {
LogCursors []LogCursor `json:"logCursors"`
}
type ArtifactsViewItem struct {
Name string `json:"name"`
Size int64 `json:"size"`
Status string `json:"status"`
2023-01-31 09:45:19 +08:00
}
type ViewResponse struct {
Artifacts []*ArtifactsViewItem `json:"artifacts"`
2023-01-31 09:45:19 +08:00
State struct {
Run struct {
2026-03-28 10:41:34 +01:00
RepoID int64 `json:"repoId"`
Link string `json:"link"`
Title string `json:"title"`
TitleHTML template.HTML `json:"titleHTML"`
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"`
CanRerunFailed bool `json:"canRerunFailed"`
CanDeleteArtifact bool `json:"canDeleteArtifact"`
Done bool `json:"done"`
WorkflowID string `json:"workflowID"`
WorkflowLink string `json:"workflowLink"`
IsSchedule bool `json:"isSchedule"`
Jobs []*ViewJob `json:"jobs"`
Commit ViewCommit `json:"commit"`
2026-03-22 02:04:39 +01:00
// Summary view: run duration and trigger time/event
Duration string `json:"duration"`
TriggeredAt int64 `json:"triggeredAt"` // unix seconds for relative time
TriggerEvent string `json:"triggerEvent"` // e.g. pull_request, push, schedule
2023-01-31 09:45:19 +08:00
} `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"`
JobID string `json:"jobId,omitempty"`
Name string `json:"name"`
Status string `json:"status"`
CanRerun bool `json:"canRerun"`
Duration string `json:"duration"`
Needs []string `json:"needs,omitempty"`
2023-01-31 09:45:19 +08:00
}
2023-03-27 19:34:09 +09:00
type ViewCommit struct {
2024-04-26 11:22:45 +09:00
ShortSha string `json:"shortSHA"`
Link string `json:"link"`
Pusher ViewUser `json:"pusher"`
Branch ViewBranch `json:"branch"`
2023-03-27 19:34:09 +09:00
}
type ViewUser struct {
DisplayName string `json:"displayName"`
Link string `json:"link"`
}
type ViewBranch struct {
Name string `json:"name"`
Link string `json:"link"`
IsDeleted bool `json:"isDeleted"`
2023-03-27 19:34:09 +09:00
}
2023-01-31 09:45:19 +08:00
type ViewJobStep struct {
Summary string `json:"summary"`
Duration string `json:"duration"`
Status string `json:"status"`
}
type ViewStepLog struct {
2023-05-29 16:18:36 +08:00
Step int `json:"step"`
Cursor int64 `json:"cursor"`
Lines []*ViewStepLogLine `json:"lines"`
Started int64 `json:"started"`
2023-01-31 09:45:19 +08:00
}
type ViewStepLogLine struct {
Index int64 `json:"index"`
Message string `json:"message"`
Timestamp float64 `json:"timestamp"`
}
func getActionsViewArtifacts(ctx context.Context, repoID, runID int64) (artifactsViewItems []*ArtifactsViewItem, err error) {
2026-03-22 02:04:39 +01:00
artifacts, err := actions_model.ListUploadedArtifactsMeta(ctx, repoID, runID)
if err != nil {
return nil, err
}
for _, art := range artifacts {
artifactsViewItems = append(artifactsViewItems, &ArtifactsViewItem{
Name: art.ArtifactName,
Size: art.FileSize,
Status: util.Iif(art.Status == actions_model.ArtifactStatusExpired, "expired", "completed"),
})
}
return artifactsViewItems, nil
}
2023-01-31 09:45:19 +08:00
func ViewPost(ctx *context_module.Context) {
2026-03-22 02:04:39 +01:00
run, jobs := getCurrentRunJobsByPathParam(ctx)
2023-01-31 09:45:19 +08:00
if ctx.Written() {
return
}
2023-03-27 19:34:09 +09:00
if err := run.LoadAttributes(ctx); err != nil {
ctx.ServerError("run.LoadAttributes", err)
2023-03-27 19:34:09 +09:00
return
}
2023-01-31 09:45:19 +08:00
resp := &ViewResponse{}
2026-03-22 02:04:39 +01:00
fillViewRunResponseSummary(ctx, resp, run, jobs)
if ctx.Written() {
return
}
fillViewRunResponseCurrentJob(ctx, resp, run, jobs)
if ctx.Written() {
return
}
ctx.JSON(http.StatusOK, resp)
}
func fillViewRunResponseSummary(ctx *context_module.Context, resp *ViewResponse, run *actions_model.ActionRun, jobs []*actions_model.ActionRunJob) {
var err error
resp.Artifacts, err = getActionsViewArtifacts(ctx, ctx.Repo.Repository.ID, run.ID)
if err != nil {
2026-03-22 02:04:39 +01:00
ctx.ServerError("getActionsViewArtifacts", err)
return
}
2023-01-31 09:45:19 +08:00
2026-03-28 10:41:34 +01:00
resp.State.Run.RepoID = ctx.Repo.Repository.ID
2024-12-17 09:15:18 +08:00
// the title for the "run" is from the commit message
2023-01-31 09:45:19 +08:00
resp.State.Run.Title = run.Title
resp.State.Run.TitleHTML = templates.NewRenderUtils(ctx).RenderCommitMessage(run.Title, ctx.Repo.Repository)
2023-02-01 06:46:10 +08:00
resp.State.Run.Link = run.Link()
2023-01-31 09:45:19 +08:00
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)
2024-02-18 18:33:50 +08:00
resp.State.Run.CanDeleteArtifact = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
if resp.State.Run.CanRerun {
for _, job := range jobs {
if job.Status == actions_model.StatusFailure || job.Status == actions_model.StatusCancelled {
resp.State.Run.CanRerunFailed = true
break
}
}
}
2023-01-31 09:45:19 +08:00
resp.State.Run.Done = run.Status.IsDone()
2024-04-26 11:22:45 +09:00
resp.State.Run.WorkflowID = run.WorkflowID
resp.State.Run.WorkflowLink = run.WorkflowLink()
resp.State.Run.IsSchedule = run.IsSchedule()
2023-01-31 09:45:19 +08:00
resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead fo 'null' in json
2023-03-04 14:41:37 +09:00
resp.State.Run.Status = run.Status.String()
2023-01-31 09:45:19 +08:00
for _, v := range jobs {
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &ViewJob{
ID: v.ID,
JobID: v.JobID,
2023-01-31 09:45:19 +08:00
Name: v.Name,
Status: v.Status.String(),
2025-07-07 01:47:02 +08:00
CanRerun: resp.State.Run.CanRerun,
2023-04-08 07:20:50 +09:00
Duration: v.Duration().String(),
Needs: v.Needs,
2023-01-31 09:45:19 +08:00
})
}
2023-03-27 19:34:09 +09:00
pusher := ViewUser{
DisplayName: run.TriggerUser.GetDisplayName(),
Link: run.TriggerUser.HomeLink(),
}
branch := ViewBranch{
Name: run.PrettyRef(),
Link: run.RefLink(),
}
refName := git.RefName(run.Ref)
if refName.IsBranch() {
b, err := git_model.GetBranch(ctx, ctx.Repo.Repository.ID, refName.ShortName())
if err != nil && !git_model.IsErrBranchNotExist(err) {
log.Error("GetBranch: %v", err)
} else if git_model.IsErrBranchNotExist(err) || (b != nil && b.IsDeleted) {
branch.IsDeleted = true
}
}
2023-03-27 19:34:09 +09:00
resp.State.Run.Commit = ViewCommit{
2024-04-26 11:22:45 +09:00
ShortSha: base.ShortSha(run.CommitSHA),
Link: fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA),
Pusher: pusher,
Branch: branch,
2023-03-27 19:34:09 +09:00
}
2026-03-22 02:04:39 +01:00
resp.State.Run.Duration = run.Duration().String()
resp.State.Run.TriggeredAt = run.Created.AsTime().Unix()
resp.State.Run.TriggerEvent = run.TriggerEvent
}
func fillViewRunResponseCurrentJob(ctx *context_module.Context, resp *ViewResponse, run *actions_model.ActionRun, jobs []*actions_model.ActionRunJob) {
req := web.GetForm(ctx).(*ViewRequest)
current, hasPathParam := findCurrentJobByPathParam(ctx, jobs)
if current == nil {
if hasPathParam {
ctx.NotFound(nil)
}
return
}
2023-03-27 19:34:09 +09:00
2023-01-31 09:45:19 +08:00
var task *actions_model.ActionTask
if current.TaskID > 0 {
var err error
task, err = actions_model.GetTaskByID(ctx, current.TaskID)
if err != nil {
ctx.ServerError("actions_model.GetTaskByID", err)
2023-01-31 09:45:19 +08:00
return
}
task.Job = current
if err := task.LoadAttributes(ctx); err != nil {
ctx.ServerError("task.LoadAttributes", err)
2023-01-31 09:45:19 +08:00
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.TrString("actions.need_approval_desc")
}
2023-01-31 09:45:19 +08:00
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, logs, err := convertToViewModel(ctx, ctx.Locale, req.LogCursors, task)
if err != nil {
2025-06-20 14:14:00 +02:00
ctx.ServerError("convertToViewModel", err)
return
2023-01-31 09:45:19 +08:00
}
resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, steps...)
resp.Logs.StepsLog = append(resp.Logs.StepsLog, logs...)
}
}
func convertToViewModel(ctx context.Context, locale translation.Locale, cursors []LogCursor, task *actions_model.ActionTask) ([]*ViewJobStep, []*ViewStepLog, error) {
var viewJobs []*ViewJobStep
var logs []*ViewStepLog
steps := actions.FullSteps(task)
for _, v := range steps {
viewJobs = append(viewJobs, &ViewJobStep{
Summary: v.Name,
Duration: v.Duration().String(),
Status: v.Status.String(),
})
}
for _, cursor := range cursors {
if !cursor.Expanded {
continue
}
2023-01-31 09:45:19 +08:00
step := steps[cursor.Step]
// if task log is expired, return a consistent log line
if task.LogExpired {
if cursor.Cursor == 0 {
logs = append(logs, &ViewStepLog{
Step: cursor.Step,
Cursor: 1,
Lines: []*ViewStepLogLine{
{
Index: 1,
Message: locale.TrString("actions.runs.expire_log_message"),
// Timestamp doesn't mean anything when the log is expired.
// Set it to the task's updated time since it's probably the time when the log has expired.
Timestamp: float64(task.Updated.AsTime().UnixNano()) / float64(time.Second),
2024-08-02 08:42:08 +08:00
},
},
Started: int64(step.Started),
})
2024-08-02 08:42:08 +08:00
}
continue
}
2024-08-02 08:42:08 +08:00
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]
logRows, err := actions.ReadLogs(ctx, task.LogInStorage, task.LogFilename, offset, length)
if err != nil {
return nil, nil, fmt.Errorf("actions.ReadLogs: %w", err)
2023-01-31 09:45:19 +08:00
}
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),
})
}
2023-01-31 09:45:19 +08:00
}
logs = append(logs, &ViewStepLog{
Step: cursor.Step,
Cursor: cursor.Cursor + int64(len(logLines)),
Lines: logLines,
Started: int64(step.Started),
})
2023-01-31 09:45:19 +08:00
}
return viewJobs, logs, nil
2023-01-31 09:45:19 +08:00
}
// checkRunRerunAllowed checks whether a rerun is permitted for the given run,
// writing the appropriate JSON error to ctx and returning false when it is not.
func checkRunRerunAllowed(ctx *context_module.Context, run *actions_model.ActionRun) bool {
if !run.Status.IsDone() {
ctx.JSONError(ctx.Locale.Tr("actions.runs.not_done"))
return false
}
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 false
}
return true
}
// Rerun will rerun jobs in the given run
// If jobIDStr is a blank string, it means rerun all jobs
func Rerun(ctx *context_module.Context) {
2026-03-22 02:04:39 +01:00
run, jobs := getCurrentRunJobsByPathParam(ctx)
if ctx.Written() {
2023-01-31 09:45:19 +08:00
return
}
if !checkRunRerunAllowed(ctx, run) {
2025-10-29 19:08:59 -06:00
return
}
2026-03-22 02:04:39 +01:00
currentJob, hasPathParam := findCurrentJobByPathParam(ctx, jobs)
if hasPathParam && currentJob == nil {
ctx.NotFound(nil)
return
}
var jobsToRerun []*actions_model.ActionRunJob
2026-03-22 02:04:39 +01:00
if currentJob != nil {
jobsToRerun = actions_service.GetAllRerunJobs(currentJob, jobs)
} else {
jobsToRerun = jobs
}
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobsToRerun); err != nil {
ctx.ServerError("RerunWorkflowRunJobs", err)
return
}
ctx.JSONOK()
}
// RerunFailed reruns all failed jobs in the given run
func RerunFailed(ctx *context_module.Context) {
2026-03-22 02:04:39 +01:00
run, jobs := getCurrentRunJobsByPathParam(ctx)
if ctx.Written() {
return
}
if !checkRunRerunAllowed(ctx, run) {
return
2025-10-29 19:08:59 -06:00
}
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, actions_service.GetFailedRerunJobs(jobs)); err != nil {
ctx.ServerError("RerunWorkflowRunJobs", err)
2025-10-29 19:08:59 -06:00
return
}
2025-07-07 01:47:02 +08:00
ctx.JSONOK()
}
2023-06-29 05:58:56 +03:00
func Logs(ctx *context_module.Context) {
2026-03-22 02:04:39 +01:00
run := getCurrentRunByPathParam(ctx)
if ctx.Written() {
2023-06-29 05:58:56 +03:00
return
}
2026-03-22 02:04:39 +01:00
jobID := ctx.PathParamInt64("job")
2023-06-29 05:58:56 +03:00
2026-03-22 02:04:39 +01:00
if err := common.DownloadActionsRunJobLogsWithID(ctx.Base, ctx.Repo.Repository, run.ID, jobID); err != nil {
ctx.NotFoundOrServerError("DownloadActionsRunJobLogsWithID", func(err error) bool {
2025-03-26 11:30:52 -07:00
return errors.Is(err, util.ErrNotExist)
}, err)
2023-06-29 05:58:56 +03:00
}
}
2023-01-31 09:45:19 +08:00
func Cancel(ctx *context_module.Context) {
2026-03-22 02:04:39 +01:00
run, jobs := getCurrentRunJobsByPathParam(ctx)
2023-01-31 09:45:19 +08:00
if ctx.Written() {
return
}
2025-10-10 12:58:55 -06:00
var updatedJobs []*actions_model.ActionRunJob
2025-03-11 18:40:38 +01:00
2023-01-31 09:45:19 +08:00
if err := db.WithTx(ctx, func(ctx context.Context) error {
2025-10-10 12:58:55 -06:00
cancelledJobs, err := actions_model.CancelJobs(ctx, jobs)
if err != nil {
return fmt.Errorf("cancel jobs: %w", err)
2023-01-31 09:45:19 +08:00
}
2025-10-10 12:58:55 -06:00
updatedJobs = append(updatedJobs, cancelledJobs...)
2023-01-31 09:45:19 +08:00
return nil
}); err != nil {
2025-06-20 14:14:00 +02:00
ctx.ServerError("StopTask", err)
2023-01-31 09:45:19 +08:00
return
}
actions_service.CreateCommitStatusForRunJobs(ctx, run, jobs...)
2025-10-10 12:58:55 -06:00
actions_service.EmitJobsIfReadyByJobs(updatedJobs)
2025-10-10 12:58:55 -06:00
for _, job := range updatedJobs {
2025-03-11 18:40:38 +01:00
_ = job.LoadAttributes(ctx)
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
}
2025-10-10 12:58:55 -06:00
if len(updatedJobs) > 0 {
job := updatedJobs[0]
2025-06-20 14:14:00 +02:00
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
}
2025-08-25 00:30:56 +08:00
ctx.JSONOK()
2023-01-31 09:45:19 +08:00
}
func Approve(ctx *context_module.Context) {
2026-03-22 02:04:39 +01:00
run := getCurrentRunByPathParam(ctx)
if ctx.Written() {
return
}
approveRuns(ctx, []int64{run.ID})
if ctx.Written() {
return
}
ctx.JSONOK()
}
func approveRuns(ctx *context_module.Context, runIDs []int64) {
doer := ctx.Doer
repo := ctx.Repo.Repository
updatedJobs := make([]*actions_model.ActionRunJob, 0)
runMap := make(map[int64]*actions_model.ActionRun, len(runIDs))
runJobs := make(map[int64][]*actions_model.ActionRunJob, len(runIDs))
2025-03-11 18:40:38 +01:00
2025-10-10 12:58:55 -06:00
err := db.WithTx(ctx, func(ctx context.Context) (err error) {
for _, runID := range runIDs {
run, err := actions_model.GetRunByRepoAndID(ctx, repo.ID, runID)
2025-10-10 12:58:55 -06:00
if err != nil {
return err
}
runMap[run.ID] = run
run.Repo = repo
run.NeedApproval = false
run.ApprovedBy = doer.ID
if err := actions_model.UpdateRun(ctx, run, "need_approval", "approved_by"); err != nil {
return err
}
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
if err != nil {
return err
}
runJobs[run.ID] = jobs
for _, job := range jobs {
job.Status, err = actions_service.PrepareToStartJobWithConcurrency(ctx, job)
if err != nil {
return err
}
if job.Status == actions_model.StatusWaiting {
n, err := actions_model.UpdateRunJob(ctx, job, nil, "status")
if err != nil {
return err
}
if n > 0 {
updatedJobs = append(updatedJobs, job)
}
2025-03-11 18:40:38 +01:00
}
}
}
return nil
2025-10-10 12:58:55 -06:00
})
if err != nil {
ctx.NotFoundOrServerError("approveRuns", func(err error) bool {
return errors.Is(err, util.ErrNotExist)
}, err)
return
}
for runID, run := range runMap {
actions_service.CreateCommitStatusForRunJobs(ctx, run, runJobs[runID]...)
}
2025-10-10 12:58:55 -06:00
if len(updatedJobs) > 0 {
job := updatedJobs[0]
2025-06-20 14:14:00 +02:00
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
}
2025-10-10 12:58:55 -06:00
for _, job := range updatedJobs {
2025-03-11 18:40:38 +01:00
_ = job.LoadAttributes(ctx)
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
}
}
2025-05-14 03:18:13 +08:00
func Delete(ctx *context_module.Context) {
2026-03-22 02:04:39 +01:00
run := getCurrentRunByPathParam(ctx)
if ctx.Written() {
2025-05-14 03:18:13 +08:00
return
}
if !run.Status.IsDone() {
ctx.JSONError(ctx.Tr("actions.runs.not_done"))
return
}
if err := actions_service.DeleteRun(ctx, run); err != nil {
ctx.ServerError("DeleteRun", err)
return
}
ctx.JSONOK()
}
2026-03-22 02:04:39 +01:00
// getRunJobs loads the run and its jobs for runID
// Any error will be written to the ctx, empty jobs will also result in 404 error, then the return values are all nil.
func getCurrentRunJobsByPathParam(ctx *context_module.Context) (*actions_model.ActionRun, []*actions_model.ActionRunJob) {
run := getCurrentRunByPathParam(ctx)
if ctx.Written() {
return nil, nil
2023-01-31 09:45:19 +08:00
}
run.Repo = ctx.Repo.Repository
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
if err != nil {
2025-05-15 05:40:10 +08:00
ctx.ServerError("GetRunJobsByRunID", err)
2026-03-22 02:04:39 +01:00
return nil, nil
2023-01-31 09:45:19 +08:00
}
if len(jobs) == 0 {
2025-05-15 05:40:10 +08:00
ctx.NotFound(nil)
2026-03-22 02:04:39 +01:00
return nil, nil
2023-01-31 09:45:19 +08:00
}
for _, job := range jobs {
job.Run = run
2023-01-31 09:45:19 +08:00
}
2026-03-22 02:04:39 +01:00
return run, jobs
2023-01-31 09:45:19 +08:00
}
2023-05-19 21:37:57 +08:00
2024-02-18 18:33:50 +08:00
func ArtifactsDeleteView(ctx *context_module.Context) {
2026-03-22 02:04:39 +01:00
run := getCurrentRunByPathParam(ctx)
if ctx.Written() {
2024-02-18 18:33:50 +08:00
return
}
2026-03-22 02:04:39 +01:00
artifactName := ctx.PathParam("artifact_name")
if err := actions_model.SetArtifactNeedDelete(ctx, run.ID, artifactName); err != nil {
2025-06-20 14:14:00 +02:00
ctx.ServerError("SetArtifactNeedDelete", err)
2024-02-18 18:33:50 +08:00
return
}
ctx.JSON(http.StatusOK, struct{}{})
}
2023-05-19 21:37:57 +08:00
func ArtifactsDownloadView(ctx *context_module.Context) {
2026-03-22 02:04:39 +01:00
run := getCurrentRunByPathParam(ctx)
if ctx.Written() {
2023-05-19 21:37:57 +08:00
return
}
2026-03-22 02:04:39 +01:00
artifactName := ctx.PathParam("artifact_name")
artifacts, err := db.Find[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{
RunID: run.ID,
ArtifactName: artifactName,
})
2023-05-19 21:37:57 +08:00
if err != nil {
2025-06-20 14:14:00 +02:00
ctx.ServerError("FindArtifacts", err)
2023-05-19 21:37:57 +08:00
return
}
if len(artifacts) == 0 {
2025-02-17 14:13:17 +08:00
ctx.HTTPError(http.StatusNotFound, "artifact not found")
return
}
2023-05-19 21:37:57 +08:00
2024-02-18 18:33:50 +08:00
// if artifacts status is not uploaded-confirmed, treat it as not found
for _, art := range artifacts {
if art.Status != actions_model.ArtifactStatusUploadConfirmed {
2025-02-17 14:13:17 +08:00
ctx.HTTPError(http.StatusNotFound, "artifact not found")
2024-02-18 18:33:50 +08:00
return
}
}
// A v4 Artifact may only contain a single file
// Multiple files are uploaded as a single file archive
// All other cases fall back to the legacy v1v3 zip handling below
if len(artifacts) == 1 && actions.IsArtifactV4(artifacts[0]) {
err := actions.DownloadArtifactV4(ctx.Base, artifacts[0])
2024-03-02 10:12:17 +01:00
if err != nil {
2025-06-20 14:14:00 +02:00
ctx.ServerError("DownloadArtifactV4", err)
2024-03-02 10:12:17 +01:00
return
}
return
}
// Artifacts using the v1-v3 backend are stored as multiple individual files per artifact on the backend
// Those need to be zipped for download
ctx.Resp.Header().Set("Content-Disposition", httplib.EncodeContentDispositionAttachment(artifactName+".zip"))
zipWriter := zip.NewWriter(ctx.Resp)
defer zipWriter.Close()
writeArtifactToZip := func(art *actions_model.ActionArtifact) error {
f, err := storage.ActionsArtifacts.Open(art.StoragePath)
if err != nil {
return fmt.Errorf("ActionsArtifacts.Open: %w", err)
}
defer f.Close()
var r io.ReadCloser = f
if art.ContentEncodingOrType == actions_model.ContentEncodingV3Gzip {
r, err = gzip.NewReader(f)
if err != nil {
return fmt.Errorf("gzip.NewReader: %w", err)
}
}
defer r.Close()
w, err := zipWriter.Create(art.ArtifactPath)
if err != nil {
return fmt.Errorf("zipWriter.Create: %w", err)
}
_, err = io.Copy(w, r)
if err != nil {
return fmt.Errorf("io.Copy: %w", err)
}
return nil
}
for _, art := range artifacts {
err := writeArtifactToZip(art)
if err != nil {
ctx.ServerError("writeArtifactToZip", err)
return
}
}
2023-05-19 21:37:57 +08:00
}
2023-08-14 23:14:30 +08:00
func ApproveAllChecks(ctx *context_module.Context) {
repo := ctx.Repo.Repository
commitID := ctx.FormString("commit_id")
commitStatuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, commitID, db.ListOptionsAll)
if err != nil {
ctx.ServerError("GetLatestCommitStatus", err)
return
}
runs, err := actions_service.GetRunsFromCommitStatuses(ctx, commitStatuses)
if err != nil {
ctx.ServerError("GetRunsFromCommitStatuses", err)
return
}
runIDs := make([]int64, 0, len(runs))
for _, run := range runs {
if run.NeedApproval {
runIDs = append(runIDs, run.ID)
}
}
if len(runIDs) == 0 {
ctx.JSONOK()
return
}
approveRuns(ctx, runIDs)
if ctx.Written() {
return
}
ctx.Flash.Success(ctx.Tr("actions.approve_all_success"))
ctx.JSONOK()
}
2023-08-14 23:14:30 +08:00
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.JSONError("workflow is required")
2023-08-14 23:14:30 +08:00
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.UpdateRepoUnitConfig(ctx, cfgUnit); err != nil {
2023-08-14 23:14:30 +08:00
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)
}
func Run(ctx *context_module.Context) {
redirectURL := fmt.Sprintf("%s/actions?workflow=%s&actor=%s&status=%s", ctx.Repo.RepoLink, url.QueryEscape(ctx.FormString("workflow")),
url.QueryEscape(ctx.FormString("actor")), url.QueryEscape(ctx.FormString("status")))
workflowID := ctx.FormString("workflow")
if len(workflowID) == 0 {
ctx.ServerError("workflow", nil)
return
}
ref := ctx.FormString("ref")
if len(ref) == 0 {
ctx.ServerError("ref", nil)
return
}
_, err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, ref, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error {
for name, config := range workflowDispatch.Inputs {
value := ctx.Req.PostFormValue(name)
if config.Type == "boolean" {
inputs[name] = strconv.FormatBool(ctx.FormBool(name))
} else if value != "" {
inputs[name] = value
} else {
inputs[name] = config.Default
}
}
return nil
})
if err != nil {
if errTr := util.ErrorAsTranslatable(err); errTr != nil {
ctx.Flash.Error(errTr.Translate(ctx.Locale))
ctx.Redirect(redirectURL)
} else {
ctx.ServerError("DispatchActionWorkflow", err)
}
return
}
ctx.Flash.Success(ctx.Tr("actions.workflow.run_success", workflowID))
ctx.Redirect(redirectURL)
}