From 64e0ba10de1163791c2f073c6dbb156b6c888255 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 31 Mar 2024 11:57:14 +0800 Subject: [PATCH 1/6] Add project workflow feature so users can define how to execute steps when project related events fired --- models/project/board.go | 12 +++ models/project/issue.go | 5 ++ models/project/project.go | 24 +++++- modules/projects/workflow.go | 45 ++++++++++ modules/projects/workflow_test.go | 46 ++++++++++ services/projects/workflow_notifier.go | 111 +++++++++++++++++++++++++ 6 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 modules/projects/workflow.go create mode 100644 modules/projects/workflow_test.go create mode 100644 services/projects/workflow_notifier.go diff --git a/models/project/board.go b/models/project/board.go index 5f142a356c..210eaa1dc1 100644 --- a/models/project/board.go +++ b/models/project/board.go @@ -219,6 +219,18 @@ func GetBoard(ctx context.Context, boardID int64) (*Board, error) { return board, nil } +func GetBoardByProjectIDAndBoardName(ctx context.Context, projectID int64, boardName string) (*Board, error) { + board := new(Board) + has, err := db.GetEngine(ctx).Where("project_id=? AND title=?", projectID, boardName).Get(board) + if err != nil { + return nil, err + } else if !has { + return nil, ErrProjectBoardNotExist{ProjectID: projectID, Name: boardName} + } + + return board, nil +} + // UpdateBoard updates a project board func UpdateBoard(ctx context.Context, board *Board) error { var fieldToUpdate []string diff --git a/models/project/issue.go b/models/project/issue.go index ebc9719de5..c98544b0a4 100644 --- a/models/project/issue.go +++ b/models/project/issue.go @@ -75,6 +75,11 @@ func (p *Project) NumOpenIssues(ctx context.Context) int { return int(c) } +func MoveIssueToAnotherBoard(ctx context.Context, issueID int64, newBoard *Board) error { + _, err := db.GetEngine(ctx).Exec("UPDATE `project_issue` SET project_board_id=? WHERE issue_id=?", newBoard.ID, issueID) + return err +} + // MoveIssuesOnProjectBoard moves or keeps issues in a column and sorts them inside that column func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs map[int64]int64) error { return db.WithTx(ctx, func(ctx context.Context) error { diff --git a/models/project/project.go b/models/project/project.go index 8f9ee2a99e..994c4441eb 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -52,6 +52,7 @@ const ( type ErrProjectNotExist struct { ID int64 RepoID int64 + Name string } // IsErrProjectNotExist checks if an error is a ErrProjectNotExist @@ -61,6 +62,9 @@ func IsErrProjectNotExist(err error) bool { } func (err ErrProjectNotExist) Error() string { + if err.RepoID > 0 && len(err.Name) > 0 { + return fmt.Sprintf("projects does not exist [repo_id: %d, name: %s]", err.RepoID, err.Name) + } return fmt.Sprintf("projects does not exist [id: %d]", err.ID) } @@ -70,7 +74,9 @@ func (err ErrProjectNotExist) Unwrap() error { // ErrProjectBoardNotExist represents a "ProjectBoardNotExist" kind of error. type ErrProjectBoardNotExist struct { - BoardID int64 + BoardID int64 + ProjectID int64 + Name string } // IsErrProjectBoardNotExist checks if an error is a ErrProjectBoardNotExist @@ -80,6 +86,9 @@ func IsErrProjectBoardNotExist(err error) bool { } func (err ErrProjectBoardNotExist) Error() string { + if err.ProjectID > 0 && len(err.Name) > 0 { + return fmt.Sprintf("project board does not exist [project_id: %d, name: %s]", err.ProjectID, err.Name) + } return fmt.Sprintf("project board does not exist [id: %d]", err.BoardID) } @@ -293,6 +302,19 @@ func GetProjectByID(ctx context.Context, id int64) (*Project, error) { return p, nil } +// GetProjectByName returns the projects in a repository +func GetProjectByName(ctx context.Context, repoID int64, name string) (*Project, error) { + p := new(Project) + has, err := db.GetEngine(ctx).Where("repo_id=? AND title=?", repoID, name).Get(p) + if err != nil { + return nil, err + } else if !has { + return nil, ErrProjectNotExist{RepoID: repoID, Name: name} + } + + return p, nil +} + // GetProjectForRepoByID returns the projects in a repository func GetProjectForRepoByID(ctx context.Context, repoID, id int64) (*Project, error) { p := new(Project) diff --git a/modules/projects/workflow.go b/modules/projects/workflow.go new file mode 100644 index 0000000000..751c611801 --- /dev/null +++ b/modules/projects/workflow.go @@ -0,0 +1,45 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package projects + +// Action represents an action that can be taken in a workflow +type Action struct { + SetValue string +} + +const ( + // Project workflow event names + EventItemAddedToProject = "item_added_to_project" + EventItemClosed = "item_closed" +) + +type Event struct { + Name string + Types []string + Actions []Action +} + +type Workflow struct { + Name string + Events []Event +} + +func ParseWorkflow(content string) (*Workflow, error) { + return &Workflow{}, nil +} + +func (w *Workflow) FireAction(evtName string, f func(action Action) error) error { + for _, evt := range w.Events { + if evt.Name == evtName { + for _, action := range evt.Actions { + // Do something with action + if err := f(action); err != nil { + return err + } + } + break + } + } + return nil +} diff --git a/modules/projects/workflow_test.go b/modules/projects/workflow_test.go new file mode 100644 index 0000000000..17147335dd --- /dev/null +++ b/modules/projects/workflow_test.go @@ -0,0 +1,46 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package projects + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseWorkflow(t *testing.T) { + workflowFile := ` +name: Test Workflow +on: + item_added_to_project: + types: [issue, pull_request] + action: + - set_value: "status=Todo" + + item_closed: + types: [issue, pull_request] + action: + - remove_label: "" + + item_reopened: + action: + + code_changes_requested: + action: + + code_review_approved: + action: + + pull_request_merged: + action: + + auto_add_to_project: + action: +` + + wf, err := ParseWorkflow(workflowFile) + assert.NoError(t, err) + + assert.Equal(t, "Test Workflow", wf.Name) +} diff --git a/services/projects/workflow_notifier.go b/services/projects/workflow_notifier.go new file mode 100644 index 0000000000..1b778fdedb --- /dev/null +++ b/services/projects/workflow_notifier.go @@ -0,0 +1,111 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package projects + +import ( + "context" + "strings" + + issues_model "code.gitea.io/gitea/models/issues" + project_model "code.gitea.io/gitea/models/project" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/log" + project_module "code.gitea.io/gitea/modules/projects" + notify_service "code.gitea.io/gitea/services/notify" +) + +func init() { + notify_service.RegisterNotifier(&workflowNotifier{}) +} + +type workflowNotifier struct { + notify_service.NullNotifier +} + +var _ notify_service.Notifier = &workflowNotifier{} + +// NewNotifier create a new workflowNotifier notifier +func NewNotifier() notify_service.Notifier { + return &workflowNotifier{} +} + +func (m *workflowNotifier) IssueChangeStatus(ctx context.Context, doer *user_model.User, commitID string, issue *issues_model.Issue, actionComment *issues_model.Comment, isClosed bool) { + if isClosed { + if err := issue.LoadRepo(ctx); err != nil { + log.Error("IssueChangeStatus: LoadRepo: %v", err) + return + } + gitRepo, err := gitrepo.OpenRepository(ctx, issue.Repo) + if err != nil { + log.Error("IssueChangeStatus: OpenRepository: %v", err) + return + } + defer gitRepo.Close() + + // Get the commit object for the ref + commit, err := gitRepo.GetCommit(issue.Repo.DefaultBranch) + if err != nil { + log.Error("gitRepo.GetCommit: %w", err) + return + } + + tree, err := commit.SubTree(".gitea/projects") + if _, ok := err.(git.ErrNotExist); ok { + return + } + if err != nil { + log.Error("commit.SubTree: %w", err) + return + } + + entries, err := tree.ListEntriesRecursiveFast() + if err != nil { + log.Error("tree.ListEntriesRecursiveFast: %w", err) + return + } + + ret := make(git.Entries, 0, len(entries)) + for _, entry := range entries { + if strings.HasSuffix(entry.Name(), ".yml") || strings.HasSuffix(entry.Name(), ".yaml") { + ret = append(ret, entry) + } + } + if len(ret) == 0 { + return + } + + for _, entry := range ret { + workflowContent, err := commit.GetFileContent(".gitea/projects/"+entry.Name(), 1024*1024) + if err != nil { + log.Error("gitRepo.GetCommit: %w", err) + return + } + + wf, err := project_module.ParseWorkflow(workflowContent) + if err != nil { + log.Error("IssueChangeStatus: OpenRepository: %v", err) + return + } + projectName := strings.TrimSuffix(strings.TrimSuffix(entry.Name(), ".yml"), ".yaml") + project, err := project_model.GetProjectByName(ctx, issue.RepoID, projectName) + if err != nil { + log.Error("IssueChangeStatus: GetProjectByName: %v", err) + return + } + if err := wf.FireAction(project_module.EventItemClosed, func(action project_module.Action) error { + board, err := project_model.GetBoardByProjectIDAndBoardName(ctx, project.ID, action.SetValue) + if err != nil { + log.Error("IssueChangeStatus: GetBoardByProjectIDAndBoardName: %v", err) + return err + } + return project_model.MoveIssueToAnotherBoard(ctx, issue.ID, board) + }); err != nil { + log.Error("IssueChangeStatus: FireAction: %v", err) + return + } + } + } +} From 95cecbb2943723990aa772c5de97d8b339e17ac8 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 5 Apr 2024 14:18:51 +0800 Subject: [PATCH 2/6] Add NewIssue events --- models/project/issue.go | 8 ++ modules/projects/workflow.go | 6 +- services/projects/workflow_notifier.go | 152 ++++++++++++++++--------- 3 files changed, 110 insertions(+), 56 deletions(-) diff --git a/models/project/issue.go b/models/project/issue.go index c98544b0a4..313cfd7a4f 100644 --- a/models/project/issue.go +++ b/models/project/issue.go @@ -75,6 +75,14 @@ func (p *Project) NumOpenIssues(ctx context.Context) int { return int(c) } +func AddIssueToBoard(ctx context.Context, issueID int64, newBoard *Board) error { + return db.Insert(ctx, &ProjectIssue{ + IssueID: issueID, + ProjectID: newBoard.ProjectID, + ProjectBoardID: newBoard.ID, + }) +} + func MoveIssueToAnotherBoard(ctx context.Context, issueID int64, newBoard *Board) error { _, err := db.GetEngine(ctx).Exec("UPDATE `project_issue` SET project_board_id=? WHERE issue_id=?", newBoard.ID, issueID) return err diff --git a/modules/projects/workflow.go b/modules/projects/workflow.go index 751c611801..0127b12959 100644 --- a/modules/projects/workflow.go +++ b/modules/projects/workflow.go @@ -12,6 +12,7 @@ const ( // Project workflow event names EventItemAddedToProject = "item_added_to_project" EventItemClosed = "item_closed" + EventItem ) type Event struct { @@ -21,8 +22,9 @@ type Event struct { } type Workflow struct { - Name string - Events []Event + Name string + Events []Event + ProjectID int64 } func ParseWorkflow(content string) (*Workflow, error) { diff --git a/services/projects/workflow_notifier.go b/services/projects/workflow_notifier.go index 1b778fdedb..77aac20dd8 100644 --- a/services/projects/workflow_notifier.go +++ b/services/projects/workflow_notifier.go @@ -9,6 +9,7 @@ import ( issues_model "code.gitea.io/gitea/models/issues" project_model "code.gitea.io/gitea/models/project" + repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" @@ -32,71 +33,114 @@ func NewNotifier() notify_service.Notifier { return &workflowNotifier{} } +func findRepoProjectsWorkflows(ctx context.Context, repo *repo_model.Repository) ([]*project_module.Workflow, error) { + gitRepo, err := gitrepo.OpenRepository(ctx, repo) + if err != nil { + log.Error("IssueChangeStatus: OpenRepository: %v", err) + return nil, err + } + defer gitRepo.Close() + + // Get the commit object for the ref + commit, err := gitRepo.GetCommit(repo.DefaultBranch) + if err != nil { + log.Error("gitRepo.GetCommit: %w", err) + return nil, err + } + + tree, err := commit.SubTree(".gitea/projects") + if _, ok := err.(git.ErrNotExist); ok { + return nil, nil + } + if err != nil { + log.Error("commit.SubTree: %w", err) + return nil, err + } + + entries, err := tree.ListEntriesRecursiveFast() + if err != nil { + log.Error("tree.ListEntriesRecursiveFast: %w", err) + return nil, err + } + + ret := make(git.Entries, 0, len(entries)) + for _, entry := range entries { + if strings.HasSuffix(entry.Name(), ".yml") || strings.HasSuffix(entry.Name(), ".yaml") { + ret = append(ret, entry) + } + } + if len(ret) == 0 { + return nil, nil + } + + wfs := make([]*project_module.Workflow, 0, len(ret)) + for _, entry := range ret { + workflowContent, err := commit.GetFileContent(".gitea/projects/"+entry.Name(), 1024*1024) + if err != nil { + log.Error("gitRepo.GetCommit: %w", err) + return nil, err + } + + wf, err := project_module.ParseWorkflow(workflowContent) + if err != nil { + log.Error("IssueChangeStatus: OpenRepository: %v", err) + return nil, err + } + projectName := strings.TrimSuffix(strings.TrimSuffix(entry.Name(), ".yml"), ".yaml") + project, err := project_model.GetProjectByName(ctx, repo.ID, projectName) + if err != nil { + log.Error("IssueChangeStatus: GetProjectByName: %v", err) + return nil, err + } + wf.ProjectID = project.ID + + wfs = append(wfs, wf) + } + return wfs, nil +} + +func (m *workflowNotifier) NewIssue(ctx context.Context, issue *issues_model.Issue, mentions []*user_model.User) { + if err := issue.LoadRepo(ctx); err != nil { + log.Error("NewIssue: LoadRepo: %v", err) + return + } + wfs, err := findRepoProjectsWorkflows(ctx, issue.Repo) + if err != nil { + log.Error("NewIssue: findRepoProjectsWorkflows: %v", err) + return + } + + for _, wf := range wfs { + if err := wf.FireAction(project_module.EventItemClosed, func(action project_module.Action) error { + board, err := project_model.GetBoardByProjectIDAndBoardName(ctx, wf.ProjectID, action.SetValue) + if err != nil { + log.Error("NewIssue: GetBoardByProjectIDAndBoardName: %v", err) + return err + } + return project_model.AddIssueToBoard(ctx, issue.ID, board) + }); err != nil { + log.Error("NewIssue: FireAction: %v", err) + return + } + } +} +} + func (m *workflowNotifier) IssueChangeStatus(ctx context.Context, doer *user_model.User, commitID string, issue *issues_model.Issue, actionComment *issues_model.Comment, isClosed bool) { if isClosed { if err := issue.LoadRepo(ctx); err != nil { log.Error("IssueChangeStatus: LoadRepo: %v", err) return } - gitRepo, err := gitrepo.OpenRepository(ctx, issue.Repo) + wfs, err := findRepoProjectsWorkflows(ctx, issue.Repo) if err != nil { - log.Error("IssueChangeStatus: OpenRepository: %v", err) - return - } - defer gitRepo.Close() - - // Get the commit object for the ref - commit, err := gitRepo.GetCommit(issue.Repo.DefaultBranch) - if err != nil { - log.Error("gitRepo.GetCommit: %w", err) + log.Error("IssueChangeStatus: findRepoProjectsWorkflows: %v", err) return } - tree, err := commit.SubTree(".gitea/projects") - if _, ok := err.(git.ErrNotExist); ok { - return - } - if err != nil { - log.Error("commit.SubTree: %w", err) - return - } - - entries, err := tree.ListEntriesRecursiveFast() - if err != nil { - log.Error("tree.ListEntriesRecursiveFast: %w", err) - return - } - - ret := make(git.Entries, 0, len(entries)) - for _, entry := range entries { - if strings.HasSuffix(entry.Name(), ".yml") || strings.HasSuffix(entry.Name(), ".yaml") { - ret = append(ret, entry) - } - } - if len(ret) == 0 { - return - } - - for _, entry := range ret { - workflowContent, err := commit.GetFileContent(".gitea/projects/"+entry.Name(), 1024*1024) - if err != nil { - log.Error("gitRepo.GetCommit: %w", err) - return - } - - wf, err := project_module.ParseWorkflow(workflowContent) - if err != nil { - log.Error("IssueChangeStatus: OpenRepository: %v", err) - return - } - projectName := strings.TrimSuffix(strings.TrimSuffix(entry.Name(), ".yml"), ".yaml") - project, err := project_model.GetProjectByName(ctx, issue.RepoID, projectName) - if err != nil { - log.Error("IssueChangeStatus: GetProjectByName: %v", err) - return - } + for _, wf := range wfs { if err := wf.FireAction(project_module.EventItemClosed, func(action project_module.Action) error { - board, err := project_model.GetBoardByProjectIDAndBoardName(ctx, project.ID, action.SetValue) + board, err := project_model.GetBoardByProjectIDAndBoardName(ctx, wf.ProjectID, action.SetValue) if err != nil { log.Error("IssueChangeStatus: GetBoardByProjectIDAndBoardName: %v", err) return err From fb12ed211e6475d40284a9494231146f9db51328 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 8 Jan 2025 17:25:02 -0800 Subject: [PATCH 3/6] add workflow --- models/project/workflows.go | 30 ++++++++++++++++++++++++++++++ routers/web/web.go | 3 +++ 2 files changed, 33 insertions(+) create mode 100644 models/project/workflows.go diff --git a/models/project/workflows.go b/models/project/workflows.go new file mode 100644 index 0000000000..6c8ee57cc4 --- /dev/null +++ b/models/project/workflows.go @@ -0,0 +1,30 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package project + +import "code.gitea.io/gitea/models/db" + +type WorkflowEvent string + +const ( + WorkflowEventItemAddedToProject WorkflowEvent = "item_added_to_project" + WorkflowEventItemReopened WorkflowEvent = "item_reopened" + WorkflowEventItemClosed WorkflowEvent = "item_closed" + WorkflowEventCodeChangesRequested WorkflowEvent = "code_changes_requested" + WorkflowEventCodeReviewApproved WorkflowEvent = "code_review_approved" + WorkflowEventPullRequestMerged WorkflowEvent = "pull_request_merged" + WorkflowEventAutoArchiveItems WorkflowEvent = "auto_archive_items" + WorkflowEventAutoAddToProject WorkflowEvent = "auto_add_to_project" + WorkflowEventAutoCloseIssue WorkflowEvent = "auto_close_issue" +) + +type ProjectWorkflow struct { + ID int64 + ProjectID int64 `xorm:"index"` + WorkflowEvent WorkflowEvent `xorm:"index"` +} + +func init() { + db.RegisterModel(new(ProjectWorkflow)) +} diff --git a/routers/web/web.go b/routers/web/web.go index 32d65865ac..0886c5d1ba 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1383,6 +1383,9 @@ func registerRoutes(m *web.Router) { m.Group("/{username}/{reponame}/projects", func() { m.Get("", repo.Projects) m.Get("/{id}", repo.ViewProject) + m.Group("/{id}/workflows", func() { + m.Get("", repo.Workflows) + }) m.Group("", func() { //nolint:dupl m.Get("/new", repo.RenderNewProject) m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost) From 7fefa372710aebec4111a2952a122cb9c99149b7 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 28 Mar 2025 23:36:24 -0700 Subject: [PATCH 4/6] Some work --- models/project/workflows.go | 58 +++++++++++++++++++++++++++---- routers/web/projects/workflows.go | 14 ++++++++ routers/web/web.go | 4 ++- 3 files changed, 69 insertions(+), 7 deletions(-) create mode 100644 routers/web/projects/workflows.go diff --git a/models/project/workflows.go b/models/project/workflows.go index 6c8ee57cc4..a98c5d68f6 100644 --- a/models/project/workflows.go +++ b/models/project/workflows.go @@ -3,7 +3,13 @@ package project -import "code.gitea.io/gitea/models/db" +import ( + "context" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" +) type WorkflowEvent string @@ -19,12 +25,52 @@ const ( WorkflowEventAutoCloseIssue WorkflowEvent = "auto_close_issue" ) -type ProjectWorkflow struct { - ID int64 - ProjectID int64 `xorm:"index"` - WorkflowEvent WorkflowEvent `xorm:"index"` +type WorkflowActionType string + +const ( + WorkflowActionTypeScope WorkflowActionType = "scope" // issue, pull_request, etc. + WorkflowActionTypeLabel WorkflowActionType = "label" // choose one or more labels + WorkflowActionTypeColumn WorkflowActionType = "column" // choose one column + WorkflowActionTypeClose WorkflowActionType = "close" // close the issue +) + +type WorkflowAction struct { + ActionType WorkflowActionType + ActionValue string +} + +type ProjectWorkflowEvent struct { + ID int64 + ProjectID int64 `xorm:"unique(s)"` + WorkflowEvent WorkflowEvent `xorm:"unique(s)"` + WorkflowActions []WorkflowAction `xorm:"TEXT json"` + CreatedUnix timeutil.TimeStamp `xorm:"created"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` } func init() { - db.RegisterModel(new(ProjectWorkflow)) + db.RegisterModel(new(ProjectWorkflowEvent)) +} + +func FindWorkflowEvents(ctx context.Context, projectID int64) (map[WorkflowEvent]ProjectWorkflowEvent, error) { + events := make(map[WorkflowEvent]ProjectWorkflowEvent) + if err := db.GetEngine(ctx).Where("project_id=?", projectID).Find(&events); err != nil { + return nil, err + } + res := make(map[WorkflowEvent]ProjectWorkflowEvent, len(events)) + for _, event := range events { + res[event.WorkflowEvent] = event + } + return res, nil +} + +func GetWorkflowEventByID(ctx context.Context, id int64) (*ProjectWorkflowEvent, error) { + p, exist, err := db.GetByID[ProjectWorkflowEvent](ctx, id) + if err != nil { + return nil, err + } + if !exist { + return nil, util.ErrNotExist + } + return p, nil } diff --git a/routers/web/projects/workflows.go b/routers/web/projects/workflows.go new file mode 100644 index 0000000000..afebda7d1f --- /dev/null +++ b/routers/web/projects/workflows.go @@ -0,0 +1,14 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package projects + +import "code.gitea.io/gitea/services/context" + +var tmplWorkflows = "projects/workflows" + +func Workflows(ctx *context.Context) { +} + +func WorkflowEdit(ctx *context.Context) { +} diff --git a/routers/web/web.go b/routers/web/web.go index 8bde7cd67b..e1405b2f91 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -34,6 +34,7 @@ import ( "code.gitea.io/gitea/routers/web/misc" "code.gitea.io/gitea/routers/web/org" org_setting "code.gitea.io/gitea/routers/web/org/setting" + "code.gitea.io/gitea/routers/web/projects" "code.gitea.io/gitea/routers/web/repo" "code.gitea.io/gitea/routers/web/repo/actions" repo_setting "code.gitea.io/gitea/routers/web/repo/setting" @@ -1402,7 +1403,8 @@ func registerRoutes(m *web.Router) { m.Get("", repo.Projects) m.Get("/{id}", repo.ViewProject) m.Group("/{id}/workflows", func() { - m.Get("", repo.Workflows) + m.Get("", projects.Workflows) + m.Get("/{workflow_id}", projects.WorkflowEdit) }) m.Group("", func() { //nolint:dupl m.Get("/new", repo.RenderNewProject) From b37cb32ad6204baff9717feb822b903b66cdee40 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 6 Apr 2025 12:41:25 -0700 Subject: [PATCH 5/6] Add web routers for project workflow --- models/project/workflows.go | 60 +++++++++++++++++++++++---- routers/web/projects/workflows.go | 67 +++++++++++++++++++++++++++++-- routers/web/web.go | 2 +- templates/projects/workflows.tmpl | 11 +++++ 4 files changed, 127 insertions(+), 13 deletions(-) create mode 100644 templates/projects/workflows.tmpl diff --git a/models/project/workflows.go b/models/project/workflows.go index a98c5d68f6..0bdb21c334 100644 --- a/models/project/workflows.go +++ b/models/project/workflows.go @@ -39,7 +39,7 @@ type WorkflowAction struct { ActionValue string } -type ProjectWorkflowEvent struct { +type ProjectWorkflow struct { ID int64 ProjectID int64 `xorm:"unique(s)"` WorkflowEvent WorkflowEvent `xorm:"unique(s)"` @@ -48,24 +48,52 @@ type ProjectWorkflowEvent struct { UpdatedUnix timeutil.TimeStamp `xorm:"updated"` } -func init() { - db.RegisterModel(new(ProjectWorkflowEvent)) +func (p *ProjectWorkflow) Link() string { + return "" } -func FindWorkflowEvents(ctx context.Context, projectID int64) (map[WorkflowEvent]ProjectWorkflowEvent, error) { - events := make(map[WorkflowEvent]ProjectWorkflowEvent) +func newDefaultWorkflows() []*ProjectWorkflow { + return []*ProjectWorkflow{ + { + WorkflowEvent: WorkflowEventItemAddedToProject, + WorkflowActions: []WorkflowAction{{ActionType: WorkflowActionTypeScope, ActionValue: "issue"}}, + }, + { + ProjectID: 0, + WorkflowEvent: WorkflowEventItemReopened, + WorkflowActions: []WorkflowAction{{ActionType: WorkflowActionTypeScope, ActionValue: "issue"}}, + }, + } +} + +func GetWorkflowDefaultValue(workflowIDStr string) *ProjectWorkflow { + workflows := newDefaultWorkflows() + for _, workflow := range workflows { + if workflow.WorkflowEvent == WorkflowEvent(workflowIDStr) { + return workflow + } + } + return &ProjectWorkflow{} +} + +func init() { + db.RegisterModel(new(ProjectWorkflow)) +} + +func FindWorkflowEvents(ctx context.Context, projectID int64) (map[WorkflowEvent]ProjectWorkflow, error) { + events := make(map[WorkflowEvent]ProjectWorkflow) if err := db.GetEngine(ctx).Where("project_id=?", projectID).Find(&events); err != nil { return nil, err } - res := make(map[WorkflowEvent]ProjectWorkflowEvent, len(events)) + res := make(map[WorkflowEvent]ProjectWorkflow, len(events)) for _, event := range events { res[event.WorkflowEvent] = event } return res, nil } -func GetWorkflowEventByID(ctx context.Context, id int64) (*ProjectWorkflowEvent, error) { - p, exist, err := db.GetByID[ProjectWorkflowEvent](ctx, id) +func GetWorkflowByID(ctx context.Context, id int64) (*ProjectWorkflow, error) { + p, exist, err := db.GetByID[ProjectWorkflow](ctx, id) if err != nil { return nil, err } @@ -74,3 +102,19 @@ func GetWorkflowEventByID(ctx context.Context, id int64) (*ProjectWorkflowEvent, } return p, nil } + +func GetWorkflows(ctx context.Context, projectID int64) ([]*ProjectWorkflow, error) { + events := make([]*ProjectWorkflow, 0, 10) + if err := db.GetEngine(ctx).Where("project_id=?", projectID).Find(&events); err != nil { + return nil, err + } + workflows := newDefaultWorkflows() + for i, defaultWorkflow := range workflows { + for _, workflow := range events { + if workflow.WorkflowEvent == defaultWorkflow.WorkflowEvent { + workflows[i] = workflow + } + } + } + return workflows, nil +} diff --git a/routers/web/projects/workflows.go b/routers/web/projects/workflows.go index afebda7d1f..60a9e09952 100644 --- a/routers/web/projects/workflows.go +++ b/routers/web/projects/workflows.go @@ -3,12 +3,71 @@ package projects -import "code.gitea.io/gitea/services/context" +import ( + "strconv" -var tmplWorkflows = "projects/workflows" + project_model "code.gitea.io/gitea/models/project" + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/services/context" +) + +var tmplWorkflows = templates.TplName("projects/workflows") func Workflows(ctx *context.Context) { -} + projectID := ctx.PathParamInt64("id") + p, err := project_model.GetProjectByID(ctx, projectID) + if err != nil { + if project_model.IsErrProjectNotExist(err) { + ctx.NotFound(nil) + } else { + ctx.ServerError("GetProjectByID", err) + } + return + } + if p.RepoID != ctx.Repo.Repository.ID { + ctx.NotFound(nil) + return + } -func WorkflowEdit(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("projects.workflows") + ctx.Data["PageIsWorkflows"] = true + ctx.Data["PageIsProjects"] = true + ctx.Data["PageIsProjectsWorkflows"] = true + + workflows, err := project_model.GetWorkflows(ctx, projectID) + if err != nil { + ctx.ServerError("GetWorkflows", err) + return + } + ctx.Data["Workflows"] = workflows + + workflowIDStr := ctx.PathParam("workflow_id") + var workflow *project_model.ProjectWorkflow + if workflowIDStr == "" { // get first value workflow or the first workflow + for _, wf := range workflows { + if wf.ID > 0 { + workflow = wf + break + } + } + if workflow.ID == 0 { + workflow = workflows[0] + } + } else { + workflowID, _ := strconv.ParseInt(workflowIDStr, 10, 64) + if workflowID > 0 { + var err error + workflow, err = project_model.GetWorkflowByID(ctx, workflowID) + if err != nil { + ctx.ServerError("GetWorkflowByID", err) + return + } + ctx.Data["CurWorkflow"] = workflow + } else { + workflow = project_model.GetWorkflowDefaultValue(workflowIDStr) + } + } + ctx.Data["CurWorkflow"] = workflow + + ctx.HTML(200, tmplWorkflows) } diff --git a/routers/web/web.go b/routers/web/web.go index e1405b2f91..99092d285d 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1404,7 +1404,7 @@ func registerRoutes(m *web.Router) { m.Get("/{id}", repo.ViewProject) m.Group("/{id}/workflows", func() { m.Get("", projects.Workflows) - m.Get("/{workflow_id}", projects.WorkflowEdit) + m.Get("/{workflow_id}", projects.Workflows) }) m.Group("", func() { //nolint:dupl m.Get("/new", repo.RenderNewProject) diff --git a/templates/projects/workflows.tmpl b/templates/projects/workflows.tmpl new file mode 100644 index 0000000000..b7799316ff --- /dev/null +++ b/templates/projects/workflows.tmpl @@ -0,0 +1,11 @@ +
+
+
+ +
+
+
From 7c66abcd9dc98e895353b6c10f7210faf73739ff Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 9 Jun 2025 18:26:31 -0700 Subject: [PATCH 6/6] Use database to store the project workflow data --- models/project/workflows.go | 144 +++++++++++++------- modules/projects/workflow.go | 47 ------- modules/projects/workflow_test.go | 46 ------- routers/web/projects/workflows.go | 48 ++++--- routers/web/web.go | 4 + services/projects/workflow_notifier.go | 175 ++++++++++--------------- templates/org/projects/workflows.tmpl | 13 ++ templates/projects/workflows.tmpl | 11 +- templates/repo/projects/workflows.tmpl | 14 ++ 9 files changed, 237 insertions(+), 265 deletions(-) delete mode 100644 modules/projects/workflow.go delete mode 100644 modules/projects/workflow_test.go create mode 100644 templates/org/projects/workflows.tmpl create mode 100644 templates/repo/projects/workflows.tmpl diff --git a/models/project/workflows.go b/models/project/workflows.go index 0bdb21c334..b66803a041 100644 --- a/models/project/workflows.go +++ b/models/project/workflows.go @@ -5,8 +5,10 @@ package project import ( "context" + "fmt" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" ) @@ -25,12 +27,88 @@ const ( WorkflowEventAutoCloseIssue WorkflowEvent = "auto_close_issue" ) +var workflowEvents = []WorkflowEvent{ + WorkflowEventItemAddedToProject, + WorkflowEventItemReopened, + WorkflowEventItemClosed, + WorkflowEventCodeChangesRequested, + WorkflowEventCodeReviewApproved, + WorkflowEventPullRequestMerged, + WorkflowEventAutoArchiveItems, + WorkflowEventAutoAddToProject, + WorkflowEventAutoCloseIssue, +} + +func GetWorkflowEvents() []WorkflowEvent { + return workflowEvents +} + +func (we WorkflowEvent) ToString() string { + switch we { + case WorkflowEventItemAddedToProject: + return "Item added to project" + case WorkflowEventItemReopened: + return "Item reopened" + case WorkflowEventItemClosed: + return "Item closed" + case WorkflowEventCodeChangesRequested: + return "Code changes requested" + case WorkflowEventCodeReviewApproved: + return "Code review approved" + case WorkflowEventPullRequestMerged: + return "Pull request merged" + case WorkflowEventAutoArchiveItems: + return "Auto archive items" + case WorkflowEventAutoAddToProject: + return "Auto add to project" + case WorkflowEventAutoCloseIssue: + return "Auto close issue" + default: + return string(we) + } +} + +func (we WorkflowEvent) UUID() string { + switch we { + case WorkflowEventItemAddedToProject: + return "item_added_to_project" + case WorkflowEventItemReopened: + return "item_reopened" + case WorkflowEventItemClosed: + return "item_closed" + case WorkflowEventCodeChangesRequested: + return "code_changes_requested" + case WorkflowEventCodeReviewApproved: + return "code_review_approved" + case WorkflowEventPullRequestMerged: + return "pull_request_merged" + case WorkflowEventAutoArchiveItems: + return "auto_archive_items" + case WorkflowEventAutoAddToProject: + return "auto_add_to_project" + case WorkflowEventAutoCloseIssue: + return "auto_close_issue" + default: + return string(we) + } +} + +type WorkflowFilterType string + +const ( + WorkflowFilterTypeScope WorkflowFilterType = "scope" // issue, pull_request, etc. +) + +type WorkflowFilter struct { + Type WorkflowFilterType + Value string // e.g., "issue", "pull_request", etc. +} + type WorkflowActionType string const ( - WorkflowActionTypeScope WorkflowActionType = "scope" // issue, pull_request, etc. + WorkflowActionTypeColumn WorkflowActionType = "column" // add the item to the project's column WorkflowActionTypeLabel WorkflowActionType = "label" // choose one or more labels - WorkflowActionTypeColumn WorkflowActionType = "column" // choose one column WorkflowActionTypeClose WorkflowActionType = "close" // close the issue ) @@ -42,50 +120,44 @@ type WorkflowAction struct { type ProjectWorkflow struct { ID int64 ProjectID int64 `xorm:"unique(s)"` + Project *Project `xorm:"-"` WorkflowEvent WorkflowEvent `xorm:"unique(s)"` + WorkflowFilters []WorkflowFilter `xorm:"TEXT json"` WorkflowActions []WorkflowAction `xorm:"TEXT json"` CreatedUnix timeutil.TimeStamp `xorm:"created"` UpdatedUnix timeutil.TimeStamp `xorm:"updated"` } -func (p *ProjectWorkflow) Link() string { - return "" +func (p *ProjectWorkflow) LoadProject(ctx context.Context) error { + if p.Project != nil || p.ProjectID <= 0 { + return nil + } + project, err := GetProjectByID(ctx, p.ProjectID) + if err != nil { + return err + } + p.Project = project + return nil } -func newDefaultWorkflows() []*ProjectWorkflow { - return []*ProjectWorkflow{ - { - WorkflowEvent: WorkflowEventItemAddedToProject, - WorkflowActions: []WorkflowAction{{ActionType: WorkflowActionTypeScope, ActionValue: "issue"}}, - }, - { - ProjectID: 0, - WorkflowEvent: WorkflowEventItemReopened, - WorkflowActions: []WorkflowAction{{ActionType: WorkflowActionTypeScope, ActionValue: "issue"}}, - }, +func (p *ProjectWorkflow) Link(ctx context.Context) string { + if err := p.LoadProject(ctx); err != nil { + log.Error("ProjectWorkflow Link: %v", err) + return "" } -} - -func GetWorkflowDefaultValue(workflowIDStr string) *ProjectWorkflow { - workflows := newDefaultWorkflows() - for _, workflow := range workflows { - if workflow.WorkflowEvent == WorkflowEvent(workflowIDStr) { - return workflow - } - } - return &ProjectWorkflow{} + return p.Project.Link(ctx) + fmt.Sprintf("/workflows/%d", p.ID) } func init() { db.RegisterModel(new(ProjectWorkflow)) } -func FindWorkflowEvents(ctx context.Context, projectID int64) (map[WorkflowEvent]ProjectWorkflow, error) { - events := make(map[WorkflowEvent]ProjectWorkflow) +func FindWorkflowEvents(ctx context.Context, projectID int64) (map[WorkflowEvent]*ProjectWorkflow, error) { + events := make(map[WorkflowEvent]*ProjectWorkflow) if err := db.GetEngine(ctx).Where("project_id=?", projectID).Find(&events); err != nil { return nil, err } - res := make(map[WorkflowEvent]ProjectWorkflow, len(events)) + res := make(map[WorkflowEvent]*ProjectWorkflow, len(events)) for _, event := range events { res[event.WorkflowEvent] = event } @@ -102,19 +174,3 @@ func GetWorkflowByID(ctx context.Context, id int64) (*ProjectWorkflow, error) { } return p, nil } - -func GetWorkflows(ctx context.Context, projectID int64) ([]*ProjectWorkflow, error) { - events := make([]*ProjectWorkflow, 0, 10) - if err := db.GetEngine(ctx).Where("project_id=?", projectID).Find(&events); err != nil { - return nil, err - } - workflows := newDefaultWorkflows() - for i, defaultWorkflow := range workflows { - for _, workflow := range events { - if workflow.WorkflowEvent == defaultWorkflow.WorkflowEvent { - workflows[i] = workflow - } - } - } - return workflows, nil -} diff --git a/modules/projects/workflow.go b/modules/projects/workflow.go deleted file mode 100644 index 0127b12959..0000000000 --- a/modules/projects/workflow.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package projects - -// Action represents an action that can be taken in a workflow -type Action struct { - SetValue string -} - -const ( - // Project workflow event names - EventItemAddedToProject = "item_added_to_project" - EventItemClosed = "item_closed" - EventItem -) - -type Event struct { - Name string - Types []string - Actions []Action -} - -type Workflow struct { - Name string - Events []Event - ProjectID int64 -} - -func ParseWorkflow(content string) (*Workflow, error) { - return &Workflow{}, nil -} - -func (w *Workflow) FireAction(evtName string, f func(action Action) error) error { - for _, evt := range w.Events { - if evt.Name == evtName { - for _, action := range evt.Actions { - // Do something with action - if err := f(action); err != nil { - return err - } - } - break - } - } - return nil -} diff --git a/modules/projects/workflow_test.go b/modules/projects/workflow_test.go deleted file mode 100644 index 17147335dd..0000000000 --- a/modules/projects/workflow_test.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package projects - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestParseWorkflow(t *testing.T) { - workflowFile := ` -name: Test Workflow -on: - item_added_to_project: - types: [issue, pull_request] - action: - - set_value: "status=Todo" - - item_closed: - types: [issue, pull_request] - action: - - remove_label: "" - - item_reopened: - action: - - code_changes_requested: - action: - - code_review_approved: - action: - - pull_request_merged: - action: - - auto_add_to_project: - action: -` - - wf, err := ParseWorkflow(workflowFile) - assert.NoError(t, err) - - assert.Equal(t, "Test Workflow", wf.Name) -} diff --git a/routers/web/projects/workflows.go b/routers/web/projects/workflows.go index 60a9e09952..fbdeb4537a 100644 --- a/routers/web/projects/workflows.go +++ b/routers/web/projects/workflows.go @@ -11,9 +11,14 @@ import ( "code.gitea.io/gitea/services/context" ) -var tmplWorkflows = templates.TplName("projects/workflows") +var ( + tmplRepoWorkflows = templates.TplName("repo/projects/workflows") + tmplOrgWorkflows = templates.TplName("org/projects/workflows") +) func Workflows(ctx *context.Context) { + ctx.Data["WorkflowEvents"] = project_model.GetWorkflowEvents() + projectID := ctx.PathParamInt64("id") p, err := project_model.GetProjectByID(ctx, projectID) if err != nil { @@ -24,7 +29,11 @@ func Workflows(ctx *context.Context) { } return } - if p.RepoID != ctx.Repo.Repository.ID { + if p.Type == project_model.TypeRepository && p.RepoID != ctx.Repo.Repository.ID { + ctx.NotFound(nil) + return + } + if (p.Type == project_model.TypeOrganization || p.Type == project_model.TypeIndividual) && p.OwnerID != ctx.ContextUser.ID { ctx.NotFound(nil) return } @@ -33,41 +42,44 @@ func Workflows(ctx *context.Context) { ctx.Data["PageIsWorkflows"] = true ctx.Data["PageIsProjects"] = true ctx.Data["PageIsProjectsWorkflows"] = true + ctx.Data["Project"] = p - workflows, err := project_model.GetWorkflows(ctx, projectID) + workflows, err := project_model.FindWorkflowEvents(ctx, projectID) if err != nil { ctx.ServerError("GetWorkflows", err) return } + for _, wf := range workflows { + wf.Project = p + } ctx.Data["Workflows"] = workflows workflowIDStr := ctx.PathParam("workflow_id") - var workflow *project_model.ProjectWorkflow + ctx.Data["workflowIDStr"] = workflowIDStr + var curWorkflow *project_model.ProjectWorkflow if workflowIDStr == "" { // get first value workflow or the first workflow for _, wf := range workflows { if wf.ID > 0 { - workflow = wf + curWorkflow = wf break } } - if workflow.ID == 0 { - workflow = workflows[0] - } } else { workflowID, _ := strconv.ParseInt(workflowIDStr, 10, 64) if workflowID > 0 { - var err error - workflow, err = project_model.GetWorkflowByID(ctx, workflowID) - if err != nil { - ctx.ServerError("GetWorkflowByID", err) - return + for _, wf := range workflows { + if wf.ID == workflowID { + curWorkflow = wf + break + } } - ctx.Data["CurWorkflow"] = workflow - } else { - workflow = project_model.GetWorkflowDefaultValue(workflowIDStr) } } - ctx.Data["CurWorkflow"] = workflow + ctx.Data["CurWorkflow"] = curWorkflow - ctx.HTML(200, tmplWorkflows) + if p.Type == project_model.TypeRepository { + ctx.HTML(200, tmplRepoWorkflows) + } else { + ctx.HTML(200, tmplOrgWorkflows) + } } diff --git a/routers/web/web.go b/routers/web/web.go index 83ce7f5713..7dd9c286b6 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1030,6 +1030,10 @@ func registerWebRoutes(m *web.Router) { m.Get("", org.Projects) m.Get("/{id}", org.ViewProject) }, reqUnitAccess(unit.TypeProjects, perm.AccessModeRead, true)) + m.Group("/{id}/workflows", func() { + m.Get("", projects.Workflows) + m.Get("/{workflow_id}", projects.Workflows) + }) m.Group("", func() { //nolint:dupl m.Get("/new", org.RenderNewProject) m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost) diff --git a/services/projects/workflow_notifier.go b/services/projects/workflow_notifier.go index 0b5b4d283e..3897a81914 100644 --- a/services/projects/workflow_notifier.go +++ b/services/projects/workflow_notifier.go @@ -5,16 +5,14 @@ package projects import ( "context" + "slices" "strings" issues_model "code.gitea.io/gitea/models/issues" project_model "code.gitea.io/gitea/models/project" - repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" - project_module "code.gitea.io/gitea/modules/projects" + "code.gitea.io/gitea/modules/util" notify_service "code.gitea.io/gitea/services/notify" ) @@ -33,122 +31,89 @@ func NewNotifier() notify_service.Notifier { return &workflowNotifier{} } -func findRepoProjectsWorkflows(ctx context.Context, repo *repo_model.Repository) ([]*project_module.Workflow, error) { - gitRepo, err := gitrepo.OpenRepository(ctx, repo) - if err != nil { - log.Error("IssueChangeStatus: OpenRepository: %v", err) - return nil, err - } - defer gitRepo.Close() - - // Get the commit object for the ref - commit, err := gitRepo.GetCommit(repo.DefaultBranch) - if err != nil { - log.Error("gitRepo.GetCommit: %w", err) - return nil, err - } - - tree, err := commit.SubTree(".gitea/projects") - if _, ok := err.(git.ErrNotExist); ok { - return nil, nil - } - if err != nil { - log.Error("commit.SubTree: %w", err) - return nil, err - } - - entries, err := tree.ListEntriesRecursiveFast() - if err != nil { - log.Error("tree.ListEntriesRecursiveFast: %w", err) - return nil, err - } - - ret := make(git.Entries, 0, len(entries)) - for _, entry := range entries { - if strings.HasSuffix(entry.Name(), ".yml") || strings.HasSuffix(entry.Name(), ".yaml") { - ret = append(ret, entry) - } - } - if len(ret) == 0 { - return nil, nil - } - - wfs := make([]*project_module.Workflow, 0, len(ret)) - for _, entry := range ret { - workflowContent, err := commit.GetFileContent(".gitea/projects/"+entry.Name(), 1024*1024) - if err != nil { - log.Error("gitRepo.GetCommit: %w", err) - return nil, err - } - - wf, err := project_module.ParseWorkflow(workflowContent) - if err != nil { - log.Error("IssueChangeStatus: OpenRepository: %v", err) - return nil, err - } - projectName := strings.TrimSuffix(strings.TrimSuffix(entry.Name(), ".yml"), ".yaml") - project, err := project_model.GetProjectByName(ctx, repo.ID, projectName) - if err != nil { - log.Error("IssueChangeStatus: GetProjectByName: %v", err) - return nil, err - } - wf.ProjectID = project.ID - - wfs = append(wfs, wf) - } - return wfs, nil -} - func (m *workflowNotifier) NewIssue(ctx context.Context, issue *issues_model.Issue, mentions []*user_model.User) { if err := issue.LoadRepo(ctx); err != nil { log.Error("NewIssue: LoadRepo: %v", err) return } - wfs, err := findRepoProjectsWorkflows(ctx, issue.Repo) - if err != nil { - log.Error("NewIssue: findRepoProjectsWorkflows: %v", err) + if err := issue.LoadProject(ctx); err != nil { + log.Error("NewIssue: LoadProject: %v", err) + return + } + if issue.Project == nil { return } - for _, wf := range wfs { - if err := wf.FireAction(project_module.EventItemClosed, func(action project_module.Action) error { - board, err := project_model.GetColumnByProjectIDAndColumnName(ctx, wf.ProjectID, action.SetValue) - if err != nil { - log.Error("NewIssue: GetBoardByProjectIDAndBoardName: %v", err) - return err - } - return project_model.AddIssueToColumn(ctx, issue.ID, board) - }); err != nil { - log.Error("NewIssue: FireAction: %v", err) - return - } + eventWorkflows, err := project_model.FindWorkflowEvents(ctx, issue.Project.ID) + if err != nil { + log.Error("NewIssue: FindWorkflowEvents: %v", err) + return } + + workflow := eventWorkflows[project_model.WorkflowEventItemAddedToProject] + if workflow == nil { + return + } + + fireIssueWorkflow(ctx, workflow, issue) } func (m *workflowNotifier) IssueChangeStatus(ctx context.Context, doer *user_model.User, commitID string, issue *issues_model.Issue, actionComment *issues_model.Comment, isClosed bool) { - if isClosed { - if err := issue.LoadRepo(ctx); err != nil { - log.Error("IssueChangeStatus: LoadRepo: %v", err) - return - } - wfs, err := findRepoProjectsWorkflows(ctx, issue.Repo) - if err != nil { - log.Error("IssueChangeStatus: findRepoProjectsWorkflows: %v", err) - return - } + if err := issue.LoadRepo(ctx); err != nil { + log.Error("IssueChangeStatus: LoadRepo: %v", err) + return + } + if err := issue.LoadProject(ctx); err != nil { + log.Error("NewIssue: LoadProject: %v", err) + return + } + if issue.Project == nil { + return + } - for _, wf := range wfs { - if err := wf.FireAction(project_module.EventItemClosed, func(action project_module.Action) error { - board, err := project_model.GetColumnByProjectIDAndColumnName(ctx, wf.ProjectID, action.SetValue) - if err != nil { - log.Error("IssueChangeStatus: GetBoardByProjectIDAndBoardName: %v", err) - return err - } - return project_model.MoveIssueToAnotherColumn(ctx, issue.ID, board) - }); err != nil { - log.Error("IssueChangeStatus: FireAction: %v", err) + eventWorkflows, err := project_model.FindWorkflowEvents(ctx, issue.Project.ID) + if err != nil { + log.Error("NewIssue: FindWorkflowEvents: %v", err) + return + } + + workflowEvent := util.Iif(isClosed, project_model.WorkflowEventItemClosed, project_model.WorkflowEventItemReopened) + workflow := eventWorkflows[workflowEvent] + if workflow == nil { + return + } + + fireIssueWorkflow(ctx, workflow, issue) +} + +func fireIssueWorkflow(ctx context.Context, workflow *project_model.ProjectWorkflow, issue *issues_model.Issue) { + for _, filter := range workflow.WorkflowFilters { + switch filter.Type { + case project_model.WorkflowFilterTypeScope: + values := strings.Split(filter.Value, ",") + if !(slices.Contains(values, "issue") && !issue.IsPull) || (slices.Contains(values, "pull") && issue.IsPull) { return } + default: + log.Error("NewIssue: Unsupported filter type: %s", filter.Type) + return + } + } + + for _, action := range workflow.WorkflowActions { + switch action.ActionType { + case project_model.WorkflowActionTypeColumn: + column, err := project_model.GetColumnByProjectIDAndColumnName(ctx, issue.Project.ID, action.ActionValue) + if err != nil { + log.Error("NewIssue: GetColumnByProjectIDAndColumnName: %v", err) + continue + } + if err := project_model.AddIssueToColumn(ctx, issue.ID, column); err != nil { + log.Error("NewIssue: AddIssueToColumn: %v", err) + continue + } + default: + log.Error("NewIssue: Unsupported action type: %s", action.ActionType) } } } diff --git a/templates/org/projects/workflows.tmpl b/templates/org/projects/workflows.tmpl new file mode 100644 index 0000000000..ca5124e2c8 --- /dev/null +++ b/templates/org/projects/workflows.tmpl @@ -0,0 +1,13 @@ +{{template "base/head" .}} +
+ {{if .ContextUser.IsOrganization}} + {{template "org/header" .}} + {{else}} + {{template "shared/user/org_profile_avatar" .}} +
+ {{template "user/overview/header" .}} +
+ {{end}} + {{template "projects/workflows" .}} +
+{{template "base/footer" .}} diff --git a/templates/projects/workflows.tmpl b/templates/projects/workflows.tmpl index b7799316ff..e6d1ce56b8 100644 --- a/templates/projects/workflows.tmpl +++ b/templates/projects/workflows.tmpl @@ -1,11 +1,12 @@ -
-
+
+
+ {{range .WorkflowEvents}} + {{end}}
diff --git a/templates/repo/projects/workflows.tmpl b/templates/repo/projects/workflows.tmpl new file mode 100644 index 0000000000..b3af7fe46c --- /dev/null +++ b/templates/repo/projects/workflows.tmpl @@ -0,0 +1,14 @@ +{{template "base/head" .}} +
+ {{template "repo/header" .}} + + {{template "projects/workflows" .}} +
+ +{{template "base/footer" .}}