gitea/routers/web/projects/workflows.go

550 lines
15 KiB
Go

// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package projects
import (
"net/http"
"strconv"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
project_model "code.gitea.io/gitea/models/project"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
)
var (
tmplRepoWorkflows = templates.TplName("repo/projects/workflows")
tmplOrgWorkflows = templates.TplName("org/projects/workflows")
)
// getFilterSummary returns a human-readable summary of the filters
func getFilterSummary(filters []project_model.WorkflowFilter) string {
if len(filters) == 0 {
return ""
}
for _, filter := range filters {
if filter.Type == "scope" {
switch filter.Value {
case "issue":
return " (Issues only)"
case "pull_request":
return " (Pull requests only)"
}
}
}
return ""
}
// convertFormToFilters converts form filters to WorkflowFilter objects
func convertFormToFilters(formFilters map[string]string) []project_model.WorkflowFilter {
filters := make([]project_model.WorkflowFilter, 0)
for key, value := range formFilters {
if value != "" {
filters = append(filters, project_model.WorkflowFilter{
Type: project_model.WorkflowFilterType(key),
Value: value,
})
}
}
return filters
}
// convertFormToActions converts form actions to WorkflowAction objects
func convertFormToActions(formActions map[string]any) []project_model.WorkflowAction {
actions := make([]project_model.WorkflowAction, 0)
for key, value := range formActions {
switch key {
case "column":
if strValue, ok := value.(string); ok && strValue != "" {
actions = append(actions, project_model.WorkflowAction{
ActionType: project_model.WorkflowActionTypeColumn,
ActionValue: strValue,
})
}
case "add_labels":
if labels, ok := value.([]string); ok && len(labels) > 0 {
for _, label := range labels {
if label != "" {
actions = append(actions, project_model.WorkflowAction{
ActionType: project_model.WorkflowActionTypeAddLabels,
ActionValue: label,
})
}
}
}
case "remove_labels":
if labels, ok := value.([]string); ok && len(labels) > 0 {
for _, label := range labels {
if label != "" {
actions = append(actions, project_model.WorkflowAction{
ActionType: project_model.WorkflowActionTypeRemoveLabels,
ActionValue: label,
})
}
}
}
case "closeIssue":
if boolValue, ok := value.(bool); ok && boolValue {
actions = append(actions, project_model.WorkflowAction{
ActionType: project_model.WorkflowActionTypeClose,
ActionValue: "true",
})
}
}
}
return actions
}
func WorkflowsEvents(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.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
}
workflows, err := project_model.FindWorkflowsByProjectID(ctx, projectID)
if err != nil {
ctx.ServerError("FindWorkflowsByProjectID", err)
return
}
type WorkflowConfig struct {
ID int64 `json:"id"`
EventID string `json:"event_id"`
DisplayName string `json:"display_name"`
Capabilities project_model.WorkflowEventCapabilities `json:"capabilities"`
Filters []project_model.WorkflowFilter `json:"filters"`
Actions []project_model.WorkflowAction `json:"actions"`
FilterSummary string `json:"filter_summary"` // Human readable filter description
Enabled bool `json:"enabled"`
}
outputWorkflows := make([]*WorkflowConfig, 0)
events := project_model.GetWorkflowEvents()
capabilities := project_model.GetWorkflowEventCapabilities()
// Create a map for quick lookup of existing workflows
workflowMap := make(map[project_model.WorkflowEvent][]*project_model.Workflow)
for _, wf := range workflows {
workflowMap[wf.WorkflowEvent] = append(workflowMap[wf.WorkflowEvent], wf)
}
for _, event := range events {
existingWorkflows := workflowMap[event]
if len(existingWorkflows) > 0 {
// Add all existing workflows for this event
for _, wf := range existingWorkflows {
filterSummary := getFilterSummary(wf.WorkflowFilters)
outputWorkflows = append(outputWorkflows, &WorkflowConfig{
ID: wf.ID,
EventID: strconv.FormatInt(wf.ID, 10),
DisplayName: string(ctx.Tr(wf.WorkflowEvent.LangKey())) + filterSummary,
Capabilities: capabilities[event],
Filters: wf.WorkflowFilters,
Actions: wf.WorkflowActions,
FilterSummary: filterSummary,
Enabled: wf.Enabled,
})
}
} else {
// Add placeholder for creating new workflow
outputWorkflows = append(outputWorkflows, &WorkflowConfig{
ID: 0,
EventID: event.UUID(),
DisplayName: string(ctx.Tr(event.LangKey())),
Capabilities: capabilities[event],
Filters: []project_model.WorkflowFilter{},
Actions: []project_model.WorkflowAction{},
FilterSummary: "",
Enabled: true, // Default to enabled for new workflows
})
}
}
ctx.JSON(http.StatusOK, outputWorkflows)
}
func WorkflowsColumns(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.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
}
columns, err := p.GetColumns(ctx)
if err != nil {
ctx.ServerError("GetProjectColumns", err)
return
}
type Column struct {
ID int64 `json:"id"`
Title string `json:"title"`
}
outputColumns := make([]*Column, 0, len(columns))
for _, col := range columns {
outputColumns = append(outputColumns, &Column{
ID: col.ID,
Title: col.Title,
})
}
ctx.JSON(http.StatusOK, outputColumns)
}
func WorkflowsLabels(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
}
// Only repository projects have access to labels
if p.Type != project_model.TypeRepository {
ctx.JSON(http.StatusOK, []any{})
return
}
if p.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound(nil)
return
}
// Get repository labels
labels, err := issues_model.GetLabelsByRepoID(ctx, p.RepoID, "", db.ListOptions{})
if err != nil {
ctx.ServerError("GetLabelsByRepoID", err)
return
}
type Label struct {
ID int64 `json:"id"`
Name string `json:"name"`
Color string `json:"color"`
}
outputLabels := make([]*Label, 0, len(labels))
for _, label := range labels {
outputLabels = append(outputLabels, &Label{
ID: label.ID,
Name: label.Name,
Color: label.Color,
})
}
ctx.JSON(http.StatusOK, outputLabels)
}
func Workflows(ctx *context.Context) {
workflowIDStr := ctx.PathParam("workflow_id")
if workflowIDStr == "events" {
WorkflowsEvents(ctx)
return
}
if workflowIDStr == "columns" {
WorkflowsColumns(ctx)
return
}
if workflowIDStr == "labels" {
WorkflowsLabels(ctx)
return
}
ctx.Data["WorkflowEvents"] = project_model.GetWorkflowEvents()
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.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
}
ctx.Data["Title"] = ctx.Tr("projects.workflows")
ctx.Data["PageIsWorkflows"] = true
ctx.Data["PageIsProjects"] = true
ctx.Data["PageIsProjectsWorkflows"] = true
ctx.Data["Project"] = p
workflows, err := project_model.FindWorkflowsByProjectID(ctx, projectID)
if err != nil {
ctx.ServerError("FindWorkflowsByProjectID", err)
return
}
for _, wf := range workflows {
wf.Project = p
}
ctx.Data["Workflows"] = workflows
ctx.Data["workflowIDStr"] = workflowIDStr
var curWorkflow *project_model.Workflow
if workflowIDStr == "" { // get first value workflow or the first workflow
for _, wf := range workflows {
if wf.ID > 0 {
curWorkflow = wf
break
}
}
} else {
workflowID, _ := strconv.ParseInt(workflowIDStr, 10, 64)
if workflowID > 0 {
for _, wf := range workflows {
if wf.ID == workflowID {
curWorkflow = wf
break
}
}
}
}
ctx.Data["CurWorkflow"] = curWorkflow
ctx.Data["ProjectLink"] = project_model.ProjectLinkForRepo(ctx.Repo.Repository, projectID)
if p.Type == project_model.TypeRepository {
ctx.HTML(200, tmplRepoWorkflows)
} else {
ctx.HTML(200, tmplOrgWorkflows)
}
}
type WorkflowsPostForm struct {
EventID string `form:"event_id" binding:"Required"`
Filters map[string]string `form:"filters"`
Actions map[string]any `form:"actions"`
}
func WorkflowsPost(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.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
}
form := web.GetForm(ctx).(*WorkflowsPostForm)
// Convert form data to filters and actions
filters := convertFormToFilters(form.Filters)
actions := convertFormToActions(form.Actions)
eventID, _ := strconv.ParseInt(form.EventID, 10, 64)
if eventID == 0 {
// Create a new workflow for the given event
wf := &project_model.Workflow{
ProjectID: projectID,
WorkflowEvent: project_model.WorkflowEvent(form.EventID),
WorkflowFilters: filters,
WorkflowActions: actions,
Enabled: true, // New workflows are enabled by default
}
if err := project_model.CreateWorkflow(ctx, wf); err != nil {
ctx.ServerError("CreateWorkflow", err)
return
}
// Return the newly created workflow with filter summary
filterSummary := getFilterSummary(wf.WorkflowFilters)
ctx.JSON(http.StatusOK, map[string]any{
"success": true,
"workflow": map[string]any{
"id": wf.ID,
"event_id": strconv.FormatInt(wf.ID, 10),
"display_name": string(ctx.Tr(wf.WorkflowEvent.LangKey())) + filterSummary,
"filters": wf.WorkflowFilters,
"actions": wf.WorkflowActions,
"filter_summary": filterSummary,
"enabled": wf.Enabled,
},
})
} else {
// Update an existing workflow
wf, err := project_model.GetWorkflowByID(ctx, eventID)
if err != nil {
ctx.ServerError("GetWorkflowByID", err)
return
}
if wf.ProjectID != projectID {
ctx.NotFound(nil)
return
}
wf.WorkflowFilters = filters
wf.WorkflowActions = actions
if err := project_model.UpdateWorkflow(ctx, wf); err != nil {
ctx.ServerError("UpdateWorkflow", err)
return
}
// Return the updated workflow with filter summary
filterSummary := getFilterSummary(wf.WorkflowFilters)
ctx.JSON(http.StatusOK, map[string]any{
"success": true,
"workflow": map[string]any{
"id": wf.ID,
"event_id": strconv.FormatInt(wf.ID, 10),
"display_name": string(ctx.Tr(wf.WorkflowEvent.LangKey())) + filterSummary,
"filters": wf.WorkflowFilters,
"actions": wf.WorkflowActions,
"filter_summary": filterSummary,
"enabled": wf.Enabled,
},
})
}
}
func WorkflowsStatus(ctx *context.Context) {
projectID := ctx.PathParamInt64("id")
workflowID, _ := strconv.ParseInt(ctx.PathParam("workflow_id"), 10, 64)
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.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
}
wf, err := project_model.GetWorkflowByID(ctx, workflowID)
if err != nil {
ctx.ServerError("GetWorkflowByID", err)
return
}
if wf.ProjectID != projectID {
ctx.NotFound(nil)
return
}
// Get enabled status from form
enabledStr := ctx.Req.FormValue("enabled")
enabled := enabledStr == "true"
if enabled {
if err := project_model.EnableWorkflow(ctx, workflowID); err != nil {
ctx.ServerError("EnableWorkflow", err)
return
}
} else {
if err := project_model.DisableWorkflow(ctx, workflowID); err != nil {
ctx.ServerError("DisableWorkflow", err)
return
}
}
ctx.JSON(http.StatusOK, map[string]any{
"success": true,
"enabled": wf.Enabled,
})
}
func WorkflowsDelete(ctx *context.Context) {
projectID := ctx.PathParamInt64("id")
workflowID, _ := strconv.ParseInt(ctx.PathParam("workflow_id"), 10, 64)
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.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
}
wf, err := project_model.GetWorkflowByID(ctx, workflowID)
if err != nil {
ctx.ServerError("GetWorkflowByID", err)
return
}
if wf.ProjectID != projectID {
ctx.NotFound(nil)
return
}
if err := project_model.DeleteWorkflow(ctx, workflowID); err != nil {
ctx.ServerError("DeleteWorkflow", err)
return
}
ctx.JSON(http.StatusOK, map[string]any{
"success": true,
})
}