From 049f9b8e8363123e23fe8daccaa5006de1f6b23a Mon Sep 17 00:00:00 2001 From: Jon Vaughan Date: Sun, 7 Jun 2026 16:44:31 +0100 Subject: [PATCH] feat(webhook): add endpoints for creating issues and pull requests by project name --- backend/plugins/webhook/api/deployments.go | 6 ++--- backend/plugins/webhook/api/issues.go | 25 ++++++++++++++++++-- backend/plugins/webhook/api/pull_requests.go | 25 ++++++++++++++++++-- backend/plugins/webhook/impl/impl.go | 6 +++++ 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/backend/plugins/webhook/api/deployments.go b/backend/plugins/webhook/api/deployments.go index fc2a463ef62..b65ca9d0873 100644 --- a/backend/plugins/webhook/api/deployments.go +++ b/backend/plugins/webhook/api/deployments.go @@ -126,7 +126,7 @@ func PostDeploymentsByName(input *plugin.ApiResourceInput) (*plugin.ApiResourceO // @Router /projects/:projectName/deployments [POST] func PostDeploymentsByProjectName(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { // find or create the connection for this project - connection, err, shouldReturn := getOrCreateConnection(input) + connection, err, shouldReturn := getOrCreateConnection(input, "deployments") if shouldReturn { return nil, err } @@ -134,10 +134,10 @@ func PostDeploymentsByProjectName(input *plugin.ApiResourceInput) (*plugin.ApiRe return postDeployments(input, connection, err) } -func getOrCreateConnection(input *plugin.ApiResourceInput) (*models.WebhookConnection, errors.Error, bool) { +func getOrCreateConnection(input *plugin.ApiResourceInput, webhookSuffix string) (*models.WebhookConnection, errors.Error, bool) { connection := &models.WebhookConnection{} projectName := input.Params["projectName"] - webhookName := fmt.Sprintf("%s_deployments", projectName) + webhookName := fmt.Sprintf("%s_%s", projectName, webhookSuffix) err := findByProjectName(connection, input.Params, pluginName, webhookName) dal := basicRes.GetDal() if err != nil { diff --git a/backend/plugins/webhook/api/issues.go b/backend/plugins/webhook/api/issues.go index 49ea72b51f1..a88f0763523 100644 --- a/backend/plugins/webhook/api/issues.go +++ b/backend/plugins/webhook/api/issues.go @@ -19,11 +19,12 @@ package api import ( "fmt" - "github.com/apache/incubator-devlake/core/log" - "github.com/apache/incubator-devlake/helpers/dbhelper" "net/http" "time" + "github.com/apache/incubator-devlake/core/log" + "github.com/apache/incubator-devlake/helpers/dbhelper" + "github.com/apache/incubator-devlake/core/dal" "github.com/apache/incubator-devlake/core/errors" "github.com/apache/incubator-devlake/core/models/domainlayer" @@ -112,6 +113,26 @@ func PostIssueByName(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, return postIssue(input, err, connection) } +// PostIssuesByProjectName +// @Summary create issue by project name +// @Description Create issue by project name. The webhook connection will be created automatically if it does not exist.
+// @Description example: {"url":"","issue_key":"DLK-1234","title":"a feature from DLK","description":"","epic_key":"","type":"BUG","status":"TODO","original_status":"created","story_point":0,"resolution_date":null,"created_date":"2020-01-01T12:00:00+00:00","updated_date":null,"lead_time_minutes":0,"parent_issue_key":"DLK-1200","priority":"","original_estimate_minutes":0,"time_spent_minutes":0,"time_remaining_minutes":0,"creator_id":"user1131","creator_name":"Nick name 1","assignee_id":"user1132","assignee_name":"Nick name 2","severity":"","component":""} +// @Tags plugins/webhook +// @Param body body WebhookIssueRequest true "json body" +// @Success 200 {string} noResponse "" +// @Failure 400 {string} errcode.Error "Bad Request" +// @Failure 403 {string} errcode.Error "Forbidden" +// @Failure 500 {string} errcode.Error "Internal Error" +// @Router /projects/:projectName/issues [POST] +func PostIssuesByProjectName(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + // find or create the connection for this project + connection, err, shouldReturn := getOrCreateConnection(input, "issues") + if shouldReturn { + return nil, err + } + return postIssue(input, err, connection) +} + func postIssue(input *plugin.ApiResourceInput, err errors.Error, connection *models.WebhookConnection) (*plugin.ApiResourceOutput, errors.Error) { if err != nil { return nil, err diff --git a/backend/plugins/webhook/api/pull_requests.go b/backend/plugins/webhook/api/pull_requests.go index a01bb6c4d93..50fcee5c43a 100644 --- a/backend/plugins/webhook/api/pull_requests.go +++ b/backend/plugins/webhook/api/pull_requests.go @@ -77,7 +77,7 @@ type WebhookPullRequestReq struct { // @Failure 400 {string} errcode.Error "Bad Request" // @Failure 403 {string} errcode.Error "Forbidden" // @Failure 500 {string} errcode.Error "Internal Error" -// @Router /plugins/webhook/connections/:connectionId/pullrequests [POST] +// @Router /plugins/webhook/connections/:connectionId/pull_requests [POST] func PostPullRequests(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { connection := &models.WebhookConnection{} err := connectionHelper.First(connection, input.Params) @@ -96,7 +96,7 @@ func PostPullRequests(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput // @Failure 400 {string} errcode.Error "Bad Request" // @Failure 403 {string} errcode.Error "Forbidden" // @Failure 500 {string} errcode.Error "Internal Error" -// @Router /plugins/webhook/connections/by-name/:connectionName/pullrequests [POST] +// @Router /plugins/webhook/connections/by-name/:connectionName/pull_requests [POST] func PostPullRequestsByName(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { connection := &models.WebhookConnection{} err := connectionHelper.FirstByName(connection, input.Params) @@ -104,6 +104,27 @@ func PostPullRequestsByName(input *plugin.ApiResourceInput) (*plugin.ApiResource return postPullRequests(input, connection, err) } +// PostPullRequestsByProjectName +// @Summary create pull requests by project name +// @Description Create pull request by project name. The webhook connection will be created automatically if it does not exist.
+// @Description example1: {"id": "pr1","baseRepoId": "webhook:1","headRepoId": "repo_fork1","status": "MERGED","originalStatus": "OPEN","displayTitle": "Feature: Add new functionality","description": "This PR adds new features","url": "https://github.com/org/repo/pull/1","authorName": "johndoe","authorId": "johnd123","mergedByName": "janedoe","mergedById": "janed123","parentPrId": "","pullRequestKey": 1,"createdDate": "2025-02-20T16:17:36Z","mergedDate": "2025-02-20T17:17:36Z","closedDate": null,"type": "feature","component": "backend","mergeCommitSha": "bf0a79c57dff8f5f1f393de315ee5105a535e059","headRef": "repo_fork1:feature-branch","baseRef": "main","baseCommitSha": "e73325c2c9863f42ea25871cbfaeebcb8edcf604","headCommitSha": "b22f772f1197edfafd4cc5fe679a2d299ec12837","additions": 100,"deletions": 50,"isDraft": false}
+// @Description "baseRepoId" should be equal to "webhook:{connectionId}" for consistent DORA calculations +// @Tags plugins/webhook +// @Param body body WebhookPullRequestReq true "json body" +// @Success 200 +// @Failure 400 {string} errcode.Error "Bad Request" +// @Failure 403 {string} errcode.Error "Forbidden" +// @Failure 500 {string} errcode.Error "Internal Error" +// @Router /projects/:projectName/pull_requests [POST] +func PostPullRequestsByProjectName(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + connection, err, shouldReturn := getOrCreateConnection(input, "pull_requests") + if shouldReturn { + return nil, err + } + + return postPullRequests(input, connection, err) +} + func postPullRequests(input *plugin.ApiResourceInput, connection *models.WebhookConnection, err errors.Error) (*plugin.ApiResourceOutput, errors.Error) { if err != nil { return nil, err diff --git a/backend/plugins/webhook/impl/impl.go b/backend/plugins/webhook/impl/impl.go index 9a67683584e..93f2ac5e022 100644 --- a/backend/plugins/webhook/impl/impl.go +++ b/backend/plugins/webhook/impl/impl.go @@ -131,5 +131,11 @@ func (p Webhook) ApiResources() map[string]map[string]plugin.ApiResourceHandler "projects/:projectName/deployments": { "POST": api.PostDeploymentsByProjectName, }, + "projects/:projectName/issues": { + "POST": api.PostIssuesByProjectName, + }, + "projects/:projectName/pull_requests": { + "POST": api.PostPullRequestsByProjectName, + }, } }