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