Skip to content

Commit 18546ed

Browse files
committed
Add GitHub API compatibility for workflow runs filtering
Implements additional query parameters for the workflow runs API to match GitHub's REST API specification. - Add `exclude_pull_requests` query parameter - Add `check_suite_id` parameter - Add `created` parameter with date range and comparison support - Add workflow-specific endpoint `/repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs` Builds on the workflow API foundation from #33964 to provide additional GitHub API compatibility. Reference: https://docs.github.com/en/rest/actions/workflow-runs?apiVersion=2022-11-28#list-workflow-runs-for-a-workflow
1 parent 8df59fa commit 18546ed

File tree

6 files changed

+505
-9
lines changed

6 files changed

+505
-9
lines changed

models/actions/run_list.go

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package actions
55

66
import (
77
"context"
8+
"time"
89

910
"code.gitea.io/gitea/models/db"
1011
repo_model "code.gitea.io/gitea/models/repo"
@@ -64,15 +65,19 @@ func (runs RunList) LoadRepos(ctx context.Context) error {
6465

6566
type FindRunOptions struct {
6667
db.ListOptions
67-
RepoID int64
68-
OwnerID int64
69-
WorkflowID string
70-
Ref string // the commit/tag/… that caused this workflow
71-
TriggerUserID int64
72-
TriggerEvent webhook_module.HookEventType
73-
Approved bool // not util.OptionalBool, it works only when it's true
74-
Status []Status
75-
CommitSHA string
68+
RepoID int64
69+
OwnerID int64
70+
WorkflowID string
71+
Ref string // the commit/tag/... that caused this workflow
72+
TriggerUserID int64
73+
TriggerEvent webhook_module.HookEventType
74+
Approved bool // not util.OptionalBool, it works only when it's true
75+
Status []Status
76+
CommitSHA string
77+
CreatedAfter time.Time
78+
CreatedBefore time.Time
79+
ExcludePullRequests bool
80+
CheckSuiteID int64
7681
}
7782

7883
func (opts FindRunOptions) ToConds() builder.Cond {
@@ -101,6 +106,18 @@ func (opts FindRunOptions) ToConds() builder.Cond {
101106
if opts.CommitSHA != "" {
102107
cond = cond.And(builder.Eq{"`action_run`.commit_sha": opts.CommitSHA})
103108
}
109+
if !opts.CreatedAfter.IsZero() {
110+
cond = cond.And(builder.Gte{"`action_run`.created": opts.CreatedAfter})
111+
}
112+
if !opts.CreatedBefore.IsZero() {
113+
cond = cond.And(builder.Lte{"`action_run`.created": opts.CreatedBefore})
114+
}
115+
if opts.ExcludePullRequests {
116+
cond = cond.And(builder.Neq{"`action_run`.trigger_event": webhook_module.HookEventPullRequest})
117+
}
118+
if opts.CheckSuiteID > 0 {
119+
cond = cond.And(builder.Eq{"`action_run`.check_suite_id": opts.CheckSuiteID})
120+
}
104121
return cond
105122
}
106123

models/actions/run_list_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package actions
5+
6+
import (
7+
"testing"
8+
"time"
9+
10+
"code.gitea.io/gitea/modules/webhook"
11+
12+
"github.com/stretchr/testify/assert"
13+
"xorm.io/builder"
14+
)
15+
16+
func TestFindRunOptions_ToConds_ExcludePullRequests(t *testing.T) {
17+
// Test when ExcludePullRequests is true
18+
opts := FindRunOptions{
19+
ExcludePullRequests: true,
20+
}
21+
cond := opts.ToConds()
22+
23+
// Convert the condition to SQL for assertion
24+
sql, args, err := builder.ToSQL(cond)
25+
assert.NoError(t, err)
26+
// The condition should contain the trigger_event not equal to pull_request
27+
assert.Contains(t, sql, "`action_run`.trigger_event<>")
28+
assert.Contains(t, args, webhook.HookEventPullRequest)
29+
}
30+
31+
func TestFindRunOptions_ToConds_CheckSuiteID(t *testing.T) {
32+
// Test when CheckSuiteID is set
33+
const testSuiteID int64 = 12345
34+
opts := FindRunOptions{
35+
CheckSuiteID: testSuiteID,
36+
}
37+
cond := opts.ToConds()
38+
39+
// Convert the condition to SQL for assertion
40+
sql, args, err := builder.ToSQL(cond)
41+
assert.NoError(t, err)
42+
// The condition should contain the check_suite_id equal to the test value
43+
assert.Contains(t, sql, "`action_run`.check_suite_id=")
44+
assert.Contains(t, args, testSuiteID)
45+
}
46+
47+
func TestFindRunOptions_ToConds_CreatedDateRange(t *testing.T) {
48+
// Test when CreatedAfter and CreatedBefore are set
49+
startDate := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)
50+
endDate := time.Date(2023, 12, 31, 23, 59, 59, 0, time.UTC)
51+
52+
opts := FindRunOptions{
53+
CreatedAfter: startDate,
54+
CreatedBefore: endDate,
55+
}
56+
cond := opts.ToConds()
57+
58+
// Convert the condition to SQL for assertion
59+
sql, args, err := builder.ToSQL(cond)
60+
assert.NoError(t, err)
61+
// The condition should contain created >= startDate and created <= endDate
62+
assert.Contains(t, sql, "`action_run`.created>=")
63+
assert.Contains(t, sql, "`action_run`.created<=")
64+
assert.Contains(t, args, startDate)
65+
assert.Contains(t, args, endDate)
66+
}
67+
68+
func TestFindRunOptions_ToConds_CreatedAfterOnly(t *testing.T) {
69+
// Test when only CreatedAfter is set
70+
startDate := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)
71+
72+
opts := FindRunOptions{
73+
CreatedAfter: startDate,
74+
}
75+
cond := opts.ToConds()
76+
77+
// Convert the condition to SQL for assertion
78+
sql, args, err := builder.ToSQL(cond)
79+
assert.NoError(t, err)
80+
// The condition should contain created >= startDate
81+
assert.Contains(t, sql, "`action_run`.created>=")
82+
assert.Contains(t, args, startDate)
83+
// But should not contain created <= endDate
84+
assert.NotContains(t, sql, "`action_run`.created<=")
85+
}
86+
87+
func TestFindRunOptions_ToConds_CreatedBeforeOnly(t *testing.T) {
88+
// Test when only CreatedBefore is set
89+
endDate := time.Date(2023, 12, 31, 23, 59, 59, 0, time.UTC)
90+
91+
opts := FindRunOptions{
92+
CreatedBefore: endDate,
93+
}
94+
cond := opts.ToConds()
95+
96+
// Convert the condition to SQL for assertion
97+
sql, args, err := builder.ToSQL(cond)
98+
assert.NoError(t, err)
99+
// The condition should contain created <= endDate
100+
assert.Contains(t, sql, "`action_run`.created<=")
101+
assert.Contains(t, args, endDate)
102+
// But should not contain created >= startDate
103+
assert.NotContains(t, sql, "`action_run`.created>=")
104+
}

routers/api/v1/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1203,6 +1203,7 @@ func Routes() *web.Router {
12031203
m.Put("/{workflow_id}/disable", reqRepoWriter(unit.TypeActions), repo.ActionsDisableWorkflow)
12041204
m.Put("/{workflow_id}/enable", reqRepoWriter(unit.TypeActions), repo.ActionsEnableWorkflow)
12051205
m.Post("/{workflow_id}/dispatches", reqRepoWriter(unit.TypeActions), bind(api.CreateActionWorkflowDispatch{}), repo.ActionsDispatchWorkflow)
1206+
m.Get("/{workflow_id}/runs", repo.ActionsListWorkflowRuns)
12061207
}, context.ReferencesGitRepo(), reqToken(), reqRepoReader(unit.TypeActions))
12071208

12081209
m.Group("/actions/jobs", func() {

routers/api/v1/repo/action.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,52 @@ func ActionsEnableWorkflow(ctx *context.APIContext) {
11001100
ctx.Status(http.StatusNoContent)
11011101
}
11021102

1103+
func ActionsListWorkflowRuns(ctx *context.APIContext) {
1104+
// swagger:operation GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs repository ActionsListWorkflowRuns
1105+
// ---
1106+
// summary: List workflow runs for a workflow
1107+
// produces:
1108+
// - application/json
1109+
// parameters:
1110+
// - name: owner
1111+
// in: path
1112+
// description: owner of the repo
1113+
// type: string
1114+
// required: true
1115+
// - name: repo
1116+
// in: path
1117+
// description: name of the repo
1118+
// type: string
1119+
// required: true
1120+
// - name: workflow_id
1121+
// in: path
1122+
// description: id of the workflow
1123+
// type: string
1124+
// required: true
1125+
// - name: page
1126+
// in: query
1127+
// description: page number of results to return (1-based)
1128+
// type: integer
1129+
// - name: limit
1130+
// in: query
1131+
// description: page size of results
1132+
// type: integer
1133+
// responses:
1134+
// "200":
1135+
// "$ref": "#/responses/WorkflowRunsList"
1136+
// "400":
1137+
// "$ref": "#/responses/error"
1138+
// "403":
1139+
// "$ref": "#/responses/forbidden"
1140+
// "404":
1141+
// "$ref": "#/responses/notFound"
1142+
// "422":
1143+
// "$ref": "#/responses/validationError"
1144+
// "500":
1145+
// "$ref": "#/responses/error"
1146+
shared.ListRuns(ctx, 0, ctx.Repo.Repository.ID)
1147+
}
1148+
11031149
// GetWorkflowRun Gets a specific workflow run.
11041150
func GetWorkflowRun(ctx *context.APIContext) {
11051151
// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run} repository GetWorkflowRun

routers/api/v1/shared/action.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ package shared
66
import (
77
"fmt"
88
"net/http"
9+
"strings"
10+
"time"
911

1012
actions_model "code.gitea.io/gitea/models/actions"
1113
"code.gitea.io/gitea/models/db"
@@ -123,6 +125,7 @@ func ListRuns(ctx *context.APIContext, ownerID, repoID int64) {
123125
opts := actions_model.FindRunOptions{
124126
OwnerID: ownerID,
125127
RepoID: repoID,
128+
WorkflowID: ctx.PathParam("workflow_id"),
126129
ListOptions: utils.GetListOptions(ctx),
127130
}
128131

@@ -151,6 +154,77 @@ func ListRuns(ctx *context.APIContext, ownerID, repoID int64) {
151154
if headSHA := ctx.FormString("head_sha"); headSHA != "" {
152155
opts.CommitSHA = headSHA
153156
}
157+
158+
// Handle exclude_pull_requests parameter
159+
if exclude := ctx.FormString("exclude_pull_requests"); exclude != "" {
160+
if exclude == "true" || exclude == "1" {
161+
opts.ExcludePullRequests = true
162+
}
163+
}
164+
165+
// Handle check_suite_id parameter
166+
if checkSuiteID := ctx.FormInt64("check_suite_id"); checkSuiteID > 0 {
167+
opts.CheckSuiteID = checkSuiteID
168+
}
169+
170+
// Handle created parameter for date filtering
171+
if created := ctx.FormString("created"); created != "" {
172+
// Parse the date range in the format like ">=2023-01-01", "<=2023-12-31", or "2023-01-01..2023-12-31"
173+
if strings.Contains(created, "..\u002e") {
174+
// Range format: "2023-01-01..2023-12-31"
175+
dateRange := strings.Split(created, "..")
176+
if len(dateRange) == 2 {
177+
startDate, err := time.Parse("2006-01-02", dateRange[0])
178+
if err == nil {
179+
opts.CreatedAfter = startDate
180+
}
181+
182+
endDate, err := time.Parse("2006-01-02", dateRange[1])
183+
if err == nil {
184+
// Set to end of day
185+
endDate = endDate.Add(24*time.Hour - time.Second)
186+
opts.CreatedBefore = endDate
187+
}
188+
}
189+
} else if strings.HasPrefix(created, ">=") {
190+
// Greater than or equal format: ">=2023-01-01"
191+
dateStr := strings.TrimPrefix(created, ">=")
192+
startDate, err := time.Parse("2006-01-02", dateStr)
193+
if err == nil {
194+
opts.CreatedAfter = startDate
195+
}
196+
} else if strings.HasPrefix(created, ">") {
197+
// Greater than format: ">2023-01-01"
198+
dateStr := strings.TrimPrefix(created, ">")
199+
startDate, err := time.Parse("2006-01-02", dateStr)
200+
if err == nil {
201+
opts.CreatedAfter = startDate.Add(24 * time.Hour)
202+
}
203+
} else if strings.HasPrefix(created, "<=") {
204+
// Less than or equal format: "<=2023-12-31"
205+
dateStr := strings.TrimPrefix(created, "<=")
206+
endDate, err := time.Parse("2006-01-02", dateStr)
207+
if err == nil {
208+
// Set to end of day
209+
endDate = endDate.Add(24*time.Hour - time.Second)
210+
opts.CreatedBefore = endDate
211+
}
212+
} else if strings.HasPrefix(created, "<") {
213+
// Less than format: "<2023-12-31"
214+
dateStr := strings.TrimPrefix(created, "<")
215+
endDate, err := time.Parse("2006-01-02", dateStr)
216+
if err == nil {
217+
opts.CreatedBefore = endDate
218+
}
219+
} else {
220+
// Exact date format: "2023-01-01"
221+
exactDate, err := time.Parse("2006-01-02", created)
222+
if err == nil {
223+
opts.CreatedAfter = exactDate
224+
opts.CreatedBefore = exactDate.Add(24*time.Hour - time.Second)
225+
}
226+
}
227+
}
154228

155229
runs, total, err := db.FindAndCount[actions_model.ActionRun](ctx, opts)
156230
if err != nil {

0 commit comments

Comments
 (0)