From c67e7373fb04816aed050e3e4ff04c9842e85e43 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 11 Jun 2026 21:56:14 -0500 Subject: [PATCH] fix: move required custom field validation before issue creation Validation now runs before NewIssue() to prevent orphaned issues when a required field is missing. Reuses the defs query for both validation and saving. --- routers/api/v1/repo/issue.go | 55 ++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 14c3b89da6..105000b33b 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -722,6 +722,22 @@ func CreateIssue(ctx *context.APIContext) { form.Labels = make([]int64, 0) } + // Validate required custom fields BEFORE creating the issue to avoid + // leaving orphaned issues when validation fails. + customFieldDefs, defErr := issues_model.GetCustomFieldsByOwner(ctx, ctx.Repo.Repository.OwnerID, issues_model.CustomFieldScopeIssue) + if defErr != nil { + ctx.APIErrorInternal(defErr) + return + } + for _, def := range customFieldDefs { + if def.Required { + if v, ok := form.CustomFields[def.Name]; !ok || strings.TrimSpace(v) == "" { + ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("custom field %q is required", def.Name)) + return + } + } + } + if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, form.Projects); err != nil { if errors.Is(err, user_model.ErrBlockedUser) { ctx.APIError(http.StatusForbidden, err) @@ -733,35 +749,18 @@ func CreateIssue(ctx *context.APIContext) { return } - // Save custom field values if provided (resolve field names to IDs). - // Validate required fields are present. - { - defs, defErr := issues_model.GetCustomFieldsByOwner(ctx, ctx.Repo.Repository.OwnerID, issues_model.CustomFieldScopeIssue) - if defErr != nil { - ctx.APIErrorInternal(defErr) - return + // Save custom field values (reuse defs from validation above). + if len(customFieldDefs) > 0 && len(form.CustomFields) > 0 { + vals := make(map[int64]string) + for _, def := range customFieldDefs { + if v, ok := form.CustomFields[def.Name]; ok { + vals[def.ID] = v + } } - if len(defs) > 0 { - // Check required fields. - for _, def := range defs { - if def.Required { - if v, ok := form.CustomFields[def.Name]; !ok || strings.TrimSpace(v) == "" { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("custom field %q is required", def.Name)) - return - } - } - } - vals := make(map[int64]string) - for _, def := range defs { - if v, ok := form.CustomFields[def.Name]; ok { - vals[def.ID] = v - } - } - if len(vals) > 0 { - if setErr := issues_model.SetCustomFieldValues(ctx, issue.ID, vals); setErr != nil { - ctx.APIErrorInternal(setErr) - return - } + if len(vals) > 0 { + if setErr := issues_model.SetCustomFieldValues(ctx, issue.ID, vals); setErr != nil { + ctx.APIErrorInternal(setErr) + return } } }