Skip to content

Commit 0d740a6

Browse files
Improve online runner check (go-gitea#35722)
This PR moves "no online runner" warning to the runs list. A job's `runs-on` may contain expressions like `runs-on: [self-hosted, "${{ inputs.chosen-os }}"]` so the value of `runs-on` may be different in each run. We cannot check it through the workflow file. <details> <summary>Screenshots</summary> Before: <img width="960" alt="3d2a91746271d8b1f12c8f7d20eba550" src="https://github.com/user-attachments/assets/7a972c50-db97-49d2-b12b-c1a439732a11" /> After: <img width="960" alt="image" src="https://github.com/user-attachments/assets/fc076e0e-bd08-4afe-99b9-c0eb0fd2c7e7" /> </details> This PR also splits `prepareWorkflowDispatchTemplate` function into 2 functions: - `prepareWorkflowTemplate` get and check all of the workflows - `prepareWorkflowDispatchTemplate` only prepare workflow dispatch config for `workflow_dispatch` workflows. --------- Co-authored-by: wxiaoguang <[email protected]>
1 parent 9a73a1f commit 0d740a6

File tree

6 files changed

+143
-113
lines changed

6 files changed

+143
-113
lines changed

models/actions/runner.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
repo_model "code.gitea.io/gitea/models/repo"
1515
"code.gitea.io/gitea/models/shared/types"
1616
user_model "code.gitea.io/gitea/models/user"
17+
"code.gitea.io/gitea/modules/container"
1718
"code.gitea.io/gitea/modules/optional"
1819
"code.gitea.io/gitea/modules/setting"
1920
"code.gitea.io/gitea/modules/timeutil"
@@ -173,6 +174,13 @@ func (r *ActionRunner) GenerateToken() (err error) {
173174
return err
174175
}
175176

177+
// CanMatchLabels checks whether the runner's labels can match a job's "runs-on"
178+
// See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idruns-on
179+
func (r *ActionRunner) CanMatchLabels(jobRunsOn []string) bool {
180+
runnerLabelSet := container.SetOf(r.AgentLabels...)
181+
return runnerLabelSet.Contains(jobRunsOn...) // match all labels
182+
}
183+
176184
func init() {
177185
db.RegisterModel(&ActionRunner{})
178186
}

models/actions/task.go

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
auth_model "code.gitea.io/gitea/models/auth"
1414
"code.gitea.io/gitea/models/db"
1515
"code.gitea.io/gitea/models/unit"
16-
"code.gitea.io/gitea/modules/container"
1716
"code.gitea.io/gitea/modules/log"
1817
"code.gitea.io/gitea/modules/setting"
1918
"code.gitea.io/gitea/modules/timeutil"
@@ -245,7 +244,7 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
245244
var job *ActionRunJob
246245
log.Trace("runner labels: %v", runner.AgentLabels)
247246
for _, v := range jobs {
248-
if isSubset(runner.AgentLabels, v.RunsOn) {
247+
if runner.CanMatchLabels(v.RunsOn) {
249248
job = v
250249
break
251250
}
@@ -475,20 +474,6 @@ func FindOldTasksToExpire(ctx context.Context, olderThan timeutil.TimeStamp, lim
475474
Find(&tasks)
476475
}
477476

478-
func isSubset(set, subset []string) bool {
479-
m := make(container.Set[string], len(set))
480-
for _, v := range set {
481-
m.Add(v)
482-
}
483-
484-
for _, v := range subset {
485-
if !m.Contains(v) {
486-
return false
487-
}
488-
}
489-
return true
490-
}
491-
492477
func convertTimestamp(timestamp *timestamppb.Timestamp) timeutil.TimeStamp {
493478
if timestamp.GetSeconds() == 0 && timestamp.GetNanos() == 0 {
494479
return timeutil.TimeStamp(0)

routers/web/repo/actions/actions.go

Lines changed: 119 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import (
2828
"code.gitea.io/gitea/services/context"
2929
"code.gitea.io/gitea/services/convert"
3030

31-
"github.com/nektos/act/pkg/model"
31+
act_model "github.com/nektos/act/pkg/model"
3232
"gopkg.in/yaml.v3"
3333
)
3434

@@ -38,9 +38,10 @@ const (
3838
tplViewActions templates.TplName = "repo/actions/view"
3939
)
4040

41-
type Workflow struct {
42-
Entry git.TreeEntry
43-
ErrMsg string
41+
type WorkflowInfo struct {
42+
Entry git.TreeEntry
43+
ErrMsg string
44+
Workflow *act_model.Workflow
4445
}
4546

4647
// MustEnableActions check if actions are enabled in settings
@@ -77,7 +78,11 @@ func List(ctx *context.Context) {
7778
return
7879
}
7980

80-
workflows := prepareWorkflowDispatchTemplate(ctx, commit)
81+
workflows, curWorkflowID := prepareWorkflowTemplate(ctx, commit)
82+
if ctx.Written() {
83+
return
84+
}
85+
prepareWorkflowDispatchTemplate(ctx, workflows, curWorkflowID)
8186
if ctx.Written() {
8287
return
8388
}
@@ -112,55 +117,41 @@ func WorkflowDispatchInputs(ctx *context.Context) {
112117
ctx.ServerError("GetTagCommit/GetBranchCommit", err)
113118
return
114119
}
115-
prepareWorkflowDispatchTemplate(ctx, commit)
120+
workflows, curWorkflowID := prepareWorkflowTemplate(ctx, commit)
121+
if ctx.Written() {
122+
return
123+
}
124+
prepareWorkflowDispatchTemplate(ctx, workflows, curWorkflowID)
116125
if ctx.Written() {
117126
return
118127
}
119128
ctx.HTML(http.StatusOK, tplDispatchInputsActions)
120129
}
121130

122-
func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) (workflows []Workflow) {
123-
workflowID := ctx.FormString("workflow")
124-
ctx.Data["CurWorkflow"] = workflowID
125-
ctx.Data["CurWorkflowExists"] = false
126-
127-
var curWorkflow *model.Workflow
131+
func prepareWorkflowTemplate(ctx *context.Context, commit *git.Commit) (workflows []WorkflowInfo, curWorkflowID string) {
132+
curWorkflowID = ctx.FormString("workflow")
128133

129134
_, entries, err := actions.ListWorkflows(commit)
130135
if err != nil {
131136
ctx.ServerError("ListWorkflows", err)
132-
return nil
137+
return nil, ""
133138
}
134139

135-
// Get all runner labels
136-
runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{
137-
RepoID: ctx.Repo.Repository.ID,
138-
IsOnline: optional.Some(true),
139-
WithAvailable: true,
140-
})
141-
if err != nil {
142-
ctx.ServerError("FindRunners", err)
143-
return nil
144-
}
145-
allRunnerLabels := make(container.Set[string])
146-
for _, r := range runners {
147-
allRunnerLabels.AddMultiple(r.AgentLabels...)
148-
}
149-
150-
workflows = make([]Workflow, 0, len(entries))
140+
workflows = make([]WorkflowInfo, 0, len(entries))
151141
for _, entry := range entries {
152-
workflow := Workflow{Entry: *entry}
142+
workflow := WorkflowInfo{Entry: *entry}
153143
content, err := actions.GetContentFromEntry(entry)
154144
if err != nil {
155145
ctx.ServerError("GetContentFromEntry", err)
156-
return nil
146+
return nil, ""
157147
}
158-
wf, err := model.ReadWorkflow(bytes.NewReader(content))
148+
wf, err := act_model.ReadWorkflow(bytes.NewReader(content))
159149
if err != nil {
160150
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error())
161151
workflows = append(workflows, workflow)
162152
continue
163153
}
154+
workflow.Workflow = wf
164155
// The workflow must contain at least one job without "needs". Otherwise, a deadlock will occur and no jobs will be able to run.
165156
hasJobWithoutNeeds := false
166157
// Check whether you have matching runner and a job without "needs"
@@ -173,22 +164,6 @@ func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) (
173164
if !hasJobWithoutNeeds && len(j.Needs()) == 0 {
174165
hasJobWithoutNeeds = true
175166
}
176-
runsOnList := j.RunsOn()
177-
for _, ro := range runsOnList {
178-
if strings.Contains(ro, "${{") {
179-
// Skip if it contains expressions.
180-
// The expressions could be very complex and could not be evaluated here,
181-
// so just skip it, it's OK since it's just a tooltip message.
182-
continue
183-
}
184-
if !allRunnerLabels.Contains(ro) {
185-
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", ro)
186-
break
187-
}
188-
}
189-
if workflow.ErrMsg != "" {
190-
break
191-
}
192167
}
193168
if !hasJobWithoutNeeds {
194169
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs")
@@ -197,61 +172,75 @@ func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) (
197172
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job")
198173
}
199174
workflows = append(workflows, workflow)
200-
201-
if workflow.Entry.Name() == workflowID {
202-
curWorkflow = wf
203-
ctx.Data["CurWorkflowExists"] = true
204-
}
205175
}
206176

207177
ctx.Data["workflows"] = workflows
208178
ctx.Data["RepoLink"] = ctx.Repo.Repository.Link()
209-
179+
ctx.Data["AllowDisableOrEnableWorkflow"] = ctx.Repo.IsAdmin()
210180
actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
211181
ctx.Data["ActionsConfig"] = actionsConfig
182+
ctx.Data["CurWorkflow"] = curWorkflowID
183+
ctx.Data["CurWorkflowDisabled"] = actionsConfig.IsWorkflowDisabled(curWorkflowID)
212184

213-
if len(workflowID) > 0 && ctx.Repo.CanWrite(unit.TypeActions) {
214-
ctx.Data["AllowDisableOrEnableWorkflow"] = ctx.Repo.IsAdmin()
215-
isWorkflowDisabled := actionsConfig.IsWorkflowDisabled(workflowID)
216-
ctx.Data["CurWorkflowDisabled"] = isWorkflowDisabled
217-
218-
if !isWorkflowDisabled && curWorkflow != nil {
219-
workflowDispatchConfig := workflowDispatchConfig(curWorkflow)
220-
if workflowDispatchConfig != nil {
221-
ctx.Data["WorkflowDispatchConfig"] = workflowDispatchConfig
222-
223-
branchOpts := git_model.FindBranchOptions{
224-
RepoID: ctx.Repo.Repository.ID,
225-
IsDeletedBranch: optional.Some(false),
226-
ListOptions: db.ListOptions{
227-
ListAll: true,
228-
},
229-
}
230-
branches, err := git_model.FindBranchNames(ctx, branchOpts)
231-
if err != nil {
232-
ctx.ServerError("FindBranchNames", err)
233-
return nil
234-
}
235-
// always put default branch on the top if it exists
236-
if slices.Contains(branches, ctx.Repo.Repository.DefaultBranch) {
237-
branches = util.SliceRemoveAll(branches, ctx.Repo.Repository.DefaultBranch)
238-
branches = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
239-
}
240-
ctx.Data["Branches"] = branches
185+
return workflows, curWorkflowID
186+
}
241187

242-
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
243-
if err != nil {
244-
ctx.ServerError("GetTagNamesByRepoID", err)
245-
return nil
246-
}
247-
ctx.Data["Tags"] = tags
188+
func prepareWorkflowDispatchTemplate(ctx *context.Context, workflowInfos []WorkflowInfo, curWorkflowID string) {
189+
actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
190+
if curWorkflowID == "" || !ctx.Repo.CanWrite(unit.TypeActions) || actionsConfig.IsWorkflowDisabled(curWorkflowID) {
191+
return
192+
}
193+
194+
var curWorkflow *act_model.Workflow
195+
for _, workflowInfo := range workflowInfos {
196+
if workflowInfo.Entry.Name() == curWorkflowID {
197+
if workflowInfo.Workflow == nil {
198+
log.Debug("CurWorkflowID %s is found but its workflowInfo.Workflow is nil", curWorkflowID)
199+
return
248200
}
201+
curWorkflow = workflowInfo.Workflow
202+
break
249203
}
250204
}
251-
return workflows
205+
206+
if curWorkflow == nil {
207+
return
208+
}
209+
210+
ctx.Data["CurWorkflowExists"] = true
211+
curWfDispatchCfg := workflowDispatchConfig(curWorkflow)
212+
if curWfDispatchCfg == nil {
213+
return
214+
}
215+
216+
ctx.Data["WorkflowDispatchConfig"] = curWfDispatchCfg
217+
218+
branchOpts := git_model.FindBranchOptions{
219+
RepoID: ctx.Repo.Repository.ID,
220+
IsDeletedBranch: optional.Some(false),
221+
ListOptions: db.ListOptions{
222+
ListAll: true,
223+
},
224+
}
225+
branches, err := git_model.FindBranchNames(ctx, branchOpts)
226+
if err != nil {
227+
ctx.ServerError("FindBranchNames", err)
228+
return
229+
}
230+
// always put default branch on the top
231+
branches = util.SliceRemoveAll(branches, ctx.Repo.Repository.DefaultBranch)
232+
branches = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
233+
ctx.Data["Branches"] = branches
234+
235+
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
236+
if err != nil {
237+
ctx.ServerError("GetTagNamesByRepoID", err)
238+
return
239+
}
240+
ctx.Data["Tags"] = tags
252241
}
253242

254-
func prepareWorkflowList(ctx *context.Context, workflows []Workflow) {
243+
func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo) {
255244
actorID := ctx.FormInt64("actor")
256245
status := ctx.FormInt("status")
257246
workflowID := ctx.FormString("workflow")
@@ -302,6 +291,45 @@ func prepareWorkflowList(ctx *context.Context, workflows []Workflow) {
302291
log.Error("LoadIsRefDeleted", err)
303292
}
304293

294+
// Check for each run if there is at least one online runner that can run its jobs
295+
runErrors := make(map[int64]string)
296+
runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{
297+
RepoID: ctx.Repo.Repository.ID,
298+
IsOnline: optional.Some(true),
299+
WithAvailable: true,
300+
})
301+
if err != nil {
302+
ctx.ServerError("FindRunners", err)
303+
return
304+
}
305+
for _, run := range runs {
306+
if !run.Status.In(actions_model.StatusWaiting, actions_model.StatusRunning) {
307+
continue
308+
}
309+
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
310+
if err != nil {
311+
ctx.ServerError("GetRunJobsByRunID", err)
312+
return
313+
}
314+
for _, job := range jobs {
315+
if !job.Status.IsWaiting() {
316+
continue
317+
}
318+
hasOnlineRunner := false
319+
for _, runner := range runners {
320+
if runner.CanMatchLabels(job.RunsOn) {
321+
hasOnlineRunner = true
322+
break
323+
}
324+
}
325+
if !hasOnlineRunner {
326+
runErrors[run.ID] = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", strings.Join(job.RunsOn, ","))
327+
break
328+
}
329+
}
330+
}
331+
ctx.Data["RunErrors"] = runErrors
332+
305333
ctx.Data["Runs"] = runs
306334

307335
actors, err := actions_model.GetActors(ctx, ctx.Repo.Repository.ID)
@@ -362,7 +390,7 @@ type WorkflowDispatch struct {
362390
Inputs []WorkflowDispatchInput
363391
}
364392

365-
func workflowDispatchConfig(w *model.Workflow) *WorkflowDispatch {
393+
func workflowDispatchConfig(w *act_model.Workflow) *WorkflowDispatch {
366394
switch w.RawOn.Kind {
367395
case yaml.ScalarNode:
368396
var val string

templates/repo/actions/list.tmpl

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
{{if .HasWorkflowsOrRuns}}
88
<div class="ui stackable grid">
99
<div class="four wide column">
10-
<div class="ui fluid vertical menu">
11-
<a class="item{{if not $.CurWorkflow}} active{{end}}" href="?actor={{$.CurActor}}&status={{$.CurStatus}}">{{ctx.Locale.Tr "actions.runs.all_workflows"}}</a>
10+
<div class="ui fluid vertical menu flex-items-block">
11+
<a class="item {{if not $.CurWorkflow}}active{{end}}" href="?actor={{$.CurActor}}&status={{$.CurStatus}}">{{ctx.Locale.Tr "actions.runs.all_workflows"}}</a>
1212
{{range .workflows}}
13-
<a class="item{{if eq .Entry.Name $.CurWorkflow}} active{{end}}" href="?workflow={{.Entry.Name}}&actor={{$.CurActor}}&status={{$.CurStatus}}">{{.Entry.Name}}
13+
<a class="item {{if eq .Entry.Name $.CurWorkflow}}active{{end}}" href="?workflow={{.Entry.Name}}&actor={{$.CurActor}}&status={{$.CurStatus}}">
14+
<span class="gt-ellipsis">{{.Entry.Name}}</span>
15+
1416
{{if .ErrMsg}}
15-
<span data-tooltip-content="{{.ErrMsg}}">
16-
{{svg "octicon-alert" 16 "text red"}}
17-
</span>
17+
<span class="flex-text-inline" data-tooltip-content="{{.ErrMsg}}">{{svg "octicon-alert" 16 "text red"}}</span>
1818
{{end}}
1919

2020
{{if $.ActionsConfig.IsWorkflowDisabled .Entry.Name}}

0 commit comments

Comments
 (0)