mirror of
https://github.com/go-gitea/gitea.git
synced 2026-06-17 20:27:10 +02:00
Merge cbca86372d044bc8259eb119c06192eb8acf8e7c into c68925152b1b6c8f92806cdbda9c4672dcc1608f
This commit is contained in:
commit
fe667debfb
@ -98,7 +98,14 @@ func (opts FindRunOptions) ToConds() builder.Cond {
|
||||
func (opts FindRunOptions) ToJoins() []db.JoinFunc {
|
||||
if opts.OwnerID > 0 {
|
||||
return []db.JoinFunc{func(sess db.Engine) error {
|
||||
sess.Join("INNER", "repository", "repository.id = repo_id AND repository.owner_id = ?", opts.OwnerID)
|
||||
sess.Join("INNER", "repository", "repository.id = action_run.repo_id AND repository.owner_id = ?", opts.OwnerID)
|
||||
return nil
|
||||
}}
|
||||
}
|
||||
if opts.RepoID == 0 {
|
||||
// Exclude runs whose repository has been deleted.
|
||||
return []db.JoinFunc{func(sess db.Engine) error {
|
||||
sess.Join("INNER", "repository", "repository.id = action_run.repo_id")
|
||||
return nil
|
||||
}}
|
||||
}
|
||||
|
||||
@ -130,6 +130,8 @@ type ActionWorkflowRun struct {
|
||||
Conclusion string `json:"conclusion,omitempty"`
|
||||
PullRequests []*PullRequestMinimal `json:"pull_requests"`
|
||||
// swagger:strfmt date-time
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
// swagger:strfmt date-time
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
// swagger:strfmt date-time
|
||||
CompletedAt time.Time `json:"completed_at"`
|
||||
|
||||
@ -1341,8 +1341,14 @@ func Routes() *web.Router {
|
||||
m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun)
|
||||
m.Post("/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowRun)
|
||||
m.Post("/rerun-failed-jobs", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunFailedWorkflowRun)
|
||||
m.Get("/jobs", repo.ListWorkflowRunJobs)
|
||||
m.Post("/jobs/{job_id}/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowJob)
|
||||
m.Post("/cancel", reqToken(), reqRepoWriter(unit.TypeActions), repo.CancelWorkflowRun)
|
||||
m.Post("/approve", reqToken(), reqRepoWriter(unit.TypeActions), repo.ApproveWorkflowRun)
|
||||
m.Group("/jobs", func() {
|
||||
m.Get("", repo.ListWorkflowRunJobs)
|
||||
m.Get("/{job_id}/logs", reqToken(), reqRepoReader(unit.TypeActions), repo.GetWorkflowJobLogs)
|
||||
m.Post("/{job_id}/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowJob)
|
||||
})
|
||||
m.Get("/logs", reqToken(), reqRepoReader(unit.TypeActions), repo.GetWorkflowRunLogs)
|
||||
m.Get("/artifacts", repo.GetArtifactsOfRun)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1298,6 +1298,16 @@ func getCurrentRepoActionRunAttemptByNumber(ctx *context.APIContext) (*actions_m
|
||||
return run, attempt
|
||||
}
|
||||
|
||||
func respondRepoActionWorkflowRun(ctx *context.APIContext, run *actions_model.ActionRun) {
|
||||
run.Repo = ctx.Repo.Repository
|
||||
convertedRun, err := convert.ToActionWorkflowRun(ctx, run, nil, false)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convertedRun)
|
||||
}
|
||||
|
||||
// GetWorkflowRun Gets a specific workflow run.
|
||||
func GetWorkflowRun(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run} repository GetWorkflowRun
|
||||
@ -1334,12 +1344,7 @@ func GetWorkflowRun(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
convertedRun, err := convert.ToActionWorkflowRun(ctx, run, nil, false)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convertedRun)
|
||||
respondRepoActionWorkflowRun(ctx, run)
|
||||
}
|
||||
|
||||
// GetWorkflowRunAttempt Gets a specific workflow run attempt.
|
||||
|
||||
@ -6,6 +6,7 @@ package repo
|
||||
import (
|
||||
actions_model "gitea.dev/models/actions"
|
||||
"gitea.dev/routers/common"
|
||||
actions_service "gitea.dev/services/actions"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
@ -45,13 +46,196 @@ func DownloadActionsRunJobLogs(ctx *context.APIContext) {
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
if err = curJob.LoadRepo(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
err = common.DownloadActionsRunJobLogs(ctx.Base, ctx.Repo.Repository, curJob)
|
||||
if err != nil {
|
||||
ctx.APIErrorAuto(err)
|
||||
}
|
||||
}
|
||||
|
||||
func CancelWorkflowRun(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/actions/runs/{run}/cancel repository cancelWorkflowRun
|
||||
// ---
|
||||
// summary: Cancel a workflow run and its jobs
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repository
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: run
|
||||
// in: path
|
||||
// description: run ID
|
||||
// type: integer
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// description: success
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
run, jobs := getCurrentRepoActionRunJobsByID(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := actions_service.CancelRun(ctx, run, jobs); err != nil {
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
run = getCurrentRepoActionRunByID(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
respondRepoActionWorkflowRun(ctx, run)
|
||||
}
|
||||
|
||||
func ApproveWorkflowRun(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/actions/runs/{run}/approve repository approveWorkflowRun
|
||||
// ---
|
||||
// summary: Approve a workflow run that requires approval
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repository
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: run
|
||||
// in: path
|
||||
// description: run ID
|
||||
// type: integer
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// description: success
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
run := getCurrentRepoActionRunByID(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
// GitHub-compatible: return 200 if already approved (idempotent)
|
||||
if !run.NeedApproval {
|
||||
respondRepoActionWorkflowRun(ctx, run)
|
||||
return
|
||||
}
|
||||
|
||||
if err := actions_service.ApproveRuns(ctx, ctx.Repo.Repository, ctx.Doer, []int64{run.ID}); err != nil {
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
run = getCurrentRepoActionRunByID(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
respondRepoActionWorkflowRun(ctx, run)
|
||||
}
|
||||
|
||||
func GetWorkflowRunLogs(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/logs repository getWorkflowRunLogs
|
||||
// ---
|
||||
// summary: Download workflow run logs as archive
|
||||
// produces:
|
||||
// - application/zip
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repository
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: run
|
||||
// in: path
|
||||
// description: run ID
|
||||
// type: integer
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// description: Logs archive
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
run := getCurrentRepoActionRunByID(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := common.DownloadActionsRunAllJobLogs(ctx.Base, ctx.Repo.Repository, run.ID); err != nil {
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func GetWorkflowJobLogs(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/jobs/{job_id}/logs repository getWorkflowJobLogs
|
||||
// ---
|
||||
// summary: Download job logs as plain text
|
||||
// produces:
|
||||
// - text/plain
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repository
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: run
|
||||
// in: path
|
||||
// description: run ID
|
||||
// type: integer
|
||||
// required: true
|
||||
// - name: job_id
|
||||
// in: path
|
||||
// description: id of the job
|
||||
// type: integer
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// description: Job logs
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
run := getCurrentRepoActionRunByID(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
jobID := ctx.PathParamInt64("job_id")
|
||||
if err := common.DownloadActionsRunJobLogsWithID(ctx.Base, ctx.Repo.Repository, run.ID, jobID); err != nil {
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,8 +4,11 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"strings"
|
||||
|
||||
@ -13,59 +16,170 @@ import (
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/actions"
|
||||
"gitea.dev/modules/httplib"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
context_module "gitea.dev/services/context"
|
||||
)
|
||||
|
||||
func DownloadActionsRunJobLogsWithID(ctx *context.Base, ctxRepo *repo_model.Repository, runID, jobID int64) error {
|
||||
var (
|
||||
workflowNameReplacer = strings.NewReplacer(`"`, "", "\r", "", "\n", "", "/", "-", `\`, "-")
|
||||
jobNameReplacer = strings.NewReplacer("/", "-", `\`, "-", "..", "__")
|
||||
)
|
||||
|
||||
func sanitizeWorkflowFileName(workflowID string) string {
|
||||
if p := strings.Index(workflowID, "."); p > 0 {
|
||||
workflowID = workflowID[:p]
|
||||
}
|
||||
return workflowNameReplacer.Replace(workflowID)
|
||||
}
|
||||
|
||||
func sanitizeJobFileName(name string) string {
|
||||
return jobNameReplacer.Replace(name)
|
||||
}
|
||||
|
||||
func jobLogFileName(workflowID, jobName string, taskID int64) string {
|
||||
return fmt.Sprintf("%s-%s-%d.log", sanitizeWorkflowFileName(workflowID), sanitizeJobFileName(jobName), taskID)
|
||||
}
|
||||
|
||||
func resolveJobLogTask(ctx context.Context, job *actions_model.ActionRunJob) (*actions_model.ActionTask, error) {
|
||||
taskID := job.EffectiveTaskID()
|
||||
if taskID == 0 {
|
||||
return nil, util.NewNotExistErrorf("job not started")
|
||||
}
|
||||
|
||||
task, err := actions_model.GetTaskByID(ctx, taskID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetTaskByID: %w", err)
|
||||
}
|
||||
|
||||
if task.LogExpired {
|
||||
return nil, util.NewNotExistErrorf("logs have been cleaned up")
|
||||
}
|
||||
if task.LogLength == 0 {
|
||||
return nil, util.NewNotExistErrorf("logs not found")
|
||||
}
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func openTaskLogs(ctx context.Context, task *actions_model.ActionTask) (io.ReadSeekCloser, error) {
|
||||
reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) || errors.Is(err, util.ErrNotExist) {
|
||||
return nil, util.NewNotExistErrorf("logs not found")
|
||||
}
|
||||
return nil, fmt.Errorf("OpenLogs: %w", err)
|
||||
}
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
func openJobTaskLogs(ctx context.Context, job *actions_model.ActionRunJob) (io.ReadSeekCloser, *actions_model.ActionTask, error) {
|
||||
task, err := resolveJobLogTask(ctx, job)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
reader, err := openTaskLogs(ctx, task)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return reader, task, nil
|
||||
}
|
||||
|
||||
func appendJobLogToZip(ctx context.Context, zipWriter *zip.Writer, workflowID string, job *actions_model.ActionRunJob, task *actions_model.ActionTask) error {
|
||||
reader, err := openTaskLogs(ctx, task)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
zipFile, err := zipWriter.Create(jobLogFileName(workflowID, job.Name, task.ID))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Create zip entry for job %d: %w", job.ID, err)
|
||||
}
|
||||
if _, err = io.Copy(zipFile, reader); err != nil {
|
||||
return fmt.Errorf("Write job %d logs to zip: %w", job.ID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DownloadActionsRunJobLogsWithID(ctx *context_module.Base, ctxRepo *repo_model.Repository, runID, jobID int64) error {
|
||||
job, err := actions_model.GetRunJobByRunAndID(ctx, runID, jobID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := job.LoadRepo(ctx); err != nil {
|
||||
return fmt.Errorf("LoadRepo: %w", err)
|
||||
}
|
||||
return DownloadActionsRunJobLogs(ctx, ctxRepo, job)
|
||||
}
|
||||
|
||||
func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository, curJob *actions_model.ActionRunJob) error {
|
||||
if curJob.Repo.ID != ctxRepo.ID {
|
||||
return util.NewNotExistErrorf("job not found")
|
||||
func DownloadActionsRunAllJobLogs(ctx *context_module.Base, ctxRepo *repo_model.Repository, runID int64) error {
|
||||
runJobs, err := actions_model.GetLatestAttemptJobsByRepoAndRunID(ctx, ctxRepo.ID, runID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetLatestAttemptJobsByRepoAndRunID: %w", err)
|
||||
}
|
||||
|
||||
taskID := curJob.EffectiveTaskID()
|
||||
if taskID == 0 {
|
||||
return util.NewNotExistErrorf("job not started")
|
||||
if len(runJobs) == 0 {
|
||||
return util.NewNotExistErrorf("no jobs found for run %d", runID)
|
||||
}
|
||||
|
||||
if err := runJobs[0].LoadRun(ctx); err != nil {
|
||||
return fmt.Errorf("LoadRun: %w", err)
|
||||
}
|
||||
workflowID := runJobs[0].Run.WorkflowID
|
||||
|
||||
type jobLogEntry struct {
|
||||
job *actions_model.ActionRunJob
|
||||
task *actions_model.ActionTask
|
||||
}
|
||||
logEntries := make([]jobLogEntry, 0, len(runJobs))
|
||||
for _, job := range runJobs {
|
||||
task, err := resolveJobLogTask(ctx, job)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
logEntries = append(logEntries, jobLogEntry{job: job, task: task})
|
||||
}
|
||||
if len(logEntries) == 0 {
|
||||
return util.NewNotExistErrorf("logs not found")
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set("Content-Type", "application/zip")
|
||||
ctx.Resp.Header().Set("Content-Disposition", httplib.EncodeContentDispositionAttachment(
|
||||
fmt.Sprintf("%s-run-%d-logs.zip", sanitizeWorkflowFileName(workflowID), runID),
|
||||
))
|
||||
|
||||
zipWriter := zip.NewWriter(ctx.Resp)
|
||||
defer zipWriter.Close()
|
||||
|
||||
// Best-effort: the response headers and zip stream are already committed, so a
|
||||
// failure to read one job's logs must not abort the whole archive. Log and skip.
|
||||
for _, entry := range logEntries {
|
||||
if err := appendJobLogToZip(ctx, zipWriter, workflowID, entry.job, entry.task); err != nil {
|
||||
log.Error("Failed to add logs for job %d to zip: %v", entry.job.ID, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DownloadActionsRunJobLogs(ctx *context_module.Base, ctxRepo *repo_model.Repository, curJob *actions_model.ActionRunJob) error {
|
||||
if curJob.RepoID != ctxRepo.ID {
|
||||
return util.NewNotExistErrorf("job not found")
|
||||
}
|
||||
|
||||
if err := curJob.LoadRun(ctx); err != nil {
|
||||
return fmt.Errorf("LoadRun: %w", err)
|
||||
}
|
||||
|
||||
task, err := actions_model.GetTaskByID(ctx, taskID)
|
||||
reader, task, err := openJobTaskLogs(ctx, curJob)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetTaskByID: %w", err)
|
||||
}
|
||||
|
||||
if task.LogExpired {
|
||||
return util.NewNotExistErrorf("logs have been cleaned up")
|
||||
}
|
||||
|
||||
reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return util.NewNotExistErrorf("logs not found")
|
||||
}
|
||||
return fmt.Errorf("OpenLogs: %w", err)
|
||||
return err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
workflowName := curJob.Run.WorkflowID
|
||||
if p := strings.Index(workflowName, "."); p > 0 {
|
||||
workflowName = workflowName[0:p]
|
||||
}
|
||||
ctx.ServeContent(reader, context.ServeHeaderOptions{
|
||||
Filename: fmt.Sprintf("%v-%v-%v.log", workflowName, curJob.Name, task.ID),
|
||||
ctx.ServeContent(reader, context_module.ServeHeaderOptions{
|
||||
Filename: jobLogFileName(curJob.Run.WorkflowID, curJob.Name, task.ID),
|
||||
ContentLength: &task.LogSize,
|
||||
ContentType: "text/plain; charset=utf-8",
|
||||
ContentDisposition: httplib.ContentDispositionAttachment,
|
||||
|
||||
@ -950,27 +950,10 @@ func Cancel(ctx *context_module.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var updatedJobs []*actions_model.ActionRunJob
|
||||
|
||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
cancelledJobs, err := actions_model.CancelJobs(ctx, jobs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cancel jobs: %w", err)
|
||||
}
|
||||
updatedJobs = append(updatedJobs, cancelledJobs...)
|
||||
return nil
|
||||
}); err != nil {
|
||||
ctx.ServerError("StopTask", err)
|
||||
if err := actions_service.CancelRun(ctx, run, jobs); err != nil {
|
||||
ctx.ServerError("CancelRun", err)
|
||||
return
|
||||
}
|
||||
|
||||
actions_service.CreateCommitStatusForRunJobs(ctx, run, jobs...)
|
||||
actions_service.EmitJobsIfReadyByJobs(updatedJobs)
|
||||
|
||||
actions_service.NotifyWorkflowJobsStatusUpdate(ctx, updatedJobs...)
|
||||
if len(updatedJobs) > 0 {
|
||||
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, run.RepoID, run.ID)
|
||||
}
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
|
||||
36
services/actions/cancel.go
Normal file
36
services/actions/cancel.go
Normal file
@ -0,0 +1,36 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
actions_model "gitea.dev/models/actions"
|
||||
"gitea.dev/models/db"
|
||||
)
|
||||
|
||||
// CancelRun cancels all cancellable jobs in a run, updates commit statuses,
|
||||
// and fires downstream notifications including job-emitter queue entries.
|
||||
func CancelRun(ctx context.Context, run *actions_model.ActionRun, jobs []*actions_model.ActionRunJob) error {
|
||||
var updatedJobs []*actions_model.ActionRunJob
|
||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
cancelled, err := actions_model.CancelJobs(ctx, jobs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("CancelJobs: %w", err)
|
||||
}
|
||||
updatedJobs = cancelled
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
CreateCommitStatusForRunJobs(ctx, run, jobs...)
|
||||
EmitJobsIfReadyByJobs(updatedJobs)
|
||||
NotifyWorkflowJobsStatusUpdate(ctx, updatedJobs...)
|
||||
if len(updatedJobs) > 0 {
|
||||
NotifyWorkflowRunStatusUpdateWithReload(ctx, run.RepoID, run.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -116,10 +116,10 @@ func TestToActionWorkflowRun_UsesTriggerEvent(t *testing.T) {
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 803})
|
||||
run.Repo = repo
|
||||
// Scheduled runs keep Event as the registration event (push) and use TriggerEvent as the real trigger.
|
||||
run.Event = "push"
|
||||
run.TriggerEvent = "schedule"
|
||||
run.Repo = repo
|
||||
|
||||
apiRun, err := ToActionWorkflowRun(t.Context(), run, nil, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -240,6 +240,7 @@ func ToActionTask(ctx context.Context, t *actions_model.ActionTask) (*api.Action
|
||||
if err := t.Job.Run.LoadRepo(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &api.ActionTask{
|
||||
ID: t.ID,
|
||||
Name: t.Job.Name,
|
||||
@ -250,7 +251,7 @@ func ToActionTask(ctx context.Context, t *actions_model.ActionTask) (*api.Action
|
||||
DisplayTitle: t.Job.Run.Title,
|
||||
Status: t.Status.String(),
|
||||
WorkflowID: t.Job.Run.WorkflowID,
|
||||
URL: httplib.MakeAbsoluteURL(ctx, t.Job.Run.Link()),
|
||||
URL: httplib.MakeAbsoluteURL(ctx, t.GetRunLink()),
|
||||
CreatedAt: t.Created.AsLocalTime(),
|
||||
UpdatedAt: t.Updated.AsLocalTime(),
|
||||
RunStartedAt: t.Started.AsLocalTime(),
|
||||
@ -263,9 +264,10 @@ func ToActionWorkflowRun(ctx context.Context, run *actions_model.ActionRun, atte
|
||||
}
|
||||
|
||||
if attempt == nil {
|
||||
attempt, _, err = run.GetLatestAttempt(ctx)
|
||||
if err != nil {
|
||||
if latestAttempt, has, err := run.GetLatestAttempt(ctx); err != nil {
|
||||
return nil, err
|
||||
} else if has {
|
||||
attempt = latestAttempt
|
||||
}
|
||||
}
|
||||
|
||||
@ -310,6 +312,7 @@ func ToActionWorkflowRun(ctx context.Context, run *actions_model.ActionRun, atte
|
||||
HTMLURL: run.HTMLURL(ctx),
|
||||
RunNumber: run.Index,
|
||||
RunAttempt: runAttempt,
|
||||
CreatedAt: run.Created.AsLocalTime(),
|
||||
StartedAt: startedAt,
|
||||
CompletedAt: completedAt,
|
||||
Event: run.TriggerEvent,
|
||||
|
||||
196
templates/swagger/v1_json.tmpl
generated
196
templates/swagger/v1_json.tmpl
generated
@ -5457,6 +5457,55 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/actions/runs/{run}/approve": {
|
||||
"post": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"repository"
|
||||
],
|
||||
"summary": "Approve a workflow run that requires approval",
|
||||
"operationId": "approveWorkflowRun",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "owner of the repo",
|
||||
"name": "owner",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the repository",
|
||||
"name": "repo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "run ID",
|
||||
"name": "run",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "success"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/responses/error"
|
||||
},
|
||||
"403": {
|
||||
"$ref": "#/responses/forbidden"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/actions/runs/{run}/artifacts": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@ -5633,6 +5682,55 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/actions/runs/{run}/cancel": {
|
||||
"post": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"repository"
|
||||
],
|
||||
"summary": "Cancel a workflow run and its jobs",
|
||||
"operationId": "cancelWorkflowRun",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "owner of the repo",
|
||||
"name": "owner",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the repository",
|
||||
"name": "repo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "run ID",
|
||||
"name": "run",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "success"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/responses/error"
|
||||
},
|
||||
"403": {
|
||||
"$ref": "#/responses/forbidden"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/actions/runs/{run}/jobs": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@ -5712,6 +5810,56 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/actions/runs/{run}/jobs/{job_id}/logs": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"text/plain"
|
||||
],
|
||||
"tags": [
|
||||
"repository"
|
||||
],
|
||||
"summary": "Download job logs as plain text",
|
||||
"operationId": "getWorkflowJobLogs",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "owner of the repo",
|
||||
"name": "owner",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the repository",
|
||||
"name": "repo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "run ID",
|
||||
"name": "run",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "id of the job",
|
||||
"name": "job_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Job logs"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/actions/runs/{run}/jobs/{job_id}/rerun": {
|
||||
"post": {
|
||||
"produces": [
|
||||
@ -5774,6 +5922,49 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/actions/runs/{run}/logs": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/zip"
|
||||
],
|
||||
"tags": [
|
||||
"repository"
|
||||
],
|
||||
"summary": "Download workflow run logs as archive",
|
||||
"operationId": "getWorkflowRunLogs",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "owner of the repo",
|
||||
"name": "owner",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the repository",
|
||||
"name": "repo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "run ID",
|
||||
"name": "run",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Logs archive"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/actions/runs/{run}/rerun": {
|
||||
"post": {
|
||||
"produces": [
|
||||
@ -22505,6 +22696,11 @@
|
||||
"type": "string",
|
||||
"x-go-name": "Conclusion"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"x-go-name": "CreatedAt"
|
||||
},
|
||||
"display_title": {
|
||||
"type": "string",
|
||||
"x-go-name": "DisplayTitle"
|
||||
|
||||
210
templates/swagger/v1_openapi3_json.tmpl
generated
210
templates/swagger/v1_openapi3_json.tmpl
generated
@ -2315,6 +2315,11 @@
|
||||
"type": "string",
|
||||
"x-go-name": "Conclusion"
|
||||
},
|
||||
"created_at": {
|
||||
"format": "date-time",
|
||||
"type": "string",
|
||||
"x-go-name": "CreatedAt"
|
||||
},
|
||||
"display_title": {
|
||||
"type": "string",
|
||||
"x-go-name": "DisplayTitle"
|
||||
@ -16582,6 +16587,58 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/actions/runs/{run}/approve": {
|
||||
"post": {
|
||||
"operationId": "approveWorkflowRun",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "owner of the repo",
|
||||
"in": "path",
|
||||
"name": "owner",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "name of the repository",
|
||||
"in": "path",
|
||||
"name": "repo",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "run ID",
|
||||
"in": "path",
|
||||
"name": "run",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "success"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/components/responses/error"
|
||||
},
|
||||
"403": {
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/components/responses/notFound"
|
||||
}
|
||||
},
|
||||
"summary": "Approve a workflow run that requires approval",
|
||||
"tags": [
|
||||
"repository"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/actions/runs/{run}/artifacts": {
|
||||
"get": {
|
||||
"operationId": "getArtifactsOfRun",
|
||||
@ -16779,6 +16836,58 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/actions/runs/{run}/cancel": {
|
||||
"post": {
|
||||
"operationId": "cancelWorkflowRun",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "owner of the repo",
|
||||
"in": "path",
|
||||
"name": "owner",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "name of the repository",
|
||||
"in": "path",
|
||||
"name": "repo",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "run ID",
|
||||
"in": "path",
|
||||
"name": "run",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "success"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/components/responses/error"
|
||||
},
|
||||
"403": {
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/components/responses/notFound"
|
||||
}
|
||||
},
|
||||
"summary": "Cancel a workflow run and its jobs",
|
||||
"tags": [
|
||||
"repository"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/actions/runs/{run}/jobs": {
|
||||
"get": {
|
||||
"operationId": "listWorkflowRunJobs",
|
||||
@ -16871,6 +16980,61 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/actions/runs/{run}/jobs/{job_id}/logs": {
|
||||
"get": {
|
||||
"operationId": "getWorkflowJobLogs",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "owner of the repo",
|
||||
"in": "path",
|
||||
"name": "owner",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "name of the repository",
|
||||
"in": "path",
|
||||
"name": "repo",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "run ID",
|
||||
"in": "path",
|
||||
"name": "run",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "id of the job",
|
||||
"in": "path",
|
||||
"name": "job_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Job logs"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/components/responses/notFound"
|
||||
}
|
||||
},
|
||||
"summary": "Download job logs as plain text",
|
||||
"tags": [
|
||||
"repository"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/actions/runs/{run}/jobs/{job_id}/rerun": {
|
||||
"post": {
|
||||
"operationId": "rerunWorkflowJob",
|
||||
@ -16938,6 +17102,52 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/actions/runs/{run}/logs": {
|
||||
"get": {
|
||||
"operationId": "getWorkflowRunLogs",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "owner of the repo",
|
||||
"in": "path",
|
||||
"name": "owner",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "name of the repository",
|
||||
"in": "path",
|
||||
"name": "repo",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "run ID",
|
||||
"in": "path",
|
||||
"name": "run",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Logs archive"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/components/responses/notFound"
|
||||
}
|
||||
},
|
||||
"summary": "Download workflow run logs as archive",
|
||||
"tags": [
|
||||
"repository"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/actions/runs/{run}/rerun": {
|
||||
"post": {
|
||||
"operationId": "rerunWorkflowRun",
|
||||
|
||||
@ -4,46 +4,31 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
actions_model "gitea.dev/models/actions"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/perm"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAPIActionsWorkflowRun(t *testing.T) {
|
||||
func TestAPIActionsGetWorkflowRun(t *testing.T) {
|
||||
defer prepareTestEnvActionsArtifacts(t)()
|
||||
t.Run("GetWorkflowRun", testAPIActionsGetWorkflowRun)
|
||||
t.Run("GetWorkflowJob", testAPIActionsGetWorkflowJob)
|
||||
t.Run("ListUserWorkflows", testAPIActionsListUserWorkflows)
|
||||
t.Run("ListRepoWorkflows", testAPIActionsListRepoWorkflows)
|
||||
t.Run("DeleteRunCheckPermission", testAPIActionsDeleteRunCheckPermission)
|
||||
t.Run("DeleteRunRunning", testAPIActionsDeleteRunRunning)
|
||||
t.Run("DeleteRunGeneral", testAPIActionsDeleteRunGeneral)
|
||||
|
||||
t.Run("RerunWorkflowRun", func(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
testAPIActionsRerunWorkflowRun(t)
|
||||
})
|
||||
t.Run("RerunWorkflowJob", func(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
testAPIActionsRerunWorkflowJob(t)
|
||||
})
|
||||
}
|
||||
|
||||
func testAPIActionsGetWorkflowRun(t *testing.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)
|
||||
@ -74,8 +59,10 @@ func testAPIActionsGetWorkflowRun(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/jobs", repo.FullName())).AddTokenAuth(token)
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/jobs", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
jobList := DecodeJSON(t, resp, &api.ActionWorkflowJobsResponse{})
|
||||
|
||||
job198Idx := slices.IndexFunc(jobList.Entries, func(job *api.ActionWorkflowJob) bool { return job.ID == 198 })
|
||||
@ -86,7 +73,9 @@ func testAPIActionsGetWorkflowRun(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func testAPIActionsGetWorkflowJob(t *testing.T) {
|
||||
func TestAPIActionsGetWorkflowJob(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)
|
||||
@ -103,7 +92,9 @@ func testAPIActionsGetWorkflowJob(t *testing.T) {
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func testAPIActionsDeleteRunCheckPermission(t *testing.T) {
|
||||
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)
|
||||
@ -111,7 +102,9 @@ func testAPIActionsDeleteRunCheckPermission(t *testing.T) {
|
||||
testAPIActionsDeleteRun(t, repo, token, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func testAPIActionsDeleteRunGeneral(t *testing.T) {
|
||||
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)
|
||||
@ -126,7 +119,9 @@ func testAPIActionsDeleteRunGeneral(t *testing.T) {
|
||||
testAPIActionsDeleteRun(t, repo, token, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func testAPIActionsDeleteRunRunning(t *testing.T) {
|
||||
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)
|
||||
@ -144,17 +139,18 @@ func testAPIActionsDeleteRun(t *testing.T, repo *repo_model.Repository, token st
|
||||
}
|
||||
|
||||
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)
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/artifacts", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
listResp := DecodeJSON(t, resp, &api.ActionArtifactsResponse{})
|
||||
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)
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/tasks", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
listResp := DecodeJSON(t, resp, &api.ActionTaskResponse{})
|
||||
|
||||
findTask1 := false
|
||||
findTask2 := false
|
||||
for _, entry := range listResp.Entries {
|
||||
@ -171,7 +167,9 @@ func testAPIActionsDeleteRunListTasks(t *testing.T, repo *repo_model.Repository,
|
||||
assert.Equal(t, expected, findTask2)
|
||||
}
|
||||
|
||||
func testAPIActionsRerunWorkflowRun(t *testing.T) {
|
||||
func TestAPIActionsRerunWorkflowRun(t *testing.T) {
|
||||
defer prepareTestEnvActionsArtifacts(t)()
|
||||
|
||||
t.Run("NotDone", func(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
@ -191,10 +189,11 @@ func testAPIActionsRerunWorkflowRun(t *testing.T) {
|
||||
readToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/rerun", repo.FullName())).AddTokenAuth(writeToken)
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/rerun", repo.FullName())).
|
||||
AddTokenAuth(writeToken)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
rerunResp := DecodeJSON(t, resp, &api.ActionWorkflowRun{})
|
||||
|
||||
rerunResp := DecodeJSON(t, resp, &api.ActionWorkflowRun{})
|
||||
assert.Equal(t, int64(795), rerunResp.ID)
|
||||
assert.Equal(t, "queued", rerunResp.Status)
|
||||
assert.Equal(t, "c2d72f548424103f01ee1dc02889c1e2bff816b0", rerunResp.HeadSha)
|
||||
@ -232,7 +231,160 @@ func testAPIActionsRerunWorkflowRun(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func testAPIActionsRerunWorkflowJob(t *testing.T) {
|
||||
func TestAPIActionsCancelWorkflowRun(t *testing.T) {
|
||||
defer prepareTestEnvActionsArtifacts(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
ownerSession := loginUser(t, owner.Name)
|
||||
ownerToken := getTokenForLoggedInUser(t, ownerSession, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/793/cancel", repo.FullName())).
|
||||
AddTokenAuth(ownerToken)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
cancelledRun := DecodeJSON(t, resp, &api.ActionWorkflowRun{})
|
||||
assert.Equal(t, int64(793), cancelledRun.ID)
|
||||
assert.Equal(t, "completed", cancelledRun.Status)
|
||||
assert.Equal(t, "cancelled", cancelledRun.Conclusion)
|
||||
})
|
||||
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/999999/cancel", repo.FullName())).
|
||||
AddTokenAuth(ownerToken)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("ForbiddenWithoutPermission", func(t *testing.T) {
|
||||
// user2 is not the owner of repo4 (owned by user5)
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user2Session := loginUser(t, user2.Name)
|
||||
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/793/cancel", repo.FullName())).
|
||||
AddTokenAuth(user2Token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIActionsApproveWorkflowRun(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
// user2 is the owner of the base repo
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user2Session := loginUser(t, user2.Name)
|
||||
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
// user4 is the owner of the fork repo
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
user4Token := getTokenForLoggedInUser(t, loginUser(t, user4.Name), auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
apiBaseRepo := createActionsTestRepo(t, user2Token, "approve-workflow-run", false)
|
||||
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiBaseRepo.ID})
|
||||
user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
defer doAPIDeleteRepository(user2APICtx)(t)
|
||||
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, baseRepo.OwnerName, baseRepo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
|
||||
|
||||
// init workflow
|
||||
wfTreePath := ".gitea/workflows/approve.yml"
|
||||
wfFileContent := `name: Approve
|
||||
on: pull_request
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo test
|
||||
`
|
||||
opts := getWorkflowCreateFileOptions(user2, baseRepo.DefaultBranch, "create %s"+wfTreePath, wfFileContent)
|
||||
createWorkflowFile(t, user2Token, baseRepo.OwnerName, baseRepo.Name, wfTreePath, opts)
|
||||
|
||||
// user4 forks the repo
|
||||
forkName := "approve-workflow-run-fork"
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseRepo.OwnerName, baseRepo.Name),
|
||||
&api.CreateForkOption{
|
||||
Name: &forkName,
|
||||
}).AddTokenAuth(user4Token)
|
||||
resp := MakeRequest(t, req, http.StatusAccepted)
|
||||
apiForkRepo := DecodeJSON(t, resp, &api.Repository{})
|
||||
forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiForkRepo.ID})
|
||||
user4APICtx := NewAPITestContext(t, user4.Name, forkRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
defer doAPIDeleteRepository(user4APICtx)(t)
|
||||
|
||||
// user4 creates a pull request from a branch
|
||||
doAPICreateFile(user4APICtx, "test.txt", &api.CreateFileOptions{
|
||||
FileOptions: api.FileOptions{
|
||||
NewBranchName: "feature/test",
|
||||
Message: "create test.txt",
|
||||
Author: api.Identity{
|
||||
Name: user4.Name,
|
||||
Email: user4.Email,
|
||||
},
|
||||
Committer: api.Identity{
|
||||
Name: user4.Name,
|
||||
Email: user4.Email,
|
||||
},
|
||||
Dates: api.CommitDateOptions{
|
||||
Author: time.Now(),
|
||||
Committer: time.Now(),
|
||||
},
|
||||
},
|
||||
ContentBase64: base64.StdEncoding.EncodeToString([]byte("test")),
|
||||
})(t)
|
||||
_, err := doAPICreatePullRequest(user4APICtx, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, user4.Name+":feature/test")(t)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// check run
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, TriggerUserID: user4.ID})
|
||||
assert.True(t, run.NeedApproval)
|
||||
assert.Equal(t, actions_model.StatusBlocked, run.Status)
|
||||
|
||||
// Test approve workflow run via API
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/%d/approve", baseRepo.FullName(), run.ID)).
|
||||
AddTokenAuth(user2Token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// Verify run was approved and jobs unblocked
|
||||
updatedRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run.ID})
|
||||
assert.False(t, updatedRun.NeedApproval)
|
||||
assert.Equal(t, user2.ID, updatedRun.ApprovedBy)
|
||||
jobs, err := actions_model.GetLatestAttemptJobsByRepoAndRunID(t.Context(), baseRepo.ID, run.ID)
|
||||
require.NoError(t, err)
|
||||
for _, job := range jobs {
|
||||
assert.Equal(t, actions_model.StatusWaiting, job.Status)
|
||||
}
|
||||
|
||||
// Test approve already approved run (idempotency — returns 200 like GitHub)
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/%d/approve", baseRepo.FullName(), run.ID)).
|
||||
AddTokenAuth(user2Token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
idempotentRun := DecodeJSON(t, resp, &api.ActionWorkflowRun{})
|
||||
assert.NotEqual(t, "waiting", idempotentRun.Status, "already-approved run should not be blocked")
|
||||
|
||||
// Test approve non-existent run
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/999999/approve", baseRepo.FullName())).
|
||||
AddTokenAuth(user2Token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
// Test approve by non-owner (user4 should get forbidden before being added as collaborator)
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/%d/approve", baseRepo.FullName(), run.ID)).
|
||||
AddTokenAuth(user4Token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
|
||||
// Add user4 as a collaborator with write access
|
||||
doAPIAddCollaborator(user2APICtx, user4.Name, perm.AccessModeWrite)(t)
|
||||
|
||||
// Test approve by writer-but-non-admin (user4 should now succeed)
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/%d/approve", baseRepo.FullName(), run.ID)).
|
||||
AddTokenAuth(user4Token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
approvedRun := DecodeJSON(t, resp, &api.ActionWorkflowRun{})
|
||||
assert.NotEqual(t, "waiting", approvedRun.Status, "approved run should not be blocked")
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIActionsRerunWorkflowJob(t *testing.T) {
|
||||
defer prepareTestEnvActionsArtifacts(t)()
|
||||
|
||||
t.Run("NotDone", func(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
@ -252,10 +404,11 @@ func testAPIActionsRerunWorkflowJob(t *testing.T) {
|
||||
readToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/jobs/199/rerun", repo.FullName())).AddTokenAuth(writeToken)
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/jobs/199/rerun", repo.FullName())).
|
||||
AddTokenAuth(writeToken)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
rerunResp := DecodeJSON(t, resp, &api.ActionWorkflowJob{})
|
||||
|
||||
rerunResp := DecodeJSON(t, resp, &api.ActionWorkflowJob{})
|
||||
job199Rerun := getLatestAttemptJobByTemplateJobID(t, 795, 199)
|
||||
assert.Equal(t, job199Rerun.ID, rerunResp.ID)
|
||||
assert.Equal(t, "queued", rerunResp.Status)
|
||||
@ -293,7 +446,9 @@ func testAPIActionsRerunWorkflowJob(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func testAPIActionsListUserWorkflows(t *testing.T) {
|
||||
func TestAPIActionsListUserWorkflows(t *testing.T) {
|
||||
defer prepareTestEnvActionsArtifacts(t)()
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
session := loginUser(t, user.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser)
|
||||
@ -353,7 +508,9 @@ func testAPIActionsListUserWorkflows(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func testAPIActionsListRepoWorkflows(t *testing.T) {
|
||||
func TestAPIActionsListRepoWorkflows(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)
|
||||
@ -372,3 +529,57 @@ func testAPIActionsListRepoWorkflows(t *testing.T) {
|
||||
assert.NotNil(t, run.TriggerActor, "trigger_actor should be populated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIActionsGetWorkflowRunLogs(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)
|
||||
|
||||
t.Run("NoLogs", func(t *testing.T) {
|
||||
// Run 795 has jobs but fixture tasks have no log output in storage.
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/logs", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("NoLogsAfterRerun", func(t *testing.T) {
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/rerun", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/logs", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/999999/logs", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIActionsGetWorkflowJobLogs(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)
|
||||
|
||||
t.Run("NoLogFile", func(t *testing.T) {
|
||||
// Job 198 exists but has no log file in the test fixture
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/jobs/198/logs", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("JobNotFound", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/jobs/999999/logs", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user