diff --git a/models/actions/runner.go b/models/actions/runner.go index 81d4249ae0..84398b143b 100644 --- a/models/actions/runner.go +++ b/models/actions/runner.go @@ -14,6 +14,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/shared/types" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" @@ -173,6 +174,13 @@ func (r *ActionRunner) GenerateToken() (err error) { return err } +// CanMatchLabels checks whether the runner's labels can match a job's "runs-on" +// See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idruns-on +func (r *ActionRunner) CanMatchLabels(jobRunsOn []string) bool { + runnerLabelSet := container.SetOf(r.AgentLabels...) + return runnerLabelSet.Contains(jobRunsOn...) // match all labels +} + func init() { db.RegisterModel(&ActionRunner{}) } diff --git a/models/actions/task.go b/models/actions/task.go index 7417af8b45..8b4ecf28f7 100644 --- a/models/actions/task.go +++ b/models/actions/task.go @@ -13,7 +13,6 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" @@ -245,7 +244,7 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask var job *ActionRunJob log.Trace("runner labels: %v", runner.AgentLabels) for _, v := range jobs { - if isSubset(runner.AgentLabels, v.RunsOn) { + if runner.CanMatchLabels(v.RunsOn) { job = v break } @@ -475,20 +474,6 @@ func FindOldTasksToExpire(ctx context.Context, olderThan timeutil.TimeStamp, lim Find(&tasks) } -func isSubset(set, subset []string) bool { - m := make(container.Set[string], len(set)) - for _, v := range set { - m.Add(v) - } - - for _, v := range subset { - if !m.Contains(v) { - return false - } - } - return true -} - func convertTimestamp(timestamp *timestamppb.Timestamp) timeutil.TimeStamp { if timestamp.GetSeconds() == 0 && timestamp.GetNanos() == 0 { return timeutil.TimeStamp(0) diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index 202da407d2..43c56752e8 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -28,7 +28,7 @@ import ( "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" - "github.com/nektos/act/pkg/model" + act_model "github.com/nektos/act/pkg/model" "gopkg.in/yaml.v3" ) @@ -38,9 +38,10 @@ const ( tplViewActions templates.TplName = "repo/actions/view" ) -type Workflow struct { - Entry git.TreeEntry - ErrMsg string +type WorkflowInfo struct { + Entry git.TreeEntry + ErrMsg string + Workflow *act_model.Workflow } // MustEnableActions check if actions are enabled in settings @@ -77,7 +78,11 @@ func List(ctx *context.Context) { return } - workflows := prepareWorkflowDispatchTemplate(ctx, commit) + workflows, curWorkflowID := prepareWorkflowTemplate(ctx, commit) + if ctx.Written() { + return + } + prepareWorkflowDispatchTemplate(ctx, workflows, curWorkflowID) if ctx.Written() { return } @@ -112,55 +117,41 @@ func WorkflowDispatchInputs(ctx *context.Context) { ctx.ServerError("GetTagCommit/GetBranchCommit", err) return } - prepareWorkflowDispatchTemplate(ctx, commit) + workflows, curWorkflowID := prepareWorkflowTemplate(ctx, commit) + if ctx.Written() { + return + } + prepareWorkflowDispatchTemplate(ctx, workflows, curWorkflowID) if ctx.Written() { return } ctx.HTML(http.StatusOK, tplDispatchInputsActions) } -func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) (workflows []Workflow) { - workflowID := ctx.FormString("workflow") - ctx.Data["CurWorkflow"] = workflowID - ctx.Data["CurWorkflowExists"] = false - - var curWorkflow *model.Workflow +func prepareWorkflowTemplate(ctx *context.Context, commit *git.Commit) (workflows []WorkflowInfo, curWorkflowID string) { + curWorkflowID = ctx.FormString("workflow") _, entries, err := actions.ListWorkflows(commit) if err != nil { ctx.ServerError("ListWorkflows", err) - return nil + return nil, "" } - // Get all runner labels - runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{ - RepoID: ctx.Repo.Repository.ID, - IsOnline: optional.Some(true), - WithAvailable: true, - }) - if err != nil { - ctx.ServerError("FindRunners", err) - return nil - } - allRunnerLabels := make(container.Set[string]) - for _, r := range runners { - allRunnerLabels.AddMultiple(r.AgentLabels...) - } - - workflows = make([]Workflow, 0, len(entries)) + workflows = make([]WorkflowInfo, 0, len(entries)) for _, entry := range entries { - workflow := Workflow{Entry: *entry} + workflow := WorkflowInfo{Entry: *entry} content, err := actions.GetContentFromEntry(entry) if err != nil { ctx.ServerError("GetContentFromEntry", err) - return nil + return nil, "" } - wf, err := model.ReadWorkflow(bytes.NewReader(content)) + wf, err := act_model.ReadWorkflow(bytes.NewReader(content)) if err != nil { workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error()) workflows = append(workflows, workflow) continue } + workflow.Workflow = wf // The workflow must contain at least one job without "needs". Otherwise, a deadlock will occur and no jobs will be able to run. hasJobWithoutNeeds := false // Check whether you have matching runner and a job without "needs" @@ -173,22 +164,6 @@ func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) ( if !hasJobWithoutNeeds && len(j.Needs()) == 0 { hasJobWithoutNeeds = true } - runsOnList := j.RunsOn() - for _, ro := range runsOnList { - if strings.Contains(ro, "${{") { - // Skip if it contains expressions. - // The expressions could be very complex and could not be evaluated here, - // so just skip it, it's OK since it's just a tooltip message. - continue - } - if !allRunnerLabels.Contains(ro) { - workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", ro) - break - } - } - if workflow.ErrMsg != "" { - break - } } if !hasJobWithoutNeeds { workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs") @@ -197,61 +172,75 @@ func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) ( workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job") } workflows = append(workflows, workflow) - - if workflow.Entry.Name() == workflowID { - curWorkflow = wf - ctx.Data["CurWorkflowExists"] = true - } } ctx.Data["workflows"] = workflows ctx.Data["RepoLink"] = ctx.Repo.Repository.Link() - + ctx.Data["AllowDisableOrEnableWorkflow"] = ctx.Repo.IsAdmin() actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() ctx.Data["ActionsConfig"] = actionsConfig + ctx.Data["CurWorkflow"] = curWorkflowID + ctx.Data["CurWorkflowDisabled"] = actionsConfig.IsWorkflowDisabled(curWorkflowID) - if len(workflowID) > 0 && ctx.Repo.CanWrite(unit.TypeActions) { - ctx.Data["AllowDisableOrEnableWorkflow"] = ctx.Repo.IsAdmin() - isWorkflowDisabled := actionsConfig.IsWorkflowDisabled(workflowID) - ctx.Data["CurWorkflowDisabled"] = isWorkflowDisabled - - if !isWorkflowDisabled && curWorkflow != nil { - workflowDispatchConfig := workflowDispatchConfig(curWorkflow) - if workflowDispatchConfig != nil { - ctx.Data["WorkflowDispatchConfig"] = workflowDispatchConfig - - branchOpts := git_model.FindBranchOptions{ - RepoID: ctx.Repo.Repository.ID, - IsDeletedBranch: optional.Some(false), - ListOptions: db.ListOptions{ - ListAll: true, - }, - } - branches, err := git_model.FindBranchNames(ctx, branchOpts) - if err != nil { - ctx.ServerError("FindBranchNames", err) - return nil - } - // always put default branch on the top if it exists - if slices.Contains(branches, ctx.Repo.Repository.DefaultBranch) { - branches = util.SliceRemoveAll(branches, ctx.Repo.Repository.DefaultBranch) - branches = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...) - } - ctx.Data["Branches"] = branches - - tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID) - if err != nil { - ctx.ServerError("GetTagNamesByRepoID", err) - return nil - } - ctx.Data["Tags"] = tags - } - } - } - return workflows + return workflows, curWorkflowID } -func prepareWorkflowList(ctx *context.Context, workflows []Workflow) { +func prepareWorkflowDispatchTemplate(ctx *context.Context, workflowInfos []WorkflowInfo, curWorkflowID string) { + actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() + if curWorkflowID == "" || !ctx.Repo.CanWrite(unit.TypeActions) || actionsConfig.IsWorkflowDisabled(curWorkflowID) { + return + } + + var curWorkflow *act_model.Workflow + for _, workflowInfo := range workflowInfos { + if workflowInfo.Entry.Name() == curWorkflowID { + if workflowInfo.Workflow == nil { + log.Debug("CurWorkflowID %s is found but its workflowInfo.Workflow is nil", curWorkflowID) + return + } + curWorkflow = workflowInfo.Workflow + break + } + } + + if curWorkflow == nil { + return + } + + ctx.Data["CurWorkflowExists"] = true + curWfDispatchCfg := workflowDispatchConfig(curWorkflow) + if curWfDispatchCfg == nil { + return + } + + ctx.Data["WorkflowDispatchConfig"] = curWfDispatchCfg + + branchOpts := git_model.FindBranchOptions{ + RepoID: ctx.Repo.Repository.ID, + IsDeletedBranch: optional.Some(false), + ListOptions: db.ListOptions{ + ListAll: true, + }, + } + branches, err := git_model.FindBranchNames(ctx, branchOpts) + if err != nil { + ctx.ServerError("FindBranchNames", err) + return + } + // always put default branch on the top + branches = util.SliceRemoveAll(branches, ctx.Repo.Repository.DefaultBranch) + branches = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...) + ctx.Data["Branches"] = branches + + tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID) + if err != nil { + ctx.ServerError("GetTagNamesByRepoID", err) + return + } + ctx.Data["Tags"] = tags +} + +func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo) { actorID := ctx.FormInt64("actor") status := ctx.FormInt("status") workflowID := ctx.FormString("workflow") @@ -302,6 +291,45 @@ func prepareWorkflowList(ctx *context.Context, workflows []Workflow) { log.Error("LoadIsRefDeleted", err) } + // Check for each run if there is at least one online runner that can run its jobs + runErrors := make(map[int64]string) + runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{ + RepoID: ctx.Repo.Repository.ID, + IsOnline: optional.Some(true), + WithAvailable: true, + }) + if err != nil { + ctx.ServerError("FindRunners", err) + return + } + for _, run := range runs { + if !run.Status.In(actions_model.StatusWaiting, actions_model.StatusRunning) { + continue + } + jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID) + if err != nil { + ctx.ServerError("GetRunJobsByRunID", err) + return + } + for _, job := range jobs { + if !job.Status.IsWaiting() { + continue + } + hasOnlineRunner := false + for _, runner := range runners { + if runner.CanMatchLabels(job.RunsOn) { + hasOnlineRunner = true + break + } + } + if !hasOnlineRunner { + runErrors[run.ID] = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", strings.Join(job.RunsOn, ",")) + break + } + } + } + ctx.Data["RunErrors"] = runErrors + ctx.Data["Runs"] = runs actors, err := actions_model.GetActors(ctx, ctx.Repo.Repository.ID) @@ -362,7 +390,7 @@ type WorkflowDispatch struct { Inputs []WorkflowDispatchInput } -func workflowDispatchConfig(w *model.Workflow) *WorkflowDispatch { +func workflowDispatchConfig(w *act_model.Workflow) *WorkflowDispatch { switch w.RawOn.Kind { case yaml.ScalarNode: var val string diff --git a/templates/repo/actions/list.tmpl b/templates/repo/actions/list.tmpl index 7d782c0ade..fe8c26b523 100644 --- a/templates/repo/actions/list.tmpl +++ b/templates/repo/actions/list.tmpl @@ -7,14 +7,14 @@ {{if .HasWorkflowsOrRuns}}
-