feat(actions): List workflows that were executed once but got removed from the default branch (#37835)

This commit is contained in:
Nicolas
2026-05-25 16:41:36 +02:00
committed by GitHub
parent 2775158024
commit d93bbcc0a6
12 changed files with 138 additions and 30 deletions

View File

@@ -56,3 +56,8 @@ Endpoints returning lists must
- support pagination (`page` & `limit` options in query)
- set `X-Total-Count` header via **SetTotalCountHeader** ([example](https://github.com/go-gitea/gitea/blob/7aae98cc5d4113f1e9918b7ee7dd09f67c189e3e/routers/api/v1/repo/issue.go#L444))
### Knowledge
- Partially database table migration must use `SyncWithOptions(IgnoreDrop...)`
- Template variables with "camelCase" or "snake_case" are used for restoring the form values from a submitted form

View File

@@ -132,6 +132,18 @@ func GetStatusInfoList(ctx context.Context, lang translation.Locale) []StatusInf
return statusInfoList
}
// GetRunWorkflowIDs returns all distinct WorkflowIDs that have at least
// one ActionRun in the given repo.
func GetRunWorkflowIDs(ctx context.Context, repoID int64) ([]string, error) {
ids := make([]string, 0, 10)
return ids, db.GetEngine(ctx).Table("action_run").
Where(builder.Eq{"repo_id": repoID}).
Distinct("workflow_id").
Cols("workflow_id").
Asc("workflow_id").
Find(&ids)
}
// GetActors returns a slice of Actors
func GetActors(ctx context.Context, repoID int64) ([]*user_model.User, error) {
actors := make([]*user_model.User, 0, 10)

View File

@@ -0,0 +1,24 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"testing"
"code.gitea.io/gitea/models/unittest"
"github.com/stretchr/testify/assert"
)
func TestGetRunWorkflowIDs(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
ids, err := GetRunWorkflowIDs(t.Context(), 4)
assert.NoError(t, err)
assert.Equal(t, []string{"artifact.yaml", "test.yaml"}, ids)
ids, err = GetRunWorkflowIDs(t.Context(), 999999)
assert.NoError(t, err)
assert.Empty(t, ids)
}

View File

@@ -3759,6 +3759,8 @@
"actions.runners.reset_registration_token_confirm": "Would you like to invalidate the current token and generate a new one?",
"actions.runners.reset_registration_token_success": "Runner registration token reset successfully",
"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.workflow_run_count_1": "%d workflow run",
"actions.runs.workflow_run_count_n": "%d workflow runs",
"actions.runs.commit": "Commit",

View File

@@ -90,12 +90,16 @@ func List(ctx *context.Context) {
if ctx.Written() {
return
}
otherWorkflows := prepareOtherWorkflows(ctx, workflows, curWorkflowID)
if ctx.Written() {
return
}
prepareWorkflowDispatchTemplate(ctx, workflows, curWorkflowID)
if ctx.Written() {
return
}
prepareWorkflowList(ctx, workflows)
prepareWorkflowList(ctx, workflows, otherWorkflows)
if ctx.Written() {
return
}
@@ -103,6 +107,31 @@ func List(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplListActions)
}
// prepareOtherWorkflows surfaces historical runs whose workflow file no longer
// exists on the default branch (renamed, removed, or only on other branches).
func prepareOtherWorkflows(ctx *context.Context, workflows []WorkflowInfo, curWorkflowID string) []string {
listed := make(container.Set[string], len(workflows))
for _, w := range workflows {
listed.Add(w.Entry.Name())
}
var other []string
if ctx.Repo.Repository.NumActionRuns > 0 {
ids, err := actions_model.GetRunWorkflowIDs(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("GetRunWorkflowIDs", err)
return nil
}
other = container.FilterSlice(ids, func(id string) (string, bool) {
return id, id != "" && !listed.Contains(id)
})
}
ctx.Data["OtherWorkflows"] = other
ctx.Data["CurWorkflowIsListed"] = curWorkflowID == "" || listed.Contains(curWorkflowID)
return other
}
func WorkflowDispatchInputs(ctx *context.Context) {
ref := ctx.FormString("ref")
if ref == "" {
@@ -253,7 +282,7 @@ func prepareWorkflowDispatchTemplate(ctx *context.Context, workflowInfos []Workf
ctx.Data["Tags"] = tags
}
func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo) {
func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo, otherWorkflows []string) {
actorID := ctx.FormInt64("actor")
status := ctx.FormInt("status")
workflowID := ctx.FormString("workflow")
@@ -367,7 +396,7 @@ func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo) {
pager := context.NewPagination(total, opts.PageSize, opts.Page, 5)
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0
ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(otherWorkflows) > 0 || len(runs) > 0
ctx.Data["CanWriteRepoUnitActions"] = ctx.Repo.Permission.CanWrite(unit.TypeActions)
}

View File

@@ -2,7 +2,7 @@
<div class="ui fluid vertical menu">
<div class="header item">{{ctx.Locale.Tr "admin.settings"}}</div>
<details class="item toggleable-item" {{if or .PageIsAdminDashboard .PageIsAdminSelfCheck}}open{{end}}>
<details class="item" {{if or .PageIsAdminDashboard .PageIsAdminSelfCheck}}open{{end}}>
<summary>{{ctx.Locale.Tr "admin.maintenance"}}</summary>
<div class="menu">
<a class="{{if .PageIsAdminDashboard}}active {{end}}item" href="{{AppSubUrl}}/-/admin">
@@ -13,7 +13,7 @@
</a>
</div>
</details>
<details class="item toggleable-item" {{if or .PageIsAdminUsers .PageIsAdminBadges .PageIsAdminEmails .PageIsAdminOrganizations .PageIsAdminAuthentications}}open{{end}}>
<details class="item" {{if or .PageIsAdminUsers .PageIsAdminBadges .PageIsAdminEmails .PageIsAdminOrganizations .PageIsAdminAuthentications}}open{{end}}>
<summary>{{ctx.Locale.Tr "admin.identity_access"}}</summary>
<div class="menu">
<a class="{{if .PageIsAdminAuthentications}}active {{end}}item" href="{{AppSubUrl}}/-/admin/auths">
@@ -33,7 +33,7 @@
</a>
</div>
</details>
<details class="item toggleable-item" {{if or .PageIsAdminRepositories (and .EnablePackages .PageIsAdminPackages)}}open{{end}}>
<details class="item" {{if or .PageIsAdminRepositories (and .EnablePackages .PageIsAdminPackages)}}open{{end}}>
<summary>{{ctx.Locale.Tr "admin.assets"}}</summary>
<div class="menu">
{{if .EnablePackages}}
@@ -48,7 +48,7 @@
</details>
<!-- Webhooks and OAuth can be both disabled here, so add this if statement to display different ui -->
{{if and (not DisableWebhooks) .EnableOAuth2}}
<details class="item toggleable-item" {{if or .PageIsAdminDefaultHooks .PageIsAdminSystemHooks .PageIsAdminApplications}}open{{end}}>
<details class="item" {{if or .PageIsAdminDefaultHooks .PageIsAdminSystemHooks .PageIsAdminApplications}}open{{end}}>
<summary>{{ctx.Locale.Tr "admin.integrations"}}</summary>
<div class="menu">
<a class="{{if .PageIsAdminApplications}}active {{end}}item" href="{{AppSubUrl}}/-/admin/applications">
@@ -72,7 +72,7 @@
{{end}}
{{end}}
{{if .EnableActions}}
<details class="item toggleable-item" {{if or .PageIsSharedSettingsRunners .PageIsSharedSettingsVariables}}open{{end}}>
<details class="item" {{if or .PageIsSharedSettingsRunners .PageIsSharedSettingsVariables}}open{{end}}>
<summary>{{ctx.Locale.Tr "actions.actions"}}</summary>
<div class="menu">
<a class="{{if .PageIsSharedSettingsRunners}}active {{end}}item" href="{{AppSubUrl}}/-/admin/actions/runners">
@@ -84,7 +84,7 @@
</div>
</details>
{{end}}
<details class="item toggleable-item" {{if or .PageIsAdminConfig}}open{{end}}>
<details class="item" {{if or .PageIsAdminConfig}}open{{end}}>
<summary>{{ctx.Locale.Tr "admin.config"}}</summary>
<div class="menu">
<a class="{{if .PageIsAdminConfigSummary}}active {{end}}item" href="{{AppSubUrl}}/-/admin/config">
@@ -98,7 +98,7 @@
<a class="{{if .PageIsAdminNotices}}active {{end}}item" href="{{AppSubUrl}}/-/admin/notices">
{{ctx.Locale.Tr "admin.notices"}}
</a>
<details class="item toggleable-item" {{if or .PageIsAdminMonitorStats .PageIsAdminMonitorCron .PageIsAdminMonitorQueue .PageIsAdminMonitorTrace}}open{{end}}>
<details class="item" {{if or .PageIsAdminMonitorStats .PageIsAdminMonitorCron .PageIsAdminMonitorQueue .PageIsAdminMonitorTrace}}open{{end}}>
<summary>{{ctx.Locale.Tr "admin.monitor"}}</summary>
<div class="menu">
<a class="{{if .PageIsAdminMonitorStats}}active {{end}}item" href="{{AppSubUrl}}/-/admin/monitor/stats">

View File

@@ -26,7 +26,7 @@
</a>
{{end}}
{{if .EnableActions}}
<details class="item toggleable-item" {{if or .PageIsOrgSettingsActionsGeneral .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables}}open{{end}}>
<details class="item" {{if or .PageIsOrgSettingsActionsGeneral .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables}}open{{end}}>
<summary>{{ctx.Locale.Tr "actions.actions"}}</summary>
<div class="menu">
<a class="{{if .PageIsOrgSettingsActionsGeneral}}active {{end}}item" href="{{.OrgLink}}/settings/actions">

View File

@@ -7,10 +7,10 @@
{{if .HasWorkflowsOrRuns}}
<div class="flex-container">
<div class="flex-container-nav">
<div class="ui fluid vertical menu flex-items-block">
<div class="ui fluid vertical menu">
<a class="item {{if not $.CurWorkflow}}active{{end}}" href="?actor={{$.CurActor}}&status={{$.CurStatus}}">{{ctx.Locale.Tr "actions.runs.all_workflows"}}</a>
{{range .workflows}}
<a class="item {{if eq .Entry.Name $.CurWorkflow}}active{{end}}" href="?workflow={{.Entry.Name}}&actor={{$.CurActor}}&status={{$.CurStatus}}">
<a class="item flex-text-block {{if eq .Entry.Name $.CurWorkflow}}active{{end}}" href="?workflow={{.Entry.Name}}&actor={{$.CurActor}}&status={{$.CurStatus}}">
<span class="gt-ellipsis">{{.DisplayName}}</span>
{{if .ErrMsg}}
@@ -22,6 +22,23 @@
{{end}}
</a>
{{end}}
{{if .OtherWorkflows}}
<details class="item"{{if not $.CurWorkflowIsListed}} open{{end}}>
<summary data-tooltip-content="{{ctx.Locale.Tr "actions.runs.other_workflows_tooltip"}}">
<span class="flex-text-block">
{{ctx.Locale.Tr "actions.runs.other_workflows"}}
<span class="ui label">{{len .OtherWorkflows}}</span>
</span>
</summary>
<div class="menu items-full-width">
{{range .OtherWorkflows}}
<a class="item {{if eq . $.CurWorkflow}}active{{end}}" href="?workflow={{.}}&actor={{$.CurActor}}&status={{$.CurStatus}}">
<span class="gt-ellipsis">{{.}}</span>
</a>
{{end}}
</div>
</details>
{{end}}
</div>
</div>
<div class="flex-container-main">
@@ -67,7 +84,7 @@
</div>
</div>
{{if .AllowDisableOrEnableWorkflow}}
{{if and .AllowDisableOrEnableWorkflow .CurWorkflowIsListed $.CurWorkflow}}
<button class="ui jump dropdown btn interact-bg tw-p-2">
{{svg "octicon-kebab-horizontal"}}
<div class="menu">

View File

@@ -38,7 +38,7 @@
</a>
{{end}}
{{end}}
<details class="item toggleable-item" {{if or .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables .PageIsActionsSettingsGeneral}}open{{end}}>
<details class="item" {{if or .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables .PageIsActionsSettingsGeneral}}open{{end}}>
<summary>{{ctx.Locale.Tr "actions.actions"}}</summary>
<div class="menu">
<a class="{{if .PageIsActionsSettingsGeneral}}active {{end}}item" href="{{.RepoLink}}/settings/actions/general">

View File

@@ -34,7 +34,7 @@
</a>
{{end}}
{{if .EnableActions}}
<details class="item toggleable-item" {{if or .PageIsUserSettingsActionsGeneral .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables}}open{{end}}>
<details class="item" {{if or .PageIsUserSettingsActionsGeneral .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables}}open{{end}}>
<summary>{{ctx.Locale.Tr "actions.actions"}}</summary>
<div class="menu">
<a class="{{if .PageIsUserSettingsActionsGeneral}}active {{end}}item" href="{{AppSubUrl}}/user/settings/actions/general">

View File

@@ -311,6 +311,7 @@
.ui.vertical.menu .item > .menu {
margin: 0.5em -1.14285714em 0;
}
.ui.vertical.menu .menu .item {
background: none;
padding: 0.5em 1.33333333em;

View File

@@ -1,25 +1,21 @@
details.toggleable-item {
user-select: none !important;
padding: 0 !important;
.ui.vertical.menu > details.item {
user-select: none;
padding: 0;
}
details.toggleable-item .menu {
margin: 4px 0 10px !important;
}
details.toggleable-item summary {
.ui.vertical.menu > details.item summary {
display: flex;
justify-content: space-between;
justify-content: space-between; /* make the "::after" right-aligned */
align-items: center;
padding: 0.92857143em 1.14285714em;
padding: 16px 13px; /* match Fomantic menu item padding */
}
details.toggleable-item summary::marker, /* Chrome, Edge, Firefox */
details.toggleable-item summary::-webkit-details-marker /* Safari */ {
.ui.vertical.menu > details.item > summary::marker, /* Chrome, Edge, Firefox */
.ui.vertical.menu > details.item > summary::-webkit-details-marker /* Safari */ {
display: none;
}
details.toggleable-item summary::after {
.ui.vertical.menu > details.item > summary::after {
transition: transform 0.25s ease;
content: "";
width: 14px;
@@ -32,6 +28,28 @@ details.toggleable-item summary::after {
border: 1px solid var(--color-body); /* workaround https://bugzilla.mozilla.org/show_bug.cgi?id=1671784 */
}
details.toggleable-item[open] summary::after {
.ui.vertical.menu > details.item[open] > summary::after {
transform: rotate(90deg);
}
/* default toggleable details menu: items don't have menu item padding, don't change background color on hover or active */
.ui.vertical.menu > details.item > .menu {
margin: 0 0 10px; /* only need the space between the current details menu and next item */
}
/* full width toggleable details menu: items have menu item padding, change background color on hover and active */
.ui.vertical.menu > details.item > .menu.items-full-width {
margin: 0;
}
.ui.vertical.menu > details.item > .menu.items-full-width > .item {
padding: 16px 13px; /* match Fomantic menu item padding */
}
.ui.vertical.menu > details.item > .menu.items-full-width > .item.active {
background: var(--color-active);
}
.ui.vertical.menu > details.item > .menu.items-full-width > .item:hover {
background: var(--color-hover); /* ".ui.vertical.menu .menu .item" resets the hover background, so we need to add it again */
}