0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-04-26 13:12:59 +02:00
gitea/routers/web/projects/workflows.go
2025-12-29 22:31:39 -08:00

597 lines
17 KiB
Go

// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package projects
import (
stdCtx "context"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"code.gitea.io/gitea/models/db"
project_model "code.gitea.io/gitea/models/project"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/services/context"
project_service "code.gitea.io/gitea/services/projects"
)
var (
tmplRepoWorkflows = templates.TplName("repo/projects/workflows")
tmplOrgWorkflows = templates.TplName("org/projects/workflows")
)
// convertFormToFilters converts form filters to WorkflowFilter objects
func convertFormToFilters(ctx stdCtx.Context, project *project_model.Project, formFilters map[string]any) []project_model.WorkflowFilter {
filters := make([]project_model.WorkflowFilter, 0)
for key, value := range formFilters {
switch key {
case string(project_model.WorkflowFilterTypeLabels):
// Handle labels array
if labelInterfaces, ok := value.([]any); ok && len(labelInterfaces) > 0 {
for _, labelInterface := range labelInterfaces {
if label, ok := labelInterface.(string); ok && label != "" {
labelID, _ := strconv.ParseInt(label, 10, 64)
if project_service.CanProjectAddLabel(ctx, project, labelID) {
filters = append(filters, project_model.WorkflowFilter{
Type: project_model.WorkflowFilterTypeLabels,
Value: label,
})
}
}
}
}
case string(project_model.WorkflowFilterTypeSourceColumn), string(project_model.WorkflowFilterTypeTargetColumn):
if strValue, ok := value.(string); ok && strValue != "" {
strValueInt, _ := strconv.ParseInt(strValue, 10, 64)
if strValueInt > 0 {
col, _ := project_model.GetColumnByProjectIDAndColumnID(ctx, project.ID, strValueInt)
if col == nil {
continue
}
filters = append(filters, project_model.WorkflowFilter{
Type: project_model.WorkflowFilterType(key),
Value: strconv.FormatInt(strValueInt, 10),
})
}
}
default:
// Handle string values (issue_type, column)
if strValue, ok := value.(string); ok && strValue != "" {
filters = append(filters, project_model.WorkflowFilter{
Type: project_model.WorkflowFilterType(key),
Value: strValue,
})
}
}
}
return filters
}
// convertFormToActions converts form actions to WorkflowAction objects
func convertFormToActions(ctx stdCtx.Context, project *project_model.Project, formActions map[string]any) []project_model.WorkflowAction {
actions := make([]project_model.WorkflowAction, 0)
for key, value := range formActions {
switch key {
case string(project_model.WorkflowActionTypeColumn):
if colValue, ok := value.(string); ok {
colValueInt, _ := strconv.ParseInt(colValue, 10, 64)
if colValueInt > 0 {
col, _ := project_model.GetColumnByProjectIDAndColumnID(ctx, project.ID, colValueInt)
if col == nil {
continue
}
actions = append(actions, project_model.WorkflowAction{
Type: project_model.WorkflowActionTypeColumn,
Value: strconv.FormatInt(colValueInt, 10),
})
}
}
case string(project_model.WorkflowActionTypeAddLabels):
// Handle both []string and []any from JSON unmarshaling
if labels, ok := value.([]string); ok && len(labels) > 0 {
for _, label := range labels {
if label != "" {
labelID, _ := strconv.ParseInt(label, 10, 64)
if project_service.CanProjectAddLabel(ctx, project, labelID) {
actions = append(actions, project_model.WorkflowAction{
Type: project_model.WorkflowActionTypeAddLabels,
Value: label,
})
}
}
}
} else if labelInterfaces, ok := value.([]any); ok && len(labelInterfaces) > 0 {
for _, labelInterface := range labelInterfaces {
if label, ok := labelInterface.(string); ok && label != "" {
labelID, _ := strconv.ParseInt(label, 10, 64)
if !project_service.CanProjectAddLabel(ctx, project, labelID) {
continue
}
actions = append(actions, project_model.WorkflowAction{
Type: project_model.WorkflowActionTypeAddLabels,
Value: label,
})
}
}
}
case string(project_model.WorkflowActionTypeRemoveLabels):
// Handle both []string and []any from JSON unmarshaling
if labels, ok := value.([]string); ok && len(labels) > 0 {
for _, label := range labels {
if label != "" {
labelID, _ := strconv.ParseInt(label, 10, 64)
if !project_service.CanProjectAddLabel(ctx, project, labelID) {
continue
}
actions = append(actions, project_model.WorkflowAction{
Type: project_model.WorkflowActionTypeRemoveLabels,
Value: label,
})
}
}
} else if labelInterfaces, ok := value.([]any); ok && len(labelInterfaces) > 0 {
for _, labelInterface := range labelInterfaces {
if label, ok := labelInterface.(string); ok && label != "" {
labelID, _ := strconv.ParseInt(label, 10, 64)
if !project_service.CanProjectAddLabel(ctx, project, labelID) {
continue
}
actions = append(actions, project_model.WorkflowAction{
Type: project_model.WorkflowActionTypeRemoveLabels,
Value: label,
})
}
}
}
case string(project_model.WorkflowActionTypeIssueState):
if strValue, ok := value.(string); ok {
v := strings.ToLower(strValue)
if v == "close" || v == "reopen" {
actions = append(actions, project_model.WorkflowAction{
Type: project_model.WorkflowActionTypeIssueState,
Value: v,
})
}
}
}
}
return actions
}
type WorkflowConfig struct {
ID int64 `json:"id"`
EventID string `json:"event_id"`
DisplayName string `json:"display_name"`
WorkflowEvent string `json:"workflow_event"` // The workflow event
Capabilities project_model.WorkflowEventCapabilities `json:"capabilities"`
Filters []project_model.WorkflowFilter `json:"filters"`
Actions []project_model.WorkflowAction `json:"actions"`
Summary string `json:"summary"` // Human readable filter description
Enabled bool `json:"enabled"`
IsConfigured bool `json:"is_configured"` // Whether this workflow is configured/saved
}
func WorkflowsEvents(ctx *context.Context, project *project_model.Project) {
workflows, err := project_model.FindWorkflowsByProjectID(ctx, project.ID)
if err != nil {
ctx.ServerError("FindWorkflowsByProjectID", err)
return
}
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 {
workflowSummary := project_service.GetWorkflowSummary(ctx, wf)
outputWorkflows = append(outputWorkflows, &WorkflowConfig{
ID: wf.ID,
EventID: strconv.FormatInt(wf.ID, 10),
DisplayName: string(ctx.Tr(wf.WorkflowEvent.LangKey())),
WorkflowEvent: string(wf.WorkflowEvent),
Capabilities: capabilities[event],
Filters: wf.WorkflowFilters,
Actions: wf.WorkflowActions,
Summary: workflowSummary,
Enabled: wf.Enabled,
IsConfigured: true,
})
}
} else {
// Add placeholder for creating new workflow
outputWorkflows = append(outputWorkflows, &WorkflowConfig{
ID: 0,
EventID: event.EventID(),
DisplayName: string(ctx.Tr(event.LangKey())),
WorkflowEvent: string(event),
Capabilities: capabilities[event],
Summary: "",
Enabled: true, // Default to enabled for new workflows
IsConfigured: false,
})
}
}
ctx.JSON(http.StatusOK, outputWorkflows)
}
func WorkflowsColumns(ctx *context.Context, project *project_model.Project) {
columns, err := project.GetColumns(ctx)
if err != nil {
ctx.ServerError("GetProjectColumns", err)
return
}
type Column struct {
ID int64 `json:"id"`
Title string `json:"title"`
Color string `json:"color"`
}
outputColumns := make([]*Column, 0, len(columns))
for _, col := range columns {
outputColumns = append(outputColumns, &Column{
ID: col.ID,
Title: col.Title,
Color: col.Color,
})
}
ctx.JSON(http.StatusOK, outputColumns)
}
func WorkflowsLabels(ctx *context.Context, project *project_model.Project) {
labels, err := project_service.GetProjectLabels(ctx, project)
if err != nil {
ctx.ServerError("GetProjectLabels", err)
return
}
type Label struct {
ID int64 `json:"id"`
Name string `json:"name"`
Color string `json:"color"`
Description string `json:"description"`
Exclusive bool `json:"exclusive"`
ExclusiveOrder int `json:"exclusiveOrder"`
}
outputLabels := make([]*Label, 0, len(labels))
for _, label := range labels {
outputLabels = append(outputLabels, &Label{
ID: label.ID,
Name: label.Name,
Color: label.Color,
Description: label.Description,
Exclusive: label.Exclusive,
ExclusiveOrder: label.ExclusiveOrder,
})
}
ctx.JSON(http.StatusOK, outputLabels)
}
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.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
}
workflowIDStr := ctx.PathParam("workflow_id")
if workflowIDStr == "events" {
WorkflowsEvents(ctx, p)
return
}
if workflowIDStr == "columns" {
WorkflowsColumns(ctx, p)
return
}
if workflowIDStr == "labels" {
WorkflowsLabels(ctx, p)
return
}
ctx.Data["WorkflowEvents"] = project_model.GetWorkflowEvents()
ctx.Data["Title"] = ctx.Tr("projects.workflows")
ctx.Data["IsProjectsPage"] = 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 `json:"event_id"`
Filters map[string]any `json:"filters"`
Actions map[string]any `json:"actions"`
}
// WorkflowsPost handles creating or updating a workflow
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
}
// Handle both form data and JSON data
// Handle JSON data
form := &WorkflowsPostForm{}
content, err := io.ReadAll(ctx.Req.Body)
if err != nil {
ctx.ServerError("ReadRequestBody", err)
return
}
defer ctx.Req.Body.Close()
if err := json.Unmarshal(content, &form); err != nil {
ctx.ServerError("DecodeWorkflowsPostForm", err)
return
}
if form.EventID == "" {
ctx.JSONError("EventID is required")
return
}
// Convert form data to filters and actions
filters := convertFormToFilters(ctx, p, form.Filters)
actions := convertFormToActions(ctx, p, form.Actions)
// Validate: at least one action must be configured
if len(actions) == 0 {
ctx.JSONError(ctx.Tr("projects.workflows.error.at_least_one_action"))
return
}
eventID, _ := strconv.ParseInt(form.EventID, 10, 64)
if eventID == 0 {
// if it's not a real database id, check if it's the event string
if !project_model.IsValidWorkflowEvent(form.EventID) {
ctx.JSONError(fmt.Sprintf("EventID %s is invalid", form.EventID))
return
}
// 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
workflowSummary := project_service.GetWorkflowSummary(ctx, wf)
ctx.JSON(http.StatusOK, map[string]any{
"success": true,
"workflow": WorkflowConfig{
ID: wf.ID,
EventID: strconv.FormatInt(wf.ID, 10),
DisplayName: string(ctx.Tr(wf.WorkflowEvent.LangKey())),
Filters: wf.WorkflowFilters,
Actions: wf.WorkflowActions,
Summary: workflowSummary,
Enabled: wf.Enabled,
},
})
return
}
// 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
workflowSummary := project_service.GetWorkflowSummary(ctx, wf)
ctx.JSON(http.StatusOK, map[string]any{
"success": true,
"workflow": WorkflowConfig{
ID: wf.ID,
EventID: strconv.FormatInt(wf.ID, 10),
DisplayName: string(ctx.Tr(wf.WorkflowEvent.LangKey())),
Filters: wf.WorkflowFilters,
Actions: wf.WorkflowActions,
Summary: workflowSummary,
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
_ = ctx.Req.ParseForm()
enabledStr := ctx.Req.FormValue("enabled")
enabled, _ := strconv.ParseBool(enabledStr)
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 {
if db.IsErrNotExist(err) {
ctx.NotFound(nil)
} else {
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,
})
}