code.gitea.io/gitea@v1.21.7/routers/web/repo/actions/view.go (about)

     1  // Copyright 2022 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package actions
     5  
     6  import (
     7  	"archive/zip"
     8  	"compress/gzip"
     9  	"context"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"net/http"
    14  	"net/url"
    15  	"strings"
    16  	"time"
    17  
    18  	actions_model "code.gitea.io/gitea/models/actions"
    19  	"code.gitea.io/gitea/models/db"
    20  	repo_model "code.gitea.io/gitea/models/repo"
    21  	"code.gitea.io/gitea/models/unit"
    22  	"code.gitea.io/gitea/modules/actions"
    23  	"code.gitea.io/gitea/modules/base"
    24  	context_module "code.gitea.io/gitea/modules/context"
    25  	"code.gitea.io/gitea/modules/storage"
    26  	"code.gitea.io/gitea/modules/timeutil"
    27  	"code.gitea.io/gitea/modules/util"
    28  	"code.gitea.io/gitea/modules/web"
    29  	actions_service "code.gitea.io/gitea/services/actions"
    30  
    31  	"xorm.io/builder"
    32  )
    33  
    34  func View(ctx *context_module.Context) {
    35  	ctx.Data["PageIsActions"] = true
    36  	runIndex := ctx.ParamsInt64("run")
    37  	jobIndex := ctx.ParamsInt64("job")
    38  	ctx.Data["RunIndex"] = runIndex
    39  	ctx.Data["JobIndex"] = jobIndex
    40  	ctx.Data["ActionsURL"] = ctx.Repo.RepoLink + "/actions"
    41  
    42  	if getRunJobs(ctx, runIndex, jobIndex); ctx.Written() {
    43  		return
    44  	}
    45  
    46  	ctx.HTML(http.StatusOK, tplViewActions)
    47  }
    48  
    49  type ViewRequest struct {
    50  	LogCursors []struct {
    51  		Step     int   `json:"step"`
    52  		Cursor   int64 `json:"cursor"`
    53  		Expanded bool  `json:"expanded"`
    54  	} `json:"logCursors"`
    55  }
    56  
    57  type ViewResponse struct {
    58  	State struct {
    59  		Run struct {
    60  			Link       string     `json:"link"`
    61  			Title      string     `json:"title"`
    62  			Status     string     `json:"status"`
    63  			CanCancel  bool       `json:"canCancel"`
    64  			CanApprove bool       `json:"canApprove"` // the run needs an approval and the doer has permission to approve
    65  			CanRerun   bool       `json:"canRerun"`
    66  			Done       bool       `json:"done"`
    67  			Jobs       []*ViewJob `json:"jobs"`
    68  			Commit     ViewCommit `json:"commit"`
    69  		} `json:"run"`
    70  		CurrentJob struct {
    71  			Title  string         `json:"title"`
    72  			Detail string         `json:"detail"`
    73  			Steps  []*ViewJobStep `json:"steps"`
    74  		} `json:"currentJob"`
    75  	} `json:"state"`
    76  	Logs struct {
    77  		StepsLog []*ViewStepLog `json:"stepsLog"`
    78  	} `json:"logs"`
    79  }
    80  
    81  type ViewJob struct {
    82  	ID       int64  `json:"id"`
    83  	Name     string `json:"name"`
    84  	Status   string `json:"status"`
    85  	CanRerun bool   `json:"canRerun"`
    86  	Duration string `json:"duration"`
    87  }
    88  
    89  type ViewCommit struct {
    90  	LocaleCommit   string     `json:"localeCommit"`
    91  	LocalePushedBy string     `json:"localePushedBy"`
    92  	ShortSha       string     `json:"shortSHA"`
    93  	Link           string     `json:"link"`
    94  	Pusher         ViewUser   `json:"pusher"`
    95  	Branch         ViewBranch `json:"branch"`
    96  }
    97  
    98  type ViewUser struct {
    99  	DisplayName string `json:"displayName"`
   100  	Link        string `json:"link"`
   101  }
   102  
   103  type ViewBranch struct {
   104  	Name string `json:"name"`
   105  	Link string `json:"link"`
   106  }
   107  
   108  type ViewJobStep struct {
   109  	Summary  string `json:"summary"`
   110  	Duration string `json:"duration"`
   111  	Status   string `json:"status"`
   112  }
   113  
   114  type ViewStepLog struct {
   115  	Step    int                `json:"step"`
   116  	Cursor  int64              `json:"cursor"`
   117  	Lines   []*ViewStepLogLine `json:"lines"`
   118  	Started int64              `json:"started"`
   119  }
   120  
   121  type ViewStepLogLine struct {
   122  	Index     int64   `json:"index"`
   123  	Message   string  `json:"message"`
   124  	Timestamp float64 `json:"timestamp"`
   125  }
   126  
   127  func ViewPost(ctx *context_module.Context) {
   128  	req := web.GetForm(ctx).(*ViewRequest)
   129  	runIndex := ctx.ParamsInt64("run")
   130  	jobIndex := ctx.ParamsInt64("job")
   131  
   132  	current, jobs := getRunJobs(ctx, runIndex, jobIndex)
   133  	if ctx.Written() {
   134  		return
   135  	}
   136  	run := current.Run
   137  	if err := run.LoadAttributes(ctx); err != nil {
   138  		ctx.Error(http.StatusInternalServerError, err.Error())
   139  		return
   140  	}
   141  
   142  	resp := &ViewResponse{}
   143  
   144  	resp.State.Run.Title = run.Title
   145  	resp.State.Run.Link = run.Link()
   146  	resp.State.Run.CanCancel = !run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
   147  	resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions)
   148  	resp.State.Run.CanRerun = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
   149  	resp.State.Run.Done = run.Status.IsDone()
   150  	resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead fo 'null' in json
   151  	resp.State.Run.Status = run.Status.String()
   152  	for _, v := range jobs {
   153  		resp.State.Run.Jobs = append(resp.State.Run.Jobs, &ViewJob{
   154  			ID:       v.ID,
   155  			Name:     v.Name,
   156  			Status:   v.Status.String(),
   157  			CanRerun: v.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions),
   158  			Duration: v.Duration().String(),
   159  		})
   160  	}
   161  
   162  	pusher := ViewUser{
   163  		DisplayName: run.TriggerUser.GetDisplayName(),
   164  		Link:        run.TriggerUser.HomeLink(),
   165  	}
   166  	branch := ViewBranch{
   167  		Name: run.PrettyRef(),
   168  		Link: run.RefLink(),
   169  	}
   170  	resp.State.Run.Commit = ViewCommit{
   171  		LocaleCommit:   ctx.Tr("actions.runs.commit"),
   172  		LocalePushedBy: ctx.Tr("actions.runs.pushed_by"),
   173  		ShortSha:       base.ShortSha(run.CommitSHA),
   174  		Link:           fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA),
   175  		Pusher:         pusher,
   176  		Branch:         branch,
   177  	}
   178  
   179  	var task *actions_model.ActionTask
   180  	if current.TaskID > 0 {
   181  		var err error
   182  		task, err = actions_model.GetTaskByID(ctx, current.TaskID)
   183  		if err != nil {
   184  			ctx.Error(http.StatusInternalServerError, err.Error())
   185  			return
   186  		}
   187  		task.Job = current
   188  		if err := task.LoadAttributes(ctx); err != nil {
   189  			ctx.Error(http.StatusInternalServerError, err.Error())
   190  			return
   191  		}
   192  	}
   193  
   194  	resp.State.CurrentJob.Title = current.Name
   195  	resp.State.CurrentJob.Detail = current.Status.LocaleString(ctx.Locale)
   196  	if run.NeedApproval {
   197  		resp.State.CurrentJob.Detail = ctx.Locale.Tr("actions.need_approval_desc")
   198  	}
   199  	resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json
   200  	resp.Logs.StepsLog = make([]*ViewStepLog, 0)          // marshal to '[]' instead fo 'null' in json
   201  	if task != nil {
   202  		steps := actions.FullSteps(task)
   203  
   204  		for _, v := range steps {
   205  			resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, &ViewJobStep{
   206  				Summary:  v.Name,
   207  				Duration: v.Duration().String(),
   208  				Status:   v.Status.String(),
   209  			})
   210  		}
   211  
   212  		for _, cursor := range req.LogCursors {
   213  			if !cursor.Expanded {
   214  				continue
   215  			}
   216  
   217  			step := steps[cursor.Step]
   218  
   219  			logLines := make([]*ViewStepLogLine, 0) // marshal to '[]' instead fo 'null' in json
   220  
   221  			index := step.LogIndex + cursor.Cursor
   222  			validCursor := cursor.Cursor >= 0 &&
   223  				// !(cursor.Cursor < step.LogLength) when the frontend tries to fetch next line before it's ready.
   224  				// So return the same cursor and empty lines to let the frontend retry.
   225  				cursor.Cursor < step.LogLength &&
   226  				// !(index < task.LogIndexes[index]) when task data is older than step data.
   227  				// It can be fixed by making sure write/read tasks and steps in the same transaction,
   228  				// but it's easier to just treat it as fetching the next line before it's ready.
   229  				index < int64(len(task.LogIndexes))
   230  
   231  			if validCursor {
   232  				length := step.LogLength - cursor.Cursor
   233  				offset := task.LogIndexes[index]
   234  				var err error
   235  				logRows, err := actions.ReadLogs(ctx, task.LogInStorage, task.LogFilename, offset, length)
   236  				if err != nil {
   237  					ctx.Error(http.StatusInternalServerError, err.Error())
   238  					return
   239  				}
   240  
   241  				for i, row := range logRows {
   242  					logLines = append(logLines, &ViewStepLogLine{
   243  						Index:     cursor.Cursor + int64(i) + 1, // start at 1
   244  						Message:   row.Content,
   245  						Timestamp: float64(row.Time.AsTime().UnixNano()) / float64(time.Second),
   246  					})
   247  				}
   248  			}
   249  
   250  			resp.Logs.StepsLog = append(resp.Logs.StepsLog, &ViewStepLog{
   251  				Step:    cursor.Step,
   252  				Cursor:  cursor.Cursor + int64(len(logLines)),
   253  				Lines:   logLines,
   254  				Started: int64(step.Started),
   255  			})
   256  		}
   257  	}
   258  
   259  	ctx.JSON(http.StatusOK, resp)
   260  }
   261  
   262  // Rerun will rerun jobs in the given run
   263  // jobIndex = 0 means rerun all jobs
   264  func Rerun(ctx *context_module.Context) {
   265  	runIndex := ctx.ParamsInt64("run")
   266  	jobIndex := ctx.ParamsInt64("job")
   267  
   268  	run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
   269  	if err != nil {
   270  		ctx.Error(http.StatusInternalServerError, err.Error())
   271  		return
   272  	}
   273  
   274  	// can not rerun job when workflow is disabled
   275  	cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
   276  	cfg := cfgUnit.ActionsConfig()
   277  	if cfg.IsWorkflowDisabled(run.WorkflowID) {
   278  		ctx.JSONError(ctx.Locale.Tr("actions.workflow.disabled"))
   279  		return
   280  	}
   281  
   282  	job, jobs := getRunJobs(ctx, runIndex, jobIndex)
   283  	if ctx.Written() {
   284  		return
   285  	}
   286  
   287  	if jobIndex != 0 {
   288  		jobs = []*actions_model.ActionRunJob{job}
   289  	}
   290  
   291  	for _, j := range jobs {
   292  		if err := rerunJob(ctx, j); err != nil {
   293  			ctx.Error(http.StatusInternalServerError, err.Error())
   294  			return
   295  		}
   296  	}
   297  
   298  	ctx.JSON(http.StatusOK, struct{}{})
   299  }
   300  
   301  func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob) error {
   302  	status := job.Status
   303  	if !status.IsDone() {
   304  		return nil
   305  	}
   306  
   307  	job.TaskID = 0
   308  	job.Status = actions_model.StatusWaiting
   309  	job.Started = 0
   310  	job.Stopped = 0
   311  
   312  	if err := db.WithTx(ctx, func(ctx context.Context) error {
   313  		_, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": status}, "task_id", "status", "started", "stopped")
   314  		return err
   315  	}); err != nil {
   316  		return err
   317  	}
   318  
   319  	actions_service.CreateCommitStatus(ctx, job)
   320  	return nil
   321  }
   322  
   323  func Logs(ctx *context_module.Context) {
   324  	runIndex := ctx.ParamsInt64("run")
   325  	jobIndex := ctx.ParamsInt64("job")
   326  
   327  	job, _ := getRunJobs(ctx, runIndex, jobIndex)
   328  	if ctx.Written() {
   329  		return
   330  	}
   331  	if job.TaskID == 0 {
   332  		ctx.Error(http.StatusNotFound, "job is not started")
   333  		return
   334  	}
   335  
   336  	err := job.LoadRun(ctx)
   337  	if err != nil {
   338  		ctx.Error(http.StatusInternalServerError, err.Error())
   339  		return
   340  	}
   341  
   342  	task, err := actions_model.GetTaskByID(ctx, job.TaskID)
   343  	if err != nil {
   344  		ctx.Error(http.StatusInternalServerError, err.Error())
   345  		return
   346  	}
   347  	if task.LogExpired {
   348  		ctx.Error(http.StatusNotFound, "logs have been cleaned up")
   349  		return
   350  	}
   351  
   352  	reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename)
   353  	if err != nil {
   354  		ctx.Error(http.StatusInternalServerError, err.Error())
   355  		return
   356  	}
   357  	defer reader.Close()
   358  
   359  	workflowName := job.Run.WorkflowID
   360  	if p := strings.Index(workflowName, "."); p > 0 {
   361  		workflowName = workflowName[0:p]
   362  	}
   363  	ctx.ServeContent(reader, &context_module.ServeHeaderOptions{
   364  		Filename:           fmt.Sprintf("%v-%v-%v.log", workflowName, job.Name, task.ID),
   365  		ContentLength:      &task.LogSize,
   366  		ContentType:        "text/plain",
   367  		ContentTypeCharset: "utf-8",
   368  		Disposition:        "attachment",
   369  	})
   370  }
   371  
   372  func Cancel(ctx *context_module.Context) {
   373  	runIndex := ctx.ParamsInt64("run")
   374  
   375  	_, jobs := getRunJobs(ctx, runIndex, -1)
   376  	if ctx.Written() {
   377  		return
   378  	}
   379  
   380  	if err := db.WithTx(ctx, func(ctx context.Context) error {
   381  		for _, job := range jobs {
   382  			status := job.Status
   383  			if status.IsDone() {
   384  				continue
   385  			}
   386  			if job.TaskID == 0 {
   387  				job.Status = actions_model.StatusCancelled
   388  				job.Stopped = timeutil.TimeStampNow()
   389  				n, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped")
   390  				if err != nil {
   391  					return err
   392  				}
   393  				if n == 0 {
   394  					return fmt.Errorf("job has changed, try again")
   395  				}
   396  				continue
   397  			}
   398  			if err := actions_model.StopTask(ctx, job.TaskID, actions_model.StatusCancelled); err != nil {
   399  				return err
   400  			}
   401  		}
   402  		return nil
   403  	}); err != nil {
   404  		ctx.Error(http.StatusInternalServerError, err.Error())
   405  		return
   406  	}
   407  
   408  	actions_service.CreateCommitStatus(ctx, jobs...)
   409  
   410  	ctx.JSON(http.StatusOK, struct{}{})
   411  }
   412  
   413  func Approve(ctx *context_module.Context) {
   414  	runIndex := ctx.ParamsInt64("run")
   415  
   416  	current, jobs := getRunJobs(ctx, runIndex, -1)
   417  	if ctx.Written() {
   418  		return
   419  	}
   420  	run := current.Run
   421  	doer := ctx.Doer
   422  
   423  	if err := db.WithTx(ctx, func(ctx context.Context) error {
   424  		run.NeedApproval = false
   425  		run.ApprovedBy = doer.ID
   426  		if err := actions_model.UpdateRun(ctx, run, "need_approval", "approved_by"); err != nil {
   427  			return err
   428  		}
   429  		for _, job := range jobs {
   430  			if len(job.Needs) == 0 && job.Status.IsBlocked() {
   431  				job.Status = actions_model.StatusWaiting
   432  				_, err := actions_model.UpdateRunJob(ctx, job, nil, "status")
   433  				if err != nil {
   434  					return err
   435  				}
   436  			}
   437  		}
   438  		return nil
   439  	}); err != nil {
   440  		ctx.Error(http.StatusInternalServerError, err.Error())
   441  		return
   442  	}
   443  
   444  	actions_service.CreateCommitStatus(ctx, jobs...)
   445  
   446  	ctx.JSON(http.StatusOK, struct{}{})
   447  }
   448  
   449  // getRunJobs gets the jobs of runIndex, and returns jobs[jobIndex], jobs.
   450  // Any error will be written to the ctx.
   451  // It never returns a nil job of an empty jobs, if the jobIndex is out of range, it will be treated as 0.
   452  func getRunJobs(ctx *context_module.Context, runIndex, jobIndex int64) (*actions_model.ActionRunJob, []*actions_model.ActionRunJob) {
   453  	run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
   454  	if err != nil {
   455  		if errors.Is(err, util.ErrNotExist) {
   456  			ctx.Error(http.StatusNotFound, err.Error())
   457  			return nil, nil
   458  		}
   459  		ctx.Error(http.StatusInternalServerError, err.Error())
   460  		return nil, nil
   461  	}
   462  	run.Repo = ctx.Repo.Repository
   463  
   464  	jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
   465  	if err != nil {
   466  		ctx.Error(http.StatusInternalServerError, err.Error())
   467  		return nil, nil
   468  	}
   469  	if len(jobs) == 0 {
   470  		ctx.Error(http.StatusNotFound, err.Error())
   471  		return nil, nil
   472  	}
   473  
   474  	for _, v := range jobs {
   475  		v.Run = run
   476  	}
   477  
   478  	if jobIndex >= 0 && jobIndex < int64(len(jobs)) {
   479  		return jobs[jobIndex], jobs
   480  	}
   481  	return jobs[0], jobs
   482  }
   483  
   484  type ArtifactsViewResponse struct {
   485  	Artifacts []*ArtifactsViewItem `json:"artifacts"`
   486  }
   487  
   488  type ArtifactsViewItem struct {
   489  	Name   string `json:"name"`
   490  	Size   int64  `json:"size"`
   491  	Status string `json:"status"`
   492  }
   493  
   494  func ArtifactsView(ctx *context_module.Context) {
   495  	runIndex := ctx.ParamsInt64("run")
   496  	run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
   497  	if err != nil {
   498  		if errors.Is(err, util.ErrNotExist) {
   499  			ctx.Error(http.StatusNotFound, err.Error())
   500  			return
   501  		}
   502  		ctx.Error(http.StatusInternalServerError, err.Error())
   503  		return
   504  	}
   505  	artifacts, err := actions_model.ListUploadedArtifactsMeta(ctx, run.ID)
   506  	if err != nil {
   507  		ctx.Error(http.StatusInternalServerError, err.Error())
   508  		return
   509  	}
   510  	artifactsResponse := ArtifactsViewResponse{
   511  		Artifacts: make([]*ArtifactsViewItem, 0, len(artifacts)),
   512  	}
   513  	for _, art := range artifacts {
   514  		status := "completed"
   515  		if art.Status == int64(actions_model.ArtifactStatusExpired) {
   516  			status = "expired"
   517  		}
   518  		artifactsResponse.Artifacts = append(artifactsResponse.Artifacts, &ArtifactsViewItem{
   519  			Name:   art.ArtifactName,
   520  			Size:   art.FileSize,
   521  			Status: status,
   522  		})
   523  	}
   524  	ctx.JSON(http.StatusOK, artifactsResponse)
   525  }
   526  
   527  func ArtifactsDownloadView(ctx *context_module.Context) {
   528  	runIndex := ctx.ParamsInt64("run")
   529  	artifactName := ctx.Params("artifact_name")
   530  
   531  	run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
   532  	if err != nil {
   533  		if errors.Is(err, util.ErrNotExist) {
   534  			ctx.Error(http.StatusNotFound, err.Error())
   535  			return
   536  		}
   537  		ctx.Error(http.StatusInternalServerError, err.Error())
   538  		return
   539  	}
   540  
   541  	artifacts, err := actions_model.ListArtifactsByRunIDAndName(ctx, run.ID, artifactName)
   542  	if err != nil {
   543  		ctx.Error(http.StatusInternalServerError, err.Error())
   544  		return
   545  	}
   546  	if len(artifacts) == 0 {
   547  		ctx.Error(http.StatusNotFound, "artifact not found")
   548  		return
   549  	}
   550  
   551  	ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName))
   552  
   553  	writer := zip.NewWriter(ctx.Resp)
   554  	defer writer.Close()
   555  	for _, art := range artifacts {
   556  
   557  		f, err := storage.ActionsArtifacts.Open(art.StoragePath)
   558  		if err != nil {
   559  			ctx.Error(http.StatusInternalServerError, err.Error())
   560  			return
   561  		}
   562  
   563  		var r io.ReadCloser
   564  		if art.ContentEncoding == "gzip" {
   565  			r, err = gzip.NewReader(f)
   566  			if err != nil {
   567  				ctx.Error(http.StatusInternalServerError, err.Error())
   568  				return
   569  			}
   570  		} else {
   571  			r = f
   572  		}
   573  		defer r.Close()
   574  
   575  		w, err := writer.Create(art.ArtifactPath)
   576  		if err != nil {
   577  			ctx.Error(http.StatusInternalServerError, err.Error())
   578  			return
   579  		}
   580  		if _, err := io.Copy(w, r); err != nil {
   581  			ctx.Error(http.StatusInternalServerError, err.Error())
   582  			return
   583  		}
   584  	}
   585  }
   586  
   587  func DisableWorkflowFile(ctx *context_module.Context) {
   588  	disableOrEnableWorkflowFile(ctx, false)
   589  }
   590  
   591  func EnableWorkflowFile(ctx *context_module.Context) {
   592  	disableOrEnableWorkflowFile(ctx, true)
   593  }
   594  
   595  func disableOrEnableWorkflowFile(ctx *context_module.Context, isEnable bool) {
   596  	workflow := ctx.FormString("workflow")
   597  	if len(workflow) == 0 {
   598  		ctx.ServerError("workflow", nil)
   599  		return
   600  	}
   601  
   602  	cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
   603  	cfg := cfgUnit.ActionsConfig()
   604  
   605  	if isEnable {
   606  		cfg.EnableWorkflow(workflow)
   607  	} else {
   608  		cfg.DisableWorkflow(workflow)
   609  	}
   610  
   611  	if err := repo_model.UpdateRepoUnit(cfgUnit); err != nil {
   612  		ctx.ServerError("UpdateRepoUnit", err)
   613  		return
   614  	}
   615  
   616  	if isEnable {
   617  		ctx.Flash.Success(ctx.Tr("actions.workflow.enable_success", workflow))
   618  	} else {
   619  		ctx.Flash.Success(ctx.Tr("actions.workflow.disable_success", workflow))
   620  	}
   621  
   622  	redirectURL := fmt.Sprintf("%s/actions?workflow=%s&actor=%s&status=%s", ctx.Repo.RepoLink, url.QueryEscape(workflow),
   623  		url.QueryEscape(ctx.FormString("actor")), url.QueryEscape(ctx.FormString("status")))
   624  	ctx.JSONRedirect(redirectURL)
   625  }