Skip to content
13 changes: 13 additions & 0 deletions models/actions/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -173,6 +174,18 @@ 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...)
for _, v := range jobRunsOn {
if !runnerLabelSet.Contains(v) {
return false
}
}
return true
}

func init() {
db.RegisterModel(&ActionRunner{})
}
Expand Down
17 changes: 1 addition & 16 deletions models/actions/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
Expand Down
207 changes: 120 additions & 87 deletions routers/web/repo/actions/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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
Expand Down Expand Up @@ -77,7 +78,11 @@ func List(ctx *context.Context) {
return
}

workflows := prepareWorkflowDispatchTemplate(ctx, commit)
workflows := prepareWorkflowTemplate(ctx, commit)
if ctx.Written() {
return
}
prepareWorkflowDispatchTemplate(ctx, workflows)
if ctx.Written() {
return
}
Expand Down Expand Up @@ -112,55 +117,42 @@ func WorkflowDispatchInputs(ctx *context.Context) {
ctx.ServerError("GetTagCommit/GetBranchCommit", err)
return
}
prepareWorkflowDispatchTemplate(ctx, commit)
workflows := prepareWorkflowTemplate(ctx, commit)
if ctx.Written() {
return
}
prepareWorkflowDispatchTemplate(ctx, workflows)
if ctx.Written() {
return
}
ctx.HTML(http.StatusOK, tplDispatchInputsActions)
}

func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) (workflows []Workflow) {
func prepareWorkflowTemplate(ctx *context.Context, commit *git.Commit) (workflows []WorkflowInfo) {
workflowID := ctx.FormString("workflow")
ctx.Data["CurWorkflow"] = workflowID
ctx.Data["CurWorkflowExists"] = false

var curWorkflow *model.Workflow

_, entries, err := actions.ListWorkflows(commit)
if err != nil {
ctx.ServerError("ListWorkflows", err)
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
}
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"
Expand All @@ -173,22 +165,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")
Expand All @@ -197,61 +173,79 @@ 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

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
if len(workflowID) > 0 {
ctx.Data["CurWorkflowDisabled"] = actionsConfig.IsWorkflowDisabled(workflowID)
}

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
}

func prepareWorkflowDispatchTemplate(ctx *context.Context, workflowInfos []WorkflowInfo) {
workflowID := ctx.FormString("workflow")
actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
if len(workflowID) == 0 || !ctx.Repo.CanWrite(unit.TypeActions) || actionsConfig.IsWorkflowDisabled(workflowID) {
return
}

ctx.Data["CurWorkflowExists"] = false
var curWorkflow *act_model.Workflow
for _, workflowInfo := range workflowInfos {
if workflowInfo.Entry.Name() == workflowID {
if workflowInfo.Workflow == nil {
log.Error("%s workflowInfo.Workflow is nil", workflowID)
return
}
curWorkflow = workflowInfo.Workflow
ctx.Data["CurWorkflowExists"] = true
break
}
}
return workflows

if curWorkflow == nil {
return
}

workflowDispatchConfig := workflowDispatchConfig(curWorkflow)
if workflowDispatchConfig == nil {
return
}

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
}
// 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 []Workflow) {
func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo) {
actorID := ctx.FormInt64("actor")
status := ctx.FormInt("status")
workflowID := ctx.FormString("workflow")
Expand Down Expand Up @@ -302,6 +296,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)
Expand Down Expand Up @@ -362,7 +395,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
Expand Down
6 changes: 6 additions & 0 deletions templates/repo/actions/runs_list.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@
{{ctx.Locale.Tr "actions.runs.pushed_by"}}
<a href="{{$run.TriggerUser.HomeLink}}">{{$run.TriggerUser.GetDisplayName}}</a>
{{- end -}}
{{$errMsg := index $.RunErrors $run.ID}}
{{if $errMsg}}
<span data-tooltip-content="{{$errMsg}}">
{{svg "octicon-alert" 16 "text red"}}
</span>
{{end}}
</div>
</div>
<div class="flex-item-trailing">
Expand Down