Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
209 changes: 122 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,81 @@ 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 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
}
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 +298,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 +397,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