workflow_dispatch use workflow from trigger branch (#33098)

* htmx updates the input form on branch switch
* add workflow warning to dispatch modal
* use name if description of input is empty
* show error if workflow_dispatch not available on branch

Closes #33073
Closes #33099

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
ChristopherHX 2025-01-05 14:47:18 +01:00 committed by GitHub
parent 3078826d01
commit 4237736029
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 189 additions and 122 deletions

View File

@ -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.

View File

@ -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 {
@ -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
workflowID := ctx.FormString("workflow")
actorID := ctx.FormInt64("actor")
status := ctx.FormInt("status")
ctx.Data["CurWorkflow"] = workflowID
var workflows []Workflow
var curWorkflow *model.Workflow
if empty, err := ctx.Repo.GitRepo.IsEmpty(); err != nil {
ctx.ServerError("IsEmpty", err)
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
if err != nil {
ctx.ServerError("GetBranchCommit", 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,
})
if err != nil {
ctx.ServerError("FindRunners", err)
return
}
allRunnerLabels := make(container.Set[string])
for _, r := range runners {
allRunnerLabels.AddMultiple(r.AgentLabels...)
}
workflows := prepareWorkflowDispatchTemplate(ctx, commit)
if ctx.Written() {
return
}
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)
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")
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))
for _, entry := range entries {
workflow := Workflow{Entry: *entry}
content, err := actions.GetContentFromEntry(entry)
if err != nil {
ctx.ServerError("GetContentFromEntry", err)
return nil
}
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
}
// 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")
}
workflows = append(workflows, workflow)
if workflow.Entry.Name() == workflowID {
curWorkflow = wf
if workflow.ErrMsg != "" {
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")
}
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()
page := ctx.FormInt("page")
if page <= 0 {
page = 1
}
actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
ctx.Data["ActionsConfig"] = actionsConfig
@ -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) {
@ -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
@ -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.

View File

@ -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

View File

@ -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("").
@ -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() {

View File

@ -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"}}
@ -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>

View File

@ -0,0 +1,45 @@
{{if not .WorkflowDispatchConfig}}
<div class="ui error message tw-block">{{/* using "ui message" in "ui form" needs to force to display */}}
{{if not .CurWorkflowExists}}
{{ctx.Locale.Tr "actions.workflow.not_found" $.CurWorkflow}}
{{else}}
{{ctx.Locale.Tr "actions.workflow.has_no_workflow_dispatch" $.CurWorkflow}}
{{end}}
</div>
{{else}}
{{range $item := .WorkflowDispatchConfig.Inputs}}
<div class="ui field {{if .Required}}required{{end}}">
{{if eq .Type "choice"}}
<label>{{or .Description .Name}}:</label>
{{/* htmx won't initialize the fomantic dropdown, so it is a standard "select" input */}}
<select class="ui selection dropdown" name="{{.Name}}">
{{range .Options}}
<option value="{{.}}" {{if eq $item.Default .}}selected{{end}}>{{.}}</option>
{{end}}
</select>
{{else if eq .Type "boolean"}}
{{/* htmx doesn't trigger our JS code to attach fomantic label to checkbox, so here we use standard checkbox */}}
<label class="tw-flex flex-text-inline">
<input type="checkbox" name="{{.Name}}" {{if eq .Default "true"}}checked{{end}}>
{{or .Description .Name}}
</label>
{{else if eq .Type "number"}}
<label>{{or .Description .Name}}:</label>
<input name="{{.Name}}" value="{{.Default}}" {{if .Required}}required{{end}}>
{{else}}
<label>{{or .Description .Name}}:</label>
<input name="{{.Name}}" value="{{.Default}}" {{if .Required}}required{{end}}>
{{end}}
</div>
{{end}}
<div class="ui field">
<button class="ui tiny primary button" type="submit">{{ctx.Locale.Tr "actions.workflow.run"}}</button>
</div>
{{end}}
{{range .workflows}}
{{if and .ErrMsg (eq .Entry.Name $.CurWorkflow)}}
<div class="ui field">
<div>{{svg "octicon-alert" 16 "text red"}} {{.ErrMsg}}</div>
</div>
{{end}}
{{end}}