mirror of
				https://github.com/go-gitea/gitea
				synced 2025-09-28 03:28:13 +00:00 
			
		
		
		
	Add endpoint deleting workflow run (#34337)
Add endpoint deleting workflow run Resolves #26219 /claim #26219 --------- Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		| @@ -16,6 +16,7 @@ import ( | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/json" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| @@ -343,13 +344,13 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork | ||||
| 	return committer.Commit() | ||||
| } | ||||
|  | ||||
| func GetRunByID(ctx context.Context, id int64) (*ActionRun, error) { | ||||
| func GetRunByRepoAndID(ctx context.Context, repoID, runID int64) (*ActionRun, error) { | ||||
| 	var run ActionRun | ||||
| 	has, err := db.GetEngine(ctx).Where("id=?", id).Get(&run) | ||||
| 	has, err := db.GetEngine(ctx).Where("id=? AND repo_id=?", runID, repoID).Get(&run) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} else if !has { | ||||
| 		return nil, fmt.Errorf("run with id %d: %w", id, util.ErrNotExist) | ||||
| 		return nil, fmt.Errorf("run with id %d: %w", runID, util.ErrNotExist) | ||||
| 	} | ||||
|  | ||||
| 	return &run, nil | ||||
| @@ -420,17 +421,10 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error { | ||||
|  | ||||
| 	if run.Status != 0 || slices.Contains(cols, "status") { | ||||
| 		if run.RepoID == 0 { | ||||
| 			run, err = GetRunByID(ctx, run.ID) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			setting.PanicInDevOrTesting("RepoID should not be 0") | ||||
| 		} | ||||
| 		if run.Repo == nil { | ||||
| 			repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			run.Repo = repo | ||||
| 		if err = run.LoadRepo(ctx); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil { | ||||
| 			return err | ||||
|   | ||||
| @@ -51,7 +51,7 @@ func (job *ActionRunJob) Duration() time.Duration { | ||||
|  | ||||
| func (job *ActionRunJob) LoadRun(ctx context.Context) error { | ||||
| 	if job.Run == nil { | ||||
| 		run, err := GetRunByID(ctx, job.RunID) | ||||
| 		run, err := GetRunByRepoAndID(ctx, job.RepoID, job.RunID) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| @@ -142,7 +142,7 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col | ||||
| 	{ | ||||
| 		// Other goroutines may aggregate the status of the run and update it too. | ||||
| 		// So we need load the run and its jobs before updating the run. | ||||
| 		run, err := GetRunByID(ctx, job.RunID) | ||||
| 		run, err := GetRunByRepoAndID(ctx, job.RepoID, job.RunID) | ||||
| 		if err != nil { | ||||
| 			return 0, err | ||||
| 		} | ||||
|   | ||||
| @@ -48,6 +48,7 @@ func (tasks TaskList) LoadAttributes(ctx context.Context) error { | ||||
| type FindTaskOptions struct { | ||||
| 	db.ListOptions | ||||
| 	RepoID        int64 | ||||
| 	JobID         int64 | ||||
| 	OwnerID       int64 | ||||
| 	CommitSHA     string | ||||
| 	Status        Status | ||||
| @@ -61,6 +62,9 @@ func (opts FindTaskOptions) ToConds() builder.Cond { | ||||
| 	if opts.RepoID > 0 { | ||||
| 		cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) | ||||
| 	} | ||||
| 	if opts.JobID > 0 { | ||||
| 		cond = cond.And(builder.Eq{"job_id": opts.JobID}) | ||||
| 	} | ||||
| 	if opts.OwnerID > 0 { | ||||
| 		cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) | ||||
| 	} | ||||
|   | ||||
| @@ -105,3 +105,39 @@ | ||||
|   created_unix: 1730330775 | ||||
|   updated_unix: 1730330775 | ||||
|   expired_unix: 1738106775 | ||||
|  | ||||
| - | ||||
|   id: 24 | ||||
|   run_id: 795 | ||||
|   runner_id: 1 | ||||
|   repo_id: 2 | ||||
|   owner_id: 2 | ||||
|   commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 | ||||
|   storage_path: "27/5/1730330775594233150.chunk" | ||||
|   file_size: 1024 | ||||
|   file_compressed_size: 1024 | ||||
|   content_encoding: "application/zip" | ||||
|   artifact_path: "artifact-795-1.zip" | ||||
|   artifact_name: "artifact-795-1" | ||||
|   status: 2 | ||||
|   created_unix: 1730330775 | ||||
|   updated_unix: 1730330775 | ||||
|   expired_unix: 1738106775 | ||||
|  | ||||
| - | ||||
|   id: 25 | ||||
|   run_id: 795 | ||||
|   runner_id: 1 | ||||
|   repo_id: 2 | ||||
|   owner_id: 2 | ||||
|   commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 | ||||
|   storage_path: "27/5/1730330775594233150.chunk" | ||||
|   file_size: 1024 | ||||
|   file_compressed_size: 1024 | ||||
|   content_encoding: "application/zip" | ||||
|   artifact_path: "artifact-795-2.zip" | ||||
|   artifact_name: "artifact-795-2" | ||||
|   status: 2 | ||||
|   created_unix: 1730330775 | ||||
|   updated_unix: 1730330775 | ||||
|   expired_unix: 1738106775 | ||||
|   | ||||
| @@ -48,7 +48,7 @@ | ||||
|   commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" | ||||
|   event: "push" | ||||
|   is_fork_pull_request: 0 | ||||
|   status: 1 | ||||
|   status: 6 # running | ||||
|   started: 1683636528 | ||||
|   stopped: 1683636626 | ||||
|   created: 1683636108 | ||||
| @@ -74,3 +74,23 @@ | ||||
|   updated: 1683636626 | ||||
|   need_approval: 0 | ||||
|   approved_by: 0 | ||||
|  | ||||
| - | ||||
|   id: 795 | ||||
|   title: "to be deleted (test)" | ||||
|   repo_id: 2 | ||||
|   owner_id: 2 | ||||
|   workflow_id: "test.yaml" | ||||
|   index: 191 | ||||
|   trigger_user_id: 1 | ||||
|   ref: "refs/heads/test" | ||||
|   commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" | ||||
|   event: "push" | ||||
|   is_fork_pull_request: 0 | ||||
|   status: 2 | ||||
|   started: 1683636528 | ||||
|   stopped: 1683636626 | ||||
|   created: 1683636108 | ||||
|   updated: 1683636626 | ||||
|   need_approval: 0 | ||||
|   approved_by: 0 | ||||
|   | ||||
| @@ -69,3 +69,33 @@ | ||||
|   status: 5 | ||||
|   started: 1683636528 | ||||
|   stopped: 1683636626 | ||||
|  | ||||
| - | ||||
|   id: 198 | ||||
|   run_id: 795 | ||||
|   repo_id: 2 | ||||
|   owner_id: 2 | ||||
|   commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 | ||||
|   is_fork_pull_request: 0 | ||||
|   name: job_1 | ||||
|   attempt: 1 | ||||
|   job_id: job_1 | ||||
|   task_id: 53 | ||||
|   status: 1 | ||||
|   started: 1683636528 | ||||
|   stopped: 1683636626 | ||||
|  | ||||
| - | ||||
|   id: 199 | ||||
|   run_id: 795 | ||||
|   repo_id: 2 | ||||
|   owner_id: 2 | ||||
|   commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 | ||||
|   is_fork_pull_request: 0 | ||||
|   name: job_2 | ||||
|   attempt: 1 | ||||
|   job_id: job_2 | ||||
|   task_id: 54 | ||||
|   status: 2 | ||||
|   started: 1683636528 | ||||
|   stopped: 1683636626 | ||||
|   | ||||
| @@ -117,3 +117,43 @@ | ||||
|   log_length: 707 | ||||
|   log_size: 90179 | ||||
|   log_expired: 0 | ||||
| - | ||||
|   id: 53 | ||||
|   job_id: 198 | ||||
|   attempt: 1 | ||||
|   runner_id: 1 | ||||
|   status: 1 | ||||
|   started: 1683636528 | ||||
|   stopped: 1683636626 | ||||
|   repo_id: 2 | ||||
|   owner_id: 2 | ||||
|   commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 | ||||
|   is_fork_pull_request: 0 | ||||
|   token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784223 | ||||
|   token_salt: ffffffffff | ||||
|   token_last_eight: ffffffff | ||||
|   log_filename: artifact-test2/2f/47.log | ||||
|   log_in_storage: 1 | ||||
|   log_length: 0 | ||||
|   log_size: 0 | ||||
|   log_expired: 0 | ||||
| - | ||||
|   id: 54 | ||||
|   job_id: 199 | ||||
|   attempt: 1 | ||||
|   runner_id: 1 | ||||
|   status: 2 | ||||
|   started: 1683636528 | ||||
|   stopped: 1683636626 | ||||
|   repo_id: 2 | ||||
|   owner_id: 2 | ||||
|   commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 | ||||
|   is_fork_pull_request: 0 | ||||
|   token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784224 | ||||
|   token_salt: ffffffffff | ||||
|   token_last_eight: ffffffff | ||||
|   log_filename: artifact-test2/2f/47.log | ||||
|   log_in_storage: 1 | ||||
|   log_length: 0 | ||||
|   log_size: 0 | ||||
|   log_expired: 0 | ||||
|   | ||||
| @@ -3811,6 +3811,9 @@ runs.no_workflows.documentation = For more information on Gitea Actions, see <a | ||||
| runs.no_runs = The workflow has no runs yet. | ||||
| runs.empty_commit_message = (empty commit message) | ||||
| runs.expire_log_message = Logs have been purged because they were too old. | ||||
| runs.delete = Delete workflow run | ||||
| runs.delete.description = Are you sure you want to permanently delete this workflow run? This action cannot be undone. | ||||
| runs.not_done = This workflow run is not done. | ||||
|  | ||||
| workflow.disable = Disable Workflow | ||||
| workflow.disable_success = Workflow '%s' disabled successfully. | ||||
|   | ||||
| @@ -1279,7 +1279,10 @@ func Routes() *web.Router { | ||||
| 				}, reqToken(), reqAdmin()) | ||||
| 				m.Group("/actions", func() { | ||||
| 					m.Get("/tasks", repo.ListActionTasks) | ||||
| 					m.Get("/runs/{run}/artifacts", repo.GetArtifactsOfRun) | ||||
| 					m.Group("/runs/{run}", func() { | ||||
| 						m.Get("/artifacts", repo.GetArtifactsOfRun) | ||||
| 						m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun) | ||||
| 					}) | ||||
| 					m.Get("/artifacts", repo.GetArtifacts) | ||||
| 					m.Group("/artifacts/{artifact_id}", func() { | ||||
| 						m.Get("", repo.GetArtifact) | ||||
|   | ||||
| @@ -1061,6 +1061,58 @@ func GetArtifactsOfRun(ctx *context.APIContext) { | ||||
| 	ctx.JSON(http.StatusOK, &res) | ||||
| } | ||||
|  | ||||
| // DeleteActionRun Delete a workflow run | ||||
| func DeleteActionRun(ctx *context.APIContext) { | ||||
| 	// swagger:operation DELETE /repos/{owner}/{repo}/actions/runs/{run} repository deleteActionRun | ||||
| 	// --- | ||||
| 	// summary: Delete a workflow run | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: owner | ||||
| 	//   in: path | ||||
| 	//   description: name of the owner | ||||
| 	//   type: string | ||||
| 	//   required: true | ||||
| 	// - name: repo | ||||
| 	//   in: path | ||||
| 	//   description: name of the repository | ||||
| 	//   type: string | ||||
| 	//   required: true | ||||
| 	// - name: run | ||||
| 	//   in: path | ||||
| 	//   description: runid of the workflow run | ||||
| 	//   type: integer | ||||
| 	//   required: true | ||||
| 	// responses: | ||||
| 	//   "204": | ||||
| 	//     description: "No Content" | ||||
| 	//   "400": | ||||
| 	//     "$ref": "#/responses/error" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
|  | ||||
| 	runID := ctx.PathParamInt64("run") | ||||
| 	run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID) | ||||
| 	if errors.Is(err, util.ErrNotExist) { | ||||
| 		ctx.APIError(http.StatusNotFound, err) | ||||
| 		return | ||||
| 	} else if err != nil { | ||||
| 		ctx.APIErrorInternal(err) | ||||
| 		return | ||||
| 	} | ||||
| 	if !run.Status.IsDone() { | ||||
| 		ctx.APIError(http.StatusBadRequest, "this workflow run is not done") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if err := actions_service.DeleteRun(ctx, run); err != nil { | ||||
| 		ctx.APIErrorInternal(err) | ||||
| 		return | ||||
| 	} | ||||
| 	ctx.Status(http.StatusNoContent) | ||||
| } | ||||
|  | ||||
| // GetArtifacts Lists all artifacts for a repository. | ||||
| func GetArtifacts(ctx *context.APIContext) { | ||||
| 	// swagger:operation GET /repos/{owner}/{repo}/actions/artifacts repository getArtifacts | ||||
|   | ||||
| @@ -317,6 +317,8 @@ func prepareWorkflowList(ctx *context.Context, workflows []Workflow) { | ||||
| 	pager.AddParamFromRequest(ctx.Req) | ||||
| 	ctx.Data["Page"] = pager | ||||
| 	ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0 | ||||
|  | ||||
| 	ctx.Data["AllowDeleteWorkflowRuns"] = ctx.Repo.CanWrite(unit.TypeActions) | ||||
| } | ||||
|  | ||||
| // loadIsRefDeleted loads the IsRefDeleted field for each run in the list. | ||||
|   | ||||
| @@ -577,6 +577,33 @@ func Approve(ctx *context_module.Context) { | ||||
| 	ctx.JSON(http.StatusOK, struct{}{}) | ||||
| } | ||||
|  | ||||
| func Delete(ctx *context_module.Context) { | ||||
| 	runIndex := getRunIndex(ctx) | ||||
| 	repoID := ctx.Repo.Repository.ID | ||||
|  | ||||
| 	run, err := actions_model.GetRunByIndex(ctx, repoID, runIndex) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, util.ErrNotExist) { | ||||
| 			ctx.JSONErrorNotFound() | ||||
| 			return | ||||
| 		} | ||||
| 		ctx.ServerError("GetRunByIndex", err) | ||||
| 		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() | ||||
| } | ||||
|  | ||||
| // getRunJobs gets the jobs of runIndex, and returns jobs[jobIndex], jobs. | ||||
| // Any error will be written to the ctx. | ||||
| // It never returns a nil job of an empty jobs, if the jobIndex is out of range, it will be treated as 0. | ||||
|   | ||||
| @@ -264,7 +264,7 @@ func MergeUpstream(ctx *context.Context) { | ||||
| 	_, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, branchName) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, util.ErrNotExist) { | ||||
| 			ctx.JSONError(ctx.Tr("error.not_found")) | ||||
| 			ctx.JSONErrorNotFound() | ||||
| 			return | ||||
| 		} else if pull_service.IsErrMergeConflicts(err) { | ||||
| 			ctx.JSONError(ctx.Tr("repo.pulls.merge_conflict")) | ||||
|   | ||||
| @@ -1447,6 +1447,7 @@ func registerWebRoutes(m *web.Router) { | ||||
| 			}) | ||||
| 			m.Post("/cancel", reqRepoActionsWriter, actions.Cancel) | ||||
| 			m.Post("/approve", reqRepoActionsWriter, actions.Approve) | ||||
| 			m.Post("/delete", reqRepoActionsWriter, actions.Delete) | ||||
| 			m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView) | ||||
| 			m.Delete("/artifacts/{artifact_name}", reqRepoActionsWriter, actions.ArtifactsDeleteView) | ||||
| 			m.Post("/rerun", reqRepoActionsWriter, actions.Rerun) | ||||
|   | ||||
| @@ -5,12 +5,14 @@ package actions | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"time" | ||||
|  | ||||
| 	actions_model "code.gitea.io/gitea/models/actions" | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	actions_module "code.gitea.io/gitea/modules/actions" | ||||
| 	"code.gitea.io/gitea/modules/container" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/storage" | ||||
| @@ -27,7 +29,7 @@ func Cleanup(ctx context.Context) error { | ||||
| 	} | ||||
|  | ||||
| 	// clean up old logs | ||||
| 	if err := CleanupLogs(ctx); err != nil { | ||||
| 	if err := CleanupExpiredLogs(ctx); err != nil { | ||||
| 		return fmt.Errorf("cleanup logs: %w", err) | ||||
| 	} | ||||
|  | ||||
| @@ -98,8 +100,15 @@ func cleanNeedDeleteArtifacts(taskCtx context.Context) error { | ||||
|  | ||||
| const deleteLogBatchSize = 100 | ||||
|  | ||||
| // CleanupLogs removes logs which are older than the configured retention time | ||||
| func CleanupLogs(ctx context.Context) error { | ||||
| func removeTaskLog(ctx context.Context, task *actions_model.ActionTask) { | ||||
| 	if err := actions_module.RemoveLogs(ctx, task.LogInStorage, task.LogFilename); err != nil { | ||||
| 		log.Error("Failed to remove log %s (in storage %v) of task %v: %v", task.LogFilename, task.LogInStorage, task.ID, err) | ||||
| 		// do not return error here, go on | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // CleanupExpiredLogs removes logs which are older than the configured retention time | ||||
| func CleanupExpiredLogs(ctx context.Context) error { | ||||
| 	olderThan := timeutil.TimeStampNow().AddDuration(-time.Duration(setting.Actions.LogRetentionDays) * 24 * time.Hour) | ||||
|  | ||||
| 	count := 0 | ||||
| @@ -109,10 +118,7 @@ func CleanupLogs(ctx context.Context) error { | ||||
| 			return fmt.Errorf("find old tasks: %w", err) | ||||
| 		} | ||||
| 		for _, task := range tasks { | ||||
| 			if err := actions_module.RemoveLogs(ctx, task.LogInStorage, task.LogFilename); err != nil { | ||||
| 				log.Error("Failed to remove log %s (in storage %v) of task %v: %v", task.LogFilename, task.LogInStorage, task.ID, err) | ||||
| 				// do not return error here, go on | ||||
| 			} | ||||
| 			removeTaskLog(ctx, task) | ||||
| 			task.LogIndexes = nil // clear log indexes since it's a heavy field | ||||
| 			task.LogExpired = true | ||||
| 			if err := actions_model.UpdateTask(ctx, task, "log_indexes", "log_expired"); err != nil { | ||||
| @@ -148,3 +154,91 @@ func CleanupEphemeralRunners(ctx context.Context) error { | ||||
| 	log.Info("Removed %d runners", affected) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // DeleteRun deletes workflow run, including all logs and artifacts. | ||||
| func DeleteRun(ctx context.Context, run *actions_model.ActionRun) error { | ||||
| 	if !run.Status.IsDone() { | ||||
| 		return errors.New("run is not done") | ||||
| 	} | ||||
|  | ||||
| 	repoID := run.RepoID | ||||
|  | ||||
| 	jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	jobIDs := container.FilterSlice(jobs, func(j *actions_model.ActionRunJob) (int64, bool) { | ||||
| 		return j.ID, true | ||||
| 	}) | ||||
| 	tasks := make(actions_model.TaskList, 0) | ||||
| 	if len(jobIDs) > 0 { | ||||
| 		if err := db.GetEngine(ctx).Where("repo_id = ?", repoID).In("job_id", jobIDs).Find(&tasks); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	artifacts, err := db.Find[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{ | ||||
| 		RepoID: repoID, | ||||
| 		RunID:  run.ID, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	var recordsToDelete []any | ||||
|  | ||||
| 	recordsToDelete = append(recordsToDelete, &actions_model.ActionRun{ | ||||
| 		RepoID: repoID, | ||||
| 		ID:     run.ID, | ||||
| 	}) | ||||
| 	recordsToDelete = append(recordsToDelete, &actions_model.ActionRunJob{ | ||||
| 		RepoID: repoID, | ||||
| 		RunID:  run.ID, | ||||
| 	}) | ||||
| 	for _, tas := range tasks { | ||||
| 		recordsToDelete = append(recordsToDelete, &actions_model.ActionTask{ | ||||
| 			RepoID: repoID, | ||||
| 			ID:     tas.ID, | ||||
| 		}) | ||||
| 		recordsToDelete = append(recordsToDelete, &actions_model.ActionTaskStep{ | ||||
| 			RepoID: repoID, | ||||
| 			TaskID: tas.ID, | ||||
| 		}) | ||||
| 		recordsToDelete = append(recordsToDelete, &actions_model.ActionTaskOutput{ | ||||
| 			TaskID: tas.ID, | ||||
| 		}) | ||||
| 	} | ||||
| 	recordsToDelete = append(recordsToDelete, &actions_model.ActionArtifact{ | ||||
| 		RepoID: repoID, | ||||
| 		RunID:  run.ID, | ||||
| 	}) | ||||
|  | ||||
| 	if err := db.WithTx(ctx, func(ctx context.Context) error { | ||||
| 		// TODO: Deleting task records could break current ephemeral runner implementation. This is a temporary workaround suggested by ChristopherHX. | ||||
| 		// Since you delete potentially the only task an ephemeral act_runner has ever run, please delete the affected runners first. | ||||
| 		// one of | ||||
| 		//    call cleanup ephemeral runners first | ||||
| 		//    delete affected ephemeral act_runners | ||||
| 		//    I would make ephemeral runners fully delete directly before formally finishing the task | ||||
| 		// | ||||
| 		// See also: https://github.com/go-gitea/gitea/pull/34337#issuecomment-2862222788 | ||||
| 		if err := CleanupEphemeralRunners(ctx); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		return db.DeleteBeans(ctx, recordsToDelete...) | ||||
| 	}); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// Delete files on storage | ||||
| 	for _, tas := range tasks { | ||||
| 		removeTaskLog(ctx, tas) | ||||
| 	} | ||||
| 	for _, art := range artifacts { | ||||
| 		if err := storage.ActionsArtifacts.Delete(art.StoragePath); err != nil { | ||||
| 			log.Error("remove artifact file %q: %v", art.StoragePath, err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|   | ||||
| @@ -245,7 +245,7 @@ func APIContexter() func(http.Handler) http.Handler { | ||||
| // APIErrorNotFound handles 404s for APIContext | ||||
| // String will replace message, errors will be added to a slice | ||||
| func (ctx *APIContext) APIErrorNotFound(objs ...any) { | ||||
| 	message := ctx.Locale.TrString("error.not_found") | ||||
| 	var message string | ||||
| 	var errs []string | ||||
| 	for _, obj := range objs { | ||||
| 		// Ignore nil | ||||
| @@ -259,9 +259,8 @@ func (ctx *APIContext) APIErrorNotFound(objs ...any) { | ||||
| 			message = obj.(string) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	ctx.JSON(http.StatusNotFound, map[string]any{ | ||||
| 		"message": message, | ||||
| 		"message": util.IfZero(message, "not found"), // do not use locale in API | ||||
| 		"url":     setting.API.SwaggerURL, | ||||
| 		"errors":  errs, | ||||
| 	}) | ||||
|   | ||||
| @@ -23,6 +23,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/templates" | ||||
| 	"code.gitea.io/gitea/modules/translation" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	"code.gitea.io/gitea/modules/web" | ||||
| 	"code.gitea.io/gitea/modules/web/middleware" | ||||
| 	web_types "code.gitea.io/gitea/modules/web/types" | ||||
| @@ -261,3 +262,11 @@ func (ctx *Context) JSONError(msg any) { | ||||
| 		panic(fmt.Sprintf("unsupported type: %T", msg)) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (ctx *Context) JSONErrorNotFound(optMsg ...string) { | ||||
| 	msg := util.OptionalArg(optMsg) | ||||
| 	if msg == "" { | ||||
| 		msg = ctx.Locale.TrString("error.not_found") | ||||
| 	} | ||||
| 	ctx.JSON(http.StatusNotFound, map[string]any{"errorMessage": msg, "renderFormat": "text"}) | ||||
| } | ||||
|   | ||||
| @@ -5,37 +5,46 @@ | ||||
| 		<h2>{{if $.IsFiltered}}{{ctx.Locale.Tr "actions.runs.no_results"}}{{else}}{{ctx.Locale.Tr "actions.runs.no_runs"}}{{end}}</h2> | ||||
| 	</div> | ||||
| 	{{end}} | ||||
| 	{{range .Runs}} | ||||
| 	{{range $run := .Runs}} | ||||
| 		<div class="flex-item tw-items-center"> | ||||
| 			<div class="flex-item-leading"> | ||||
| 				{{template "repo/actions/status" (dict "status" .Status.String)}} | ||||
| 				{{template "repo/actions/status" (dict "status" $run.Status.String)}} | ||||
| 			</div> | ||||
| 			<div class="flex-item-main"> | ||||
| 				<a class="flex-item-title" title="{{.Title}}" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}"> | ||||
| 					{{if .Title}}{{.Title}}{{else}}{{ctx.Locale.Tr "actions.runs.empty_commit_message"}}{{end}} | ||||
| 				<a class="flex-item-title" title="{{$run.Title}}" href="{{$run.Link}}"> | ||||
| 					{{or $run.Title (ctx.Locale.Tr "actions.runs.empty_commit_message")}} | ||||
| 				</a> | ||||
| 				<div class="flex-item-body"> | ||||
| 					<span><b>{{if not $.CurWorkflow}}{{.WorkflowID}} {{end}}#{{.Index}}</b>:</span> | ||||
| 					{{- if .ScheduleID -}} | ||||
| 					<span><b>{{if not $.CurWorkflow}}{{$run.WorkflowID}} {{end}}#{{$run.Index}}</b>:</span> | ||||
| 					{{- if $run.ScheduleID -}} | ||||
| 						{{ctx.Locale.Tr "actions.runs.scheduled"}} | ||||
| 					{{- else -}} | ||||
| 						{{ctx.Locale.Tr "actions.runs.commit"}} | ||||
| 						<a href="{{$.RepoLink}}/commit/{{.CommitSHA}}">{{ShortSha .CommitSHA}}</a> | ||||
| 						<a href="{{$.RepoLink}}/commit/{{$run.CommitSHA}}">{{ShortSha $run.CommitSHA}}</a> | ||||
| 						{{ctx.Locale.Tr "actions.runs.pushed_by"}} | ||||
| 						<a href="{{.TriggerUser.HomeLink}}">{{.TriggerUser.GetDisplayName}}</a> | ||||
| 						<a href="{{$run.TriggerUser.HomeLink}}">{{$run.TriggerUser.GetDisplayName}}</a> | ||||
| 					{{- end -}} | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="flex-item-trailing"> | ||||
| 				{{if .IsRefDeleted}} | ||||
| 					<span class="ui label run-list-ref gt-ellipsis tw-line-through" data-tooltip-content="{{.PrettyRef}}">{{.PrettyRef}}</span> | ||||
| 				{{if $run.IsRefDeleted}} | ||||
| 					<span class="ui label run-list-ref gt-ellipsis tw-line-through" data-tooltip-content="{{$run.PrettyRef}}">{{$run.PrettyRef}}</span> | ||||
| 				{{else}} | ||||
| 					<a class="ui label run-list-ref gt-ellipsis" href="{{.RefLink}}" data-tooltip-content="{{.PrettyRef}}">{{.PrettyRef}}</a> | ||||
| 					<a class="ui label run-list-ref gt-ellipsis" href="{{$run.RefLink}}" data-tooltip-content="{{$run.PrettyRef}}">{{$run.PrettyRef}}</a> | ||||
| 				{{end}} | ||||
| 				<div class="run-list-item-right"> | ||||
| 					<div class="run-list-meta">{{svg "octicon-calendar" 16}}{{DateUtils.TimeSince .Updated}}</div> | ||||
| 					<div class="run-list-meta">{{svg "octicon-stopwatch" 16}}{{.Duration}}</div> | ||||
| 					<div class="run-list-meta">{{svg "octicon-calendar" 16}}{{DateUtils.TimeSince $run.Updated}}</div> | ||||
| 					<div class="run-list-meta">{{svg "octicon-stopwatch" 16}}{{$run.Duration}}</div> | ||||
| 				</div> | ||||
| 				{{if and ($.AllowDeleteWorkflowRuns) ($run.Status.IsDone)}} | ||||
| 					<button class="btn interact-bg link-action tw-p-2" | ||||
| 						data-url="{{$run.Link}}/delete" | ||||
| 						data-modal-confirm="{{ctx.Locale.Tr "actions.runs.delete.description"}}" | ||||
| 						data-tooltip-content="{{ctx.Locale.Tr "actions.runs.delete"}}" | ||||
| 					> | ||||
| 						{{svg "octicon-trash"}} | ||||
| 					</button> | ||||
| 				{{end}} | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	{{end}} | ||||
|   | ||||
| @@ -37,7 +37,7 @@ | ||||
| 				<span class="color-text-light-2"> | ||||
| 					{{ctx.Locale.Tr "settings.added_on" (DateUtils.AbsoluteShort .CreatedUnix)}} | ||||
| 				</span> | ||||
| 				<button class="ui btn interact-bg show-modal tw-p-2" | ||||
| 				<button class="btn interact-bg show-modal tw-p-2" | ||||
| 					data-modal="#add-secret-modal" | ||||
| 					data-modal-form.action="{{$.Link}}" | ||||
| 					data-modal-header="{{ctx.Locale.Tr "secrets.edit_secret"}}" | ||||
| @@ -49,7 +49,7 @@ | ||||
| 				> | ||||
| 					{{svg "octicon-pencil"}} | ||||
| 				</button> | ||||
| 				<button class="ui btn interact-bg link-action tw-p-2" | ||||
| 				<button class="btn interact-bg link-action tw-p-2" | ||||
| 					data-url="{{$.Link}}/delete?id={{.ID}}" | ||||
| 					data-modal-confirm="{{ctx.Locale.Tr "secrets.deletion.description"}}" | ||||
| 					data-tooltip-content="{{ctx.Locale.Tr "secrets.deletion"}}" | ||||
|   | ||||
							
								
								
									
										46
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										46
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							| @@ -4758,6 +4758,52 @@ | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/repos/{owner}/{repo}/actions/runs/{run}": { | ||||
|       "delete": { | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "repository" | ||||
|         ], | ||||
|         "summary": "Delete a workflow run", | ||||
|         "operationId": "deleteActionRun", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "name of the owner", | ||||
|             "name": "owner", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           }, | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "name of the repository", | ||||
|             "name": "repo", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           }, | ||||
|           { | ||||
|             "type": "integer", | ||||
|             "description": "runid of the workflow run", | ||||
|             "name": "run", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "204": { | ||||
|             "description": "No Content" | ||||
|           }, | ||||
|           "400": { | ||||
|             "$ref": "#/responses/error" | ||||
|           }, | ||||
|           "404": { | ||||
|             "$ref": "#/responses/notFound" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/repos/{owner}/{repo}/actions/runs/{run}/artifacts": { | ||||
|       "get": { | ||||
|         "produces": [ | ||||
|   | ||||
							
								
								
									
										181
									
								
								tests/integration/actions_delete_run_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								tests/integration/actions_delete_run_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,181 @@ | ||||
| // Copyright 2025 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
|  | ||||
| package integration | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	actions_model "code.gitea.io/gitea/models/actions" | ||||
| 	auth_model "code.gitea.io/gitea/models/auth" | ||||
| 	"code.gitea.io/gitea/models/unittest" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/json" | ||||
| 	"code.gitea.io/gitea/routers/web/repo/actions" | ||||
|  | ||||
| 	runnerv1 "code.gitea.io/actions-proto-go/runner/v1" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"google.golang.org/protobuf/types/known/timestamppb" | ||||
| ) | ||||
|  | ||||
| func TestActionsDeleteRun(t *testing.T) { | ||||
| 	now := time.Now() | ||||
| 	testCase := struct { | ||||
| 		treePath         string | ||||
| 		fileContent      string | ||||
| 		outcomes         map[string]*mockTaskOutcome | ||||
| 		expectedStatuses map[string]string | ||||
| 	}{ | ||||
| 		treePath: ".gitea/workflows/test1.yml", | ||||
| 		fileContent: `name: test1 | ||||
| on: | ||||
|   push: | ||||
|     paths: | ||||
|       - .gitea/workflows/test1.yml | ||||
| jobs: | ||||
|   job1: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - run: echo job1 | ||||
|   job2: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - run: echo job2 | ||||
|   job3: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - run: echo job3 | ||||
| `, | ||||
| 		outcomes: map[string]*mockTaskOutcome{ | ||||
| 			"job1": { | ||||
| 				result: runnerv1.Result_RESULT_SUCCESS, | ||||
| 				logRows: []*runnerv1.LogRow{ | ||||
| 					{ | ||||
| 						Time:    timestamppb.New(now.Add(4 * time.Second)), | ||||
| 						Content: "  \U0001F433  docker create image", | ||||
| 					}, | ||||
| 					{ | ||||
| 						Time:    timestamppb.New(now.Add(5 * time.Second)), | ||||
| 						Content: "job1", | ||||
| 					}, | ||||
| 					{ | ||||
| 						Time:    timestamppb.New(now.Add(6 * time.Second)), | ||||
| 						Content: "\U0001F3C1  Job succeeded", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			"job2": { | ||||
| 				result: runnerv1.Result_RESULT_SUCCESS, | ||||
| 				logRows: []*runnerv1.LogRow{ | ||||
| 					{ | ||||
| 						Time:    timestamppb.New(now.Add(4 * time.Second)), | ||||
| 						Content: "  \U0001F433  docker create image", | ||||
| 					}, | ||||
| 					{ | ||||
| 						Time:    timestamppb.New(now.Add(5 * time.Second)), | ||||
| 						Content: "job2", | ||||
| 					}, | ||||
| 					{ | ||||
| 						Time:    timestamppb.New(now.Add(6 * time.Second)), | ||||
| 						Content: "\U0001F3C1  Job succeeded", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			"job3": { | ||||
| 				result: runnerv1.Result_RESULT_SUCCESS, | ||||
| 				logRows: []*runnerv1.LogRow{ | ||||
| 					{ | ||||
| 						Time:    timestamppb.New(now.Add(4 * time.Second)), | ||||
| 						Content: "  \U0001F433  docker create image", | ||||
| 					}, | ||||
| 					{ | ||||
| 						Time:    timestamppb.New(now.Add(5 * time.Second)), | ||||
| 						Content: "job3", | ||||
| 					}, | ||||
| 					{ | ||||
| 						Time:    timestamppb.New(now.Add(6 * time.Second)), | ||||
| 						Content: "\U0001F3C1  Job succeeded", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		expectedStatuses: map[string]string{ | ||||
| 			"job1": actions_model.StatusSuccess.String(), | ||||
| 			"job2": actions_model.StatusSuccess.String(), | ||||
| 			"job3": actions_model.StatusSuccess.String(), | ||||
| 		}, | ||||
| 	} | ||||
| 	onGiteaRun(t, func(t *testing.T, u *url.URL) { | ||||
| 		user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) | ||||
| 		session := loginUser(t, user2.Name) | ||||
| 		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) | ||||
|  | ||||
| 		apiRepo := createActionsTestRepo(t, token, "actions-delete-run-test", false) | ||||
| 		runner := newMockRunner() | ||||
| 		runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"}, false) | ||||
|  | ||||
| 		opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, "create "+testCase.treePath, testCase.fileContent) | ||||
| 		createWorkflowFile(t, token, user2.Name, apiRepo.Name, testCase.treePath, opts) | ||||
|  | ||||
| 		runIndex := "" | ||||
| 		for i := 0; i < len(testCase.outcomes); i++ { | ||||
| 			task := runner.fetchTask(t) | ||||
| 			jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id) | ||||
| 			outcome := testCase.outcomes[jobName] | ||||
| 			assert.NotNil(t, outcome) | ||||
| 			runner.execTask(t, task, outcome) | ||||
| 			runIndex = task.Context.GetFields()["run_number"].GetStringValue() | ||||
| 			assert.Equal(t, "1", runIndex) | ||||
| 		} | ||||
|  | ||||
| 		for i := 0; i < len(testCase.outcomes); i++ { | ||||
| 			req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d", user2.Name, apiRepo.Name, runIndex, i), map[string]string{ | ||||
| 				"_csrf": GetUserCSRFToken(t, session), | ||||
| 			}) | ||||
| 			resp := session.MakeRequest(t, req, http.StatusOK) | ||||
| 			var listResp actions.ViewResponse | ||||
| 			err := json.Unmarshal(resp.Body.Bytes(), &listResp) | ||||
| 			assert.NoError(t, err) | ||||
| 			assert.Len(t, listResp.State.Run.Jobs, 3) | ||||
|  | ||||
| 			req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d/logs", user2.Name, apiRepo.Name, runIndex, i)). | ||||
| 				AddTokenAuth(token) | ||||
| 			MakeRequest(t, req, http.StatusOK) | ||||
| 		} | ||||
|  | ||||
| 		req := NewRequestWithValues(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s", user2.Name, apiRepo.Name, runIndex), map[string]string{ | ||||
| 			"_csrf": GetUserCSRFToken(t, session), | ||||
| 		}) | ||||
| 		session.MakeRequest(t, req, http.StatusOK) | ||||
|  | ||||
| 		req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/delete", user2.Name, apiRepo.Name, runIndex), map[string]string{ | ||||
| 			"_csrf": GetUserCSRFToken(t, session), | ||||
| 		}) | ||||
| 		session.MakeRequest(t, req, http.StatusOK) | ||||
|  | ||||
| 		req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/delete", user2.Name, apiRepo.Name, runIndex), map[string]string{ | ||||
| 			"_csrf": GetUserCSRFToken(t, session), | ||||
| 		}) | ||||
| 		session.MakeRequest(t, req, http.StatusNotFound) | ||||
|  | ||||
| 		req = NewRequestWithValues(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s", user2.Name, apiRepo.Name, runIndex), map[string]string{ | ||||
| 			"_csrf": GetUserCSRFToken(t, session), | ||||
| 		}) | ||||
| 		session.MakeRequest(t, req, http.StatusNotFound) | ||||
|  | ||||
| 		for i := 0; i < len(testCase.outcomes); i++ { | ||||
| 			req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d", user2.Name, apiRepo.Name, runIndex, i), map[string]string{ | ||||
| 				"_csrf": GetUserCSRFToken(t, session), | ||||
| 			}) | ||||
| 			session.MakeRequest(t, req, http.StatusNotFound) | ||||
|  | ||||
| 			req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d/logs", user2.Name, apiRepo.Name, runIndex, i)). | ||||
| 				AddTokenAuth(token) | ||||
| 			MakeRequest(t, req, http.StatusNotFound) | ||||
| 		} | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										98
									
								
								tests/integration/api_actions_delete_run_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								tests/integration/api_actions_delete_run_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | ||||
| // Copyright 2025 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
|  | ||||
| package integration | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"testing" | ||||
|  | ||||
| 	auth_model "code.gitea.io/gitea/models/auth" | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	"code.gitea.io/gitea/models/unittest" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/json" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func TestAPIActionsDeleteRunCheckPermission(t *testing.T) { | ||||
| 	defer prepareTestEnvActionsArtifacts(t)() | ||||
|  | ||||
| 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) | ||||
| 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) | ||||
| 	session := loginUser(t, user.Name) | ||||
| 	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) | ||||
| 	testAPIActionsDeleteRun(t, repo, token, http.StatusNotFound) | ||||
| } | ||||
|  | ||||
| func TestAPIActionsDeleteRun(t *testing.T) { | ||||
| 	defer prepareTestEnvActionsArtifacts(t)() | ||||
|  | ||||
| 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) | ||||
| 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) | ||||
| 	session := loginUser(t, user.Name) | ||||
| 	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) | ||||
|  | ||||
| 	testAPIActionsDeleteRunListArtifacts(t, repo, token, 2) | ||||
| 	testAPIActionsDeleteRunListTasks(t, repo, token, true) | ||||
| 	testAPIActionsDeleteRun(t, repo, token, http.StatusNoContent) | ||||
|  | ||||
| 	testAPIActionsDeleteRunListArtifacts(t, repo, token, 0) | ||||
| 	testAPIActionsDeleteRunListTasks(t, repo, token, false) | ||||
| 	testAPIActionsDeleteRun(t, repo, token, http.StatusNotFound) | ||||
| } | ||||
|  | ||||
| func TestAPIActionsDeleteRunRunning(t *testing.T) { | ||||
| 	defer prepareTestEnvActionsArtifacts(t)() | ||||
|  | ||||
| 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) | ||||
| 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) | ||||
| 	session := loginUser(t, user.Name) | ||||
| 	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) | ||||
|  | ||||
| 	req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/actions/runs/793", repo.FullName())). | ||||
| 		AddTokenAuth(token) | ||||
| 	MakeRequest(t, req, http.StatusBadRequest) | ||||
| } | ||||
|  | ||||
| func testAPIActionsDeleteRun(t *testing.T, repo *repo_model.Repository, token string, expected int) { | ||||
| 	req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795", repo.FullName())). | ||||
| 		AddTokenAuth(token) | ||||
| 	MakeRequest(t, req, expected) | ||||
| } | ||||
|  | ||||
| func testAPIActionsDeleteRunListArtifacts(t *testing.T, repo *repo_model.Repository, token string, artifacts int) { | ||||
| 	req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/artifacts", repo.FullName())). | ||||
| 		AddTokenAuth(token) | ||||
| 	resp := MakeRequest(t, req, http.StatusOK) | ||||
| 	var listResp api.ActionArtifactsResponse | ||||
| 	err := json.Unmarshal(resp.Body.Bytes(), &listResp) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Len(t, listResp.Entries, artifacts) | ||||
| } | ||||
|  | ||||
| func testAPIActionsDeleteRunListTasks(t *testing.T, repo *repo_model.Repository, token string, expected bool) { | ||||
| 	req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/tasks", repo.FullName())). | ||||
| 		AddTokenAuth(token) | ||||
| 	resp := MakeRequest(t, req, http.StatusOK) | ||||
| 	var listResp api.ActionTaskResponse | ||||
| 	err := json.Unmarshal(resp.Body.Bytes(), &listResp) | ||||
| 	assert.NoError(t, err) | ||||
| 	findTask1 := false | ||||
| 	findTask2 := false | ||||
| 	for _, entry := range listResp.Entries { | ||||
| 		if entry.ID == 53 { | ||||
| 			findTask1 = true | ||||
| 			continue | ||||
| 		} | ||||
| 		if entry.ID == 54 { | ||||
| 			findTask2 = true | ||||
| 			continue | ||||
| 		} | ||||
| 	} | ||||
| 	assert.Equal(t, expected, findTask1) | ||||
| 	assert.Equal(t, expected, findTask2) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user