mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-15 02:03:19 +02:00
feat: add Actions API endpoints for workflow run and job management
- Cancel and approve workflow runs via POST /runs/{run}/cancel|approve
- Download all job logs as zip via GET /runs/{run}/logs
- Download individual job log via GET /runs/{run}/jobs/{job_id}/logs
- Stream live log cursors via POST /runs/{run}/logs
- Add CreatedAt field to ActionWorkflowRun API response
- Extract shared log streaming and cancel logic into services/actions
- Move streaming log types to modules/structs
- Add Swagger documentation for all new endpoints
- Add integration tests with subtests for all new endpoints
Co-Authored-By: Claude Sonnet 4.6 <claude-sonnet-4-6@anthropic.com>
This commit is contained in:
parent
dd17521808
commit
4bdfb2ecde
@ -129,6 +129,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"`
|
||||||
@ -226,3 +228,35 @@ type RunDetails struct {
|
|||||||
RunURL string `json:"run_url"`
|
RunURL string `json:"run_url"`
|
||||||
HTMLURL string `json:"html_url"`
|
HTMLURL string `json:"html_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ActionLogCursor represents a cursor position within a step's log
|
||||||
|
type ActionLogCursor struct {
|
||||||
|
Step int `json:"step"`
|
||||||
|
Cursor int64 `json:"cursor"`
|
||||||
|
Expanded bool `json:"expanded"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActionLogRequest is the request body for the streaming log endpoint
|
||||||
|
type ActionLogRequest struct {
|
||||||
|
LogCursors []ActionLogCursor `json:"logCursors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActionLogStepLine represents a single log line within a step
|
||||||
|
type ActionLogStepLine struct {
|
||||||
|
Index int64 `json:"index"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Timestamp float64 `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActionLogStep represents log lines for a single step with cursor state
|
||||||
|
type ActionLogStep struct {
|
||||||
|
Step int `json:"step"`
|
||||||
|
Cursor int64 `json:"cursor"`
|
||||||
|
Lines []*ActionLogStepLine `json:"lines"`
|
||||||
|
Started int64 `json:"started"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActionLogResponse is the response body for the streaming log endpoint
|
||||||
|
type ActionLogResponse struct {
|
||||||
|
StepsLog []*ActionLogStep `json:"stepsLog"`
|
||||||
|
}
|
||||||
|
|||||||
@ -1261,8 +1261,17 @@ func Routes() *web.Router {
|
|||||||
m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun)
|
m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun)
|
||||||
m.Post("/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowRun)
|
m.Post("/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowRun)
|
||||||
m.Post("/rerun-failed-jobs", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunFailedWorkflowRun)
|
m.Post("/rerun-failed-jobs", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunFailedWorkflowRun)
|
||||||
m.Get("/jobs", repo.ListWorkflowRunJobs)
|
m.Post("/cancel", reqToken(), reqRepoWriter(unit.TypeActions), repo.CancelWorkflowRun)
|
||||||
m.Post("/jobs/{job_id}/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowJob)
|
m.Post("/approve", reqToken(), reqRepoWriter(unit.TypeActions), repo.ApproveWorkflowRun)
|
||||||
|
m.Group("/jobs", func() {
|
||||||
|
m.Get("", repo.ListWorkflowRunJobs)
|
||||||
|
m.Get("/{job_id}/logs", repo.GetWorkflowJobLogs)
|
||||||
|
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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -1272,7 +1281,7 @@ func Routes() *web.Router {
|
|||||||
m.Delete("", reqRepoWriter(unit.TypeActions), repo.DeleteArtifact)
|
m.Delete("", reqRepoWriter(unit.TypeActions), repo.DeleteArtifact)
|
||||||
})
|
})
|
||||||
m.Get("/artifacts/{artifact_id}/zip", repo.DownloadArtifact)
|
m.Get("/artifacts/{artifact_id}/zip", repo.DownloadArtifact)
|
||||||
}, reqRepoReader(unit.TypeActions))
|
}, reqRepoReader(unit.TypeActions), context.ReferencesGitRepo(true))
|
||||||
m.Group("/keys", func() {
|
m.Group("/keys", func() {
|
||||||
m.Combo("").Get(repo.ListDeployKeys).
|
m.Combo("").Get(repo.ListDeployKeys).
|
||||||
Post(bind(api.CreateKeyOption{}), repo.CreateDeployKey)
|
Post(bind(api.CreateKeyOption{}), repo.CreateDeployKey)
|
||||||
|
|||||||
@ -5,11 +5,19 @@ package repo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
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/json"
|
||||||
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"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"
|
||||||
|
"code.gitea.io/gitea/services/convert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func DownloadActionsRunJobLogs(ctx *context.APIContext) {
|
func DownloadActionsRunJobLogs(ctx *context.APIContext) {
|
||||||
@ -66,3 +74,439 @@ func DownloadActionsRunJobLogs(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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, err := getRunID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
ctx.APIErrorNotFound(err)
|
||||||
|
} else {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jobs, err := getRunJobs(ctx, run)
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := actions_service.CancelRun(ctx, run, jobs); err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedRun, has, err := db.GetByID[actions_model.ActionRun](ctx, run.ID)
|
||||||
|
if err != nil || !has {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, updatedRun, nil)
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.JSON(http.StatusOK, convertedRun)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, run, err := getRunID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
ctx.APIErrorNotFound(err)
|
||||||
|
} else {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !run.NeedApproval {
|
||||||
|
ctx.APIError(http.StatusBadRequest, "Run does not require approval")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := actions_service.ApproveRuns(ctx, ctx.Repo.Repository, ctx.Doer, []int64{runID}); err != nil {
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
ctx.APIErrorNotFound(err)
|
||||||
|
} else {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload run to reflect post-approval state.
|
||||||
|
updatedRun, has, err := db.GetByID[actions_model.ActionRun](ctx, runID)
|
||||||
|
if err != nil || !has {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, updatedRun, nil)
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.JSON(http.StatusOK, convertedRun)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 getRunJobs(ctx *context.APIContext, run *actions_model.ActionRun) ([]*actions_model.ActionRunJob, error) {
|
||||||
|
run.Repo = ctx.Repo.Repository
|
||||||
|
jobs, err := actions_model.GetLatestAttemptJobsByRepoAndRunID(ctx, run.RepoID, run.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, v := range jobs {
|
||||||
|
v.Run = run
|
||||||
|
}
|
||||||
|
return jobs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRunJobsAndCurrent(ctx *context.APIContext, run *actions_model.ActionRun, jobIndex int64) (*actions_model.ActionRunJob, []*actions_model.ActionRunJob, error) {
|
||||||
|
jobs, err := getRunJobs(ctx, run)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if len(jobs) == 0 {
|
||||||
|
return nil, nil, util.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
if jobIndex >= 0 {
|
||||||
|
if jobIndex >= int64(len(jobs)) {
|
||||||
|
return nil, nil, util.ErrNotExist
|
||||||
|
}
|
||||||
|
return jobs[jobIndex], jobs, nil
|
||||||
|
}
|
||||||
|
return jobs[0], jobs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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.APIErrorNotFound(err)
|
||||||
|
} 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.APIErrorNotFound(err)
|
||||||
|
} else {
|
||||||
|
ctx.APIErrorInternal(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"
|
||||||
|
|
||||||
|
runID, _, err := getRunID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
ctx.APIErrorNotFound(err)
|
||||||
|
} else {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jobID := ctx.PathParamInt64("job_id")
|
||||||
|
|
||||||
|
job, err := actions_model.GetRunJobByRunAndID(ctx, runID, jobID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
ctx.APIErrorNotFound(err)
|
||||||
|
} else {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
job.Repo = ctx.Repo.Repository
|
||||||
|
|
||||||
|
if err = common.DownloadActionsRunJobLogs(ctx.Base, ctx.Repo.Repository, job); err != nil {
|
||||||
|
if errors.Is(err, util.ErrNotExist) || errors.Is(err, os.ErrNotExist) {
|
||||||
|
ctx.APIErrorNotFound(err)
|
||||||
|
} else {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
_, run, err := getRunID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
ctx.APIErrorNotFound(err)
|
||||||
|
} else {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jobIndex := int64(-1)
|
||||||
|
if ctx.FormString("job") != "" {
|
||||||
|
jobIndex = int64(ctx.FormInt("job"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var req api.ActionLogRequest
|
||||||
|
if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil {
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
req = api.ActionLogRequest{LogCursors: []api.ActionLogCursor{}}
|
||||||
|
} else {
|
||||||
|
ctx.APIError(http.StatusBadRequest, "Invalid request body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
current, _, err := getRunJobsAndCurrent(ctx, run, jobIndex)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
ctx.APIErrorNotFound(err)
|
||||||
|
} 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 := &api.ActionLogResponse{
|
||||||
|
StepsLog: make([]*api.ActionLogStep, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
if task != nil {
|
||||||
|
logs, err := actions_service.ReadStepLogs(ctx, req.LogCursors, task, "Log has expired and is no longer available")
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.StepsLog = append(response.StepsLog, logs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|||||||
@ -4,7 +4,11 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
actions_model "code.gitea.io/gitea/models/actions"
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
@ -26,6 +30,79 @@ func DownloadActionsRunJobLogsWithID(ctx *context.Base, ctxRepo *repo_model.Repo
|
|||||||
return DownloadActionsRunJobLogs(ctx, ctxRepo, job)
|
return DownloadActionsRunJobLogs(ctx, ctxRepo, job)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DownloadActionsRunAllJobLogs(ctx *context.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)
|
||||||
|
}
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
safeWorkflowName := strings.NewReplacer(`"`, "", "\r", "", "\n", "", "/", "-", `\`, "-").Replace(workflowName)
|
||||||
|
|
||||||
|
// 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"`, safeWorkflowName, 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 || task.LogLength == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
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; sanitize to prevent Zip Slip
|
||||||
|
safeJobName := strings.NewReplacer("/", "-", `\`, "-", "..", "__").Replace(job.Name)
|
||||||
|
fileName := fmt.Sprintf("%s-%s-%d.log", safeWorkflowName, safeJobName, 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")
|
||||||
@ -49,8 +126,15 @@ func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository
|
|||||||
return util.NewNotExistErrorf("logs have been cleaned up")
|
return util.NewNotExistErrorf("logs have been cleaned up")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if task.LogLength == 0 {
|
||||||
|
return util.NewNotExistErrorf("logs not found")
|
||||||
|
}
|
||||||
|
|
||||||
reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename)
|
reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) || errors.Is(err, util.ErrNotExist) {
|
||||||
|
return util.NewNotExistErrorf("logs not found")
|
||||||
|
}
|
||||||
return fmt.Errorf("OpenLogs: %w", err)
|
return fmt.Errorf("OpenLogs: %w", err)
|
||||||
}
|
}
|
||||||
defer reader.Close()
|
defer reader.Close()
|
||||||
|
|||||||
36
services/actions/cancel.go
Normal file
36
services/actions/cancel.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
|
"code.gitea.io/gitea/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
|
||||||
|
}
|
||||||
80
services/actions/log.go
Normal file
80
services/actions/log.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
|
"code.gitea.io/gitea/modules/actions"
|
||||||
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadStepLogs reads log lines for the given cursor positions from a task.
|
||||||
|
// expiredMessage is used as the log content when the task's logs have expired.
|
||||||
|
func ReadStepLogs(ctx context.Context, cursors []api.ActionLogCursor, task *actions_model.ActionTask, expiredMessage string) ([]*api.ActionLogStep, error) {
|
||||||
|
var logs []*api.ActionLogStep
|
||||||
|
steps := actions.FullSteps(task)
|
||||||
|
|
||||||
|
for _, cursor := range cursors {
|
||||||
|
if !cursor.Expanded {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if cursor.Step >= len(steps) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
step := steps[cursor.Step]
|
||||||
|
|
||||||
|
if task.LogExpired {
|
||||||
|
if cursor.Cursor == 0 {
|
||||||
|
logs = append(logs, &api.ActionLogStep{
|
||||||
|
Step: cursor.Step,
|
||||||
|
Cursor: 1,
|
||||||
|
Lines: []*api.ActionLogStepLine{{
|
||||||
|
Index: 1,
|
||||||
|
Message: expiredMessage,
|
||||||
|
Timestamp: float64(task.Updated.AsTime().UnixNano()) / float64(time.Second),
|
||||||
|
}},
|
||||||
|
Started: int64(step.Started),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logLines := make([]*api.ActionLogStepLine, 0)
|
||||||
|
index := step.LogIndex + cursor.Cursor
|
||||||
|
validCursor := cursor.Cursor >= 0 &&
|
||||||
|
// !(cursor.Cursor < step.LogLength) when the frontend tries to fetch the next
|
||||||
|
// line before it's ready — return same cursor and empty lines to let caller retry.
|
||||||
|
cursor.Cursor < step.LogLength &&
|
||||||
|
// !(index < len(task.LogIndexes)) when task data is older than step data.
|
||||||
|
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, fmt.Errorf("actions.ReadLogs: %w", err)
|
||||||
|
}
|
||||||
|
for i, row := range logRows {
|
||||||
|
logLines = append(logLines, &api.ActionLogStepLine{
|
||||||
|
Index: cursor.Cursor + int64(i) + 1, // 1-based
|
||||||
|
Message: row.Content,
|
||||||
|
Timestamp: float64(row.Time.AsTime().UnixNano()) / float64(time.Second),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logs = append(logs, &api.ActionLogStep{
|
||||||
|
Step: cursor.Step,
|
||||||
|
Cursor: cursor.Cursor + int64(len(logLines)),
|
||||||
|
Lines: logLines,
|
||||||
|
Started: int64(step.Started),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return logs, nil
|
||||||
|
}
|
||||||
@ -815,8 +815,7 @@ func (n *actionsNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *rep
|
|||||||
log.Error("GetActionWorkflow: %v", err)
|
log.Error("GetActionWorkflow: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
run.Repo = repo
|
convertedRun, err := convert.ToActionWorkflowRun(ctx, repo, run, nil)
|
||||||
convertedRun, err := convert.ToActionWorkflowRun(ctx, run, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("ToActionWorkflowRun: %v", err)
|
log.Error("ToActionWorkflowRun: %v", err)
|
||||||
return
|
return
|
||||||
|
|||||||
@ -29,7 +29,6 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/actions"
|
"code.gitea.io/gitea/modules/actions"
|
||||||
"code.gitea.io/gitea/modules/container"
|
"code.gitea.io/gitea/modules/container"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/httplib"
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
@ -223,18 +222,14 @@ func ToTag(repo *repo_model.Repository, t *git.Tag) *api.Tag {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToActionTask convert an actions_model.ActionTask to an api.ActionTask
|
// ToActionTask convert a actions_model.ActionTask to an api.ActionTask
|
||||||
func ToActionTask(ctx context.Context, t *actions_model.ActionTask) (*api.ActionTask, error) {
|
func ToActionTask(ctx context.Context, t *actions_model.ActionTask) (*api.ActionTask, error) {
|
||||||
// don't need Steps here, only need to load job and its run
|
if err := t.LoadAttributes(ctx); err != nil {
|
||||||
if err := t.LoadJob(ctx); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := t.Job.LoadRun(ctx); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := t.Job.Run.LoadRepo(ctx); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
url := strings.TrimSuffix(setting.AppURL, "/") + t.GetRunLink()
|
||||||
|
|
||||||
return &api.ActionTask{
|
return &api.ActionTask{
|
||||||
ID: t.ID,
|
ID: t.ID,
|
||||||
Name: t.Job.Name,
|
Name: t.Job.Name,
|
||||||
@ -245,25 +240,23 @@ func ToActionTask(ctx context.Context, t *actions_model.ActionTask) (*api.Action
|
|||||||
DisplayTitle: t.Job.Run.Title,
|
DisplayTitle: t.Job.Run.Title,
|
||||||
Status: t.Status.String(),
|
Status: t.Status.String(),
|
||||||
WorkflowID: t.Job.Run.WorkflowID,
|
WorkflowID: t.Job.Run.WorkflowID,
|
||||||
URL: httplib.MakeAbsoluteURL(ctx, t.Job.Run.Link()),
|
URL: url,
|
||||||
CreatedAt: t.Created.AsLocalTime(),
|
CreatedAt: t.Created.AsLocalTime(),
|
||||||
UpdatedAt: t.Updated.AsLocalTime(),
|
UpdatedAt: t.Updated.AsLocalTime(),
|
||||||
RunStartedAt: t.Started.AsLocalTime(),
|
RunStartedAt: t.Started.AsLocalTime(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToActionWorkflowRun(ctx context.Context, run *actions_model.ActionRun, attempt *actions_model.ActionRunAttempt) (_ *api.ActionWorkflowRun, err error) {
|
func ToActionWorkflowRun(ctx context.Context, repo *repo_model.Repository, run *actions_model.ActionRun, attempt *actions_model.ActionRunAttempt) (*api.ActionWorkflowRun, error) {
|
||||||
if err := run.LoadRepo(ctx); err != nil {
|
if err := run.LoadAttributes(ctx); err != nil {
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := run.LoadTriggerUser(ctx); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if attempt == nil {
|
if attempt == nil {
|
||||||
attempt, _, err = run.GetLatestAttempt(ctx)
|
if latestAttempt, has, err := run.GetLatestAttempt(ctx); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
|
} else if has {
|
||||||
|
attempt = latestAttempt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,7 +272,6 @@ func ToActionWorkflowRun(ctx context.Context, run *actions_model.ActionRun, atte
|
|||||||
var previousAttemptURL *string
|
var previousAttemptURL *string
|
||||||
|
|
||||||
if attempt != nil {
|
if attempt != nil {
|
||||||
attempt.Run = run
|
|
||||||
if err := attempt.LoadAttributes(ctx); err != nil {
|
if err := attempt.LoadAttributes(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -289,17 +281,19 @@ func ToActionWorkflowRun(ctx context.Context, run *actions_model.ActionRun, atte
|
|||||||
completedAt = attempt.Stopped.AsLocalTime()
|
completedAt = attempt.Stopped.AsLocalTime()
|
||||||
triggerUser = attempt.TriggerUser
|
triggerUser = attempt.TriggerUser
|
||||||
if attempt.Attempt > 1 {
|
if attempt.Attempt > 1 {
|
||||||
previousAttemptURL = new(fmt.Sprintf("%s/actions/runs/%d/attempts/%d", run.Repo.APIURL(ctx), run.ID, attempt.Attempt-1))
|
url := fmt.Sprintf("%s/actions/runs/%d/attempts/%d", repo.APIURL(), run.ID, attempt.Attempt-1)
|
||||||
|
previousAttemptURL = &url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &api.ActionWorkflowRun{
|
return &api.ActionWorkflowRun{
|
||||||
ID: run.ID,
|
ID: run.ID,
|
||||||
URL: fmt.Sprintf("%s/actions/runs/%d", run.Repo.APIURL(ctx), run.ID),
|
URL: fmt.Sprintf("%s/actions/runs/%d", repo.APIURL(), run.ID),
|
||||||
PreviousAttemptURL: previousAttemptURL,
|
PreviousAttemptURL: previousAttemptURL,
|
||||||
HTMLURL: run.HTMLURL(ctx),
|
HTMLURL: run.HTMLURL(),
|
||||||
RunNumber: run.Index,
|
RunNumber: run.Index,
|
||||||
RunAttempt: runAttempt,
|
RunAttempt: runAttempt,
|
||||||
|
CreatedAt: run.Created.AsLocalTime(),
|
||||||
StartedAt: startedAt,
|
StartedAt: startedAt,
|
||||||
CompletedAt: completedAt,
|
CompletedAt: completedAt,
|
||||||
Event: run.TriggerEvent,
|
Event: run.TriggerEvent,
|
||||||
@ -309,7 +303,7 @@ func ToActionWorkflowRun(ctx context.Context, run *actions_model.ActionRun, atte
|
|||||||
Status: status,
|
Status: status,
|
||||||
Conclusion: conclusion,
|
Conclusion: conclusion,
|
||||||
Path: fmt.Sprintf("%s@%s", run.WorkflowID, run.Ref),
|
Path: fmt.Sprintf("%s@%s", run.WorkflowID, run.Ref),
|
||||||
Repository: ToRepo(ctx, run.Repo, access_model.Permission{AccessMode: perm.AccessModeNone}),
|
Repository: ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeNone}),
|
||||||
TriggerActor: ToUser(ctx, triggerUser, nil),
|
TriggerActor: ToUser(ctx, triggerUser, nil),
|
||||||
Actor: ToUser(ctx, actor, nil),
|
Actor: ToUser(ctx, actor, nil),
|
||||||
}, nil
|
}, nil
|
||||||
@ -407,11 +401,11 @@ func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, task
|
|||||||
return &api.ActionWorkflowJob{
|
return &api.ActionWorkflowJob{
|
||||||
ID: job.ID,
|
ID: job.ID,
|
||||||
// missing api endpoint for this location
|
// missing api endpoint for this location
|
||||||
URL: fmt.Sprintf("%s/actions/jobs/%d", repo.APIURL(ctx), job.ID),
|
URL: fmt.Sprintf("%s/actions/jobs/%d", repo.APIURL(), job.ID),
|
||||||
HTMLURL: fmt.Sprintf("%s/jobs/%d", job.Run.HTMLURL(ctx), job.ID),
|
HTMLURL: fmt.Sprintf("%s/jobs/%d", job.Run.HTMLURL(), job.ID),
|
||||||
RunID: job.RunID,
|
RunID: job.RunID,
|
||||||
// Missing api endpoint for this location, artifacts are available under a nested url
|
// Missing api endpoint for this location, artifacts are available under a nested url
|
||||||
RunURL: fmt.Sprintf("%s/actions/runs/%d", repo.APIURL(ctx), job.RunID),
|
RunURL: fmt.Sprintf("%s/actions/runs/%d", repo.APIURL(), job.RunID),
|
||||||
Name: job.Name,
|
Name: job.Name,
|
||||||
Labels: job.RunsOn,
|
Labels: job.RunsOn,
|
||||||
RunAttempt: job.Attempt,
|
RunAttempt: job.Attempt,
|
||||||
@ -711,7 +705,7 @@ func ToOrganization(ctx context.Context, org *organization.Organization) *api.Or
|
|||||||
Description: org.Description,
|
Description: org.Description,
|
||||||
Website: org.Website,
|
Website: org.Website,
|
||||||
Location: org.Location,
|
Location: org.Location,
|
||||||
Visibility: api.UserVisibility(org.Visibility.String()),
|
Visibility: org.Visibility.String(),
|
||||||
RepoAdminChangeTeamAccess: org.RepoAdminChangeTeamAccess,
|
RepoAdminChangeTeamAccess: org.RepoAdminChangeTeamAccess,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -740,7 +734,7 @@ func ToTeams(ctx context.Context, teams []*organization.Team, loadOrgs bool) ([]
|
|||||||
Description: t.Description,
|
Description: t.Description,
|
||||||
IncludesAllRepositories: t.IncludesAllRepositories,
|
IncludesAllRepositories: t.IncludesAllRepositories,
|
||||||
CanCreateOrgRepo: t.CanCreateOrgRepo,
|
CanCreateOrgRepo: t.CanCreateOrgRepo,
|
||||||
Permission: api.AccessLevelName(t.AccessMode.ToString()),
|
Permission: t.AccessMode.ToString(),
|
||||||
Units: t.GetUnitNames(),
|
Units: t.GetUnitNames(),
|
||||||
UnitsMap: t.GetUnitsMap(),
|
UnitsMap: t.GetUnitsMap(),
|
||||||
}
|
}
|
||||||
|
|||||||
471
templates/swagger/v1_json.tmpl
generated
471
templates/swagger/v1_json.tmpl
generated
@ -5421,6 +5421,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": [
|
||||||
@ -5597,6 +5646,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": [
|
||||||
@ -5661,6 +5759,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": {
|
"/repos/{owner}/{repo}/actions/runs/{run}/jobs/{job_id}/rerun": {
|
||||||
"post": {
|
"post": {
|
||||||
"produces": [
|
"produces": [
|
||||||
@ -5723,6 +5871,164 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/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": {
|
"/repos/{owner}/{repo}/actions/runs/{run}/rerun": {
|
||||||
"post": {
|
"post": {
|
||||||
"produces": [
|
"produces": [
|
||||||
@ -22066,6 +22372,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"
|
||||||
@ -22299,14 +22610,12 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"permission": {
|
"permission": {
|
||||||
"description": "Permission level to grant the collaborator\nread RepoWritePermissionRead\nwrite RepoWritePermissionWrite\nadmin RepoWritePermissionAdmin",
|
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"read",
|
"read",
|
||||||
"write",
|
"write",
|
||||||
"admin"
|
"admin"
|
||||||
],
|
],
|
||||||
"x-go-enum-desc": "read RepoWritePermissionRead\nwrite RepoWritePermissionWrite\nadmin RepoWritePermissionAdmin",
|
|
||||||
"x-go-name": "Permission"
|
"x-go-name": "Permission"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -23841,15 +24150,6 @@
|
|||||||
"format": "int64",
|
"format": "int64",
|
||||||
"x-go-name": "Milestone"
|
"x-go-name": "Milestone"
|
||||||
},
|
},
|
||||||
"projects": {
|
|
||||||
"description": "list of project ids",
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int64"
|
|
||||||
},
|
|
||||||
"x-go-name": "Projects"
|
|
||||||
},
|
|
||||||
"ref": {
|
"ref": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "Ref"
|
"x-go-name": "Ref"
|
||||||
@ -24042,14 +24342,13 @@
|
|||||||
"x-go-name": "UserName"
|
"x-go-name": "UserName"
|
||||||
},
|
},
|
||||||
"visibility": {
|
"visibility": {
|
||||||
"description": "possible values are `public` (default), `limited` or `private`\npublic UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
|
"description": "possible values are `public` (default), `limited` or `private`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"public",
|
"public",
|
||||||
"limited",
|
"limited",
|
||||||
"private"
|
"private"
|
||||||
],
|
],
|
||||||
"x-go-enum-desc": "public UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
|
|
||||||
"x-go-name": "Visibility"
|
"x-go-name": "Visibility"
|
||||||
},
|
},
|
||||||
"website": {
|
"website": {
|
||||||
@ -24334,13 +24633,12 @@
|
|||||||
"x-go-name": "Name"
|
"x-go-name": "Name"
|
||||||
},
|
},
|
||||||
"object_format_name": {
|
"object_format_name": {
|
||||||
"description": "ObjectFormatName of the underlying git repository, empty string for default (sha1)\nsha1 ObjectFormatSHA1\nsha256 ObjectFormatSHA256",
|
"description": "ObjectFormatName of the underlying git repository, empty string for default (sha1)",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"sha1",
|
"sha1",
|
||||||
"sha256"
|
"sha256"
|
||||||
],
|
],
|
||||||
"x-go-enum-desc": "sha1 ObjectFormatSHA1\nsha256 ObjectFormatSHA256",
|
|
||||||
"x-go-name": "ObjectFormatName"
|
"x-go-name": "ObjectFormatName"
|
||||||
},
|
},
|
||||||
"private": {
|
"private": {
|
||||||
@ -24493,7 +24791,6 @@
|
|||||||
"write",
|
"write",
|
||||||
"admin"
|
"admin"
|
||||||
],
|
],
|
||||||
"x-go-enum-desc": "read RepoWritePermissionRead\nwrite RepoWritePermissionWrite\nadmin RepoWritePermissionAdmin",
|
|
||||||
"x-go-name": "Permission"
|
"x-go-name": "Permission"
|
||||||
},
|
},
|
||||||
"units": {
|
"units": {
|
||||||
@ -24588,14 +24885,8 @@
|
|||||||
"x-go-name": "Username"
|
"x-go-name": "Username"
|
||||||
},
|
},
|
||||||
"visibility": {
|
"visibility": {
|
||||||
"description": "User visibility level: public, limited, or private\npublic UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
|
"description": "User visibility level: public, limited, or private",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
|
||||||
"public",
|
|
||||||
"limited",
|
|
||||||
"private"
|
|
||||||
],
|
|
||||||
"x-go-enum-desc": "public UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
|
|
||||||
"x-go-name": "Visibility"
|
"x-go-name": "Visibility"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -25107,15 +25398,6 @@
|
|||||||
"format": "int64",
|
"format": "int64",
|
||||||
"x-go-name": "Milestone"
|
"x-go-name": "Milestone"
|
||||||
},
|
},
|
||||||
"projects": {
|
|
||||||
"description": "list of project ids to set (replaces existing projects)",
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int64"
|
|
||||||
},
|
|
||||||
"x-go-name": "Projects"
|
|
||||||
},
|
|
||||||
"ref": {
|
"ref": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "Ref"
|
"x-go-name": "Ref"
|
||||||
@ -25229,14 +25511,13 @@
|
|||||||
"x-go-name": "RepoAdminChangeTeamAccess"
|
"x-go-name": "RepoAdminChangeTeamAccess"
|
||||||
},
|
},
|
||||||
"visibility": {
|
"visibility": {
|
||||||
"description": "possible values are `public`, `limited` or `private`\npublic UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
|
"description": "possible values are `public`, `limited` or `private`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"public",
|
"public",
|
||||||
"limited",
|
"limited",
|
||||||
"private"
|
"private"
|
||||||
],
|
],
|
||||||
"x-go-enum-desc": "public UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
|
|
||||||
"x-go-name": "Visibility"
|
"x-go-name": "Visibility"
|
||||||
},
|
},
|
||||||
"website": {
|
"website": {
|
||||||
@ -25510,21 +25791,6 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "MirrorInterval"
|
"x-go-name": "MirrorInterval"
|
||||||
},
|
},
|
||||||
"mirror_password": {
|
|
||||||
"description": "authentication password for the remote repository (mirrors)",
|
|
||||||
"type": "string",
|
|
||||||
"x-go-name": "MirrorPassword"
|
|
||||||
},
|
|
||||||
"mirror_token": {
|
|
||||||
"description": "authentication token for the remote repository (mirrors)",
|
|
||||||
"type": "string",
|
|
||||||
"x-go-name": "MirrorToken"
|
|
||||||
},
|
|
||||||
"mirror_username": {
|
|
||||||
"description": "authentication username for the remote repository (mirrors)",
|
|
||||||
"type": "string",
|
|
||||||
"x-go-name": "MirrorUsername"
|
|
||||||
},
|
|
||||||
"name": {
|
"name": {
|
||||||
"description": "name of the repository",
|
"description": "name of the repository",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -25615,7 +25881,6 @@
|
|||||||
"write",
|
"write",
|
||||||
"admin"
|
"admin"
|
||||||
],
|
],
|
||||||
"x-go-enum-desc": "read RepoWritePermissionRead\nwrite RepoWritePermissionWrite\nadmin RepoWritePermissionAdmin",
|
|
||||||
"x-go-name": "Permission"
|
"x-go-name": "Permission"
|
||||||
},
|
},
|
||||||
"units": {
|
"units": {
|
||||||
@ -25746,14 +26011,8 @@
|
|||||||
"x-go-name": "SourceID"
|
"x-go-name": "SourceID"
|
||||||
},
|
},
|
||||||
"visibility": {
|
"visibility": {
|
||||||
"description": "User visibility level: public, limited, or private\npublic UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
|
"description": "User visibility level: public, limited, or private",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
|
||||||
"public",
|
|
||||||
"limited",
|
|
||||||
"private"
|
|
||||||
],
|
|
||||||
"x-go-enum-desc": "public UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
|
|
||||||
"x-go-name": "Visibility"
|
"x-go-name": "Visibility"
|
||||||
},
|
},
|
||||||
"website": {
|
"website": {
|
||||||
@ -26655,13 +26914,6 @@
|
|||||||
"format": "int64",
|
"format": "int64",
|
||||||
"x-go-name": "PinOrder"
|
"x-go-name": "PinOrder"
|
||||||
},
|
},
|
||||||
"projects": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/Project"
|
|
||||||
},
|
|
||||||
"x-go-name": "Projects"
|
|
||||||
},
|
|
||||||
"pull_request": {
|
"pull_request": {
|
||||||
"$ref": "#/definitions/PullRequestMeta"
|
"$ref": "#/definitions/PullRequestMeta"
|
||||||
},
|
},
|
||||||
@ -27711,14 +27963,8 @@
|
|||||||
"x-go-name": "UserName"
|
"x-go-name": "UserName"
|
||||||
},
|
},
|
||||||
"visibility": {
|
"visibility": {
|
||||||
"description": "The visibility level of the organization (public, limited, private)\npublic UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
|
"description": "The visibility level of the organization (public, limited, private)",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
|
||||||
"public",
|
|
||||||
"limited",
|
|
||||||
"private"
|
|
||||||
],
|
|
||||||
"x-go-enum-desc": "public UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
|
|
||||||
"x-go-name": "Visibility"
|
"x-go-name": "Visibility"
|
||||||
},
|
},
|
||||||
"website": {
|
"website": {
|
||||||
@ -28014,67 +28260,6 @@
|
|||||||
},
|
},
|
||||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
},
|
},
|
||||||
"Project": {
|
|
||||||
"description": "Project represents a project",
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"closed_at": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"x-go-name": "Closed"
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"x-go-name": "Created"
|
|
||||||
},
|
|
||||||
"creator_id": {
|
|
||||||
"description": "CreatorID is the user who created the project",
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int64",
|
|
||||||
"x-go-name": "CreatorID"
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"description": "Description provides details about the project",
|
|
||||||
"type": "string",
|
|
||||||
"x-go-name": "Description"
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"description": "ID is the unique identifier for the project",
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int64",
|
|
||||||
"x-go-name": "ID"
|
|
||||||
},
|
|
||||||
"is_closed": {
|
|
||||||
"description": "IsClosed indicates if the project is closed",
|
|
||||||
"type": "boolean",
|
|
||||||
"x-go-name": "IsClosed"
|
|
||||||
},
|
|
||||||
"owner_id": {
|
|
||||||
"description": "OwnerID is the owner of the project (for org-level projects)",
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int64",
|
|
||||||
"x-go-name": "OwnerID"
|
|
||||||
},
|
|
||||||
"repo_id": {
|
|
||||||
"description": "RepoID is the repository this project belongs to (for repo-level projects)",
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int64",
|
|
||||||
"x-go-name": "RepoID"
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"description": "Title is the title of the project",
|
|
||||||
"type": "string",
|
|
||||||
"x-go-name": "Title"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"x-go-name": "Updated"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
|
||||||
},
|
|
||||||
"PublicKey": {
|
"PublicKey": {
|
||||||
"description": "PublicKey publickey is a user key to push code to repository",
|
"description": "PublicKey publickey is a user key to push code to repository",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@ -28763,16 +28948,8 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"permission": {
|
"permission": {
|
||||||
"description": "Permission level of the collaborator\nnone AccessLevelNameNone\nread AccessLevelNameRead\nwrite AccessLevelNameWrite\nadmin AccessLevelNameAdmin\nowner AccessLevelNameOwner",
|
"description": "Permission level of the collaborator",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
|
||||||
"none",
|
|
||||||
"read",
|
|
||||||
"write",
|
|
||||||
"admin",
|
|
||||||
"owner"
|
|
||||||
],
|
|
||||||
"x-go-enum-desc": "none AccessLevelNameNone\nread AccessLevelNameRead\nwrite AccessLevelNameWrite\nadmin AccessLevelNameAdmin\nowner AccessLevelNameOwner",
|
|
||||||
"x-go-name": "Permission"
|
"x-go-name": "Permission"
|
||||||
},
|
},
|
||||||
"role_name": {
|
"role_name": {
|
||||||
@ -29049,13 +29226,12 @@
|
|||||||
"x-go-name": "Name"
|
"x-go-name": "Name"
|
||||||
},
|
},
|
||||||
"object_format_name": {
|
"object_format_name": {
|
||||||
"description": "ObjectFormatName of the underlying git repository\nsha1 ObjectFormatSHA1\nsha256 ObjectFormatSHA256",
|
"description": "ObjectFormatName of the underlying git repository",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"sha1",
|
"sha1",
|
||||||
"sha256"
|
"sha256"
|
||||||
],
|
],
|
||||||
"x-go-enum-desc": "sha1 ObjectFormatSHA1\nsha256 ObjectFormatSHA256",
|
|
||||||
"x-go-name": "ObjectFormatName"
|
"x-go-name": "ObjectFormatName"
|
||||||
},
|
},
|
||||||
"open_issues_count": {
|
"open_issues_count": {
|
||||||
@ -29429,7 +29605,6 @@
|
|||||||
"admin",
|
"admin",
|
||||||
"owner"
|
"owner"
|
||||||
],
|
],
|
||||||
"x-go-enum-desc": "none AccessLevelNameNone\nread AccessLevelNameRead\nwrite AccessLevelNameWrite\nadmin AccessLevelNameAdmin\nowner AccessLevelNameOwner",
|
|
||||||
"x-go-name": "Permission"
|
"x-go-name": "Permission"
|
||||||
},
|
},
|
||||||
"units": {
|
"units": {
|
||||||
@ -29976,14 +30151,8 @@
|
|||||||
"x-go-name": "StarredRepos"
|
"x-go-name": "StarredRepos"
|
||||||
},
|
},
|
||||||
"visibility": {
|
"visibility": {
|
||||||
"description": "User visibility level option: public, limited, private\npublic UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
|
"description": "User visibility level option: public, limited, private",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
|
||||||
"public",
|
|
||||||
"limited",
|
|
||||||
"private"
|
|
||||||
],
|
|
||||||
"x-go-enum-desc": "public UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
|
|
||||||
"x-go-name": "Visibility"
|
"x-go-name": "Visibility"
|
||||||
},
|
},
|
||||||
"website": {
|
"website": {
|
||||||
|
|||||||
@ -4,10 +4,14 @@
|
|||||||
package integration
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
actions_model "code.gitea.io/gitea/models/actions"
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
@ -15,35 +19,17 @@ import (
|
|||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/json"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/tests"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAPIActionsWorkflowRun(t *testing.T) {
|
func TestAPIActionsGetWorkflowRun(t *testing.T) {
|
||||||
defer prepareTestEnvActionsArtifacts(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})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
session := loginUser(t, user.Name)
|
session := loginUser(t, user.Name)
|
||||||
@ -74,9 +60,13 @@ func testAPIActionsGetWorkflowRun(t *testing.T) {
|
|||||||
})
|
})
|
||||||
require.NoError(t, err)
|
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)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
jobList := DecodeJSON(t, resp, &api.ActionWorkflowJobsResponse{})
|
|
||||||
|
var jobList api.ActionWorkflowJobsResponse
|
||||||
|
err = json.Unmarshal(resp.Body.Bytes(), &jobList)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
job198Idx := slices.IndexFunc(jobList.Entries, func(job *api.ActionWorkflowJob) bool { return job.ID == 198 })
|
job198Idx := slices.IndexFunc(jobList.Entries, func(job *api.ActionWorkflowJob) bool { return job.ID == 198 })
|
||||||
require.NotEqual(t, -1, job198Idx, "expected to find job 198 in run 795 jobs list")
|
require.NotEqual(t, -1, job198Idx, "expected to find job 198 in run 795 jobs list")
|
||||||
@ -86,7 +76,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})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
session := loginUser(t, user.Name)
|
session := loginUser(t, user.Name)
|
||||||
@ -103,7 +95,9 @@ func testAPIActionsGetWorkflowJob(t *testing.T) {
|
|||||||
MakeRequest(t, req, http.StatusNotFound)
|
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})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
session := loginUser(t, user.Name)
|
session := loginUser(t, user.Name)
|
||||||
@ -111,7 +105,9 @@ func testAPIActionsDeleteRunCheckPermission(t *testing.T) {
|
|||||||
testAPIActionsDeleteRun(t, repo, token, http.StatusNotFound)
|
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})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
session := loginUser(t, user.Name)
|
session := loginUser(t, user.Name)
|
||||||
@ -126,7 +122,9 @@ func testAPIActionsDeleteRunGeneral(t *testing.T) {
|
|||||||
testAPIActionsDeleteRun(t, repo, token, http.StatusNotFound)
|
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})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
session := loginUser(t, user.Name)
|
session := loginUser(t, user.Name)
|
||||||
@ -144,17 +142,22 @@ func testAPIActionsDeleteRun(t *testing.T, repo *repo_model.Repository, token st
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testAPIActionsDeleteRunListArtifacts(t *testing.T, repo *repo_model.Repository, token string, artifacts int) {
|
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)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
listResp := DecodeJSON(t, resp, &api.ActionArtifactsResponse{})
|
var listResp api.ActionArtifactsResponse
|
||||||
|
err := json.Unmarshal(resp.Body.Bytes(), &listResp)
|
||||||
|
assert.NoError(t, err)
|
||||||
assert.Len(t, listResp.Entries, artifacts)
|
assert.Len(t, listResp.Entries, artifacts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAPIActionsDeleteRunListTasks(t *testing.T, repo *repo_model.Repository, token string, expected bool) {
|
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)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
listResp := DecodeJSON(t, resp, &api.ActionTaskResponse{})
|
var listResp api.ActionTaskResponse
|
||||||
|
err := json.Unmarshal(resp.Body.Bytes(), &listResp)
|
||||||
|
assert.NoError(t, err)
|
||||||
findTask1 := false
|
findTask1 := false
|
||||||
findTask2 := false
|
findTask2 := false
|
||||||
for _, entry := range listResp.Entries {
|
for _, entry := range listResp.Entries {
|
||||||
@ -171,7 +174,9 @@ func testAPIActionsDeleteRunListTasks(t *testing.T, repo *repo_model.Repository,
|
|||||||
assert.Equal(t, expected, findTask2)
|
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) {
|
t.Run("NotDone", func(t *testing.T) {
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
@ -191,10 +196,13 @@ func testAPIActionsRerunWorkflowRun(t *testing.T) {
|
|||||||
readToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
|
readToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
|
||||||
|
|
||||||
t.Run("Success", func(t *testing.T) {
|
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)
|
resp := MakeRequest(t, req, http.StatusCreated)
|
||||||
rerunResp := DecodeJSON(t, resp, &api.ActionWorkflowRun{})
|
|
||||||
|
|
||||||
|
var rerunResp api.ActionWorkflowRun
|
||||||
|
err := json.Unmarshal(resp.Body.Bytes(), &rerunResp)
|
||||||
|
require.NoError(t, err)
|
||||||
assert.Equal(t, int64(795), rerunResp.ID)
|
assert.Equal(t, int64(795), rerunResp.ID)
|
||||||
assert.Equal(t, "queued", rerunResp.Status)
|
assert.Equal(t, "queued", rerunResp.Status)
|
||||||
assert.Equal(t, "c2d72f548424103f01ee1dc02889c1e2bff816b0", rerunResp.HeadSha)
|
assert.Equal(t, "c2d72f548424103f01ee1dc02889c1e2bff816b0", rerunResp.HeadSha)
|
||||||
@ -232,7 +240,139 @@ 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)
|
||||||
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIActionsRerunWorkflowJob(t *testing.T) {
|
||||||
|
defer prepareTestEnvActionsArtifacts(t)()
|
||||||
|
|
||||||
t.Run("NotDone", func(t *testing.T) {
|
t.Run("NotDone", func(t *testing.T) {
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
@ -252,10 +392,13 @@ func testAPIActionsRerunWorkflowJob(t *testing.T) {
|
|||||||
readToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
|
readToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
|
||||||
|
|
||||||
t.Run("Success", func(t *testing.T) {
|
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)
|
resp := MakeRequest(t, req, http.StatusCreated)
|
||||||
rerunResp := DecodeJSON(t, resp, &api.ActionWorkflowJob{})
|
|
||||||
|
|
||||||
|
var rerunResp api.ActionWorkflowJob
|
||||||
|
err := json.Unmarshal(resp.Body.Bytes(), &rerunResp)
|
||||||
|
require.NoError(t, err)
|
||||||
job199Rerun := getLatestAttemptJobByTemplateJobID(t, 795, 199)
|
job199Rerun := getLatestAttemptJobByTemplateJobID(t, 795, 199)
|
||||||
assert.Equal(t, job199Rerun.ID, rerunResp.ID)
|
assert.Equal(t, job199Rerun.ID, rerunResp.ID)
|
||||||
assert.Equal(t, "queued", rerunResp.Status)
|
assert.Equal(t, "queued", rerunResp.Status)
|
||||||
@ -293,58 +436,77 @@ func testAPIActionsRerunWorkflowJob(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAPIActionsListUserWorkflows(t *testing.T) {
|
func TestAPIActionsGetWorkflowRunLogs(t *testing.T) {
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
defer prepareTestEnvActionsArtifacts(t)()
|
||||||
session := loginUser(t, user.Name)
|
|
||||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser)
|
|
||||||
|
|
||||||
t.Run("Runs", func(t *testing.T) {
|
|
||||||
req := NewRequest(t, "GET", "/api/v1/user/actions/runs").AddTokenAuth(token)
|
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
|
||||||
runs := DecodeJSON(t, resp, &api.ActionWorkflowRunsResponse{})
|
|
||||||
|
|
||||||
assert.Positive(t, runs.TotalCount)
|
|
||||||
assert.NotEmpty(t, runs.Entries)
|
|
||||||
|
|
||||||
for _, run := range runs.Entries {
|
|
||||||
assert.NotEmpty(t, run.DisplayTitle, "display_title should be populated")
|
|
||||||
assert.NotNil(t, run.Repository, "repository should be populated via batch loading")
|
|
||||||
assert.NotEmpty(t, run.Repository.FullName, "repository full_name should be populated")
|
|
||||||
assert.NotNil(t, run.TriggerActor, "trigger_actor should be populated via batch loading")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Jobs", func(t *testing.T) {
|
|
||||||
req := NewRequest(t, "GET", "/api/v1/user/actions/jobs").AddTokenAuth(token)
|
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
|
||||||
jobs := DecodeJSON(t, resp, &api.ActionWorkflowJobsResponse{})
|
|
||||||
|
|
||||||
assert.Positive(t, jobs.TotalCount)
|
|
||||||
assert.NotEmpty(t, jobs.Entries)
|
|
||||||
|
|
||||||
for _, job := range jobs.Entries {
|
|
||||||
assert.NotEmpty(t, job.Name, "job name should be populated")
|
|
||||||
assert.NotEmpty(t, job.HTMLURL, "html_url should be populated via batch-loaded repo")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func testAPIActionsListRepoWorkflows(t *testing.T) {
|
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
session := loginUser(t, user.Name)
|
session := loginUser(t, user.Name)
|
||||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
|
||||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs", repo.FullName())).AddTokenAuth(token)
|
t.Run("Success", func(t *testing.T) {
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/logs", repo.FullName())).
|
||||||
runs := DecodeJSON(t, resp, &api.ActionWorkflowRunsResponse{})
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
assert.Positive(t, runs.TotalCount)
|
t.Run("NotFound", func(t *testing.T) {
|
||||||
assert.NotEmpty(t, runs.Entries)
|
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/999999/logs", repo.FullName())).
|
||||||
|
AddTokenAuth(token)
|
||||||
for _, run := range runs.Entries {
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
assert.NotNil(t, run.Repository, "repository should be populated from ctx.Repo")
|
})
|
||||||
assert.Equal(t, repo.FullName(), run.Repository.FullName, "repository full_name should match")
|
}
|
||||||
assert.NotNil(t, run.TriggerActor, "trigger_actor should be populated")
|
|
||||||
}
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
t.Run("EmptyCursors", func(t *testing.T) {
|
||||||
|
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)
|
||||||
|
|
||||||
|
var logResp map[string]any
|
||||||
|
err := json.Unmarshal(resp.Body.Bytes(), &logResp)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Contains(t, logResp, "stepsLog")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("WithCursor", func(t *testing.T) {
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("NotFound", func(t *testing.T) {
|
||||||
|
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