@@ -12,7 +12,9 @@ import (
1212
1313 actions_model "gitea.dev/models/actions"
1414 "gitea.dev/models/db"
15+ "gitea.dev/modules/container"
1516 "gitea.dev/modules/log"
17+ "gitea.dev/modules/optional"
1618 "gitea.dev/modules/setting"
1719 "gitea.dev/modules/templates"
1820 "gitea.dev/modules/util"
@@ -168,6 +170,95 @@ func Runners(ctx *context.Context) {
168170 ctx .HTML (http .StatusOK , rCtx .RunnersTemplate )
169171}
170172
173+ // Queue renders the Actions build queue: waiting jobs in runner pickup order plus currently running jobs.
174+ func Queue (ctx * context.Context ) {
175+ ctx .Data ["PageIsSharedSettingsQueue" ] = true
176+ ctx .Data ["Title" ] = ctx .Tr ("actions.actions" )
177+ ctx .Data ["PageType" ] = "queue"
178+
179+ rCtx , err := getRunnersCtx (ctx )
180+ if err != nil {
181+ ctx .ServerError ("getRunnersCtx" , err )
182+ return
183+ }
184+
185+ // scope narrows the query to the current context. Admin (both 0) sees the whole instance, which is
186+ // the only scope where the order is globally exact; repo/org/user scopes show the correct relative
187+ // order of their own jobs, but not their absolute position within the instance-wide queue.
188+ scope := func (opts * actions_model.FindRunJobOptions ) {
189+ if rCtx .IsRepo {
190+ opts .RepoID = rCtx .RepoID
191+ } else if rCtx .IsOrg || rCtx .IsUser {
192+ opts .OwnerID = rCtx .OwnerID
193+ }
194+ }
195+
196+ page := max (ctx .FormInt ("page" ), 1 )
197+
198+ // Mirror the runner pickup predicate (see CreateTaskForRunner): waiting, unclaimed, non-reusable-caller jobs.
199+ queuedOpts := actions_model.FindRunJobOptions {
200+ ListOptions : db.ListOptions {Page : page , PageSize : 50 },
201+ Statuses : []actions_model.Status {actions_model .StatusWaiting },
202+ IsReusableCaller : optional .Some (false ), // reusable callers never run on a runner, so they are not queued
203+ HasTask : optional .Some (false ), // a claimed job already has a task and is no longer queued
204+ OrderBy : actions_model .QueuedJobsOrderBy ,
205+ }
206+ scope (& queuedOpts )
207+ queuedJobs , queuedTotal , err := db .FindAndCount [actions_model.ActionRunJob ](ctx , queuedOpts )
208+ if err != nil {
209+ ctx .ServerError ("FindQueuedJobs" , err )
210+ return
211+ }
212+ if err := actions_model .ActionJobList (queuedJobs ).LoadAttributes (ctx , true ); err != nil {
213+ ctx .ServerError ("LoadAttributes" , err )
214+ return
215+ }
216+
217+ // running jobs are bounded by the number of online runners, so a single capped page is enough.
218+ runningOpts := actions_model.FindRunJobOptions {
219+ ListOptions : db.ListOptions {Page : 1 , PageSize : 100 },
220+ Statuses : []actions_model.Status {actions_model .StatusRunning },
221+ OrderBy : actions_model .RunningJobsOrderBy ,
222+ }
223+ scope (& runningOpts )
224+ runningJobs , err := db .Find [actions_model.ActionRunJob ](ctx , runningOpts )
225+ if err != nil {
226+ ctx .ServerError ("FindRunningJobs" , err )
227+ return
228+ }
229+ if err := actions_model .ActionJobList (runningJobs ).LoadAttributes (ctx , true ); err != nil {
230+ ctx .ServerError ("LoadAttributes" , err )
231+ return
232+ }
233+
234+ ctx .Data ["QueuedJobs" ] = queuedJobs
235+ ctx .Data ["QueuedTotal" ] = queuedTotal
236+ ctx .Data ["QueueOffset" ] = (page - 1 ) * queuedOpts .PageSize // absolute position of the first row on this page
237+ ctx .Data ["RunningJobs" ] = runningJobs
238+ ctx .Data ["ShowRepoColumn" ] = ! rCtx .IsRepo
239+
240+ pager := context .NewPagination (queuedTotal , queuedOpts .PageSize , page , 5 )
241+ pager .AddParamFromRequest (ctx .Req )
242+ pager .RemoveParam (container .SetOf ("refresh" )) // keep the auto-refresh flag out of the page links
243+ ctx .Data ["Page" ] = pager
244+
245+ // The list auto-refreshes by re-fetching just the fragment: poll faster while there is activity,
246+ // slower when idle so quiet pages don't hammer the server.
247+ hasActivity := len (queuedJobs ) > 0 || len (runningJobs ) > 0
248+ refreshInterval := util .Iif [int64 ](hasActivity , 3000 , 10000 )
249+ if ! setting .IsProd {
250+ refreshInterval = util .Iif [int64 ](hasActivity , 1000 , 2000 ) // faster in dev for easier debugging
251+ }
252+ ctx .Data ["QueueRefreshIntervalMs" ] = refreshInterval
253+ ctx .Data ["QueueRefreshLink" ] = templates .QueryBuild (setting .AppSubURL + ctx .Req .RequestURI , "refresh" , "1" )
254+
255+ if ctx .FormBool ("refresh" ) {
256+ ctx .HTML (http .StatusOK , "shared/actions/queue_list" )
257+ return
258+ }
259+ ctx .HTML (http .StatusOK , rCtx .RunnersTemplate )
260+ }
261+
171262// RunnersEdit renders runner edit page for repository level
172263func RunnersEdit (ctx * context.Context ) {
173264 ctx .Data ["PageIsSharedSettingsRunners" ] = true
0 commit comments