Skip to content

workflow_dispatch use workflow from trigger branch #33098

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jan 5, 2025
1 change: 1 addition & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3765,6 +3765,7 @@ workflow.not_found = Workflow '%s' not found.
workflow.run_success = Workflow '%s' run successfully.
workflow.from_ref = Use workflow from
workflow.has_workflow_dispatch = This workflow has a workflow_dispatch event trigger.
workflow.has_no_workflow_dispatch = Workflow '%s' has no workflow_dispatch event trigger.

need_approval_desc = Need approval to run workflows for fork pull request.

Expand Down
216 changes: 131 additions & 85 deletions routers/web/repo/actions/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ import (
)

const (
tplListActions templates.TplName = "repo/actions/list"
tplViewActions templates.TplName = "repo/actions/view"
tplListActions templates.TplName = "repo/actions/list"
tplDispatchInputsActions templates.TplName = "repo/actions/workflow_dispatch_inputs"
tplViewActions templates.TplName = "repo/actions/view"
)

type Workflow struct {
Expand Down Expand Up @@ -64,107 +65,143 @@ func MustEnableActions(ctx *context.Context) {
func List(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("actions.actions")
ctx.Data["PageIsActions"] = true

commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
if err != nil {
ctx.ServerError("GetBranchCommit", err)
return
}

workflows := prepareWorkflowDispatchTemplate(ctx, commit)
if ctx.Written() {
return
}

prepareWorkflowList(ctx, workflows)
if ctx.Written() {
return
}

ctx.HTML(http.StatusOK, tplListActions)
}

func WorkflowDispatchInputs(ctx *context.Context) {
ref := ctx.FormString("ref")
if ref == "" {
ctx.NotFound("WorkflowDispatchInputs: no ref", nil)
return
}
// get target commit of run from specified ref
refName := git.RefName(ref)
var commit *git.Commit
var err error
if refName.IsTag() {
commit, err = ctx.Repo.GitRepo.GetTagCommit(refName.TagName())
} else if refName.IsBranch() {
commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName.BranchName())
} else {
ctx.ServerError("UnsupportedRefType", nil)
return
}
if err != nil {
ctx.ServerError("GetTagCommit/GetBranchCommit", err)
return
}
prepareWorkflowDispatchTemplate(ctx, commit)
if ctx.Written() {
return
}
ctx.HTML(http.StatusOK, tplDispatchInputsActions)
}

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

var workflows []Workflow
var curWorkflow *model.Workflow
if empty, err := ctx.Repo.GitRepo.IsEmpty(); err != nil {
ctx.ServerError("IsEmpty", err)
return
} else if !empty {
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
if err != nil {
ctx.ServerError("GetBranchCommit", err)
return
}
entries, err := actions.ListWorkflows(commit)
if err != nil {
ctx.ServerError("ListWorkflows", err)
return
}

// 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,
})
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))
for _, entry := range entries {
workflow := Workflow{Entry: *entry}
content, err := actions.GetContentFromEntry(entry)
if err != nil {
ctx.ServerError("FindRunners", err)
return
ctx.ServerError("GetContentFromEntry", err)
return nil
}
allRunnerLabels := make(container.Set[string])
for _, r := range runners {
allRunnerLabels.AddMultiple(r.AgentLabels...)
wf, err := 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
}

workflows = make([]Workflow, 0, len(entries))
for _, entry := range entries {
workflow := Workflow{Entry: *entry}
content, err := actions.GetContentFromEntry(entry)
if err != nil {
ctx.ServerError("GetContentFromEntry", err)
return
}
wf, err := model.ReadWorkflow(bytes.NewReader(content))
if err != nil {
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error())
workflows = append(workflows, workflow)
// 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"
emptyJobsNumber := 0
for _, j := range wf.Jobs {
if j == nil {
emptyJobsNumber++
continue
}
// 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 have matching runner and a job without "needs"
emptyJobsNumber := 0
for _, j := range wf.Jobs {
if j == nil {
emptyJobsNumber++
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 !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 != "" {
if !allRunnerLabels.Contains(ro) {
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", ro)
break
}
}
if !hasJobWithoutNeeds {
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs")
}
if emptyJobsNumber == len(wf.Jobs) {
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job")
if workflow.ErrMsg != "" {
break
}
workflows = append(workflows, workflow)
}
if !hasJobWithoutNeeds {
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs")
}
if emptyJobsNumber == len(wf.Jobs) {
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job")
}
workflows = append(workflows, workflow)

if workflow.Entry.Name() == workflowID {
curWorkflow = wf
}
if workflow.Entry.Name() == workflowID {
curWorkflow = wf
ctx.Data["CurWorkflowExists"] = true
}
}

ctx.Data["workflows"] = workflows
ctx.Data["RepoLink"] = ctx.Repo.Repository.Link()

page := ctx.FormInt("page")
if page <= 0 {
page = 1
}

actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
ctx.Data["ActionsConfig"] = actionsConfig

Expand All @@ -188,7 +225,7 @@ func List(ctx *context.Context) {
branches, err := git_model.FindBranchNames(ctx, branchOpts)
if err != nil {
ctx.ServerError("FindBranchNames", err)
return
return nil
}
// always put default branch on the top if it exists
if slices.Contains(branches, ctx.Repo.Repository.DefaultBranch) {
Expand All @@ -200,12 +237,23 @@ func List(ctx *context.Context) {
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("GetTagNamesByRepoID", err)
return
return nil
}
ctx.Data["Tags"] = tags
}
}
}
return workflows
}

func prepareWorkflowList(ctx *context.Context, workflows []Workflow) {
actorID := ctx.FormInt64("actor")
status := ctx.FormInt("status")
workflowID := ctx.FormString("workflow")
page := ctx.FormInt("page")
if page <= 0 {
page = 1
}

// if status or actor query param is not given to frontend href, (href="/<repoLink>/actions")
// they will be 0 by default, which indicates get all status or actors
Expand Down Expand Up @@ -264,8 +312,6 @@ func List(ctx *context.Context) {
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0

ctx.HTML(http.StatusOK, tplListActions)
}

// loadIsRefDeleted loads the IsRefDeleted field for each run in the list.
Expand Down
9 changes: 2 additions & 7 deletions routers/web/repo/actions/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -812,13 +812,8 @@ func Run(ctx *context_module.Context) {
return
}

// get workflow entry from default branch commit
defaultBranchCommit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
if err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
entries, err := actions.ListWorkflows(defaultBranchCommit)
// get workflow entry from runTargetCommit
entries, err := actions.ListWorkflows(runTargetCommit)
if err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
return
Expand Down
3 changes: 2 additions & 1 deletion routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -1412,6 +1412,7 @@ func registerRoutes(m *web.Router) {
m.Post("/disable", reqRepoAdmin, actions.DisableWorkflowFile)
m.Post("/enable", reqRepoAdmin, actions.EnableWorkflowFile)
m.Post("/run", reqRepoActionsWriter, actions.Run)
m.Get("/workflow-dispatch-inputs", reqRepoActionsWriter, actions.WorkflowDispatchInputs)

m.Group("/runs/{run}", func() {
m.Combo("").
Expand All @@ -1433,7 +1434,7 @@ func registerRoutes(m *web.Router) {
m.Group("/workflows/{workflow_name}", func() {
m.Get("/badge.svg", actions.GetWorkflowBadge)
})
}, optSignIn, context.RepoAssignment, reqRepoActionsReader, actions.MustEnableActions)
}, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoActionsReader, actions.MustEnableActions)
// end "/{username}/{reponame}/actions"

m.Group("/{username}/{reponame}/wiki", func() {
Expand Down
27 changes: 3 additions & 24 deletions templates/repo/actions/workflow_dispatch.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<label>{{ctx.Locale.Tr "actions.workflow.from_ref"}}:</label>
</span>
<div class="ui inline field dropdown button select-branch branch-selector-dropdown ellipsis-items-nowrap">
<input type="hidden" name="ref" value="refs/heads/{{index .Branches 0}}">
<input type="hidden" name="ref" hx-sync="this:replace" hx-target="#runWorkflowDispatchModalInputs" hx-swap="innerHTML" hx-get="{{$.Link}}/workflow-dispatch-inputs?workflow={{$.CurWorkflow}}" hx-trigger="change" value="refs/heads/{{index .Branches 0}}">
{{svg "octicon-git-branch" 14}}
<div class="default text">{{index .Branches 0}}</div>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
Expand Down Expand Up @@ -49,30 +49,9 @@

<div class="divider"></div>

{{range $item := .WorkflowDispatchConfig.Inputs}}
<div class="ui field {{if .Required}}required{{end}}">
{{if eq .Type "choice"}}
<label>{{.Description}}:</label>
<select class="ui selection type dropdown" name="{{.Name}}">
{{range .Options}}
<option value="{{.}}" {{if eq $item.Default .}}selected{{end}} >{{.}}</option>
{{end}}
</select>
{{else if eq .Type "boolean"}}
<div class="ui inline checkbox">
<label>{{.Description}}</label>
<input type="checkbox" name="{{.Name}}" {{if eq .Default "true"}}checked{{end}}>
</div>
{{else if eq .Type "number"}}
<label>{{.Description}}:</label>
<input name="{{.Name}}" value="{{.Default}}" {{if .Required}}required{{end}}>
{{else}}
<label>{{.Description}}:</label>
<input name="{{.Name}}" value="{{.Default}}" {{if .Required}}required{{end}}>
{{end}}
<div id="runWorkflowDispatchModalInputs">
{{template "repo/actions/workflow_dispatch_inputs" .}}
</div>
{{end}}
<button class="ui tiny primary button" type="submit">Submit</button>
</form>
</div>
</div>
Loading
Loading