From 5ff4878d76726abe3dc94b0574fc74ddaee30107 Mon Sep 17 00:00:00 2001 From: "jon.vaughan" Date: Tue, 9 Jun 2026 18:28:24 +0100 Subject: [PATCH 1/2] feat(webhook): Issues and PullRequests new endpoint by projectName --- backend/plugins/webhook/api/deployments.go | 8 +- backend/plugins/webhook/api/issues.go | 27 +- backend/plugins/webhook/api/pull_requests.go | 27 +- backend/plugins/webhook/impl/impl.go | 252 ++++++++++++------- 4 files changed, 209 insertions(+), 105 deletions(-) diff --git a/backend/plugins/webhook/api/deployments.go b/backend/plugins/webhook/api/deployments.go index fc2a463ef62..e3a62ae0dfc 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 { @@ -386,4 +386,4 @@ func findByProjectName(connection interface{}, params map[string]string, pluginN dal := basicRes.GetDal() return dal.First(connection, clauses...) -} +} \ No newline at end of file diff --git a/backend/plugins/webhook/api/issues.go b/backend/plugins/webhook/api/issues.go index 49ea72b51f1..56f374b5c55 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 @@ -289,4 +310,4 @@ func closeIssue(input *plugin.ApiResourceInput, err errors.Error, connection *mo } return &plugin.ApiResourceOutput{Body: nil, Status: http.StatusOK}, nil -} +} \ No newline at end of file diff --git a/backend/plugins/webhook/api/pull_requests.go b/backend/plugins/webhook/api/pull_requests.go index a01bb6c4d93..3e05fdb3ca8 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 @@ -173,4 +194,4 @@ func CreatePullRequest(connection *models.WebhookConnection, request *WebhookPul return err } return nil -} +} \ No newline at end of file diff --git a/backend/plugins/webhook/impl/impl.go b/backend/plugins/webhook/impl/impl.go index 9a67683584e..3e05fdb3ca8 100644 --- a/backend/plugins/webhook/impl/impl.go +++ b/backend/plugins/webhook/impl/impl.go @@ -15,121 +15,183 @@ See the License for the specific language governing permissions and limitations under the License. */ -package impl +package api import ( - "github.com/apache/incubator-devlake/core/context" + "fmt" + "net/http" + "time" + "github.com/apache/incubator-devlake/core/dal" + "github.com/apache/incubator-devlake/core/log" + + "github.com/apache/incubator-devlake/helpers/dbhelper" + "github.com/go-playground/validator/v10" + "github.com/apache/incubator-devlake/core/errors" - coreModels "github.com/apache/incubator-devlake/core/models" + "github.com/apache/incubator-devlake/core/models/domainlayer" + "github.com/apache/incubator-devlake/core/models/domainlayer/code" "github.com/apache/incubator-devlake/core/plugin" - "github.com/apache/incubator-devlake/plugins/webhook/api" + "github.com/apache/incubator-devlake/helpers/pluginhelper/api" "github.com/apache/incubator-devlake/plugins/webhook/models" - "github.com/apache/incubator-devlake/plugins/webhook/models/migrationscripts" ) -// make sure interface is implemented -var _ interface { - plugin.PluginMeta - plugin.PluginInit - plugin.PluginApi - plugin.PluginModel - plugin.PluginMigration - plugin.DataSourcePluginBlueprintV200 -} = (*Webhook)(nil) - -type Webhook struct{} - -func (p Webhook) Description() string { - return "collect some Webhook data" +type WebhookPullRequestReq struct { + Id string `mapstructure:"id" validate:"required"` + BaseRepoId string `mapstructure:"baseRepoId"` + HeadRepoId string `mapstructure:"headRepoId"` + Status string `mapstructure:"status" validate:"omitempty,oneof=OPEN CLOSED MERGED"` + OriginalStatus string `mapstructure:"originalStatus"` + Title string `mapstructure:"displayTitle" validate:"required"` + Description string `mapstructure:"description"` + Url string `mapstructure:"url"` + AuthorName string `mapstructure:"authorName"` + AuthorId string `mapstructure:"authorId"` + MergedByName string `mapstructure:"mergedByName"` + MergedById string `mapstructure:"mergedById"` + ParentPrId string `mapstructure:"parentPrId"` + PullRequestKey int `mapstructure:"pullRequestKey" validate:"required"` + CreatedDate time.Time `mapstructure:"createdDate" validate:"required"` + MergedDate *time.Time `mapstructure:"mergedDate"` + ClosedDate *time.Time `mapstructure:"closedDate"` + Type string `mapstructure:"type"` + Component string `mapstructure:"component"` + MergeCommitSha string `mapstructure:"mergeCommitSha"` + HeadRef string `mapstructure:"headRef"` + BaseRef string `mapstructure:"baseRef"` + BaseCommitSha string `mapstructure:"baseCommitSha"` + HeadCommitSha string `mapstructure:"headCommitSha"` + Additions int `mapstructure:"additions"` + Deletions int `mapstructure:"deletions"` + IsDraft bool `mapstructure:"isDraft"` } -func (p Webhook) Name() string { - return "webhook" +// PostPullRequests +// @Summary create pull requests by webhook +// @Description Create pull request by webhook.
+// @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" must be equal to "webhook:{connectionId}" for this to work correctly and calculate DORA metrics +// @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 /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) + + return postPullRequests(input, connection, err) } -func (p Webhook) Init(basicRes context.BasicRes) errors.Error { - api.Init(basicRes, p) +// PostPullRequestsByName +// @Summary create pull requests by webhook name +// @Description Create pull request by webhook name.
+// @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" must be equal to "webhook:{connectionId}" for this to work correctly and calculate DORA metrics +// @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 /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) - return nil + return postPullRequests(input, connection, err) } -func (p Webhook) GetTablesInfo() []dal.Tabler { - return []dal.Tabler{ - &models.WebhookConnection{}, +// 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 } -} -func (p Webhook) MakeDataSourcePipelinePlanV200( - connectionId uint64, - _ []*coreModels.BlueprintScope, -) (pp coreModels.PipelinePlan, sc []plugin.Scope, err errors.Error) { - return api.MakeDataSourcePipelinePlanV200(connectionId) + return postPullRequests(input, connection, err) } -// RootPkgPath information lost when compiled as plugin(.so) -func (p Webhook) RootPkgPath() string { - return "github.com/apache/incubator-devlake/plugins/webhook" -} +func postPullRequests(input *plugin.ApiResourceInput, connection *models.WebhookConnection, err errors.Error) (*plugin.ApiResourceOutput, errors.Error) { + if err != nil { + return nil, err + } + // get request + request := &WebhookPullRequestReq{} + err = api.DecodeMapStruct(input.Body, request, true) + if err != nil { + return &plugin.ApiResourceOutput{Body: err.Error(), Status: http.StatusBadRequest}, nil + } + // validate + vld = validator.New() + err = errors.Convert(vld.Struct(request)) + if err != nil { + return nil, errors.BadInput.Wrap(vld.Struct(request), `input json error`) + } + txHelper := dbhelper.NewTxHelper(basicRes, &err) + defer txHelper.End() + tx := txHelper.Begin() + if err := CreatePullRequest(connection, request, tx, logger); err != nil { + logger.Error(err, "create pull requests") + return nil, err + } -func (p Webhook) MigrationScripts() []plugin.MigrationScript { - return migrationscripts.All() + return &plugin.ApiResourceOutput{Body: nil, Status: http.StatusOK}, nil } -func (p Webhook) ApiResources() map[string]map[string]plugin.ApiResourceHandler { - return map[string]map[string]plugin.ApiResourceHandler{ - "connections": { - "POST": api.PostConnections, - "GET": api.ListConnections, - }, - "connections/:connectionId": { - "GET": api.GetConnection, - "PATCH": api.PatchConnection, - "DELETE": api.DeleteConnection, - }, - "connections/:connectionId/deployments": { - "POST": api.PostDeployments, - }, - "connections/:connectionId/pull_requests": { - "POST": api.PostPullRequests, - }, - "connections/:connectionId/issues": { - "POST": api.PostIssue, - }, - "connections/:connectionId/issue/:issueKey/close": { - "POST": api.CloseIssue, - }, - ":connectionId/deployments": { - "POST": api.PostDeployments, - }, - ":connectionId/pull_requests": { - "POST": api.PostPullRequests, - }, - ":connectionId/issues": { - "POST": api.PostIssue, - }, - ":connectionId/issue/:issueKey/close": { - "POST": api.CloseIssue, - }, - "connections/by-name/:connectionName": { - "GET": api.GetConnectionByName, - "PATCH": api.PatchConnectionByName, - "DELETE": api.DeleteConnectionByName, - }, - "connections/by-name/:connectionName/deployments": { - "POST": api.PostDeploymentsByName, - }, - "connections/by-name/:connectionName/pull_requests": { - "POST": api.PostPullRequestsByName, - }, - "connections/by-name/:connectionName/issues": { - "POST": api.PostIssueByName, - }, - "connections/by-name/:connectionName/issue/:issueKey/close": { - "POST": api.CloseIssueByName, - }, - "projects/:projectName/deployments": { - "POST": api.PostDeploymentsByProjectName, - }, +func CreatePullRequest(connection *models.WebhookConnection, request *WebhookPullRequestReq, tx dal.Transaction, logger log.Logger) errors.Error { + // validation + if request == nil { + return errors.BadInput.New("request body is nil") } -} + // create a pull_request record + pullRequest := &code.PullRequest{ + DomainEntity: domainlayer.DomainEntity{ + Id: fmt.Sprintf("%s:%d:%d", "webhook", connection.ID, request.PullRequestKey), + }, + BaseRepoId: fmt.Sprintf("%s:%d", "webhook", connection.ID), + HeadRepoId: request.HeadRepoId, + Status: request.Status, + OriginalStatus: request.OriginalStatus, + Title: request.Title, + Description: request.Description, + Url: request.Url, + AuthorName: request.AuthorName, + AuthorId: request.AuthorId, + MergedByName: request.MergedByName, + MergedById: request.MergedById, + ParentPrId: request.ParentPrId, + PullRequestKey: request.PullRequestKey, + CreatedDate: request.CreatedDate, + MergedDate: request.MergedDate, + ClosedDate: request.ClosedDate, + Type: request.Type, + Component: request.Component, + MergeCommitSha: request.MergeCommitSha, + HeadRef: request.HeadRef, + BaseRef: request.BaseRef, + BaseCommitSha: request.BaseCommitSha, + HeadCommitSha: request.HeadCommitSha, + Additions: request.Additions, + Deletions: request.Deletions, + IsDraft: request.IsDraft, + } + if err := tx.CreateOrUpdate(pullRequest); err != nil { + logger.Error(err, "failed to save pull request") + return err + } + return nil +} \ No newline at end of file From 075c27318112a48aeda09ddd62732434999a463e Mon Sep 17 00:00:00 2001 From: "jon.vaughan" Date: Tue, 9 Jun 2026 18:37:36 +0100 Subject: [PATCH 2/2] ci: fix invalid push --- backend/plugins/webhook/impl/impl.go | 256 +++++++++++---------------- 1 file changed, 100 insertions(+), 156 deletions(-) diff --git a/backend/plugins/webhook/impl/impl.go b/backend/plugins/webhook/impl/impl.go index 3e05fdb3ca8..34e6599850e 100644 --- a/backend/plugins/webhook/impl/impl.go +++ b/backend/plugins/webhook/impl/impl.go @@ -15,183 +15,127 @@ See the License for the specific language governing permissions and limitations under the License. */ -package api +package impl import ( - "fmt" - "net/http" - "time" - + "github.com/apache/incubator-devlake/core/context" "github.com/apache/incubator-devlake/core/dal" - "github.com/apache/incubator-devlake/core/log" - - "github.com/apache/incubator-devlake/helpers/dbhelper" - "github.com/go-playground/validator/v10" - "github.com/apache/incubator-devlake/core/errors" - "github.com/apache/incubator-devlake/core/models/domainlayer" - "github.com/apache/incubator-devlake/core/models/domainlayer/code" + coreModels "github.com/apache/incubator-devlake/core/models" "github.com/apache/incubator-devlake/core/plugin" - "github.com/apache/incubator-devlake/helpers/pluginhelper/api" + "github.com/apache/incubator-devlake/plugins/webhook/api" "github.com/apache/incubator-devlake/plugins/webhook/models" + "github.com/apache/incubator-devlake/plugins/webhook/models/migrationscripts" ) -type WebhookPullRequestReq struct { - Id string `mapstructure:"id" validate:"required"` - BaseRepoId string `mapstructure:"baseRepoId"` - HeadRepoId string `mapstructure:"headRepoId"` - Status string `mapstructure:"status" validate:"omitempty,oneof=OPEN CLOSED MERGED"` - OriginalStatus string `mapstructure:"originalStatus"` - Title string `mapstructure:"displayTitle" validate:"required"` - Description string `mapstructure:"description"` - Url string `mapstructure:"url"` - AuthorName string `mapstructure:"authorName"` - AuthorId string `mapstructure:"authorId"` - MergedByName string `mapstructure:"mergedByName"` - MergedById string `mapstructure:"mergedById"` - ParentPrId string `mapstructure:"parentPrId"` - PullRequestKey int `mapstructure:"pullRequestKey" validate:"required"` - CreatedDate time.Time `mapstructure:"createdDate" validate:"required"` - MergedDate *time.Time `mapstructure:"mergedDate"` - ClosedDate *time.Time `mapstructure:"closedDate"` - Type string `mapstructure:"type"` - Component string `mapstructure:"component"` - MergeCommitSha string `mapstructure:"mergeCommitSha"` - HeadRef string `mapstructure:"headRef"` - BaseRef string `mapstructure:"baseRef"` - BaseCommitSha string `mapstructure:"baseCommitSha"` - HeadCommitSha string `mapstructure:"headCommitSha"` - Additions int `mapstructure:"additions"` - Deletions int `mapstructure:"deletions"` - IsDraft bool `mapstructure:"isDraft"` -} +// make sure interface is implemented +var _ interface { + plugin.PluginMeta + plugin.PluginInit + plugin.PluginApi + plugin.PluginModel + plugin.PluginMigration + plugin.DataSourcePluginBlueprintV200 +} = (*Webhook)(nil) -// PostPullRequests -// @Summary create pull requests by webhook -// @Description Create pull request by webhook.
-// @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" must be equal to "webhook:{connectionId}" for this to work correctly and calculate DORA metrics -// @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 /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) +type Webhook struct{} - return postPullRequests(input, connection, err) +func (p Webhook) Description() string { + return "collect some Webhook data" } -// PostPullRequestsByName -// @Summary create pull requests by webhook name -// @Description Create pull request by webhook name.
-// @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" must be equal to "webhook:{connectionId}" for this to work correctly and calculate DORA metrics -// @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 /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) +func (p Webhook) Name() string { + return "webhook" +} - return postPullRequests(input, connection, err) +func (p Webhook) Init(basicRes context.BasicRes) errors.Error { + api.Init(basicRes, p) + + return nil } -// 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 +func (p Webhook) GetTablesInfo() []dal.Tabler { + return []dal.Tabler{ + &models.WebhookConnection{}, } +} - return postPullRequests(input, connection, err) +func (p Webhook) MakeDataSourcePipelinePlanV200( + connectionId uint64, + _ []*coreModels.BlueprintScope, +) (pp coreModels.PipelinePlan, sc []plugin.Scope, err errors.Error) { + return api.MakeDataSourcePipelinePlanV200(connectionId) } -func postPullRequests(input *plugin.ApiResourceInput, connection *models.WebhookConnection, err errors.Error) (*plugin.ApiResourceOutput, errors.Error) { - if err != nil { - return nil, err - } - // get request - request := &WebhookPullRequestReq{} - err = api.DecodeMapStruct(input.Body, request, true) - if err != nil { - return &plugin.ApiResourceOutput{Body: err.Error(), Status: http.StatusBadRequest}, nil - } - // validate - vld = validator.New() - err = errors.Convert(vld.Struct(request)) - if err != nil { - return nil, errors.BadInput.Wrap(vld.Struct(request), `input json error`) - } - txHelper := dbhelper.NewTxHelper(basicRes, &err) - defer txHelper.End() - tx := txHelper.Begin() - if err := CreatePullRequest(connection, request, tx, logger); err != nil { - logger.Error(err, "create pull requests") - return nil, err - } +// RootPkgPath information lost when compiled as plugin(.so) +func (p Webhook) RootPkgPath() string { + return "github.com/apache/incubator-devlake/plugins/webhook" +} - return &plugin.ApiResourceOutput{Body: nil, Status: http.StatusOK}, nil +func (p Webhook) MigrationScripts() []plugin.MigrationScript { + return migrationscripts.All() } -func CreatePullRequest(connection *models.WebhookConnection, request *WebhookPullRequestReq, tx dal.Transaction, logger log.Logger) errors.Error { - // validation - if request == nil { - return errors.BadInput.New("request body is nil") - } - // create a pull_request record - pullRequest := &code.PullRequest{ - DomainEntity: domainlayer.DomainEntity{ - Id: fmt.Sprintf("%s:%d:%d", "webhook", connection.ID, request.PullRequestKey), - }, - BaseRepoId: fmt.Sprintf("%s:%d", "webhook", connection.ID), - HeadRepoId: request.HeadRepoId, - Status: request.Status, - OriginalStatus: request.OriginalStatus, - Title: request.Title, - Description: request.Description, - Url: request.Url, - AuthorName: request.AuthorName, - AuthorId: request.AuthorId, - MergedByName: request.MergedByName, - MergedById: request.MergedById, - ParentPrId: request.ParentPrId, - PullRequestKey: request.PullRequestKey, - CreatedDate: request.CreatedDate, - MergedDate: request.MergedDate, - ClosedDate: request.ClosedDate, - Type: request.Type, - Component: request.Component, - MergeCommitSha: request.MergeCommitSha, - HeadRef: request.HeadRef, - BaseRef: request.BaseRef, - BaseCommitSha: request.BaseCommitSha, - HeadCommitSha: request.HeadCommitSha, - Additions: request.Additions, - Deletions: request.Deletions, - IsDraft: request.IsDraft, - } - if err := tx.CreateOrUpdate(pullRequest); err != nil { - logger.Error(err, "failed to save pull request") - return err +func (p Webhook) ApiResources() map[string]map[string]plugin.ApiResourceHandler { + return map[string]map[string]plugin.ApiResourceHandler{ + "connections": { + "POST": api.PostConnections, + "GET": api.ListConnections, + }, + "connections/:connectionId": { + "GET": api.GetConnection, + "PATCH": api.PatchConnection, + "DELETE": api.DeleteConnection, + }, + "connections/:connectionId/deployments": { + "POST": api.PostDeployments, + }, + "connections/:connectionId/pull_requests": { + "POST": api.PostPullRequests, + }, + "connections/:connectionId/issues": { + "POST": api.PostIssue, + }, + "connections/:connectionId/issue/:issueKey/close": { + "POST": api.CloseIssue, + }, + ":connectionId/deployments": { + "POST": api.PostDeployments, + }, + ":connectionId/pull_requests": { + "POST": api.PostPullRequests, + }, + ":connectionId/issues": { + "POST": api.PostIssue, + }, + ":connectionId/issue/:issueKey/close": { + "POST": api.CloseIssue, + }, + "connections/by-name/:connectionName": { + "GET": api.GetConnectionByName, + "PATCH": api.PatchConnectionByName, + "DELETE": api.DeleteConnectionByName, + }, + "connections/by-name/:connectionName/deployments": { + "POST": api.PostDeploymentsByName, + }, + "connections/by-name/:connectionName/pull_requests": { + "POST": api.PostPullRequestsByName, + }, + "connections/by-name/:connectionName/issues": { + "POST": api.PostIssueByName, + }, + "connections/by-name/:connectionName/issue/:issueKey/close": { + "POST": api.CloseIssueByName, + }, + "projects/:projectName/deployments": { + "POST": api.PostDeploymentsByProjectName, + }, + "projects/:projectName/issues": { + "POST": api.PostIssuesByProjectName, + }, + "projects/:projectName/pull_requests": { + "POST": api.PostPullRequestsByProjectName, + }, } - return nil } \ No newline at end of file