mirror of
https://github.com/go-gitea/gitea.git
synced 2026-11-02 13:44:16 +09:00
enhance(actions): list workflow_call-only workflows in their own sidebar group (#40030)
A workflow whose only trigger is `workflow_call` never runs on its own, so its sidebar entry always showed an empty run list and a "Disable workflow" action that has no effect. Following the discussion with @bircni and @Zettat123, these workflows are now listed in a separate collapsed "Reusable workflows" group. Their page shows "This workflow can only be called by other workflows." instead of "no runs", and "Disable workflow" and "Create status badge" are hidden. A workflow disabled before it became `workflow_call`-only keeps its label and "Enable workflow". "Other workflows" again only lists workflows whose file is gone. Fixes: https://github.com/go-gitea/gitea/issues/39385 Co-authored-by: bircni <bircni@icloud.com> Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
6 files changed
+82
-6
No files matched your search
@@ -3854,6 +3854,9 @@
|
||||
"actions.runs.all_workflows": "All Workflows",
|
||||
"actions.runs.other_workflows": "Other workflows",
|
||||
"actions.runs.other_workflows_tooltip": "Workflows that were executed in this repository but do not exist on the default branch.",
|
||||
"actions.runs.reusable_workflows": "Reusable workflows",
|
||||
"actions.runs.reusable_workflows_tooltip": "Workflows that can only be called by other workflows.",
|
||||
"actions.runs.reusable_workflow_no_runs": "This workflow can only be called by other workflows.",
|
||||
"actions.runs.workflow_run_count_1": "%d workflow run",
|
||||
"actions.runs.workflow_run_count_n": "%d workflow runs",
|
||||
"actions.runs.commit": "Commit",
|
||||
|
||||
@@ -47,6 +47,7 @@ type WorkflowInfo struct {
|
||||
EntryName string
|
||||
ErrMsg string
|
||||
Workflow *act_model.Workflow
|
||||
Reusable bool // only triggered by workflow_call
|
||||
}
|
||||
|
||||
type workflowBadge struct {
|
||||
@@ -226,11 +227,13 @@ func prepareWorkflowTemplate(ctx *context.Context, commit *git.Commit) (workflow
|
||||
workflows = append(workflows, workflow)
|
||||
continue
|
||||
}
|
||||
if _, err := actions.GetEventsFromContent(content); err != nil {
|
||||
events, err := actions.GetEventsFromContent(content)
|
||||
if err != nil {
|
||||
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error())
|
||||
workflows = append(workflows, workflow)
|
||||
continue
|
||||
}
|
||||
workflow.Reusable = len(events) == 0 && slices.Contains(wf.On(), "workflow_call") // events never include workflow_call
|
||||
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
|
||||
@@ -262,7 +265,11 @@ func prepareWorkflowTemplate(ctx *context.Context, commit *git.Commit) (workflow
|
||||
workflows = append(workflows, workflow)
|
||||
}
|
||||
|
||||
reusableWorkflows := slices.DeleteFunc(slices.Clone(workflows), func(w WorkflowInfo) bool { return !w.Reusable })
|
||||
ctx.Data["workflows"] = workflows
|
||||
ctx.Data["ReusableWorkflows"] = reusableWorkflows
|
||||
ctx.Data["CurWorkflowIsReusable"] = ctx.FormInt64("scoped_workflow_source_repo_id") == 0 &&
|
||||
slices.ContainsFunc(reusableWorkflows, func(w WorkflowInfo) bool { return w.EntryName == curWorkflowID })
|
||||
ctx.Data["RepoLink"] = ctx.Repo.Repository.Link()
|
||||
ctx.Data["RepoID"] = ctx.Repo.Repository.ID
|
||||
ctx.Data["AllowDisableOrEnableWorkflow"] = ctx.Repo.Permission.IsAdmin()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{template "base/head" .}}
|
||||
|
||||
{{$showCreateWorkflowBadge := Iif .WorkflowBadge true false}}
|
||||
{{$showEnableDisableWorkflow := and .AllowDisableOrEnableWorkflow .CurWorkflowIsListed $.CurWorkflow}}
|
||||
{{$showCreateWorkflowBadge := and .WorkflowBadge (not .CurWorkflowIsReusable)}}
|
||||
{{$showEnableDisableWorkflow := and .AllowDisableOrEnableWorkflow .CurWorkflowIsListed $.CurWorkflow (or (not .CurWorkflowIsReusable) .CurWorkflowDisabled)}}
|
||||
|
||||
<div class="page-content repository actions">
|
||||
{{template "repo/header" .}}
|
||||
@@ -101,7 +101,7 @@
|
||||
{{end}}
|
||||
|
||||
<div class="ui attached segment">
|
||||
{{template "repo/actions/runs_list" dict "ActionRunListData" $.ActionRunListData}}
|
||||
{{template "repo/actions/runs_list" dict "ActionRunListData" $.ActionRunListData "CurWorkflowIsReusable" $.CurWorkflowIsReusable}}
|
||||
{{template "base/paginate" dict "Page" $.Page}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
{{if not $data.ActionRuns}}
|
||||
<div class="empty-placeholder">
|
||||
{{svg "octicon-no-entry" 48}}
|
||||
<h2>{{if $data.IsFiltered}}{{ctx.Locale.Tr "actions.runs.no_results"}}{{else}}{{ctx.Locale.Tr "actions.runs.no_runs"}}{{end}}</h2>
|
||||
<h2>{{if $data.IsFiltered}}{{ctx.Locale.Tr "actions.runs.no_results"}}{{else if $.CurWorkflowIsReusable}}{{ctx.Locale.Tr "actions.runs.reusable_workflow_no_runs"}}{{else}}{{ctx.Locale.Tr "actions.runs.no_runs"}}{{end}}</h2>
|
||||
</div>
|
||||
{{end}}
|
||||
{{range $run := $data.ActionRuns}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="flex-container-nav">
|
||||
<div class="ui fluid vertical menu">
|
||||
<a class="item {{if and (not $.CurWorkflow) (not $.PageIsActionsJobQueue)}}active{{end}}" href="{{$.RepoLink}}/actions?actor={{$.CurActor}}&status={{$.CurStatus}}&branch={{$.CurBranch}}">{{ctx.Locale.Tr "actions.runs.all_workflows"}}</a>
|
||||
{{range .workflows}}
|
||||
{{range .workflows}}{{if not .Reusable}}
|
||||
<a class="item flex-text-block {{if and (eq .EntryName $.CurWorkflow) (not $.CurWorkflowScopedRepoID)}}active{{end}}" href="{{$.RepoLink}}/actions?workflow={{.EntryName}}&actor={{$.CurActor}}&status={{$.CurStatus}}&branch={{$.CurBranch}}">
|
||||
<span class="gt-ellipsis" data-tooltip-content="{{.DisplayName}}">{{.DisplayName}}</span>
|
||||
|
||||
@@ -13,6 +13,29 @@
|
||||
<div class="ui red label tw-shrink-0">{{ctx.Locale.Tr "disabled"}}</div>
|
||||
{{end}}
|
||||
</a>
|
||||
{{end}}{{end}}
|
||||
{{if .ReusableWorkflows}}
|
||||
<details class="item"{{if $.CurWorkflowIsReusable}} open{{end}}>
|
||||
<summary data-tooltip-content="{{ctx.Locale.Tr "actions.runs.reusable_workflows_tooltip"}}">
|
||||
<span class="flex-text-block">
|
||||
{{ctx.Locale.Tr "actions.runs.reusable_workflows"}}
|
||||
<span class="ui label">{{len .ReusableWorkflows}}</span>
|
||||
</span>
|
||||
</summary>
|
||||
<div class="menu items-full-width">
|
||||
{{range .ReusableWorkflows}}
|
||||
<a class="item flex-text-block {{if and (eq .EntryName $.CurWorkflow) (not $.CurWorkflowScopedRepoID)}}active{{end}}" href="{{$.RepoLink}}/actions?workflow={{.EntryName}}&actor={{$.CurActor}}&status={{$.CurStatus}}&branch={{$.CurBranch}}">
|
||||
<span class="gt-ellipsis" data-tooltip-content="{{.DisplayName}}">{{.DisplayName}}</span>
|
||||
{{if .ErrMsg}}
|
||||
<span class="flex-text-inline tw-shrink-0" data-tooltip-content="{{.ErrMsg}}">{{svg "octicon-alert" 16 "tw-text-red"}}</span>
|
||||
{{end}}
|
||||
{{if $.ActionsConfig.IsWorkflowDisabled .EntryName}}
|
||||
<div class="ui red label tw-shrink-0">{{ctx.Locale.Tr "disabled"}}</div>
|
||||
{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</details>
|
||||
{{end}}
|
||||
{{range .ScopedWorkflowGroups}}
|
||||
<details class="item scoped-workflow-group"{{if .IsActive}} open{{end}}>
|
||||
|
||||
@@ -8,12 +8,14 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
runnerv1 "gitea.dev/actionslib/runner/v1"
|
||||
actions_model "gitea.dev/models/actions"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
perm_model "gitea.dev/models/perm"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
@@ -359,6 +361,47 @@ jobs:
|
||||
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runID})
|
||||
assert.Equal(t, actions_model.StatusSuccess, run.Status)
|
||||
})
|
||||
|
||||
t.Run("Sidebar groups workflow_call-only workflows", func(t *testing.T) {
|
||||
require.NoError(t, db.Insert(t.Context(), &actions_model.ActionRun{
|
||||
Title: "former push run", RepoID: repo.ID, OwnerID: repo.OwnerID, WorkflowID: "reusable2.yaml", Index: 9701,
|
||||
TriggerUserID: user2.ID, Ref: "refs/heads/" + repo.DefaultBranch, Event: "push", TriggerEvent: "push",
|
||||
Status: actions_model.StatusSuccess,
|
||||
}))
|
||||
job := "jobs:\n job1:\n runs-on: ubuntu-latest\n steps:\n - run: echo\n"
|
||||
testCreateFileInBranch(t, user2, repo, createFileInBranchOptions{}, map[string]string{
|
||||
".gitea/workflows/mixed.yaml": "on: [workflow_call, workflow_dispatch]\n" + job,
|
||||
".gitea/workflows/no-events.yaml": "on: []\n" + job,
|
||||
})
|
||||
MakeRequest(t, NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/actions/workflows/reusable2.yaml/disable", repo.FullName())).
|
||||
AddTokenAuth(user2Token), http.StatusNoContent)
|
||||
|
||||
doc := NewHTMLParser(t, user2Session.MakeRequest(t, NewRequest(t, "GET", repo.Link()+"/actions?workflow=reusable1.yaml"), http.StatusOK).Body)
|
||||
listed := func(name string) int {
|
||||
return doc.Find(fmt.Sprintf(`.flex-container-nav > .menu > a.item[href*="workflow=%s&"]`, name)).Length()
|
||||
}
|
||||
grouped := func(name string) int {
|
||||
return doc.Find(fmt.Sprintf(`.flex-container-nav details .menu a.item[href*="workflow=%s&"]`, name)).Length()
|
||||
}
|
||||
assert.Equal(t, "2", doc.Find(".flex-container-nav details summary .ui.label").Text())
|
||||
assert.Equal(t, 1, grouped("reusable1.yaml"))
|
||||
assert.Equal(t, 1, grouped("reusable2.yaml"))
|
||||
assert.Equal(t, 1, doc.Find(`.flex-container-nav details a.item[href*="workflow=reusable2.yaml&"] .ui.red.label`).Length())
|
||||
assert.Zero(t, listed("reusable1.yaml"))
|
||||
assert.Equal(t, 1, listed("caller.yaml"))
|
||||
assert.Equal(t, 1, listed("mixed.yaml"))
|
||||
assert.Equal(t, 1, listed("no-events.yaml"))
|
||||
assert.Equal(t, "This workflow can only be called by other workflows.", strings.TrimSpace(doc.Find(".empty-placeholder h2").Text()))
|
||||
assert.Zero(t, doc.Find(`[data-url*="/disable?workflow=reusable1.yaml"]`).Length())
|
||||
assert.Zero(t, doc.Find(`[data-modal="#workflow-status-badge-modal"]`).Length())
|
||||
|
||||
doc = NewHTMLParser(t, user2Session.MakeRequest(t, NewRequest(t, "GET", repo.Link()+"/actions?workflow=reusable2.yaml"), http.StatusOK).Body)
|
||||
assert.Equal(t, 1, doc.Find(`[data-url*="/enable?workflow=reusable2.yaml"]`).Length())
|
||||
|
||||
doc = NewHTMLParser(t, user2Session.MakeRequest(t, NewRequest(t, "GET", repo.Link()+"/actions?workflow=mixed.yaml"), http.StatusOK).Body)
|
||||
assert.Equal(t, 1, doc.Find(`[data-url*="/disable?workflow=mixed.yaml"]`).Length())
|
||||
assert.Equal(t, 1, doc.Find(`[data-modal="#workflow-status-badge-modal"]`).Length())
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Cross-repo reusable workflow with collaborative owner", func(t *testing.T) {
|
||||
|
||||
Reference in new issue
Block a user