Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,6 @@ require (
golang.org/x/mod v0.17.0
)

replace github.com/chenzhuoyu/iasm => github.com/cloudwego/iasm v0.2.0

//replace github.com/apache/incubator-devlake => ./
1 change: 1 addition & 0 deletions backend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
Expand Down
9 changes: 9 additions & 0 deletions backend/plugins/webhook/api/blueprint_v200.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"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/models/domainlayer/devops"
"github.com/apache/incubator-devlake/core/models/domainlayer/ticket"
"github.com/apache/incubator-devlake/core/plugin"
Expand Down Expand Up @@ -54,5 +55,13 @@ func MakeDataSourcePipelinePlanV200(connectionId uint64) (coreModels.PipelinePla
Name: connection.Name,
})

// add repos to scopes
scopes = append(scopes, &code.Repo{
DomainEntity: domainlayer.DomainEntity{
Id: fmt.Sprintf("%s:%d", "webhook", connection.ID),
},
Name: connection.Name,
})

return nil, scopes, nil
}
2 changes: 2 additions & 0 deletions backend/plugins/webhook/api/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ type WebhookConnectionResponse struct {
models.WebhookConnection
PostIssuesEndpoint string `json:"postIssuesEndpoint"`
CloseIssuesEndpoint string `json:"closeIssuesEndpoint"`
PostPullRequestsEndpoint string `json:"postPullRequestsEndpoint"`
PostPipelineTaskEndpoint string `json:"postPipelineTaskEndpoint"`
PostPipelineDeployTaskEndpoint string `json:"postPipelineDeployTaskEndpoint"`
ClosePipelineEndpoint string `json:"closePipelineEndpoint"`
Expand Down Expand Up @@ -256,6 +257,7 @@ func formatConnection(connection *models.WebhookConnection, withApiKeyInfo bool)
response := &WebhookConnectionResponse{WebhookConnection: *connection}
response.PostIssuesEndpoint = fmt.Sprintf(`/rest/plugins/webhook/connections/%d/issues`, connection.ID)
response.CloseIssuesEndpoint = fmt.Sprintf(`/rest/plugins/webhook/connections/%d/issue/:issueKey/close`, connection.ID)
response.PostPullRequestsEndpoint = fmt.Sprintf(`/rest/plugins/webhook/connections/%d/pull_requests`, connection.ID)
response.PostPipelineTaskEndpoint = fmt.Sprintf(`/rest/plugins/webhook/connections/%d/cicd_tasks`, connection.ID)
response.PostPipelineDeployTaskEndpoint = fmt.Sprintf(`/rest/plugins/webhook/connections/%d/deployments`, connection.ID)
response.ClosePipelineEndpoint = fmt.Sprintf(`/rest/plugins/webhook/connections/%d/cicd_pipeline/:pipelineName/finish`, connection.ID)
Expand Down
176 changes: 176 additions & 0 deletions backend/plugins/webhook/api/pull_requests.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package api

import (
"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"
"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/helpers/pluginhelper/api"
"github.com/apache/incubator-devlake/plugins/webhook/models"
)

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"`
}

// PostPullRequests
// @Summary create pull requests by webhook
// @Description Create pull request by webhook.<br/>
// @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}<br/>
// @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/pullrequests [POST]
func PostPullRequests(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
connection := &models.WebhookConnection{}
err := connectionHelper.First(connection, input.Params)

return postPullRequests(input, connection, err)
}

// PostPullRequestsByName
// @Summary create pull requests by webhook name
// @Description Create pull request by webhook name.<br/>
// @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}<br/>
// @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/pullrequests [POST]
func PostPullRequestsByName(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
connection := &models.WebhookConnection{}
err := connectionHelper.FirstByName(connection, input.Params)

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

return &plugin.ApiResourceOutput{Body: nil, Status: http.StatusOK}, nil
}

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
}
9 changes: 9 additions & 0 deletions backend/plugins/webhook/impl/impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ func (p Webhook) ApiResources() map[string]map[string]plugin.ApiResourceHandler
"connections/:connectionId/deployments": {
"POST": api.PostDeployments,
},
"connections/:connectionId/pull_requests": {
"POST": api.PostPullRequests,
},
"connections/:connectionId/issues": {
"POST": api.PostIssue,
},
Expand All @@ -99,6 +102,9 @@ func (p Webhook) ApiResources() map[string]map[string]plugin.ApiResourceHandler
":connectionId/deployments": {
"POST": api.PostDeployments,
},
":connectionId/pull_requests": {
"POST": api.PostPullRequests,
},
":connectionId/issues": {
"POST": api.PostIssue,
},
Expand All @@ -113,6 +119,9 @@ func (p Webhook) ApiResources() map[string]map[string]plugin.ApiResourceHandler
"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,
},
Expand Down
1 change: 1 addition & 0 deletions config-ui/src/features/connections/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const transformWebhook = (connection: IWebhookAPI): IWebhook => {
postIssuesEndpoint: connection.postIssuesEndpoint,
closeIssuesEndpoint: connection.closeIssuesEndpoint,
postPipelineDeployTaskEndpoint: connection.postPipelineDeployTaskEndpoint,
postPullRequestsEndpoint: connection.postPullRequestsEndpoint,
apiKeyId: connection.apiKey.id,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const CreateDialog = ({ open, onCancel, onSubmitAfter }: Props) => {
postIssuesEndpoint: '',
closeIssuesEndpoint: '',
postDeploymentsCurl: '',
postPullRequestsEndpoint: '',
apiKey: '',
});

Expand All @@ -55,7 +56,7 @@ export const CreateDialog = ({ open, onCancel, onSubmitAfter }: Props) => {
const [success, res] = await operator(
async () => {
const {
webhook: { id, postIssuesEndpoint, closeIssuesEndpoint, postPipelineDeployTaskEndpoint },
webhook: { id, postIssuesEndpoint, closeIssuesEndpoint, postPipelineDeployTaskEndpoint, postPullRequestsEndpoint },
apiKey,
} = await dispatch(addWebhook({ name })).unwrap();

Expand All @@ -65,6 +66,7 @@ export const CreateDialog = ({ open, onCancel, onSubmitAfter }: Props) => {
postIssuesEndpoint,
closeIssuesEndpoint,
postPipelineDeployTaskEndpoint,
postPullRequestsEndpoint,
};
},
{
Expand Down Expand Up @@ -151,6 +153,17 @@ export const CreateDialog = ({ open, onCancel, onSubmitAfter }: Props) => {
.
</p>
</Block>
<Block title="Pull Requests">
<h5>Post to register a pull request</h5>
<CopyText content={record.postPullRequestsEndpoint} />
<p>
See the{' '}
<ExternalLink link="https://devlake.apache.org/docs/Plugins/webhook#pull_requests">
full payload schema
</ExternalLink>
.
</p>
</Block>
</S.Wrapper>
)}
</Modal>
Expand Down
36 changes: 27 additions & 9 deletions config-ui/src/plugins/register/webhook/components/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ import { IWebhook } from '@/types';

export const transformURI = (prefix: string, webhook: IWebhook, apiKey: string) => {
return {
postIssuesEndpoint: `curl ${prefix}${webhook.postIssuesEndpoint} -X 'POST' -H 'Authorization: Bearer ${
apiKey ?? '{API_KEY}'
}' -d '{
postIssuesEndpoint: `curl ${prefix}${webhook.postIssuesEndpoint} -X 'POST' -H 'Authorization: Bearer ${apiKey ?? '{API_KEY}'
}' -d '{
"issueKey":"DLK-1234",
"title":"an incident from DLK",
"type":"INCIDENT",
Expand All @@ -31,12 +30,10 @@ export const transformURI = (prefix: string, webhook: IWebhook, apiKey: string)
"createdDate":"2020-01-01T12:00:00+00:00",
"updatedDate":"2020-01-01T12:00:00+00:00"
}'`,
closeIssuesEndpoint: `curl ${prefix}${webhook.closeIssuesEndpoint} -X 'POST' -H 'Authorization: Bearer ${
apiKey ?? '{API_KEY}'
}'`,
postDeploymentsCurl: `curl ${prefix}${webhook.postPipelineDeployTaskEndpoint} -X 'POST' -H 'Authorization: Bearer ${
apiKey ?? '{API_KEY}'
}' -d '{
closeIssuesEndpoint: `curl ${prefix}${webhook.closeIssuesEndpoint} -X 'POST' -H 'Authorization: Bearer ${apiKey ?? '{API_KEY}'
}'`,
postDeploymentsCurl: `curl ${prefix}${webhook.postPipelineDeployTaskEndpoint} -X 'POST' -H 'Authorization: Bearer ${apiKey ?? '{API_KEY}'
}' -d '{
"id": "Required. This will be the unique ID of the deployment",
"startedDate": "2023-01-01T12:00:00+00:00",
"finishedDate": "2023-01-01T12:00:00+00:00",
Expand All @@ -52,5 +49,26 @@ export const transformURI = (prefix: string, webhook: IWebhook, apiKey: string)
}
]
}'`,
postPullRequestsEndpoint: `curl ${prefix}${webhook.postPullRequestsEndpoint} -X 'POST' -H 'Authorization: Bearer ${apiKey ?? '{API_KEY}'
}' -d '{
"id": "Required. This will be the unique ID of the pull request",
"baseRepoId": "your-repo-id",
"headRepoId": "your-repo-id",
"status": "MERGED",
"originalStatus": "OPEN",
"displayTitle": "Feature: Add new functionality",
"description": "This PR adds new features",
"url": "https://github.com/org/repo/pull/1",
"pullRequestKey": 1,
"createdDate": "2025-02-20T16:17:36Z",
"mergedDate": "2025-02-20T17:17:36Z",
"closedDate": null,
"mergeCommitSha": "bf0a79c57dff8f5f1f393de315ee5105a535e059",
"headRef": "your-branch-name",
"baseRef": "main",
"baseCommitSha": "e73325c2c9863f42ea25871cbfaeebcb8edcf604",
"headCommitSha": "b22f772f1197edfafd4cc5fe679a2d299ec12837",
"isDraft": false
}`,
};
};
11 changes: 11 additions & 0 deletions config-ui/src/plugins/register/webhook/components/view-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@ export const ViewDialog = ({ initialId, onCancel }: Props) => {
.
</p>
</Block>
<Block title="Pull Requests">
<h5>Post to register/update a pull_request</h5>
<CopyText content={URI.postPullRequestsEndpoint} />
<p>
See the{' '}
<ExternalLink link="https://devlake.apache.org/docs/Plugins/webhook#pull_requests">
full payload schema
</ExternalLink>
.
</p>
</Block>
<Block
title="API Key"
description="If you have forgotten your API key, you can revoke the previous key and generate a new one as a replacement."
Expand Down
2 changes: 2 additions & 0 deletions config-ui/src/types/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface IWebhookAPI {
postIssuesEndpoint: string;
closeIssuesEndpoint: string;
postPipelineDeployTaskEndpoint: string;
postPullRequestsEndpoint: string;
apiKey: {
id: number;
apiKey: string;
Expand All @@ -34,5 +35,6 @@ export interface IWebhook {
postIssuesEndpoint: string;
closeIssuesEndpoint: string;
postPipelineDeployTaskEndpoint: string;
postPullRequestsEndpoint: string;
apiKeyId: number;
}
Loading