mirror of
https://github.com/go-gitea/gitea.git
synced 2025-11-02 22:03:27 +01:00
Merge 91bd7c536fe4168e801d0f365960f36af6ca472b into fe2599715730c385da38650903f3bc8400a4c919
This commit is contained in:
commit
d5e8dc523d
@ -105,8 +105,10 @@ func refreshAccesses(ctx context.Context, repo *repo_model.Repository, accessMap
|
|||||||
}
|
}
|
||||||
|
|
||||||
newAccesses := make([]Access, 0, len(accessMap))
|
newAccesses := make([]Access, 0, len(accessMap))
|
||||||
|
keysToDelete := []int64{}
|
||||||
for userID, ua := range accessMap {
|
for userID, ua := range accessMap {
|
||||||
if ua.Mode < minMode && !ua.User.IsRestricted {
|
if ua.Mode < minMode && !ua.User.IsRestricted {
|
||||||
|
keysToDelete = append(keysToDelete, userID)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,6 +118,9 @@ func refreshAccesses(ctx context.Context, repo *repo_model.Repository, accessMap
|
|||||||
Mode: ua.Mode,
|
Mode: ua.Mode,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
for _, uid := range keysToDelete {
|
||||||
|
delete(accessMap, uid)
|
||||||
|
}
|
||||||
|
|
||||||
// Delete old accesses and insert new ones for repository.
|
// Delete old accesses and insert new ones for repository.
|
||||||
if _, err = db.DeleteByBean(ctx, &Access{RepoID: repo.ID}); err != nil {
|
if _, err = db.DeleteByBean(ctx, &Access{RepoID: repo.ID}); err != nil {
|
||||||
|
|||||||
@ -123,6 +123,8 @@ type ActionWorkflowRun struct {
|
|||||||
HeadRepository *Repository `json:"head_repository,omitempty"`
|
HeadRepository *Repository `json:"head_repository,omitempty"`
|
||||||
Conclusion string `json:"conclusion,omitempty"`
|
Conclusion string `json:"conclusion,omitempty"`
|
||||||
// swagger:strfmt date-time
|
// swagger:strfmt date-time
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
// swagger:strfmt date-time
|
||||||
StartedAt time.Time `json:"started_at"`
|
StartedAt time.Time `json:"started_at"`
|
||||||
// swagger:strfmt date-time
|
// swagger:strfmt date-time
|
||||||
CompletedAt time.Time `json:"completed_at"`
|
CompletedAt time.Time `json:"completed_at"`
|
||||||
|
|||||||
@ -1272,7 +1272,17 @@ func Routes() *web.Router {
|
|||||||
m.Group("/{run}", func() {
|
m.Group("/{run}", func() {
|
||||||
m.Get("", repo.GetWorkflowRun)
|
m.Get("", repo.GetWorkflowRun)
|
||||||
m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun)
|
m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun)
|
||||||
m.Get("/jobs", repo.ListWorkflowRunJobs)
|
m.Post("/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowRun)
|
||||||
|
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.Post("/{job_id}/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowJob)
|
||||||
|
})
|
||||||
|
m.Group("/logs", func() {
|
||||||
|
m.Get("", repo.GetWorkflowRunLogs)
|
||||||
|
m.Post("", reqToken(), reqRepoReader(unit.TypeActions), repo.GetWorkflowRunLogsStream)
|
||||||
|
})
|
||||||
m.Get("/artifacts", repo.GetArtifactsOfRun)
|
m.Get("/artifacts", repo.GetArtifactsOfRun)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -4,12 +4,24 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
stdCtx "context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
actions_model "code.gitea.io/gitea/models/actions"
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/models/unit"
|
||||||
|
"code.gitea.io/gitea/modules/actions"
|
||||||
|
"code.gitea.io/gitea/modules/json"
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/routers/common"
|
"code.gitea.io/gitea/routers/common"
|
||||||
|
actions_service "code.gitea.io/gitea/services/actions"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
|
notify_service "code.gitea.io/gitea/services/notify"
|
||||||
|
|
||||||
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
func DownloadActionsRunJobLogs(ctx *context.APIContext) {
|
func DownloadActionsRunJobLogs(ctx *context.APIContext) {
|
||||||
@ -62,3 +74,775 @@ func DownloadActionsRunJobLogs(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RerunWorkflowRun(ctx *context.APIContext) {
|
||||||
|
// swagger:operation POST /repos/{owner}/{repo}/actions/runs/{run}/rerun repository rerunWorkflowRun
|
||||||
|
// ---
|
||||||
|
// summary: Rerun 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, err := getRunID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
ctx.APIError(404, "Run not found")
|
||||||
|
} else {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if workflow is disabled
|
||||||
|
cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
|
||||||
|
cfg := cfgUnit.ActionsConfig()
|
||||||
|
if cfg.IsWorkflowDisabled(run.WorkflowID) {
|
||||||
|
ctx.APIError(400, "Workflow is disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset run's start and stop time when it is done
|
||||||
|
if err := actions_service.ResetRunTimes(ctx, run); err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rerun all jobs
|
||||||
|
for _, job := range jobs {
|
||||||
|
// If the job has needs, it should be set to "blocked" status to wait for other jobs
|
||||||
|
shouldBlock := len(job.Needs) > 0
|
||||||
|
if err := actions_service.RerunJob(ctx, job, shouldBlock); err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
|
||||||
|
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
runID, _, err := getRunID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
ctx.APIError(404, "Run not found")
|
||||||
|
} else {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jobs, err := getRunJobsByRunID(ctx, runID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var updatedJobs []*actions_model.ActionRunJob
|
||||||
|
|
||||||
|
if err := db.WithTx(ctx, func(ctx stdCtx.Context) error {
|
||||||
|
for _, job := range jobs {
|
||||||
|
status := job.Status
|
||||||
|
if status.IsDone() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if job.TaskID == 0 {
|
||||||
|
job.Status = actions_model.StatusCancelled
|
||||||
|
job.Stopped = timeutil.TimeStampNow()
|
||||||
|
n, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
return errors.New("job has changed, try again")
|
||||||
|
}
|
||||||
|
if n > 0 {
|
||||||
|
updatedJobs = append(updatedJobs, job)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := actions_model.StopTask(ctx, job.TaskID, actions_model.StatusCancelled); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
actions_service.CreateCommitStatusForRunJobs(ctx, jobs[0].Run, jobs...)
|
||||||
|
|
||||||
|
for _, job := range updatedJobs {
|
||||||
|
_ = job.LoadAttributes(ctx)
|
||||||
|
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
||||||
|
}
|
||||||
|
if len(updatedJobs) > 0 {
|
||||||
|
job := updatedJobs[0]
|
||||||
|
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
|
||||||
|
notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
runID, _, err := getRunID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
ctx.APIError(404, "Run not found")
|
||||||
|
} else {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
current, jobs, err := getRunJobsAndCurrent(ctx, runID, -1)
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
run := current.Run
|
||||||
|
doer := ctx.Doer
|
||||||
|
|
||||||
|
var updatedJobs []*actions_model.ActionRunJob
|
||||||
|
|
||||||
|
if err := db.WithTx(ctx, func(ctx stdCtx.Context) error {
|
||||||
|
run.NeedApproval = false
|
||||||
|
run.ApprovedBy = doer.ID
|
||||||
|
if err := actions_model.UpdateRun(ctx, run, "need_approval", "approved_by"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, job := range jobs {
|
||||||
|
if len(job.Needs) == 0 && job.Status.IsBlocked() {
|
||||||
|
job.Status = actions_model.StatusWaiting
|
||||||
|
n, err := actions_model.UpdateRunJob(ctx, job, nil, "status")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if n > 0 {
|
||||||
|
updatedJobs = append(updatedJobs, job)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
actions_service.CreateCommitStatusForRunJobs(ctx, jobs[0].Run, jobs...)
|
||||||
|
|
||||||
|
if len(updatedJobs) > 0 {
|
||||||
|
job := updatedJobs[0]
|
||||||
|
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
|
||||||
|
notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, job := range updatedJobs {
|
||||||
|
_ = job.LoadAttributes(ctx)
|
||||||
|
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RerunWorkflowJob(ctx *context.APIContext) {
|
||||||
|
// swagger:operation POST /repos/{owner}/{repo}/actions/runs/{run}/jobs/{job_id}/rerun repository rerunWorkflowJob
|
||||||
|
// ---
|
||||||
|
// summary: Rerun a specific job and its dependent 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
|
||||||
|
// - name: job_id
|
||||||
|
// in: path
|
||||||
|
// description: id of the job
|
||||||
|
// type: integer
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// description: success
|
||||||
|
// "400":
|
||||||
|
// "$ref": "#/responses/error"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
runID, _, err := getRunID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
ctx.APIError(404, "Run not found")
|
||||||
|
} else {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jobID := ctx.PathParamInt64("job_id")
|
||||||
|
|
||||||
|
// Get all jobs for the run to handle dependencies
|
||||||
|
allJobs, err := getRunJobsByRunID(ctx, runID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the specific job in the list
|
||||||
|
var job *actions_model.ActionRunJob
|
||||||
|
for _, j := range allJobs {
|
||||||
|
if j.ID == jobID {
|
||||||
|
job = j
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if job == nil {
|
||||||
|
ctx.APIError(404, "Job not found in run")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get run from the job and check if workflow is disabled
|
||||||
|
run := allJobs[0].Run
|
||||||
|
|
||||||
|
cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
|
||||||
|
cfg := cfgUnit.ActionsConfig()
|
||||||
|
if cfg.IsWorkflowDisabled(run.WorkflowID) {
|
||||||
|
ctx.APIError(400, "Workflow is disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset run's start and stop time when it is done
|
||||||
|
if err := actions_service.ResetRunTimes(ctx, run); err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all jobs that need to be rerun (including dependencies)
|
||||||
|
rerunJobs := actions_service.GetAllRerunJobs(job, allJobs)
|
||||||
|
|
||||||
|
for _, j := range rerunJobs {
|
||||||
|
// Jobs other than the specified one should be set to "blocked" status
|
||||||
|
shouldBlock := j.JobID != job.JobID
|
||||||
|
if err := actions_service.RerunJob(ctx, j, shouldBlock); err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, j)
|
||||||
|
notify_service.WorkflowJobStatusUpdate(ctx, j.Run.Repo, j.Run.TriggerUser, j, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
func getRunID(ctx *context.APIContext) (int64, *actions_model.ActionRun, error) {
|
||||||
|
runID := ctx.PathParamInt64("run")
|
||||||
|
run, has, err := db.GetByID[actions_model.ActionRun](ctx, runID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
if !has || run.RepoID != ctx.Repo.Repository.ID {
|
||||||
|
return 0, nil, util.ErrNotExist
|
||||||
|
}
|
||||||
|
return runID, run, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRunJobsByRunID(ctx *context.APIContext, runID int64) ([]*actions_model.ActionRunJob, error) {
|
||||||
|
run, has, err := db.GetByID[actions_model.ActionRun](ctx, runID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !has || run.RepoID != ctx.Repo.Repository.ID {
|
||||||
|
return nil, util.ErrNotExist
|
||||||
|
}
|
||||||
|
run.Repo = ctx.Repo.Repository
|
||||||
|
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, v := range jobs {
|
||||||
|
v.Run = run
|
||||||
|
}
|
||||||
|
return jobs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRunJobsAndCurrent(ctx *context.APIContext, runID, jobIndex int64) (*actions_model.ActionRunJob, []*actions_model.ActionRunJob, error) {
|
||||||
|
jobs, err := getRunJobsByRunID(ctx, runID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if len(jobs) == 0 {
|
||||||
|
return nil, nil, util.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
if jobIndex >= 0 && jobIndex < int64(len(jobs)) {
|
||||||
|
return jobs[jobIndex], jobs, nil
|
||||||
|
}
|
||||||
|
return jobs[0], jobs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogCursor represents a log cursor position
|
||||||
|
type LogCursor struct {
|
||||||
|
Step int `json:"step"`
|
||||||
|
Cursor int64 `json:"cursor"`
|
||||||
|
Expanded bool `json:"expanded"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogRequest represents a log streaming request
|
||||||
|
type LogRequest struct {
|
||||||
|
LogCursors []LogCursor `json:"logCursors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogStepLine represents a single log line
|
||||||
|
type LogStepLine struct {
|
||||||
|
Index int64 `json:"index"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Timestamp float64 `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogStep represents logs for a workflow step
|
||||||
|
type LogStep struct {
|
||||||
|
Step int `json:"step"`
|
||||||
|
Cursor int64 `json:"cursor"`
|
||||||
|
Lines []*LogStepLine `json:"lines"`
|
||||||
|
Started int64 `json:"started"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogResponse represents the complete log response
|
||||||
|
type LogResponse struct {
|
||||||
|
StepsLog []*LogStep `json:"stepsLog"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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, err := getRunID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
ctx.APIError(404, "Run not found")
|
||||||
|
} else {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = common.DownloadActionsRunAllJobLogs(ctx.Base, ctx.Repo.Repository, run.ID); err != nil {
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
ctx.APIError(404, "Logs not found")
|
||||||
|
} else {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetWorkflowJobLogs(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/jobs/{job_id}/logs repository getWorkflowJobLogs
|
||||||
|
// ---
|
||||||
|
// summary: Download job logs
|
||||||
|
// 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
|
||||||
|
// - name: job_id
|
||||||
|
// in: path
|
||||||
|
// description: id of the job
|
||||||
|
// type: integer
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// description: Job logs
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
runID, _, err := getRunID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
ctx.APIError(404, "Run not found")
|
||||||
|
} else {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jobID := ctx.PathParamInt64("job_id")
|
||||||
|
|
||||||
|
// Get the job by ID and verify it belongs to the run
|
||||||
|
job, err := actions_model.GetRunJobByID(ctx, jobID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if job.RunID != runID {
|
||||||
|
ctx.APIError(404, "Job not found in this run")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = job.LoadRepo(ctx); err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = common.DownloadActionsRunJobLogs(ctx.Base, ctx.Repo.Repository, job); err != nil {
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
ctx.APIError(404, "Job logs not found")
|
||||||
|
} else {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetWorkflowRunLogsStream(ctx *context.APIContext) {
|
||||||
|
// swagger:operation POST /repos/{owner}/{repo}/actions/runs/{run}/logs repository getWorkflowRunLogsStream
|
||||||
|
// ---
|
||||||
|
// summary: Get streaming workflow run logs with cursor support
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// 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
|
||||||
|
// - name: job
|
||||||
|
// in: query
|
||||||
|
// description: job index (0-based), defaults to first job
|
||||||
|
// type: integer
|
||||||
|
// required: false
|
||||||
|
// - name: body
|
||||||
|
// in: body
|
||||||
|
// schema:
|
||||||
|
// type: object
|
||||||
|
// properties:
|
||||||
|
// logCursors:
|
||||||
|
// type: array
|
||||||
|
// items:
|
||||||
|
// type: object
|
||||||
|
// properties:
|
||||||
|
// step:
|
||||||
|
// type: integer
|
||||||
|
// cursor:
|
||||||
|
// type: integer
|
||||||
|
// expanded:
|
||||||
|
// type: boolean
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// description: Streaming logs
|
||||||
|
// schema:
|
||||||
|
// type: object
|
||||||
|
// properties:
|
||||||
|
// stepsLog:
|
||||||
|
// type: array
|
||||||
|
// items:
|
||||||
|
// type: object
|
||||||
|
// properties:
|
||||||
|
// step:
|
||||||
|
// type: integer
|
||||||
|
// cursor:
|
||||||
|
// type: integer
|
||||||
|
// lines:
|
||||||
|
// type: array
|
||||||
|
// items:
|
||||||
|
// type: object
|
||||||
|
// properties:
|
||||||
|
// index:
|
||||||
|
// type: integer
|
||||||
|
// message:
|
||||||
|
// type: string
|
||||||
|
// timestamp:
|
||||||
|
// type: number
|
||||||
|
// started:
|
||||||
|
// type: integer
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
runID, _, err := getRunID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
ctx.APIError(404, "Run not found")
|
||||||
|
} else {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jobIndex := int64(0)
|
||||||
|
if ctx.FormInt("job") > 0 {
|
||||||
|
jobIndex = int64(ctx.FormInt("job"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse log cursors from request body
|
||||||
|
var req LogRequest
|
||||||
|
if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil {
|
||||||
|
// If no body or invalid JSON, start with empty cursors
|
||||||
|
req = LogRequest{LogCursors: []LogCursor{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
current, _, err := getRunJobsAndCurrent(ctx, runID, jobIndex)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
ctx.APIError(404, "Run or job not found")
|
||||||
|
} else {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var task *actions_model.ActionTask
|
||||||
|
if current.TaskID > 0 {
|
||||||
|
task, err = actions_model.GetTaskByID(ctx, current.TaskID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
task.Job = current
|
||||||
|
if err := task.LoadAttributes(ctx); err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &LogResponse{
|
||||||
|
StepsLog: make([]*LogStep, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
if task != nil {
|
||||||
|
logs, err := convertToLogResponse(ctx, req.LogCursors, task)
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.StepsLog = append(response.StepsLog, logs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertToLogResponse(ctx *context.APIContext, cursors []LogCursor, task *actions_model.ActionTask) ([]*LogStep, error) {
|
||||||
|
var logs []*LogStep
|
||||||
|
steps := actions.FullSteps(task)
|
||||||
|
|
||||||
|
for _, cursor := range cursors {
|
||||||
|
if !cursor.Expanded {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if cursor.Step >= len(steps) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
step := steps[cursor.Step]
|
||||||
|
|
||||||
|
// if task log is expired, return a consistent log line
|
||||||
|
if task.LogExpired {
|
||||||
|
if cursor.Cursor == 0 {
|
||||||
|
logs = append(logs, &LogStep{
|
||||||
|
Step: cursor.Step,
|
||||||
|
Cursor: 1,
|
||||||
|
Lines: []*LogStepLine{
|
||||||
|
{
|
||||||
|
Index: 1,
|
||||||
|
Message: "Log has expired and is no longer available",
|
||||||
|
Timestamp: float64(task.Updated.AsTime().UnixNano()) / float64(time.Second),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Started: int64(step.Started),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logLines := make([]*LogStepLine, 0)
|
||||||
|
|
||||||
|
index := step.LogIndex + cursor.Cursor
|
||||||
|
validCursor := cursor.Cursor >= 0 &&
|
||||||
|
cursor.Cursor < step.LogLength &&
|
||||||
|
index < int64(len(task.LogIndexes))
|
||||||
|
|
||||||
|
if validCursor {
|
||||||
|
length := step.LogLength - cursor.Cursor
|
||||||
|
offset := task.LogIndexes[index]
|
||||||
|
logRows, err := actions.ReadLogs(ctx, task.LogInStorage, task.LogFilename, offset, length)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, row := range logRows {
|
||||||
|
logLines = append(logLines, &LogStepLine{
|
||||||
|
Index: cursor.Cursor + int64(i) + 1, // start at 1
|
||||||
|
Message: row.Content,
|
||||||
|
Timestamp: float64(row.Time.AsTime().UnixNano()) / float64(time.Second),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logs = append(logs, &LogStep{
|
||||||
|
Step: cursor.Step,
|
||||||
|
Cursor: cursor.Cursor + int64(len(logLines)),
|
||||||
|
Lines: logLines,
|
||||||
|
Started: int64(step.Started),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return logs, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -46,3 +46,17 @@ type swaggerResponseActionWorkflowList struct {
|
|||||||
// in:body
|
// in:body
|
||||||
Body api.ActionWorkflowResponse `json:"body"`
|
Body api.ActionWorkflowResponse `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WorkflowRunRerunRequest
|
||||||
|
// swagger:model WorkflowRunRerunRequest
|
||||||
|
type swaggerWorkflowRunRerunRequest struct {
|
||||||
|
// Enable debug logging for the re-run
|
||||||
|
EnableDebugLogging bool `json:"enable_debug_logging"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorkflowRunLogsRequest
|
||||||
|
// swagger:model WorkflowRunLogsRequest
|
||||||
|
type swaggerWorkflowRunLogsRequest struct {
|
||||||
|
// Log cursors for incremental log streaming
|
||||||
|
LogCursors []map[string]any `json:"logCursors"`
|
||||||
|
}
|
||||||
|
|||||||
@ -4,7 +4,9 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/zip"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
actions_model "code.gitea.io/gitea/models/actions"
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
@ -28,6 +30,77 @@ func DownloadActionsRunJobLogsWithIndex(ctx *context.Base, ctxRepo *repo_model.R
|
|||||||
return DownloadActionsRunJobLogs(ctx, ctxRepo, runJobs[jobIndex])
|
return DownloadActionsRunJobLogs(ctx, ctxRepo, runJobs[jobIndex])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DownloadActionsRunAllJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository, runID int64) error {
|
||||||
|
runJobs, err := actions_model.GetRunJobsByRunID(ctx, runID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("GetRunJobsByRunID: %w", err)
|
||||||
|
}
|
||||||
|
if err = runJobs.LoadRepos(ctx); err != nil {
|
||||||
|
return fmt.Errorf("LoadRepos: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runJobs) == 0 {
|
||||||
|
return util.NewNotExistErrorf("no jobs found for run %d", runID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load run for workflow name
|
||||||
|
if err := runJobs[0].LoadRun(ctx); err != nil {
|
||||||
|
return fmt.Errorf("LoadRun: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
workflowName := runJobs[0].Run.WorkflowID
|
||||||
|
if p := strings.Index(workflowName, "."); p > 0 {
|
||||||
|
workflowName = workflowName[0:p]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set headers for zip download
|
||||||
|
ctx.Resp.Header().Set("Content-Type", "application/zip")
|
||||||
|
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s-run-%d-logs.zip\"", workflowName, runID))
|
||||||
|
|
||||||
|
// Create zip writer
|
||||||
|
zipWriter := zip.NewWriter(ctx.Resp)
|
||||||
|
defer zipWriter.Close()
|
||||||
|
|
||||||
|
// Add each job's logs to the zip
|
||||||
|
for _, job := range runJobs {
|
||||||
|
if job.TaskID == 0 {
|
||||||
|
continue // Skip jobs that haven't started
|
||||||
|
}
|
||||||
|
|
||||||
|
task, err := actions_model.GetTaskByID(ctx, job.TaskID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("GetTaskByID for job %d: %w", job.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if task.LogExpired {
|
||||||
|
continue // Skip expired logs
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("OpenLogs for job %d: %w", job.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create file in zip with job name and task ID
|
||||||
|
fileName := fmt.Sprintf("%s-%s-%d.log", workflowName, job.Name, task.ID)
|
||||||
|
zipFile, err := zipWriter.Create(fileName)
|
||||||
|
if err != nil {
|
||||||
|
reader.Close()
|
||||||
|
return fmt.Errorf("Create zip file %s: %w", fileName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy log content to zip file
|
||||||
|
if _, err := io.Copy(zipFile, reader); err != nil {
|
||||||
|
reader.Close()
|
||||||
|
return fmt.Errorf("Copy logs for job %d: %w", job.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository, curJob *actions_model.ActionRunJob) error {
|
func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository, curJob *actions_model.ActionRunJob) error {
|
||||||
if curJob.Repo.ID != ctxRepo.ID {
|
if curJob.Repo.ID != ctxRepo.ID {
|
||||||
return util.NewNotExistErrorf("job not found")
|
return util.NewNotExistErrorf("job not found")
|
||||||
|
|||||||
@ -36,7 +36,6 @@ import (
|
|||||||
|
|
||||||
"github.com/nektos/act/pkg/model"
|
"github.com/nektos/act/pkg/model"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
"xorm.io/builder"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func getRunIndex(ctx *context_module.Context) int64 {
|
func getRunIndex(ctx *context_module.Context) int64 {
|
||||||
@ -476,10 +475,11 @@ func Rerun(ctx *context_module.Context) {
|
|||||||
for _, j := range jobs {
|
for _, j := range jobs {
|
||||||
// if the job has needs, it should be set to "blocked" status to wait for other jobs
|
// if the job has needs, it should be set to "blocked" status to wait for other jobs
|
||||||
shouldBlockJob := len(j.Needs) > 0 || isRunBlocked
|
shouldBlockJob := len(j.Needs) > 0 || isRunBlocked
|
||||||
if err := rerunJob(ctx, j, shouldBlockJob); err != nil {
|
if err := actions_service.RerunJob(ctx, j, shouldBlockJob); err != nil {
|
||||||
ctx.ServerError("RerunJob", err)
|
ctx.ServerError("RerunJob", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
notify_service.WorkflowJobStatusUpdate(ctx, j.Run.Repo, j.Run.TriggerUser, j, nil)
|
||||||
}
|
}
|
||||||
ctx.JSONOK()
|
ctx.JSONOK()
|
||||||
return
|
return
|
||||||
@ -490,64 +490,16 @@ func Rerun(ctx *context_module.Context) {
|
|||||||
for _, j := range rerunJobs {
|
for _, j := range rerunJobs {
|
||||||
// jobs other than the specified one should be set to "blocked" status
|
// jobs other than the specified one should be set to "blocked" status
|
||||||
shouldBlockJob := j.JobID != job.JobID || isRunBlocked
|
shouldBlockJob := j.JobID != job.JobID || isRunBlocked
|
||||||
if err := rerunJob(ctx, j, shouldBlockJob); err != nil {
|
if err := actions_service.RerunJob(ctx, j, shouldBlockJob); err != nil {
|
||||||
ctx.ServerError("RerunJob", err)
|
ctx.ServerError("RerunJob", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
notify_service.WorkflowJobStatusUpdate(ctx, j.Run.Repo, j.Run.TriggerUser, j, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.JSONOK()
|
ctx.JSONOK()
|
||||||
}
|
}
|
||||||
|
|
||||||
func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shouldBlock bool) error {
|
|
||||||
status := job.Status
|
|
||||||
if !status.IsDone() || !job.Run.Status.IsDone() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
job.TaskID = 0
|
|
||||||
job.Status = util.Iif(shouldBlock, actions_model.StatusBlocked, actions_model.StatusWaiting)
|
|
||||||
job.Started = 0
|
|
||||||
job.Stopped = 0
|
|
||||||
|
|
||||||
job.ConcurrencyGroup = ""
|
|
||||||
job.ConcurrencyCancel = false
|
|
||||||
job.IsConcurrencyEvaluated = false
|
|
||||||
if err := job.LoadRun(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
vars, err := actions_model.GetVariablesOfRun(ctx, job.Run)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get run %d variables: %w", job.Run.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if job.RawConcurrency != "" && !shouldBlock {
|
|
||||||
err = actions_service.EvaluateJobConcurrencyFillModel(ctx, job.Run, job, vars)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("evaluate job concurrency: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
job.Status, err = actions_service.PrepareToStartJobWithConcurrency(ctx, job)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
|
||||||
updateCols := []string{"task_id", "status", "started", "stopped", "concurrency_group", "concurrency_cancel", "is_concurrency_evaluated"}
|
|
||||||
_, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": status}, updateCols...)
|
|
||||||
return err
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
actions_service.CreateCommitStatusForRunJobs(ctx, job.Run, job)
|
|
||||||
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Logs(ctx *context_module.Context) {
|
func Logs(ctx *context_module.Context) {
|
||||||
runIndex := getRunIndex(ctx)
|
runIndex := getRunIndex(ctx)
|
||||||
jobIndex := ctx.PathParamInt64("job")
|
jobIndex := ctx.PathParamInt64("job")
|
||||||
|
|||||||
@ -4,10 +4,77 @@
|
|||||||
package actions
|
package actions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
actions_model "code.gitea.io/gitea/models/actions"
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/modules/container"
|
"code.gitea.io/gitea/modules/container"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ResetRunTimes resets the start and stop times for a run when it is done, for rerun
|
||||||
|
func ResetRunTimes(ctx context.Context, run *actions_model.ActionRun) error {
|
||||||
|
if run.Status.IsDone() {
|
||||||
|
run.PreviousDuration = run.Duration()
|
||||||
|
run.Started = 0
|
||||||
|
run.Stopped = 0
|
||||||
|
return actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RerunJob reruns a job, handling concurrency and status updates
|
||||||
|
func RerunJob(ctx context.Context, job *actions_model.ActionRunJob, shouldBlock bool) error {
|
||||||
|
status := job.Status
|
||||||
|
if !status.IsDone() || !job.Run.Status.IsDone() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
job.TaskID = 0
|
||||||
|
job.Status = util.Iif(shouldBlock, actions_model.StatusBlocked, actions_model.StatusWaiting)
|
||||||
|
job.Started = 0
|
||||||
|
job.Stopped = 0
|
||||||
|
|
||||||
|
job.ConcurrencyGroup = ""
|
||||||
|
job.ConcurrencyCancel = false
|
||||||
|
job.IsConcurrencyEvaluated = false
|
||||||
|
if err := job.LoadRun(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
vars, err := actions_model.GetVariablesOfRun(ctx, job.Run)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get run %d variables: %w", job.Run.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if job.RawConcurrency != "" && !shouldBlock {
|
||||||
|
err = EvaluateJobConcurrencyFillModel(ctx, job.Run, job, vars)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("evaluate job concurrency: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
job.Status, err = PrepareToStartJobWithConcurrency(ctx, job)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
|
updateCols := []string{"task_id", "status", "started", "stopped", "concurrency_group", "concurrency_cancel", "is_concurrency_evaluated"}
|
||||||
|
_, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": status}, updateCols...)
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateCommitStatusForRunJobs(ctx, job.Run, job)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetAllRerunJobs get all jobs that need to be rerun when job should be rerun
|
// GetAllRerunJobs get all jobs that need to be rerun when job should be rerun
|
||||||
func GetAllRerunJobs(job *actions_model.ActionRunJob, allJobs []*actions_model.ActionRunJob) []*actions_model.ActionRunJob {
|
func GetAllRerunJobs(job *actions_model.ActionRunJob, allJobs []*actions_model.ActionRunJob) []*actions_model.ActionRunJob {
|
||||||
rerunJobs := []*actions_model.ActionRunJob{job}
|
rerunJobs := []*actions_model.ActionRunJob{job}
|
||||||
|
|||||||
@ -258,6 +258,7 @@ func ToActionWorkflowRun(ctx context.Context, repo *repo_model.Repository, run *
|
|||||||
URL: fmt.Sprintf("%s/actions/runs/%d", repo.APIURL(), run.ID),
|
URL: fmt.Sprintf("%s/actions/runs/%d", repo.APIURL(), run.ID),
|
||||||
HTMLURL: run.HTMLURL(),
|
HTMLURL: run.HTMLURL(),
|
||||||
RunNumber: run.Index,
|
RunNumber: run.Index,
|
||||||
|
CreatedAt: run.Created.AsLocalTime(),
|
||||||
StartedAt: run.Started.AsLocalTime(),
|
StartedAt: run.Started.AsLocalTime(),
|
||||||
CompletedAt: run.Stopped.AsLocalTime(),
|
CompletedAt: run.Stopped.AsLocalTime(),
|
||||||
Event: string(run.Event),
|
Event: string(run.Event),
|
||||||
|
|||||||
416
templates/swagger/v1_json.tmpl
generated
416
templates/swagger/v1_json.tmpl
generated
@ -5266,6 +5266,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": {
|
"/repos/{owner}/{repo}/actions/runs/{run}/artifacts": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
@ -5318,6 +5367,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": {
|
"/repos/{owner}/{repo}/actions/runs/{run}/jobs": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
@ -5382,6 +5480,319 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/repos/{owner}/{repo}/actions/runs/{run}/jobs/{job_id}/logs": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/zip"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"repository"
|
||||||
|
],
|
||||||
|
"summary": "Download job logs",
|
||||||
|
"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": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"repository"
|
||||||
|
],
|
||||||
|
"summary": "Rerun a specific job and its dependent jobs",
|
||||||
|
"operationId": "rerunWorkflowJob",
|
||||||
|
"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": "success"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#/responses/error"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"$ref": "#/responses/forbidden"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/notFound"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"repository"
|
||||||
|
],
|
||||||
|
"summary": "Get streaming workflow run logs with cursor support",
|
||||||
|
"operationId": "getWorkflowRunLogsStream",
|
||||||
|
"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": "job index (0-based), defaults to first job",
|
||||||
|
"name": "job",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"logCursors": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cursor": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"expanded": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Streaming logs",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"stepsLog": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cursor": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"lines": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"index": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"started": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/notFound"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/repos/{owner}/{repo}/actions/runs/{run}/rerun": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"repository"
|
||||||
|
],
|
||||||
|
"summary": "Rerun a workflow run and its jobs",
|
||||||
|
"operationId": "rerunWorkflowRun",
|
||||||
|
"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/secrets": {
|
"/repos/{owner}/{repo}/actions/secrets": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
@ -21323,6 +21734,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "Conclusion"
|
"x-go-name": "Conclusion"
|
||||||
},
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"x-go-name": "CreatedAt"
|
||||||
|
},
|
||||||
"display_title": {
|
"display_title": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "DisplayTitle"
|
"x-go-name": "DisplayTitle"
|
||||||
|
|||||||
@ -6,6 +6,7 @@ package integration
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
@ -134,3 +135,173 @@ func testAPIActionsDeleteRunListTasks(t *testing.T, repo *repo_model.Repository,
|
|||||||
assert.Equal(t, expected, findTask1)
|
assert.Equal(t, expected, findTask1)
|
||||||
assert.Equal(t, expected, findTask2)
|
assert.Equal(t, expected, findTask2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAPIActionsRerunWorkflowRun(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)
|
||||||
|
|
||||||
|
// Test rerun existing run
|
||||||
|
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/rerun", repo.FullName())).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
// Test rerun non-existent run
|
||||||
|
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/999999/rerun", repo.FullName())).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIActionsRerunWorkflowRunPermissions(t *testing.T) {
|
||||||
|
defer prepareTestEnvActionsArtifacts(t)()
|
||||||
|
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // User without write access
|
||||||
|
session := loginUser(t, user.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
|
||||||
|
// Test rerun without permissions should fail
|
||||||
|
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/rerun", repo.FullName())).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusForbidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIActionsCancelWorkflowRun(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)
|
||||||
|
|
||||||
|
// Test cancel running workflow
|
||||||
|
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/793/cancel", repo.FullName())).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
// Test cancel non-existent run
|
||||||
|
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/999999/cancel", repo.FullName())).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIActionsCancelWorkflowRunPermissions(t *testing.T) {
|
||||||
|
defer prepareTestEnvActionsArtifacts(t)()
|
||||||
|
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // User without write access
|
||||||
|
session := loginUser(t, user.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
|
||||||
|
// Test cancel without permissions should fail
|
||||||
|
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/793/cancel", repo.FullName())).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusForbidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIActionsApproveWorkflowRun(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)
|
||||||
|
|
||||||
|
// Test approve workflow run
|
||||||
|
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/approve", repo.FullName())).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
// Test approve non-existent run
|
||||||
|
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/999999/approve", repo.FullName())).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIActionsRerunWorkflowJob(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)
|
||||||
|
|
||||||
|
// Test rerun specific job
|
||||||
|
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/jobs/192/rerun", repo.FullName())).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
// Test rerun non-existent job
|
||||||
|
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/jobs/999999/rerun", repo.FullName())).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Test get workflow run logs (archive download)
|
||||||
|
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/logs", repo.FullName())).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
// Test get non-existent run logs
|
||||||
|
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)
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/jobs/192/logs", repo.FullName())).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIActionsGetWorkflowRunLogsStream(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)
|
||||||
|
|
||||||
|
// Test streaming logs with empty cursor request
|
||||||
|
req := NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/logs", repo.FullName()), strings.NewReader(`{"logCursors": []}`)).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
// Parse response to verify structure
|
||||||
|
var logResp map[string]any
|
||||||
|
err := json.Unmarshal(resp.Body.Bytes(), &logResp)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Contains(t, logResp, "stepsLog")
|
||||||
|
|
||||||
|
// Test streaming logs with cursor request
|
||||||
|
req = NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/logs", repo.FullName()), strings.NewReader(`{"logCursors": [{"step": 0, "cursor": 0, "expanded": true}]}`)).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
// Test streaming logs for non-existent run
|
||||||
|
req = NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/999999/logs", repo.FullName()), strings.NewReader(`{"logCursors": []}`)).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user