From f4e9cb444c0dc8b5204cbb4609690937a6c594e9 Mon Sep 17 00:00:00 2001 From: bujjibabukatta Date: Sat, 13 Jun 2026 12:30:05 +0530 Subject: [PATCH] feat(webhook): allow closing incidents via request body instead of URL path --- backend/plugins/webhook/api/issues.go | 65 +++++++++++++++++++++++++++ backend/plugins/webhook/impl/impl.go | 9 ++++ 2 files changed, 74 insertions(+) diff --git a/backend/plugins/webhook/api/issues.go b/backend/plugins/webhook/api/issues.go index 49ea72b51f1..42ba29c2d32 100644 --- a/backend/plugins/webhook/api/issues.go +++ b/backend/plugins/webhook/api/issues.go @@ -234,6 +234,43 @@ func CloseIssue(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, erro return closeIssue(input, err, connection) } +// CloseIssueByBodyRequest is the body for the body-based close endpoint +type CloseIssueByBodyRequest struct { + IssueKey string `mapstructure:"issueKey" validate:"required,max=255"` + ResolutionDate *time.Time `mapstructure:"resolutionDate"` + OriginalStatus string `mapstructure:"originalStatus"` +} + +// CloseIssueByBody +// @Summary close an issue (body-based) +// @Description Close an incident by passing issueKey in the request body. +// @Description Use this when the client (e.g. Kibana) cannot construct a dynamic URL. +// @Tags plugins/webhook +// @Param connectionId path int true "connection ID" +// @Param body body CloseIssueByBodyRequest true "close request" +// @Success 200 {string} noResponse "" +// @Failure 400 {string} errcode.Error "Bad Request" +// @Failure 500 {string} errcode.Error "Internal Error" +// @Router /plugins/webhook/connections/{connectionId}/issue/close [POST] +func CloseIssueByBody(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + connection := &models.WebhookConnection{} + err := connectionHelper.First(connection, input.Params) + if err != nil { + return nil, err + } + request := &CloseIssueByBodyRequest{} + if err2 := helper.DecodeMapStruct(input.Body, request, true); err2 != nil { + return &plugin.ApiResourceOutput{Body: err2.Error(), Status: http.StatusBadRequest}, nil + } + vld = validator.New() + if err2 := errors.Convert(vld.Struct(request)); err2 != nil { + return &plugin.ApiResourceOutput{Body: err2.Error(), Status: http.StatusBadRequest}, nil + } + // Inject issueKey into input.Params so closeIssue() can read it + input.Params["issueKey"] = request.IssueKey + return closeIssue(input, err, connection) +} + // CloseIssueByName // @Summary set issue's status to DONE // @Description set issue's status to DONE @@ -247,6 +284,34 @@ func CloseIssueByName(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput err := connectionHelper.FirstByName(connection, input.Params) return closeIssue(input, err, connection) } + +// CloseIssueByBodyByName +// @Summary close an issue by connection name (body-based) +// @Description Close an incident using connection name + issueKey in request body. +// @Tags plugins/webhook +// @Param connectionName path string true "connection name" +// @Param body body CloseIssueByBodyRequest true "close request" +// @Success 200 {string} noResponse "" +// @Failure 400 {string} errcode.Error "Bad Request" +// @Failure 500 {string} errcode.Error "Internal Error" +// @Router /plugins/webhook/connections/by-name/{connectionName}/issue/close [POST] +func CloseIssueByBodyByName(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + connection := &models.WebhookConnection{} + err := connectionHelper.FirstByName(connection, input.Params) + if err != nil { + return nil, err + } + request := &CloseIssueByBodyRequest{} + if err2 := helper.DecodeMapStruct(input.Body, request, true); err2 != nil { + return &plugin.ApiResourceOutput{Body: err2.Error(), Status: http.StatusBadRequest}, nil + } + vld = validator.New() + if err2 := errors.Convert(vld.Struct(request)); err2 != nil { + return &plugin.ApiResourceOutput{Body: err2.Error(), Status: http.StatusBadRequest}, nil + } + input.Params["issueKey"] = request.IssueKey + return closeIssue(input, err, connection) +} func closeIssue(input *plugin.ApiResourceInput, err errors.Error, connection *models.WebhookConnection) (*plugin.ApiResourceOutput, errors.Error) { if err != nil { diff --git a/backend/plugins/webhook/impl/impl.go b/backend/plugins/webhook/impl/impl.go index 9a67683584e..daf07b53526 100644 --- a/backend/plugins/webhook/impl/impl.go +++ b/backend/plugins/webhook/impl/impl.go @@ -99,6 +99,9 @@ func (p Webhook) ApiResources() map[string]map[string]plugin.ApiResourceHandler "connections/:connectionId/issue/:issueKey/close": { "POST": api.CloseIssue, }, + "connections/:connectionId/issue/close": { + "POST": api.CloseIssueByBody, + }, ":connectionId/deployments": { "POST": api.PostDeployments, }, @@ -111,6 +114,9 @@ func (p Webhook) ApiResources() map[string]map[string]plugin.ApiResourceHandler ":connectionId/issue/:issueKey/close": { "POST": api.CloseIssue, }, + ":connectionId/issue/close": { + "POST": api.CloseIssueByBody, + }, "connections/by-name/:connectionName": { "GET": api.GetConnectionByName, "PATCH": api.PatchConnectionByName, @@ -128,6 +134,9 @@ func (p Webhook) ApiResources() map[string]map[string]plugin.ApiResourceHandler "connections/by-name/:connectionName/issue/:issueKey/close": { "POST": api.CloseIssueByName, }, + "connections/by-name/:connectionName/issue/close": { + "POST": api.CloseIssueByBodyByName, + }, "projects/:projectName/deployments": { "POST": api.PostDeploymentsByProjectName, },