mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-22 16:47:30 +00:00
add disable workflow feature (#26413)
As title, that's simmilar with github. ![image](https://github.com/go-gitea/gitea/assets/25342410/9e8b2444-63e0-4e87-80da-730c1e4d09d6) ![image](https://github.com/go-gitea/gitea/assets/25342410/6c3a3345-3ba7-48c9-9acd-3e621632491b) --------- Signed-off-by: a1012112796 <1012112796@qq.com> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Jason Song <i@wolfogre.com>
This commit is contained in:
parent
253737eb36
commit
19872063a3
|
@ -391,7 +391,13 @@ func (repo *Repository) MustGetUnit(ctx context.Context, tp unit.Type) *RepoUnit
|
||||||
Type: tp,
|
Type: tp,
|
||||||
Config: new(IssuesConfig),
|
Config: new(IssuesConfig),
|
||||||
}
|
}
|
||||||
|
} else if tp == unit.TypeActions {
|
||||||
|
return &RepoUnit{
|
||||||
|
Type: tp,
|
||||||
|
Config: new(ActionsConfig),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &RepoUnit{
|
return &RepoUnit{
|
||||||
Type: tp,
|
Type: tp,
|
||||||
Config: new(UnitConfig),
|
Config: new(UnitConfig),
|
||||||
|
|
|
@ -6,6 +6,7 @@ package repo
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
|
@ -162,6 +163,42 @@ func (cfg *PullRequestsConfig) GetDefaultMergeStyle() MergeStyle {
|
||||||
return MergeStyleMerge
|
return MergeStyleMerge
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ActionsConfig struct {
|
||||||
|
DisabledWorkflows []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *ActionsConfig) EnableWorkflow(file string) {
|
||||||
|
cfg.DisabledWorkflows = util.SliceRemoveAll(cfg.DisabledWorkflows, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *ActionsConfig) ToString() string {
|
||||||
|
return strings.Join(cfg.DisabledWorkflows, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *ActionsConfig) IsWorkflowDisabled(file string) bool {
|
||||||
|
return util.SliceContains(cfg.DisabledWorkflows, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *ActionsConfig) DisableWorkflow(file string) {
|
||||||
|
for _, workflow := range cfg.DisabledWorkflows {
|
||||||
|
if file == workflow {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.DisabledWorkflows = append(cfg.DisabledWorkflows, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromDB fills up a ActionsConfig from serialized format.
|
||||||
|
func (cfg *ActionsConfig) FromDB(bs []byte) error {
|
||||||
|
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToDB exports a ActionsConfig to a serialized format.
|
||||||
|
func (cfg *ActionsConfig) ToDB() ([]byte, error) {
|
||||||
|
return json.Marshal(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
// BeforeSet is invoked from XORM before setting the value of a field of this object.
|
// BeforeSet is invoked from XORM before setting the value of a field of this object.
|
||||||
func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
|
func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
|
||||||
switch colName {
|
switch colName {
|
||||||
|
@ -175,7 +212,9 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
|
||||||
r.Config = new(PullRequestsConfig)
|
r.Config = new(PullRequestsConfig)
|
||||||
case unit.TypeIssues:
|
case unit.TypeIssues:
|
||||||
r.Config = new(IssuesConfig)
|
r.Config = new(IssuesConfig)
|
||||||
case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects, unit.TypePackages, unit.TypeActions:
|
case unit.TypeActions:
|
||||||
|
r.Config = new(ActionsConfig)
|
||||||
|
case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects, unit.TypePackages:
|
||||||
fallthrough
|
fallthrough
|
||||||
default:
|
default:
|
||||||
r.Config = new(UnitConfig)
|
r.Config = new(UnitConfig)
|
||||||
|
@ -218,6 +257,11 @@ func (r *RepoUnit) ExternalTrackerConfig() *ExternalTrackerConfig {
|
||||||
return r.Config.(*ExternalTrackerConfig)
|
return r.Config.(*ExternalTrackerConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ActionsConfig returns config for unit.ActionsConfig
|
||||||
|
func (r *RepoUnit) ActionsConfig() *ActionsConfig {
|
||||||
|
return r.Config.(*ActionsConfig)
|
||||||
|
}
|
||||||
|
|
||||||
func getUnitsByRepoID(ctx context.Context, repoID int64) (units []*RepoUnit, err error) {
|
func getUnitsByRepoID(ctx context.Context, repoID int64) (units []*RepoUnit, err error) {
|
||||||
var tmpUnits []*RepoUnit
|
var tmpUnits []*RepoUnit
|
||||||
if err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Find(&tmpUnits); err != nil {
|
if err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Find(&tmpUnits); err != nil {
|
||||||
|
|
30
models/repo/repo_unit_test.go
Normal file
30
models/repo/repo_unit_test.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestActionsConfig(t *testing.T) {
|
||||||
|
cfg := &ActionsConfig{}
|
||||||
|
cfg.DisableWorkflow("test1.yaml")
|
||||||
|
assert.EqualValues(t, []string{"test1.yaml"}, cfg.DisabledWorkflows)
|
||||||
|
|
||||||
|
cfg.DisableWorkflow("test1.yaml")
|
||||||
|
assert.EqualValues(t, []string{"test1.yaml"}, cfg.DisabledWorkflows)
|
||||||
|
|
||||||
|
cfg.EnableWorkflow("test1.yaml")
|
||||||
|
assert.EqualValues(t, []string{}, cfg.DisabledWorkflows)
|
||||||
|
|
||||||
|
cfg.EnableWorkflow("test1.yaml")
|
||||||
|
assert.EqualValues(t, []string{}, cfg.DisabledWorkflows)
|
||||||
|
|
||||||
|
cfg.DisableWorkflow("test1.yaml")
|
||||||
|
cfg.DisableWorkflow("test2.yaml")
|
||||||
|
cfg.DisableWorkflow("test3.yaml")
|
||||||
|
assert.EqualValues(t, "test1.yaml,test2.yaml,test3.yaml", cfg.ToString())
|
||||||
|
}
|
|
@ -3491,6 +3491,11 @@ runs.status_no_select = All status
|
||||||
runs.no_results = No results matched.
|
runs.no_results = No results matched.
|
||||||
runs.no_runs = The workflow has no runs yet.
|
runs.no_runs = The workflow has no runs yet.
|
||||||
|
|
||||||
|
workflow.disable = Disable Workflow
|
||||||
|
workflow.disable_success = Workflow '%s' disabled successfully.
|
||||||
|
workflow.enable = Enable Workflow
|
||||||
|
workflow.enable_success = Workflow '%s' enabled successfully.
|
||||||
|
|
||||||
need_approval_desc = Need approval to run workflows for fork pull request.
|
need_approval_desc = Need approval to run workflows for fork pull request.
|
||||||
|
|
||||||
variables = Variables
|
variables = Variables
|
||||||
|
|
|
@ -137,6 +137,15 @@ func List(ctx *context.Context) {
|
||||||
actorID := ctx.FormInt64("actor")
|
actorID := ctx.FormInt64("actor")
|
||||||
status := ctx.FormInt("status")
|
status := ctx.FormInt("status")
|
||||||
ctx.Data["CurWorkflow"] = workflow
|
ctx.Data["CurWorkflow"] = workflow
|
||||||
|
|
||||||
|
actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
|
||||||
|
ctx.Data["ActionsConfig"] = actionsConfig
|
||||||
|
|
||||||
|
if len(workflow) > 0 && ctx.Repo.IsAdmin() {
|
||||||
|
ctx.Data["AllowDisableOrEnableWorkflow"] = true
|
||||||
|
ctx.Data["CurWorkflowDisabled"] = actionsConfig.IsWorkflowDisabled(workflow)
|
||||||
|
}
|
||||||
|
|
||||||
// if status or actor query param is not given to frontend href, (href="/<repoLink>/actions")
|
// 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
|
// they will be 0 by default, which indicates get all status or actors
|
||||||
ctx.Data["CurActor"] = actorID
|
ctx.Data["CurActor"] = actorID
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
|
|
||||||
actions_model "code.gitea.io/gitea/models/actions"
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
"code.gitea.io/gitea/modules/actions"
|
"code.gitea.io/gitea/modules/actions"
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
|
@ -572,3 +573,43 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DisableWorkflowFile(ctx *context_module.Context) {
|
||||||
|
disableOrEnableWorkflowFile(ctx, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnableWorkflowFile(ctx *context_module.Context) {
|
||||||
|
disableOrEnableWorkflowFile(ctx, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func disableOrEnableWorkflowFile(ctx *context_module.Context, isEnable bool) {
|
||||||
|
workflow := ctx.FormString("workflow")
|
||||||
|
if len(workflow) == 0 {
|
||||||
|
ctx.ServerError("workflow", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
|
||||||
|
cfg := cfgUnit.ActionsConfig()
|
||||||
|
|
||||||
|
if isEnable {
|
||||||
|
cfg.EnableWorkflow(workflow)
|
||||||
|
} else {
|
||||||
|
cfg.DisableWorkflow(workflow)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := repo_model.UpdateRepoUnit(cfgUnit); err != nil {
|
||||||
|
ctx.ServerError("UpdateRepoUnit", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isEnable {
|
||||||
|
ctx.Flash.Success(ctx.Tr("actions.workflow.enable_success", workflow))
|
||||||
|
} else {
|
||||||
|
ctx.Flash.Success(ctx.Tr("actions.workflow.disable_success", workflow))
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectURL := fmt.Sprintf("%s/actions?workflow=%s&actor=%s&status=%s", ctx.Repo.RepoLink, url.QueryEscape(workflow),
|
||||||
|
url.QueryEscape(ctx.FormString("actor")), url.QueryEscape(ctx.FormString("status")))
|
||||||
|
ctx.JSONRedirect(redirectURL)
|
||||||
|
}
|
||||||
|
|
|
@ -1200,6 +1200,8 @@ func registerRoutes(m *web.Route) {
|
||||||
|
|
||||||
m.Group("/actions", func() {
|
m.Group("/actions", func() {
|
||||||
m.Get("", actions.List)
|
m.Get("", actions.List)
|
||||||
|
m.Post("/disable", reqRepoAdmin, actions.DisableWorkflowFile)
|
||||||
|
m.Post("/enable", reqRepoAdmin, actions.EnableWorkflowFile)
|
||||||
|
|
||||||
m.Group("/runs/{run}", func() {
|
m.Group("/runs/{run}", func() {
|
||||||
m.Combo("").
|
m.Combo("").
|
||||||
|
|
|
@ -150,7 +150,14 @@ func notify(ctx context.Context, input *notifyInput) error {
|
||||||
if len(workflows) == 0 {
|
if len(workflows) == 0 {
|
||||||
log.Trace("repo %s with commit %s couldn't find workflows", input.Repo.RepoPath(), commit.ID)
|
log.Trace("repo %s with commit %s couldn't find workflows", input.Repo.RepoPath(), commit.ID)
|
||||||
} else {
|
} else {
|
||||||
|
actionsConfig := input.Repo.MustGetUnit(ctx, unit_model.TypeActions).ActionsConfig()
|
||||||
|
|
||||||
for _, wf := range workflows {
|
for _, wf := range workflows {
|
||||||
|
if actionsConfig.IsWorkflowDisabled(wf.EntryName) {
|
||||||
|
log.Trace("repo %s has disable workflows %s", input.Repo.RepoPath(), wf.EntryName)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if wf.TriggerEvent != actions_module.GithubEventPullRequestTarget {
|
if wf.TriggerEvent != actions_module.GithubEventPullRequestTarget {
|
||||||
detectedWorkflows = append(detectedWorkflows, wf)
|
detectedWorkflows = append(detectedWorkflows, wf)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
<div class="page-content repository actions">
|
<div class="page-content repository actions">
|
||||||
{{template "repo/header" .}}
|
{{template "repo/header" .}}
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
|
{{template "base/alert" .}}
|
||||||
|
|
||||||
<div class="ui stackable grid">
|
<div class="ui stackable grid">
|
||||||
<div class="four wide column">
|
<div class="four wide column">
|
||||||
<div class="ui fluid vertical menu">
|
<div class="ui fluid vertical menu">
|
||||||
|
@ -13,12 +15,16 @@
|
||||||
{{svg "octicon-alert" 16 "text red"}}
|
{{svg "octicon-alert" 16 "text red"}}
|
||||||
</span>
|
</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{if $.ActionsConfig.IsWorkflowDisabled .Entry.Name}}
|
||||||
|
<div class="ui red label">{{$.locale.Tr "disabled"}}</div>
|
||||||
|
{{end}}
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="twelve wide column content">
|
<div class="twelve wide column content">
|
||||||
<div class="ui secondary filter stackable menu gt-je">
|
<div class="ui secondary filter menu gt-je gt-df gt-ac">
|
||||||
<!-- Actor -->
|
<!-- Actor -->
|
||||||
<div class="ui{{if not .Actors}} disabled{{end}} dropdown jump item">
|
<div class="ui{{if not .Actors}} disabled{{end}} dropdown jump item">
|
||||||
<span class="text">{{.locale.Tr "actions.runs.actor"}}</span>
|
<span class="text">{{.locale.Tr "actions.runs.actor"}}</span>
|
||||||
|
@ -57,6 +63,17 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{if .AllowDisableOrEnableWorkflow}}
|
||||||
|
<button class="ui jump dropdown btn interact-bg gt-p-3">
|
||||||
|
{{svg "octicon-kebab-horizontal"}}
|
||||||
|
<div class="menu">
|
||||||
|
<a class="item link-action" data-url="{{$.Link}}/{{if .CurWorkflowDisabled}}enable{{else}}disable{{end}}?workflow={{$.CurWorkflow}}&actor={{.CurActor}}&status={{$.CurStatus}}">
|
||||||
|
{{if .CurWorkflowDisabled}}{{.locale.Tr "actions.workflow.enable"}}{{else}}{{.locale.Tr "actions.workflow.disable"}}{{end}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{template "repo/actions/runs_list" .}}
|
{{template "repo/actions/runs_list" .}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -653,6 +653,18 @@ a.label,
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* replace item margin on secondary menu items with gap and remove both the
|
||||||
|
negative margins on the menu as well as margin on the items */
|
||||||
|
.ui.secondary.menu {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
gap: .35714286em;
|
||||||
|
}
|
||||||
|
.ui.secondary.menu .item {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.ui.secondary.menu .dropdown.item:hover,
|
.ui.secondary.menu .dropdown.item:hover,
|
||||||
.ui.secondary.menu a.item:hover {
|
.ui.secondary.menu a.item:hover {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
|
@ -670,6 +682,11 @@ a.label,
|
||||||
padding-right: 0.85714286em;
|
padding-right: 0.85714286em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* remove the menu clearfix so that it won't add undesired gaps when using "gap" */
|
||||||
|
.ui.menu::after {
|
||||||
|
content: normal;
|
||||||
|
}
|
||||||
|
|
||||||
.ui.menu .dropdown.item .menu {
|
.ui.menu .dropdown.item .menu {
|
||||||
background: var(--color-body);
|
background: var(--color-body);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue