0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-05-11 09:15:31 +02:00

Merge e0dc7ac0b819be75a84f352bf4f78d7c1458c6db into ce089f498bce32305b2d9e8c6adfd8cb7c82f88f

This commit is contained in:
bn-zr 2026-05-09 14:25:31 +02:00 committed by GitHub
commit 252d362208
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 728 additions and 29 deletions

View File

@ -117,23 +117,48 @@ type ActionWorkflowRun struct {
// RunAttempt is 1-based for runs created after ActionRunAttempt was introduced.
// A value of 0 is a legacy-only sentinel for runs created before attempts existed
// and indicates no corresponding /attempts/{n} resource is available.
RunAttempt int64 `json:"run_attempt"`
RunNumber int64 `json:"run_number"`
RepositoryID int64 `json:"repository_id,omitempty"`
HeadSha string `json:"head_sha"`
HeadBranch string `json:"head_branch,omitempty"`
Status string `json:"status"`
Actor *User `json:"actor,omitempty"`
TriggerActor *User `json:"trigger_actor,omitempty"`
Repository *Repository `json:"repository,omitempty"`
HeadRepository *Repository `json:"head_repository,omitempty"`
Conclusion string `json:"conclusion,omitempty"`
RunAttempt int64 `json:"run_attempt"`
RunNumber int64 `json:"run_number"`
RepositoryID int64 `json:"repository_id,omitempty"`
HeadSha string `json:"head_sha"`
HeadBranch string `json:"head_branch,omitempty"`
Status string `json:"status"`
Actor *User `json:"actor,omitempty"`
TriggerActor *User `json:"trigger_actor,omitempty"`
Repository *Repository `json:"repository,omitempty"`
HeadRepository *Repository `json:"head_repository,omitempty"`
Conclusion string `json:"conclusion,omitempty"`
PullRequests []*PullRequestMinimal `json:"pull_requests"`
// swagger:strfmt date-time
StartedAt time.Time `json:"started_at"`
// swagger:strfmt date-time
CompletedAt time.Time `json:"completed_at"`
}
// PullRequestMinimal is the minimal information about a pull request, as
// returned in the `pull_requests` field of a workflow run.
type PullRequestMinimal struct {
ID int64 `json:"id"`
Number int64 `json:"number"`
URL string `json:"url"`
Head PullRequestMinimalHead `json:"head"`
Base PullRequestMinimalHead `json:"base"`
}
// PullRequestMinimalHead is a minimal description of one side of a pull request.
type PullRequestMinimalHead struct {
Ref string `json:"ref"`
SHA string `json:"sha"`
Repo PullRequestMinimalHeadRepo `json:"repo"`
}
// PullRequestMinimalHeadRepo is a minimal description of the repository on one side of a pull request.
type PullRequestMinimalHeadRepo struct {
ID int64 `json:"id"`
URL string `json:"url"`
Name string `json:"name"`
}
// ActionWorkflowRunsResponse returns ActionWorkflowRuns
type ActionWorkflowRunsResponse struct {
Entries []*ActionWorkflowRun `json:"workflow_runs"`

View File

@ -89,5 +89,5 @@ func ListWorkflowRuns(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
shared.ListRuns(ctx, 0, 0)
shared.ListRuns(ctx, 0, 0, "")
}

View File

@ -1165,6 +1165,7 @@ func Routes() *web.Router {
m.Group("/actions/workflows", func() {
m.Get("", repo.ActionsListRepositoryWorkflows)
m.Get("/{workflow_id}", repo.ActionsGetWorkflow)
m.Get("/{workflow_id}/runs", repo.ActionsListWorkflowRuns)
m.Put("/{workflow_id}/disable", reqRepoWriter(unit.TypeActions), repo.ActionsDisableWorkflow)
m.Put("/{workflow_id}/enable", reqRepoWriter(unit.TypeActions), repo.ActionsEnableWorkflow)
m.Post("/{workflow_id}/dispatches", reqRepoWriter(unit.TypeActions), bind(api.CreateActionWorkflowDispatch{}), repo.ActionsDispatchWorkflow)

View File

@ -679,7 +679,7 @@ func (Action) ListWorkflowRuns(ctx *context.APIContext) {
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.ListRuns(ctx, ctx.Org.Organization.ID, 0)
shared.ListRuns(ctx, ctx.Org.Organization.ID, 0, "")
}
var _ actions_service.API = new(Action)

View File

@ -763,6 +763,11 @@ func (Action) ListWorkflowRuns(ctx *context.APIContext) {
// description: triggering sha of the workflow run
// type: string
// required: false
// - name: exclude_pull_requests
// in: query
// description: if true, the `pull_requests` field on each returned run is emptied
// type: boolean
// required: false
// - name: page
// in: query
// description: page number of results to return (1-based)
@ -781,7 +786,7 @@ func (Action) ListWorkflowRuns(ctx *context.APIContext) {
repoID := ctx.Repo.Repository.ID
shared.ListRuns(ctx, 0, repoID)
shared.ListRuns(ctx, 0, repoID, "")
}
var _ actions_service.API = new(Action)
@ -958,6 +963,101 @@ func ActionsGetWorkflow(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, workflow)
}
func ActionsListWorkflowRuns(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs repository ActionsListWorkflowRuns
// ---
// summary: List runs for a workflow
// 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 repo
// type: string
// required: true
// - name: workflow_id
// in: path
// description: id of the workflow, must be the workflow file name (e.g. `build.yml`)
// type: string
// required: true
// - name: event
// in: query
// description: workflow event name
// type: string
// required: false
// - name: branch
// in: query
// description: workflow branch
// type: string
// required: false
// - name: status
// in: query
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
// type: string
// required: false
// - name: actor
// in: query
// description: triggered by user
// type: string
// required: false
// - name: head_sha
// in: query
// description: triggering sha of the workflow run
// type: string
// required: false
// - name: exclude_pull_requests
// in: query
// description: if true, the `pull_requests` field on each returned run is emptied
// type: boolean
// required: false
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/WorkflowRunsList"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
workflowID := ctx.PathParam("workflow_id")
// Existing runs prove the workflow is/was valid and cover historical workflows
// whose file was later removed. Fall back to a git lookup for never-run workflows.
runExists, err := db.Exist[actions_model.ActionRun](ctx, actions_model.FindRunOptions{
RepoID: ctx.Repo.Repository.ID,
WorkflowID: workflowID,
}.ToConds())
if err != nil {
ctx.APIErrorInternal(err)
return
}
if !runExists {
if _, err := convert.GetActionWorkflow(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository, workflowID); err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
}
shared.ListRuns(ctx, 0, ctx.Repo.Repository.ID, workflowID)
}
func ActionsDisableWorkflow(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/actions/workflows/{workflow_id}/disable repository ActionsDisableWorkflow
// ---
@ -1232,7 +1332,7 @@ func GetWorkflowRun(ctx *context.APIContext) {
return
}
convertedRun, err := convert.ToActionWorkflowRun(ctx, run, nil)
convertedRun, err := convert.ToActionWorkflowRun(ctx, run, nil, false)
if err != nil {
ctx.APIErrorInternal(err)
return
@ -1281,7 +1381,7 @@ func GetWorkflowRunAttempt(ctx *context.APIContext) {
return
}
convertedRun, err := convert.ToActionWorkflowRun(ctx, run, attempt)
convertedRun, err := convert.ToActionWorkflowRun(ctx, run, attempt, false)
if err != nil {
ctx.APIErrorInternal(err)
return
@ -1336,7 +1436,7 @@ func RerunWorkflowRun(ctx *context.APIContext) {
return
}
convertedRun, err := convert.ToActionWorkflowRun(ctx, run, nil)
convertedRun, err := convert.ToActionWorkflowRun(ctx, run, nil, false)
if err != nil {
ctx.APIErrorInternal(err)
return

View File

@ -129,8 +129,9 @@ func convertToInternal(s string) ([]actions_model.Status, error) {
// ownerID == 0 and repoID != 0 means all runs for the given repo
// ownerID != 0 and repoID == 0 means all runs for the given user/org
// ownerID != 0 and repoID != 0 undefined behavior
// workflowID filters runs by workflow file name (e.g. "build.yml"), empty means no filter
// Access rights are checked at the API route level
func ListRuns(ctx *context.APIContext, ownerID, repoID int64) {
func ListRuns(ctx *context.APIContext, ownerID, repoID int64, workflowID string) {
if ownerID != 0 && repoID != 0 {
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
}
@ -138,6 +139,7 @@ func ListRuns(ctx *context.APIContext, ownerID, repoID int64) {
opts := actions_model.FindRunOptions{
OwnerID: ownerID,
RepoID: repoID,
WorkflowID: workflowID,
ListOptions: listOptions,
}
@ -166,6 +168,7 @@ func ListRuns(ctx *context.APIContext, ownerID, repoID int64) {
if headSHA := ctx.FormString("head_sha"); headSHA != "" {
opts.CommitSHA = headSHA
}
excludePullRequests := ctx.FormBool("exclude_pull_requests")
runs, total, err := db.FindAndCount[actions_model.ActionRun](ctx, opts)
if err != nil {
@ -197,7 +200,7 @@ func ListRuns(ctx *context.APIContext, ownerID, repoID int64) {
res.Entries = make([]*api.ActionWorkflowRun, len(runs))
for i := range runs {
// TODO: load run attempts in batch
convertedRun, err := convert.ToActionWorkflowRun(ctx, runs[i], nil)
convertedRun, err := convert.ToActionWorkflowRun(ctx, runs[i], nil, excludePullRequests)
if err != nil {
ctx.APIErrorInternal(err)
return

View File

@ -407,7 +407,7 @@ func ListWorkflowRuns(ctx *context.APIContext) {
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.ListRuns(ctx, ctx.Doer.ID, 0)
shared.ListRuns(ctx, ctx.Doer.ID, 0, "")
}
// ListWorkflowJobs lists workflow jobs

View File

@ -816,7 +816,7 @@ func (n *actionsNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *rep
return
}
run.Repo = repo
convertedRun, err := convert.ToActionWorkflowRun(ctx, run, nil)
convertedRun, err := convert.ToActionWorkflowRun(ctx, run, nil, false)
if err != nil {
log.Error("ToActionWorkflowRun: %v", err)
return

View File

@ -121,7 +121,7 @@ func TestToActionWorkflowRun_UsesTriggerEvent(t *testing.T) {
run.Event = "push"
run.TriggerEvent = "schedule"
apiRun, err := ToActionWorkflowRun(t.Context(), run, nil)
apiRun, err := ToActionWorkflowRun(t.Context(), run, nil, false)
require.NoError(t, err)
assert.Equal(t, "schedule", apiRun.Event)
}

View File

@ -33,6 +33,7 @@ import (
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
webhook_module "code.gitea.io/gitea/modules/webhook"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/gitdiff"
@ -251,11 +252,8 @@ func ToActionTask(ctx context.Context, t *actions_model.ActionTask) (*api.Action
}, nil
}
func ToActionWorkflowRun(ctx context.Context, run *actions_model.ActionRun, attempt *actions_model.ActionRunAttempt) (_ *api.ActionWorkflowRun, err error) {
if err := run.LoadRepo(ctx); err != nil {
return nil, err
}
if err := run.LoadTriggerUser(ctx); err != nil {
func ToActionWorkflowRun(ctx context.Context, run *actions_model.ActionRun, attempt *actions_model.ActionRunAttempt, excludePullRequests bool) (_ *api.ActionWorkflowRun, err error) {
if err := run.LoadAttributes(ctx); err != nil {
return nil, err
}
@ -288,7 +286,15 @@ func ToActionWorkflowRun(ctx context.Context, run *actions_model.ActionRun, atte
completedAt = attempt.Stopped.AsLocalTime()
triggerUser = attempt.TriggerUser
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", run.Repo.APIURL(ctx), run.ID, attempt.Attempt-1)
previousAttemptURL = &url
}
}
pullRequests := []*api.PullRequestMinimal{}
if !excludePullRequests {
pullRequests, err = loadPullRequestsForRun(ctx, run)
if err != nil {
return nil, err
}
}
@ -311,6 +317,89 @@ func ToActionWorkflowRun(ctx context.Context, run *actions_model.ActionRun, atte
Repository: ToRepo(ctx, run.Repo, access_model.Permission{AccessMode: perm.AccessModeNone}),
TriggerActor: ToUser(ctx, triggerUser, nil),
Actor: ToUser(ctx, actor, nil),
PullRequests: pullRequests,
}, nil
}
// loadPullRequestsForRun returns the pull requests associated with a run, matching
// GitHub's `pull_requests` field on workflow run responses:
// - For pull_request / pull_request_review events, the PR whose ref triggered the run.
// - For push events, open PRs whose head branch matches the pushed ref in the same repo.
// - For other events, no PRs.
func loadPullRequestsForRun(ctx context.Context, run *actions_model.ActionRun) ([]*api.PullRequestMinimal, error) {
result := []*api.PullRequestMinimal{}
refName := git.RefName(run.Ref)
var prs issues_model.PullRequestList
switch {
case run.Event.IsPullRequest() || run.Event.IsPullRequestReview():
index, err := strconv.ParseInt(refName.PullName(), 10, 64)
if err != nil {
return result, nil
}
pr, err := issues_model.GetPullRequestByIndex(ctx, run.RepoID, index)
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
return result, nil
}
return nil, err
}
prs = issues_model.PullRequestList{pr}
case run.Event == webhook_module.HookEventPush:
branch := refName.BranchName()
if branch == "" {
return result, nil
}
var err error
prs, err = issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, run.RepoID, branch)
if err != nil {
return nil, err
}
default:
return result, nil
}
for _, pr := range prs {
minimal, err := toPullRequestMinimal(ctx, run.Repo, pr, run.CommitSHA)
if err != nil {
return nil, err
}
result = append(result, minimal)
}
return result, nil
}
func toPullRequestMinimal(ctx context.Context, repo *repo_model.Repository, pr *issues_model.PullRequest, headSHA string) (*api.PullRequestMinimal, error) {
if err := pr.LoadBaseRepo(ctx); err != nil {
return nil, err
}
if err := pr.LoadHeadRepo(ctx); err != nil {
return nil, err
}
headRepo := pr.HeadRepo
if headRepo == nil {
headRepo = pr.BaseRepo
}
return &api.PullRequestMinimal{
ID: pr.ID,
Number: pr.Index,
URL: fmt.Sprintf("%s/pulls/%d", repo.APIURL(ctx), pr.Index),
Head: api.PullRequestMinimalHead{
Ref: pr.HeadBranch,
SHA: headSHA,
Repo: api.PullRequestMinimalHeadRepo{
ID: headRepo.ID,
URL: headRepo.APIURL(ctx),
Name: headRepo.Name,
},
},
Base: api.PullRequestMinimalHead{
Ref: pr.BaseBranch,
SHA: pr.MergeBase,
Repo: api.PullRequestMinimalHeadRepo{
ID: pr.BaseRepo.ID,
URL: pr.BaseRepo.APIURL(ctx),
Name: pr.BaseRepo.Name,
},
},
}, nil
}
@ -507,6 +596,9 @@ func ListActionWorkflows(ctx context.Context, gitrepo *git.Repository, repo *rep
}
func GetActionWorkflow(ctx context.Context, gitrepo *git.Repository, repo *repo_model.Repository, workflowID string) (*api.ActionWorkflow, error) {
if repo.DefaultBranch == "" {
return nil, util.NewNotExistErrorf("workflow %q not found", workflowID)
}
defaultBranchCommit, err := gitrepo.GetBranchCommit(repo.DefaultBranch)
if err != nil {
return nil, err

View File

@ -1044,7 +1044,7 @@ func (*webhookNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_
}
run.Repo = repo
convertedRun, err := convert.ToActionWorkflowRun(ctx, run, nil)
convertedRun, err := convert.ToActionWorkflowRun(ctx, run, nil, false)
if err != nil {
log.Error("ToActionWorkflowRun: %v", err)
return

View File

@ -5305,6 +5305,12 @@
"name": "head_sha",
"in": "query"
},
{
"type": "boolean",
"description": "if true, the `pull_requests` field on each returned run is emptied",
"name": "exclude_pull_requests",
"in": "query"
},
{
"type": "integer",
"description": "page number of results to return (1-based)",
@ -6577,6 +6583,103 @@
}
}
},
"/repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs": {
"get": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "List runs for a workflow",
"operationId": "ActionsListWorkflowRuns",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "string",
"description": "id of the workflow, must be the workflow file name (e.g. `build.yml`)",
"name": "workflow_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "workflow event name",
"name": "event",
"in": "query"
},
{
"type": "string",
"description": "workflow branch",
"name": "branch",
"in": "query"
},
{
"type": "string",
"description": "workflow status (pending, queued, in_progress, failure, success, skipped)",
"name": "status",
"in": "query"
},
{
"type": "string",
"description": "triggered by user",
"name": "actor",
"in": "query"
},
{
"type": "string",
"description": "triggering sha of the workflow run",
"name": "head_sha",
"in": "query"
},
{
"type": "boolean",
"description": "if true, the `pull_requests` field on each returned run is emptied",
"name": "exclude_pull_requests",
"in": "query"
},
{
"type": "integer",
"description": "page number of results to return (1-based)",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "page size of results",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"$ref": "#/responses/WorkflowRunsList"
},
"400": {
"$ref": "#/responses/error"
},
"403": {
"$ref": "#/responses/forbidden"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/repos/{owner}/{repo}/activities/feeds": {
"get": {
"produces": [
@ -22103,6 +22206,13 @@
"type": "string",
"x-go-name": "PreviousAttemptURL"
},
"pull_requests": {
"type": "array",
"items": {
"$ref": "#/definitions/PullRequestMinimal"
},
"x-go-name": "PullRequests"
},
"repository": {
"$ref": "#/definitions/Repository"
},
@ -28367,6 +28477,71 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"PullRequestMinimal": {
"description": "PullRequestMinimal is the minimal information about a pull request, as\nreturned in the `pull_requests` field of a workflow run.",
"type": "object",
"properties": {
"base": {
"$ref": "#/definitions/PullRequestMinimalHead"
},
"head": {
"$ref": "#/definitions/PullRequestMinimalHead"
},
"id": {
"type": "integer",
"format": "int64",
"x-go-name": "ID"
},
"number": {
"type": "integer",
"format": "int64",
"x-go-name": "Number"
},
"url": {
"type": "string",
"x-go-name": "URL"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"PullRequestMinimalHead": {
"type": "object",
"title": "PullRequestMinimalHead is a minimal description of one side of a pull request.",
"properties": {
"ref": {
"type": "string",
"x-go-name": "Ref"
},
"repo": {
"$ref": "#/definitions/PullRequestMinimalHeadRepo"
},
"sha": {
"type": "string",
"x-go-name": "SHA"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"PullRequestMinimalHeadRepo": {
"type": "object",
"title": "PullRequestMinimalHeadRepo is a minimal description of the repository on one side of a pull request.",
"properties": {
"id": {
"type": "integer",
"format": "int64",
"x-go-name": "ID"
},
"name": {
"type": "string",
"x-go-name": "Name"
},
"url": {
"type": "string",
"x-go-name": "URL"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"PullReview": {
"description": "PullReview represents a pull request review",
"type": "object",

View File

@ -2344,6 +2344,13 @@
"type": "string",
"x-go-name": "PreviousAttemptURL"
},
"pull_requests": {
"items": {
"$ref": "#/components/schemas/PullRequestMinimal"
},
"type": "array",
"x-go-name": "PullRequests"
},
"repository": {
"$ref": "#/components/schemas/Repository"
},
@ -8612,6 +8619,73 @@
"type": "object",
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"PullRequestMinimal": {
"description": "PullRequestMinimal is the minimal information about a pull request, as\nreturned in the `pull_requests` field of a workflow run.",
"properties": {
"base": {
"$ref": "#/components/schemas/PullRequestMinimalHead"
},
"head": {
"$ref": "#/components/schemas/PullRequestMinimalHead"
},
"id": {
"format": "int64",
"type": "integer",
"x-go-name": "ID"
},
"number": {
"format": "int64",
"type": "integer",
"x-go-name": "Number"
},
"url": {
"format": "uri",
"type": "string",
"x-go-name": "URL"
}
},
"type": "object",
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"PullRequestMinimalHead": {
"properties": {
"ref": {
"type": "string",
"x-go-name": "Ref"
},
"repo": {
"$ref": "#/components/schemas/PullRequestMinimalHeadRepo"
},
"sha": {
"type": "string",
"x-go-name": "SHA"
}
},
"title": "PullRequestMinimalHead is a minimal description of one side of a pull request.",
"type": "object",
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"PullRequestMinimalHeadRepo": {
"properties": {
"id": {
"format": "int64",
"type": "integer",
"x-go-name": "ID"
},
"name": {
"type": "string",
"x-go-name": "Name"
},
"url": {
"format": "uri",
"type": "string",
"x-go-name": "URL"
}
},
"title": "PullRequestMinimalHeadRepo is a minimal description of the repository on one side of a pull request.",
"type": "object",
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"PullReview": {
"description": "PullReview represents a pull request review",
"properties": {
@ -16140,6 +16214,14 @@
"type": "string"
}
},
{
"description": "if true, the `pull_requests` field on each returned run is emptied",
"in": "query",
"name": "exclude_pull_requests",
"schema": {
"type": "boolean"
}
},
{
"description": "page number of results to return (1-based)",
"in": "query",
@ -17524,6 +17606,122 @@
]
}
},
"/repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs": {
"get": {
"operationId": "ActionsListWorkflowRuns",
"parameters": [
{
"description": "owner of the repo",
"in": "path",
"name": "owner",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "name of the repo",
"in": "path",
"name": "repo",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "id of the workflow, must be the workflow file name (e.g. `build.yml`)",
"in": "path",
"name": "workflow_id",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "workflow event name",
"in": "query",
"name": "event",
"schema": {
"type": "string"
}
},
{
"description": "workflow branch",
"in": "query",
"name": "branch",
"schema": {
"type": "string"
}
},
{
"description": "workflow status (pending, queued, in_progress, failure, success, skipped)",
"in": "query",
"name": "status",
"schema": {
"type": "string"
}
},
{
"description": "triggered by user",
"in": "query",
"name": "actor",
"schema": {
"type": "string"
}
},
{
"description": "triggering sha of the workflow run",
"in": "query",
"name": "head_sha",
"schema": {
"type": "string"
}
},
{
"description": "if true, the `pull_requests` field on each returned run is emptied",
"in": "query",
"name": "exclude_pull_requests",
"schema": {
"type": "boolean"
}
},
{
"description": "page number of results to return (1-based)",
"in": "query",
"name": "page",
"schema": {
"type": "integer"
}
},
{
"description": "page size of results",
"in": "query",
"name": "limit",
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"$ref": "#/components/responses/WorkflowRunsList"
},
"400": {
"$ref": "#/components/responses/error"
},
"403": {
"$ref": "#/components/responses/forbidden"
},
"404": {
"$ref": "#/components/responses/notFound"
}
},
"summary": "List runs for a workflow",
"tags": [
"repository"
]
}
},
"/repos/{owner}/{repo}/activities/feeds": {
"get": {
"operationId": "repoListActivityFeeds",

View File

@ -9,11 +9,15 @@ import (
"net/url"
"testing"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAPIWorkflowRun(t *testing.T) {
@ -29,6 +33,107 @@ func TestAPIWorkflowRun(t *testing.T) {
t.Run("RepoRuns", func(t *testing.T) {
testAPIWorkflowRunBasic(t, "/api/v1/repos/org3/repo5/actions", "User2", 802, auth_model.AccessTokenScopeReadRepository)
})
t.Run("RepoWorkflowRuns", func(t *testing.T) {
testAPIWorkflowRunsByWorkflowID(t, "org3", "repo5", "test.yaml", "User2", 802, auth_model.AccessTokenScopeReadRepository)
})
t.Run("PullRequestsField", testAPIWorkflowRunsPullRequestsField)
}
// testAPIWorkflowRunsPullRequestsField exercises the `pull_requests` field and the
// `exclude_pull_requests` toggle by associating an inserted run with fixture PR
// user2/repo1#3 (head: branch2, base: master).
func testAPIWorkflowRunsPullRequestsField(t *testing.T) {
defer tests.PrepareTestEnv(t)()
ctx := t.Context()
run := &actions_model.ActionRun{
RepoID: 1,
OwnerID: 2,
TriggerUserID: 2,
WorkflowID: "pr-assoc.yaml",
Index: 99001,
Ref: "refs/pull/3/head",
CommitSHA: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
Event: webhook_module.HookEventPullRequest,
TriggerEvent: "pull_request_target",
Status: actions_model.StatusSuccess,
}
require.NoError(t, db.Insert(ctx, run))
token := getUserToken(t, "User2", auth_model.AccessTokenScopeReadRepository)
runsURL := "/api/v1/repos/user2/repo1/actions/workflows/pr-assoc.yaml/runs"
req := NewRequest(t, "GET", runsURL).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
list := api.ActionWorkflowRunsResponse{}
DecodeJSON(t, resp, &list)
var got *api.ActionWorkflowRun
for _, r := range list.Entries {
if r.ID == run.ID {
got = r
break
}
}
require.NotNil(t, got, "inserted PR-triggered run not returned")
require.Len(t, got.PullRequests, 1)
pr := got.PullRequests[0]
assert.Equal(t, int64(3), pr.Number)
assert.Equal(t, "branch2", pr.Head.Ref)
assert.Equal(t, "master", pr.Base.Ref)
assert.Equal(t, int64(1), pr.Base.Repo.ID)
assert.Equal(t, "repo1", pr.Base.Repo.Name)
req = NewRequest(t, "GET", runsURL+"?exclude_pull_requests=true").AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
excluded := api.ActionWorkflowRunsResponse{}
DecodeJSON(t, resp, &excluded)
for _, r := range excluded.Entries {
if r.ID == run.ID {
assert.Empty(t, r.PullRequests)
}
}
}
func testAPIWorkflowRunsByWorkflowID(t *testing.T, owner, repo, workflowID, userUsername string, expectedRunID int64, scope ...auth_model.AccessTokenScope) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, userUsername, scope...)
workflowRunsURL := fmt.Sprintf("/api/v1/repos/%s/%s/actions/workflows/%s/runs", owner, repo, workflowID)
req := NewRequest(t, "GET", workflowRunsURL).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
runList := api.ActionWorkflowRunsResponse{}
DecodeJSON(t, resp, &runList)
found := false
for _, run := range runList.Entries {
verifyWorkflowRunCanbeFoundWithStatusFilter(t, workflowRunsURL, token, run.ID, "", run.Status, "", "", "", "")
verifyWorkflowRunCanbeFoundWithStatusFilter(t, workflowRunsURL, token, run.ID, "", "", "", run.HeadBranch, "", "")
verifyWorkflowRunCanbeFoundWithStatusFilter(t, workflowRunsURL, token, run.ID, "", "", run.Event, "", "", "")
verifyWorkflowRunCanbeFoundWithStatusFilter(t, workflowRunsURL, token, run.ID, "", "", "", "", run.TriggerActor.UserName, "")
verifyWorkflowRunCanbeFoundWithStatusFilter(t, workflowRunsURL, token, run.ID, "", "", "", "", run.TriggerActor.UserName, run.HeadSha)
if run.ID == expectedRunID {
found = true
}
}
assert.True(t, found, "expected to find run with ID %d in workflow %s runs", expectedRunID, workflowID)
req = NewRequest(t, "GET", workflowRunsURL+"?exclude_pull_requests=true").AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
excludedList := api.ActionWorkflowRunsResponse{}
DecodeJSON(t, resp, &excludedList)
excludedFound := false
for _, run := range excludedList.Entries {
assert.Empty(t, run.PullRequests, "expected pull_requests to be empty when excluded")
if run.ID == expectedRunID {
excludedFound = true
}
}
assert.True(t, excludedFound, "expected to find run with ID %d when excluding pull requests", expectedRunID)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/workflows/nonexistent.yaml/runs", owner, repo)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
}
func testAPIWorkflowRunBasic(t *testing.T, apiRootURL, userUsername string, runID int64, scope ...auth_model.AccessTokenScope) {