2023-01-31 09:45:19 +08:00
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
2023-07-21 10:42:01 +08:00
"archive/zip"
"compress/gzip"
2023-01-31 09:45:19 +08:00
"context"
"errors"
"fmt"
2024-12-08 06:23:09 +03:00
"html/template"
2023-07-21 10:42:01 +08:00
"io"
2023-01-31 09:45:19 +08:00
"net/http"
2023-07-21 10:42:01 +08:00
"net/url"
2024-02-27 15:40:21 +08:00
"strconv"
2023-01-31 09:45:19 +08:00
"time"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
2024-12-12 11:28:23 -08:00
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"
2024-08-19 10:38:40 +08:00
"code.gitea.io/gitea/modules/git"
2026-03-25 17:37:48 +01:00
"code.gitea.io/gitea/modules/httplib"
2024-08-19 10:38:40 +08:00
"code.gitea.io/gitea/modules/log"
2023-05-19 21:37:57 +08:00
"code.gitea.io/gitea/modules/storage"
2024-12-08 06:23:09 +03:00
"code.gitea.io/gitea/modules/templates"
2026-02-17 23:28:55 +01:00
"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"
2024-02-27 15:12:22 +08:00
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
2024-08-19 10:38:40 +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
2026-03-10 15:14:48 -06:00
// if run param is "latest", get the latest run id
2024-08-10 08:40:41 +08:00
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 )
2024-08-10 08:40:41 +08:00
}
2026-03-22 02:04:39 +01:00
return run
2024-08-10 08:40:41 +08:00
}
2026-04-02 18:23:29 -06:00
// 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
2026-05-17 00:42:20 -06:00
if ! byIndex {
2026-04-02 18:23:29 -06:00
// 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
2026-04-02 18:23:29 -06:00
run := resolveCurrentRunForView ( ctx )
2026-03-10 15:14:48 -06:00
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)
2026-03-10 15:14:48 -06:00
ctx . Data [ "ActionsURL" ] = ctx . Repo . RepoLink + "/actions"
2023-01-31 09:45:19 +08:00
ctx . HTML ( http . StatusOK , tplViewActions )
}
2025-05-28 20:30:00 +08:00
func ViewWorkflowFile ( ctx * context_module . Context ) {
2026-03-22 02:04:39 +01:00
run := getCurrentRunByPathParam ( ctx )
if ctx . Written ( ) {
2025-05-28 20:30:00 +08:00
return
}
2026-03-22 02:04:39 +01:00
2025-05-28 20:30:00 +08: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 )
}
2024-12-06 12:04:16 +08:00
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 {
2024-12-06 12:04:16 +08:00
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 {
2024-12-06 12:04:16 +08:00
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" `
2024-12-08 06:23:09 +03:00
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" `
2026-03-21 22:27:13 +01:00
CanRerunFailed bool ` json:"canRerunFailed" `
2024-12-08 06:23:09 +03:00
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 {
2026-02-23 16:11:33 +03:00
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 {
2024-12-12 11:28:23 -08:00
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" `
}
2026-03-10 15:14:48 -06:00
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 )
2024-12-06 12:04:16 +08:00
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 {
2024-12-06 12:04:16 +08:00
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 )
2024-12-06 12:04:16 +08:00
if err != nil {
2026-03-22 02:04:39 +01:00
ctx . ServerError ( "getActionsViewArtifacts" , err )
return
2024-12-06 12:04:16 +08:00
}
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
2025-05-09 20:42:35 +08:00
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 )
2023-02-24 15:58:49 +08:00
resp . State . Run . CanApprove = run . NeedApproval && ctx . Repo . CanWrite ( unit . TypeActions )
2023-05-01 23:14:20 +09:00
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 )
2026-03-21 22:27:13 +01:00
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 ,
2026-02-23 16:11:33 +03:00
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 ( ) ,
2026-02-23 16:11:33 +03:00
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 ( ) ,
}
2024-12-12 11:28:23 -08:00
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 {
2024-12-06 12:04:16 +08:00
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 {
2024-12-06 12:04:16 +08:00
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 )
2023-02-24 15:58:49 +08:00
if run . NeedApproval {
2024-02-15 05:48:45 +08:00
resp . State . CurrentJob . Detail = ctx . Locale . TrString ( "actions.need_approval_desc" )
2023-02-24 15:58:49 +08:00
}
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 {
2026-02-17 23:28:55 +01:00
steps , logs , err := convertToViewModel ( ctx , ctx . Locale , req . LogCursors , task )
2025-02-02 04:39:01 +01:00
if err != nil {
2025-06-20 14:14:00 +02:00
ctx . ServerError ( "convertToViewModel" , err )
2025-02-02 04:39:01 +01:00
return
2023-01-31 09:45:19 +08:00
}
2025-02-02 04:39:01 +01:00
resp . State . CurrentJob . Steps = append ( resp . State . CurrentJob . Steps , steps ... )
resp . Logs . StepsLog = append ( resp . Logs . StepsLog , logs ... )
}
}
2026-02-17 23:28:55 +01:00
func convertToViewModel ( ctx context . Context , locale translation . Locale , cursors [ ] LogCursor , task * actions_model . ActionTask ) ( [ ] * ViewJobStep , [ ] * ViewStepLog , error ) {
2025-02-02 04:39:01 +01:00
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
2025-02-02 04:39:01 +01: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 ,
2026-02-17 23:28:55 +01:00
Message : locale . TrString ( "actions.runs.expire_log_message" ) ,
2025-02-02 04:39:01 +01:00
// 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
} ,
2025-02-02 04:39:01 +01:00
} ,
Started : int64 ( step . Started ) ,
} )
2024-08-02 08:42:08 +08:00
}
2025-02-02 04:39:01 +01:00
continue
}
2024-08-02 08:42:08 +08:00
2025-02-02 04:39:01 +01: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
}
2025-02-02 04:39:01 +01: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
}
2025-02-02 04:39:01 +01: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
}
2025-02-02 04:39:01 +01:00
return viewJobs , logs , nil
2023-01-31 09:45:19 +08:00
}
2026-03-21 22:27:13 +01: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
}
2023-08-22 11:30:02 +09:00
// Rerun will rerun jobs in the given run
2026-03-10 15:14:48 -06:00
// If jobIDStr is a blank string, it means rerun all jobs
2023-08-22 11:30:02 +09:00
func Rerun ( ctx * context_module . Context ) {
2026-03-22 02:04:39 +01:00
run , jobs := getCurrentRunJobsByPathParam ( ctx )
2026-03-10 15:14:48 -06:00
if ctx . Written ( ) {
2023-01-31 09:45:19 +08:00
return
}
2026-03-21 22:27:13 +01:00
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
}
2026-03-21 22:27:13 +01:00
var jobsToRerun [ ] * actions_model . ActionRunJob
2026-03-22 02:04:39 +01:00
if currentJob != nil {
2026-03-21 22:27:13 +01:00
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 )
2023-05-01 23:14:20 +09:00
return
}
2026-03-21 22:27:13 +01:00
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 )
2026-03-21 22:27:13 +01:00
if ctx . Written ( ) {
return
}
if ! checkRunRerunAllowed ( ctx , run ) {
return
2025-10-29 19:08:59 -06:00
}
2026-03-21 22:27:13 +01:00
if err := actions_service . RerunWorkflowRunJobs ( ctx , ctx . Repo . Repository , run , actions_service . GetFailedRerunJobs ( jobs ) ) ; err != nil {
2026-03-02 22:34:06 +01:00
ctx . ServerError ( "RerunWorkflowRunJobs" , err )
2025-10-29 19:08:59 -06:00
return
2024-01-19 23:05:49 +09:00
}
2025-07-07 01:47:02 +08:00
ctx . JSONOK ( )
2023-05-01 23:14:20 +09:00
}
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 {
2026-03-10 15:14:48 -06:00
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
}
2026-03-10 15:14:48 -06:00
actions_service . CreateCommitStatusForRunJobs ( ctx , run , jobs ... )
2025-10-10 12:58:55 -06:00
actions_service . EmitJobsIfReadyByJobs ( updatedJobs )
2023-03-04 15:12:37 +08:00
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
}
2023-02-24 15:58:49 +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 } )
2023-02-24 15:58:49 +08:00
if ctx . Written ( ) {
return
}
2025-10-20 04:46:37 -06:00
ctx . JSONOK ( )
}
2026-03-10 15:14:48 -06:00
func approveRuns ( ctx * context_module . Context , runIDs [ ] int64 ) {
2023-02-24 15:58:49 +08:00
doer := ctx . Doer
2025-10-20 04:46:37 -06:00
repo := ctx . Repo . Repository
2023-02-24 15:58:49 +08:00
2025-10-20 04:46:37 -06:00
updatedJobs := make ( [ ] * actions_model . ActionRunJob , 0 )
2026-03-10 15:14:48 -06:00
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 ) {
2026-03-10 15:14:48 -06:00
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
}
2025-10-20 04:46:37 -06:00
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 )
2023-02-24 15:58:49 +08:00
if err != nil {
return err
}
2025-10-20 04:46:37 -06:00
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
}
2023-02-24 15:58:49 +08:00
}
}
return nil
2025-10-10 12:58:55 -06:00
} )
if err != nil {
2026-03-10 15:14:48 -06:00
ctx . NotFoundOrServerError ( "approveRuns" , func ( err error ) bool {
return errors . Is ( err , util . ErrNotExist )
} , err )
2023-02-24 15:58:49 +08:00
return
}
2025-10-20 04:46:37 -06:00
for runID , run := range runMap {
actions_service . CreateCommitStatusForRunJobs ( ctx , run , runJobs [ runID ] ... )
}
2023-03-29 23:27:37 +08:00
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 )
}
2023-02-24 15:58:49 +08:00
}
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
}
2026-03-10 15:14:48 -06: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" )
2023-11-24 11:49:41 +08:00
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
}
2023-07-21 10:42:01 +08:00
if len ( artifacts ) == 0 {
2025-02-17 14:13:17 +08:00
ctx . HTTPError ( http . StatusNotFound , "artifact not found" )
2023-07-21 10:42:01 +08:00
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 {
2025-02-16 01:32:54 +01:00
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
}
}
2026-03-25 17:37:48 +01:00
// 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 v1– v3 zip handling below
2025-02-16 01:32:54 +01:00
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
2026-03-25 17:37:48 +01:00
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 {
2023-07-21 10:42:01 +08:00
f , err := storage . ActionsArtifacts . Open ( art . StoragePath )
if err != nil {
2026-03-25 17:37:48 +01:00
return fmt . Errorf ( "ActionsArtifacts.Open: %w" , err )
2023-07-21 10:42:01 +08:00
}
2026-03-25 17:37:48 +01:00
defer f . Close ( )
2023-07-21 10:42:01 +08:00
2026-03-25 17:37:48 +01:00
var r io . ReadCloser = f
if art . ContentEncodingOrType == actions_model . ContentEncodingV3Gzip {
2023-07-21 10:42:01 +08:00
r , err = gzip . NewReader ( f )
if err != nil {
2026-03-25 17:37:48 +01:00
return fmt . Errorf ( "gzip.NewReader: %w" , err )
2023-07-21 10:42:01 +08:00
}
}
defer r . Close ( )
2026-03-25 17:37:48 +01:00
w , err := zipWriter . Create ( art . ArtifactPath )
2023-07-21 10:42:01 +08:00
if err != nil {
2026-03-25 17:37:48 +01:00
return fmt . Errorf ( "zipWriter.Create: %w" , err )
}
_ , err = io . Copy ( w , r )
if err != nil {
return fmt . Errorf ( "io.Copy: %w" , err )
2023-07-21 10:42:01 +08:00
}
2026-03-25 17:37:48 +01:00
return nil
}
for _ , art := range artifacts {
err := writeArtifactToZip ( art )
if err != nil {
ctx . ServerError ( "writeArtifactToZip" , err )
2023-07-21 10:42:01 +08:00
return
}
}
2023-05-19 21:37:57 +08:00
}
2023-08-14 23:14:30 +08:00
2025-10-20 04:46:37 -06: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
}
2026-03-10 15:14:48 -06:00
runIDs := make ( [ ] int64 , 0 , len ( runs ) )
2025-10-20 04:46:37 -06:00
for _ , run := range runs {
if run . NeedApproval {
2026-03-10 15:14:48 -06:00
runIDs = append ( runIDs , run . ID )
2025-10-20 04:46:37 -06:00
}
}
2026-03-10 15:14:48 -06:00
if len ( runIDs ) == 0 {
2025-10-20 04:46:37 -06:00
ctx . JSONOK ( )
return
}
2026-03-10 15:14:48 -06:00
approveRuns ( ctx , runIDs )
2025-10-20 04:46:37 -06:00
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 {
2026-05-05 10:49:17 -07:00
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 )
}
2026-03-21 23:39:47 +01:00
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 )
}
2024-08-19 10:38:40 +08:00
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
}
2026-03-01 20:58:16 +01:00
_ , err := actions_service . DispatchActionWorkflow ( ctx , ctx . Doer , ctx . Repo . Repository , ctx . Repo . GitRepo , workflowID , ref , func ( workflowDispatch * model . WorkflowDispatch , inputs map [ string ] any ) error {
2025-02-10 17:44:42 +08:00
for name , config := range workflowDispatch . Inputs {
value := ctx . Req . PostFormValue ( name )
if config . Type == "boolean" {
2025-02-11 03:05:42 +08:00
inputs [ name ] = strconv . FormatBool ( ctx . FormBool ( name ) )
2025-02-10 17:44:42 +08:00
} else if value != "" {
inputs [ name ] = value
} else {
inputs [ name ] = config . Default
}
2024-08-19 10:38:40 +08:00
}
2025-02-11 03:05:42 +08:00
return nil
} )
2025-02-10 17:44:42 +08:00
if err != nil {
2025-10-21 22:06:56 -07:00
if errTr := util . ErrorAsTranslatable ( err ) ; errTr != nil {
ctx . Flash . Error ( errTr . Translate ( ctx . Locale ) )
2025-02-11 03:05:42 +08:00
ctx . Redirect ( redirectURL )
} else {
ctx . ServerError ( "DispatchActionWorkflow" , err )
}
return
2025-02-10 17:44:42 +08:00
}
2024-08-19 10:38:40 +08:00
ctx . Flash . Success ( ctx . Tr ( "actions.workflow.run_success" , workflowID ) )
ctx . Redirect ( redirectURL )
}