// 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, }) }