mirror of https://github.com/go-gitea/gitea.git
625 lines
18 KiB
Go
625 lines
18 KiB
Go
// Copyright 2025 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package projects
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"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/json"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"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(formFilters map[string]any) []project_model.WorkflowFilter {
|
|
filters := make([]project_model.WorkflowFilter, 0)
|
|
|
|
for key, value := range formFilters {
|
|
switch key {
|
|
case "labels":
|
|
// Handle labels array
|
|
if labelInterfaces, ok := value.([]any); ok && len(labelInterfaces) > 0 {
|
|
for _, labelInterface := range labelInterfaces {
|
|
if label, ok := labelInterface.(string); ok && label != "" {
|
|
filters = append(filters, project_model.WorkflowFilter{
|
|
Type: project_model.WorkflowFilterTypeLabels,
|
|
Value: label,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
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(formActions map[string]any) []project_model.WorkflowAction {
|
|
actions := make([]project_model.WorkflowAction, 0)
|
|
|
|
for key, value := range formActions {
|
|
switch key {
|
|
case "column":
|
|
if floatValue, ok := value.(string); ok {
|
|
floatValueInt, _ := strconv.ParseInt(floatValue, 10, 64)
|
|
if floatValueInt > 0 {
|
|
actions = append(actions, project_model.WorkflowAction{
|
|
Type: project_model.WorkflowActionTypeColumn,
|
|
Value: strconv.FormatInt(floatValueInt, 10),
|
|
})
|
|
}
|
|
}
|
|
case "add_labels":
|
|
// Handle both []string and []any from JSON unmarshaling
|
|
if labels, ok := value.([]string); ok && len(labels) > 0 {
|
|
for _, label := range labels {
|
|
if label != "" {
|
|
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 != "" {
|
|
actions = append(actions, project_model.WorkflowAction{
|
|
Type: project_model.WorkflowActionTypeAddLabels,
|
|
Value: label,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
case "remove_labels":
|
|
// Handle both []string and []any from JSON unmarshaling
|
|
if labels, ok := value.([]string); ok && len(labels) > 0 {
|
|
for _, label := range labels {
|
|
if label != "" {
|
|
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 != "" {
|
|
actions = append(actions, project_model.WorkflowAction{
|
|
Type: project_model.WorkflowActionTypeRemoveLabels,
|
|
Value: label,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
case "issue_state":
|
|
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
|
|
}
|
|
|
|
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"`
|
|
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:"isConfigured"` // Whether this workflow is configured/saved
|
|
}
|
|
|
|
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) {
|
|
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"`
|
|
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) {
|
|
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"`
|
|
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) {
|
|
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["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()
|
|
log.Trace("get " + string(content))
|
|
if err := json.Unmarshal(content, &form); err != nil {
|
|
ctx.ServerError("DecodeWorkflowsPostForm", err)
|
|
return
|
|
}
|
|
if form.EventID == "" {
|
|
ctx.JSON(http.StatusBadRequest, map[string]any{"error": "InvalidEventID", "message": "EventID is required"})
|
|
return
|
|
}
|
|
|
|
// Convert form data to filters and actions
|
|
filters := convertFormToFilters(form.Filters)
|
|
actions := convertFormToActions(form.Actions)
|
|
|
|
// Validate: at least one action must be configured
|
|
if len(actions) == 0 {
|
|
ctx.JSON(http.StatusBadRequest, map[string]any{
|
|
"error": "NoActions",
|
|
"message": ctx.Tr("projects.workflows.error.at_least_one_action"),
|
|
})
|
|
return
|
|
}
|
|
|
|
eventID, _ := strconv.ParseInt(form.EventID, 10, 64)
|
|
if eventID == 0 {
|
|
// check if workflow event is valid
|
|
if !project_model.IsValidWorkflowEvent(form.EventID) {
|
|
ctx.JSON(http.StatusBadRequest, map[string]any{"error": "EventID is invalid"})
|
|
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": map[string]any{
|
|
"id": wf.ID,
|
|
"event_id": strconv.FormatInt(wf.ID, 10),
|
|
"display_name": string(ctx.Tr(wf.WorkflowEvent.LangKey())),
|
|
"filters": wf.WorkflowFilters,
|
|
"actions": wf.WorkflowActions,
|
|
"summary": workflowSummary,
|
|
"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
|
|
workflowSummary := project_service.GetWorkflowSummary(ctx, wf)
|
|
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())),
|
|
"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,
|
|
})
|
|
}
|