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: a101211279
<1012112796@qq.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Jason Song <i@wolfogre.com>
tags/v1.21.0-rc0
@@ -391,7 +391,13 @@ func (repo *Repository) MustGetUnit(ctx context.Context, tp unit.Type) *RepoUnit | |||
Type: tp, | |||
Config: new(IssuesConfig), | |||
} | |||
} else if tp == unit.TypeActions { | |||
return &RepoUnit{ | |||
Type: tp, | |||
Config: new(ActionsConfig), | |||
} | |||
} | |||
return &RepoUnit{ | |||
Type: tp, | |||
Config: new(UnitConfig), |
@@ -6,6 +6,7 @@ package repo | |||
import ( | |||
"context" | |||
"fmt" | |||
"strings" | |||
"code.gitea.io/gitea/models/db" | |||
"code.gitea.io/gitea/models/unit" | |||
@@ -162,6 +163,42 @@ func (cfg *PullRequestsConfig) GetDefaultMergeStyle() MergeStyle { | |||
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. | |||
func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) { | |||
switch colName { | |||
@@ -175,7 +212,9 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) { | |||
r.Config = new(PullRequestsConfig) | |||
case unit.TypeIssues: | |||
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 | |||
default: | |||
r.Config = new(UnitConfig) | |||
@@ -218,6 +257,11 @@ func (r *RepoUnit) ExternalTrackerConfig() *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) { | |||
var tmpUnits []*RepoUnit | |||
if err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Find(&tmpUnits); err != nil { |
@@ -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_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. | |||
variables = Variables |
@@ -137,6 +137,15 @@ func List(ctx *context.Context) { | |||
actorID := ctx.FormInt64("actor") | |||
status := ctx.FormInt("status") | |||
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") | |||
// they will be 0 by default, which indicates get all status or actors | |||
ctx.Data["CurActor"] = actorID |
@@ -17,6 +17,7 @@ import ( | |||
actions_model "code.gitea.io/gitea/models/actions" | |||
"code.gitea.io/gitea/models/db" | |||
repo_model "code.gitea.io/gitea/models/repo" | |||
"code.gitea.io/gitea/models/unit" | |||
"code.gitea.io/gitea/modules/actions" | |||
"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.Get("", actions.List) | |||
m.Post("/disable", reqRepoAdmin, actions.DisableWorkflowFile) | |||
m.Post("/enable", reqRepoAdmin, actions.EnableWorkflowFile) | |||
m.Group("/runs/{run}", func() { | |||
m.Combo(""). |
@@ -150,7 +150,14 @@ func notify(ctx context.Context, input *notifyInput) error { | |||
if len(workflows) == 0 { | |||
log.Trace("repo %s with commit %s couldn't find workflows", input.Repo.RepoPath(), commit.ID) | |||
} else { | |||
actionsConfig := input.Repo.MustGetUnit(ctx, unit_model.TypeActions).ActionsConfig() | |||
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 { | |||
detectedWorkflows = append(detectedWorkflows, wf) | |||
} |
@@ -2,6 +2,8 @@ | |||
<div class="page-content repository actions"> | |||
{{template "repo/header" .}} | |||
<div class="ui container"> | |||
{{template "base/alert" .}} | |||
<div class="ui stackable grid"> | |||
<div class="four wide column"> | |||
<div class="ui fluid vertical menu"> | |||
@@ -13,12 +15,16 @@ | |||
{{svg "octicon-alert" 16 "text red"}} | |||
</span> | |||
{{end}} | |||
{{if $.ActionsConfig.IsWorkflowDisabled .Entry.Name}} | |||
<div class="ui red label">{{$.locale.Tr "disabled"}}</div> | |||
{{end}} | |||
</a> | |||
{{end}} | |||
</div> | |||
</div> | |||
<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 --> | |||
<div class="ui{{if not .Actors}} disabled{{end}} dropdown jump item"> | |||
<span class="text">{{.locale.Tr "actions.runs.actor"}}</span> | |||
@@ -57,6 +63,17 @@ | |||
{{end}} | |||
</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> | |||
{{template "repo/actions/runs_list" .}} | |||
</div> |
@@ -653,6 +653,18 @@ a.label, | |||
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 a.item:hover { | |||
color: var(--color-text); | |||
@@ -670,6 +682,11 @@ a.label, | |||
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 { | |||
background: var(--color-body); | |||
} |