From f9b3b1aa4ffbd000f08f37fe870f5baa0118a00f Mon Sep 17 00:00:00 2001 From: Marais van Zyl Date: Mon, 13 Oct 2025 19:33:49 +0100 Subject: [PATCH 01/11] feat: add support for creating deployments by project name and retrieving connections by project and plugin name --- .../pluginhelper/api/connection_helper.go | 33 +++++++++++++++++++ backend/plugins/webhook/api/deployments.go | 21 ++++++++++++ 2 files changed, 54 insertions(+) diff --git a/backend/helpers/pluginhelper/api/connection_helper.go b/backend/helpers/pluginhelper/api/connection_helper.go index d9630f81b03..9e6a3c6557e 100644 --- a/backend/helpers/pluginhelper/api/connection_helper.go +++ b/backend/helpers/pluginhelper/api/connection_helper.go @@ -142,6 +142,39 @@ func (c *ConnectionApiHelper) FirstByName(connection interface{}, params map[str return CallDB(c.db.First, connection, dal.Where("name = ?", connectionName)) } +// FirstByProjectName finds the connection by project name and plugin name +func (c *ConnectionApiHelper) FirstByProjectName(connection interface{}, params map[string]string, pluginName string) errors.Error { + projectName := params["projectName"] + if projectName == "" { + return errors.BadInput.New("missing projectName") + } + if len(projectName) > 100 { + return errors.BadInput.New("projectName exceeds maximum length of 100 characters") + } + if pluginName == "" { + return errors.BadInput.New("missing pluginName") + } + // We need to join three tables: _tool_webhook_connections, _devlake_blueprint_connections, and _devlake_blueprints + // to find the connection associated with the given project name and plugin name. + // The SQL query would look something like this: + // SELECT wc.* + // FROM _tool_webhook_connections AS wc + // JOIN _devlake_blueprint_connections AS bc ON wc.id = bc.connection_id AND bc.plugin_name = ? + // JOIN _devlake_blueprints AS bp ON bc.blueprint_id = bp.id + // WHERE bp.project_name = ? + // LIMIT 1; + + // Using DAL to construct the query + clauses := []dal.Clause{dal.From(connection)} + clauses = append(clauses, + dal.Join("left join _devlake_blueprint_connections bc ON _tool_webhook_connections.id = bc.connection_id and bc.plugin_name = ?", pluginName), + dal.Join("left join _devlake_blueprints bp ON bc.blueprint_id = bp.id"), + dal.Where("bp.project_name = ?", projectName), + ) + + return CallDB(c.db.First, connection, clauses...) +} + // List returns all connections with password/token decrypted func (c *ConnectionApiHelper) List(connections interface{}) errors.Error { return CallDB(c.db.All, connections) diff --git a/backend/plugins/webhook/api/deployments.go b/backend/plugins/webhook/api/deployments.go index bbfb2bb83cc..146348a7aa2 100644 --- a/backend/plugins/webhook/api/deployments.go +++ b/backend/plugins/webhook/api/deployments.go @@ -109,6 +109,27 @@ func PostDeploymentsByName(input *plugin.ApiResourceInput) (*plugin.ApiResourceO return postDeployments(input, connection, err) } +// PostDeploymentsByProjectName +// @Summary create deployment by project name +// @Description Create deployment pipeline by project name.
+// @Description example1: {"repo_url":"devlake","commit_sha":"015e3d3b480e417aede5a1293bd61de9b0fd051d","start_time":"2020-01-01T12:00:00+00:00","end_time":"2020-01-01T12:59:59+00:00","environment":"PRODUCTION"}
+// @Description So we suggest request before task after deployment pipeline finish. +// @Description Both cicd_pipeline and cicd_task will be created +// @Tags plugins/webhook +// @Param body body WebhookDeploymentReq 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/deployments [POST] +func PostDeploymentsByProjectName(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + // find or create the connection for this project + connection := &models.WebhookConnection{} + err := connectionHelper.FirstByProjectName(connection, input.Params, pluginName) + + return postDeployments(input, connection, err) +} + func postDeployments(input *plugin.ApiResourceInput, connection *models.WebhookConnection, err errors.Error) (*plugin.ApiResourceOutput, errors.Error) { if err != nil { return nil, err From 6fe7d4eb9d86e8bcdec2ff35f41e904af8736d97 Mon Sep 17 00:00:00 2001 From: Marais van Zyl Date: Tue, 14 Oct 2025 09:19:17 +0100 Subject: [PATCH 02/11] refactor: replace FirstByProjectName with findByProjectName for improved clarity and maintainability --- .../pluginhelper/api/connection_helper.go | 33 ----------------- backend/plugins/webhook/api/deployments.go | 36 ++++++++++++++++++- 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/backend/helpers/pluginhelper/api/connection_helper.go b/backend/helpers/pluginhelper/api/connection_helper.go index 9e6a3c6557e..d9630f81b03 100644 --- a/backend/helpers/pluginhelper/api/connection_helper.go +++ b/backend/helpers/pluginhelper/api/connection_helper.go @@ -142,39 +142,6 @@ func (c *ConnectionApiHelper) FirstByName(connection interface{}, params map[str return CallDB(c.db.First, connection, dal.Where("name = ?", connectionName)) } -// FirstByProjectName finds the connection by project name and plugin name -func (c *ConnectionApiHelper) FirstByProjectName(connection interface{}, params map[string]string, pluginName string) errors.Error { - projectName := params["projectName"] - if projectName == "" { - return errors.BadInput.New("missing projectName") - } - if len(projectName) > 100 { - return errors.BadInput.New("projectName exceeds maximum length of 100 characters") - } - if pluginName == "" { - return errors.BadInput.New("missing pluginName") - } - // We need to join three tables: _tool_webhook_connections, _devlake_blueprint_connections, and _devlake_blueprints - // to find the connection associated with the given project name and plugin name. - // The SQL query would look something like this: - // SELECT wc.* - // FROM _tool_webhook_connections AS wc - // JOIN _devlake_blueprint_connections AS bc ON wc.id = bc.connection_id AND bc.plugin_name = ? - // JOIN _devlake_blueprints AS bp ON bc.blueprint_id = bp.id - // WHERE bp.project_name = ? - // LIMIT 1; - - // Using DAL to construct the query - clauses := []dal.Clause{dal.From(connection)} - clauses = append(clauses, - dal.Join("left join _devlake_blueprint_connections bc ON _tool_webhook_connections.id = bc.connection_id and bc.plugin_name = ?", pluginName), - dal.Join("left join _devlake_blueprints bp ON bc.blueprint_id = bp.id"), - dal.Where("bp.project_name = ?", projectName), - ) - - return CallDB(c.db.First, connection, clauses...) -} - // List returns all connections with password/token decrypted func (c *ConnectionApiHelper) List(connections interface{}) errors.Error { return CallDB(c.db.All, connections) diff --git a/backend/plugins/webhook/api/deployments.go b/backend/plugins/webhook/api/deployments.go index 146348a7aa2..5f51cfd63ff 100644 --- a/backend/plugins/webhook/api/deployments.go +++ b/backend/plugins/webhook/api/deployments.go @@ -125,7 +125,7 @@ func PostDeploymentsByName(input *plugin.ApiResourceInput) (*plugin.ApiResourceO func PostDeploymentsByProjectName(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { // find or create the connection for this project connection := &models.WebhookConnection{} - err := connectionHelper.FirstByProjectName(connection, input.Params, pluginName) + err := findByProjectName(connection, input.Params, pluginName) return postDeployments(input, connection, err) } @@ -272,3 +272,37 @@ func GenerateDeploymentCommitId(connectionId uint64, deploymentId string, repoUr urlHash16 := fmt.Sprintf("%x", md5.Sum([]byte(repoUrl)))[:16] return fmt.Sprintf("%s:%d:%s:%s:%s", "webhook", connectionId, deploymentId, urlHash16, commitSha) } + +// findByProjectName finds the connection by project name and plugin name +func findByProjectName(connection interface{}, params map[string]string, pluginName string) errors.Error { + projectName := params["projectName"] + if projectName == "" { + return errors.BadInput.New("missing projectName") + } + if len(projectName) > 100 { + return errors.BadInput.New("invalid projectName") + } + if pluginName == "" { + return errors.BadInput.New("missing pluginName") + } + // We need to join three tables: _tool_webhook_connections, _devlake_blueprint_connections, and _devlake_blueprints + // to find the connection associated with the given project name and plugin name. + // The SQL query would look something like this: + // SELECT wc.* + // FROM _tool_webhook_connections AS wc + // JOIN _devlake_blueprint_connections AS bc ON wc.id = bc.connection_id AND bc.plugin_name = ? + // JOIN _devlake_blueprints AS bp ON bc.blueprint_id = bp.id + // WHERE bp.project_name = ? + // LIMIT 1; + + // Using DAL to construct the query + clauses := []dal.Clause{dal.From(connection)} + clauses = append(clauses, + dal.Join("left join _devlake_blueprint_connections bc ON _tool_webhook_connections.id = bc.connection_id and bc.plugin_name = ?", pluginName), + dal.Join("left join _devlake_blueprints bp ON bc.blueprint_id = bp.id"), + dal.Where("bp.project_name = ?", projectName), + ) + + dal := basicRes.GetDal() + return dal.First(connection, clauses...) +} From 234df77011f01160481829037fb37fb17d2974c8 Mon Sep 17 00:00:00 2001 From: Marais van Zyl Date: Thu, 16 Oct 2025 21:24:40 +0000 Subject: [PATCH 03/11] chore: add gitattributes to help windows developers --- .gitattributes | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..ddd9cd3e7bd --- /dev/null +++ b/.gitattributes @@ -0,0 +1,34 @@ +# Ensure all text files use LF (Linux) line endings +* text=auto eol=lf + +# Treat shell scripts as text and enforce LF +*.sh text eol=lf + +# Treat Go files as text and enforce LF +*.go text eol=lf + +# Treat Python files as text and enforce LF +*.py text eol=lf + +# Treat JavaScript files as text and enforce LF +*.js text eol=lf + +# Treat Markdown files as text and enforce LF +*.md text eol=lf + +# Treat configuration files as text and enforce LF +*.yml text eol=lf +*.yaml text eol=lf +*.json text eol=lf + +# Prevent CRLF normalization for binary files +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.pdf binary +*.zip binary +*.tar binary +*.gz binary +*.bz2 binary +*.xz binary \ No newline at end of file From b79d3ac35220cbe9dc1c0a0ffd1561cef1caef9c Mon Sep 17 00:00:00 2001 From: Marais van Zyl Date: Sat, 18 Oct 2025 08:40:13 +0100 Subject: [PATCH 04/11] fix(webhook): add project deployments endpoint for webhook plugin --- backend/plugins/webhook/impl/impl.go | 3 +++ backend/server/api/router.go | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/backend/plugins/webhook/impl/impl.go b/backend/plugins/webhook/impl/impl.go index 880cd726a2f..9a67683584e 100644 --- a/backend/plugins/webhook/impl/impl.go +++ b/backend/plugins/webhook/impl/impl.go @@ -128,5 +128,8 @@ func (p Webhook) ApiResources() map[string]map[string]plugin.ApiResourceHandler "connections/by-name/:connectionName/issue/:issueKey/close": { "POST": api.CloseIssueByName, }, + "projects/:projectName/deployments": { + "POST": api.PostDeploymentsByProjectName, + }, } } diff --git a/backend/server/api/router.go b/backend/server/api/router.go index 721517cf59a..33031573a38 100644 --- a/backend/server/api/router.go +++ b/backend/server/api/router.go @@ -93,6 +93,16 @@ func RegisterRouter(r *gin.Engine, basicRes context.BasicRes) { } // mount all api resources for all plugins for pluginName, apiResources := range resources { + if pluginName == "webhook" { + // we need to register the project webhook endpoint first to avoid route conflict + const endpointName = "projects/:projectName/deployments" + if methodMap, ok := apiResources[endpointName]; ok { + if handler, ok := methodMap["POST"]; ok { + r.Handle("POST", fmt.Sprintf("/%s", endpointName), handlePluginCall(basicRes, pluginName, handler)) + delete(apiResources, endpointName) + } + } + } registerPluginEndpoints(r, basicRes, pluginName, apiResources) } } From 2aef41ce71587cd27089eb16335d2be2f457bc7b Mon Sep 17 00:00:00 2001 From: Marais van Zyl Date: Mon, 20 Oct 2025 17:55:55 +0100 Subject: [PATCH 05/11] chore(build): add .gitattributes to paths-ignore in .licenserc.yaml --- .licenserc.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.licenserc.yaml b/.licenserc.yaml index 0e5becbd8bf..0014d194757 100644 --- a/.licenserc.yaml +++ b/.licenserc.yaml @@ -35,6 +35,7 @@ header: - "**/*.svg" - "**/*.png" - ".editorconfig" + - "**/.gitattributes" - "**/.gitignore" - "**/.helmignore" - "**/.dockerignore" From 30434c23ef6e92d8ad10f0d8d4743e07bd6a142a Mon Sep 17 00:00:00 2001 From: Marais van Zyl Date: Tue, 21 Oct 2025 23:11:45 +0100 Subject: [PATCH 06/11] fix: create connection if not exist --- backend/plugins/webhook/api/deployments.go | 31 +++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/backend/plugins/webhook/api/deployments.go b/backend/plugins/webhook/api/deployments.go index 5f51cfd63ff..da697aaa1ab 100644 --- a/backend/plugins/webhook/api/deployments.go +++ b/backend/plugins/webhook/api/deployments.go @@ -26,6 +26,7 @@ import ( "github.com/apache/incubator-devlake/core/dal" "github.com/apache/incubator-devlake/core/log" + "gorm.io/gorm" "github.com/apache/incubator-devlake/helpers/dbhelper" "github.com/go-playground/validator/v10" @@ -126,6 +127,33 @@ func PostDeploymentsByProjectName(input *plugin.ApiResourceInput) (*plugin.ApiRe // find or create the connection for this project connection := &models.WebhookConnection{} err := findByProjectName(connection, input.Params, pluginName) + if err != nil { + // if not found, we will attempt to create a new connection + // Use direct comparison against the package sentinel; only treat other errors as fatal. + if errors.Is(err, gorm.ErrRecordNotFound) { + basicRes.GetLogger().Debug("creating webhook connection for project %s", input.Params["projectName"]) + connection.Name = input.Params["projectName"] + // &{Params:map[plugin:webhook] Query:map[] Body:map[name:test-webhook] Request: User:} + input = &plugin.ApiResourceInput{ + Params: map[string]string{ + "plugin": "webhook", + }, + Body: map[string]interface{}{ + "name": input.Params["projectName"], + }, + } + basicRes.GetLogger().Info("input: %+v", input) + err = connectionHelper.Create(connection, input) + if err != nil { + basicRes.GetLogger().Error(err, "failed to create webhook connection for project", "projectName", input.Params["projectName"]) + return nil, err + } + basicRes.GetLogger().Info("connection: %+v", connection) + } else { + basicRes.GetLogger().Error(err, "failed to find webhook connection for project", "projectName", input.Params["projectName"]) + return nil, err + } + } return postDeployments(input, connection, err) } @@ -295,12 +323,13 @@ func findByProjectName(connection interface{}, params map[string]string, pluginN // WHERE bp.project_name = ? // LIMIT 1; + basicRes.GetLogger().Debug("finding project webhook connection for project %s and plugin %s", projectName, pluginName) // Using DAL to construct the query clauses := []dal.Clause{dal.From(connection)} clauses = append(clauses, dal.Join("left join _devlake_blueprint_connections bc ON _tool_webhook_connections.id = bc.connection_id and bc.plugin_name = ?", pluginName), dal.Join("left join _devlake_blueprints bp ON bc.blueprint_id = bp.id"), - dal.Where("bp.project_name = ?", projectName), + dal.Where("bp.project_name = ? and _tool_webhook_connections.name = ?", projectName, projectName), ) dal := basicRes.GetDal() From 0ddce2c398510d67e3da331f704bd10558b93067 Mon Sep 17 00:00:00 2001 From: Marais van Zyl Date: Wed, 22 Oct 2025 20:58:18 +0000 Subject: [PATCH 07/11] fix: allow for null apiKeys --- config-ui/src/features/connections/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config-ui/src/features/connections/utils.ts b/config-ui/src/features/connections/utils.ts index e64e32d0b49..792213817ae 100644 --- a/config-ui/src/features/connections/utils.ts +++ b/config-ui/src/features/connections/utils.ts @@ -51,6 +51,6 @@ export const transformWebhook = (connection: IWebhookAPI): IWebhook => { closeIssuesEndpoint: connection.closeIssuesEndpoint, postPipelineDeployTaskEndpoint: connection.postPipelineDeployTaskEndpoint, postPullRequestsEndpoint: connection.postPullRequestsEndpoint, - apiKeyId: connection.apiKey.id, + apiKeyId: connection.apiKey?.id, }; }; From e13f552f1ffc0726676f3cce693ad9264ad9221b Mon Sep 17 00:00:00 2001 From: Marais van Zyl Date: Wed, 22 Oct 2025 21:31:10 +0000 Subject: [PATCH 08/11] feat: create webhook and blueprint connection if not exist --- backend/plugins/webhook/api/deployments.go | 85 ++++++++++++++++------ 1 file changed, 64 insertions(+), 21 deletions(-) diff --git a/backend/plugins/webhook/api/deployments.go b/backend/plugins/webhook/api/deployments.go index da697aaa1ab..88ab5afe544 100644 --- a/backend/plugins/webhook/api/deployments.go +++ b/backend/plugins/webhook/api/deployments.go @@ -26,12 +26,14 @@ import ( "github.com/apache/incubator-devlake/core/dal" "github.com/apache/incubator-devlake/core/log" + "github.com/apache/incubator-devlake/server/services" "gorm.io/gorm" "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/devops" "github.com/apache/incubator-devlake/core/plugin" @@ -126,31 +128,72 @@ func PostDeploymentsByName(input *plugin.ApiResourceInput) (*plugin.ApiResourceO func PostDeploymentsByProjectName(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { // find or create the connection for this project connection := &models.WebhookConnection{} + projectName := input.Params["projectName"] err := findByProjectName(connection, input.Params, pluginName) if err != nil { // if not found, we will attempt to create a new connection // Use direct comparison against the package sentinel; only treat other errors as fatal. - if errors.Is(err, gorm.ErrRecordNotFound) { - basicRes.GetLogger().Debug("creating webhook connection for project %s", input.Params["projectName"]) - connection.Name = input.Params["projectName"] - // &{Params:map[plugin:webhook] Query:map[] Body:map[name:test-webhook] Request: User:} - input = &plugin.ApiResourceInput{ - Params: map[string]string{ - "plugin": "webhook", - }, - Body: map[string]interface{}{ - "name": input.Params["projectName"], - }, - } - basicRes.GetLogger().Info("input: %+v", input) - err = connectionHelper.Create(connection, input) - if err != nil { - basicRes.GetLogger().Error(err, "failed to create webhook connection for project", "projectName", input.Params["projectName"]) - return nil, err - } - basicRes.GetLogger().Info("connection: %+v", connection) - } else { - basicRes.GetLogger().Error(err, "failed to find webhook connection for project", "projectName", input.Params["projectName"]) + if !errors.Is(err, gorm.ErrRecordNotFound) { + logger.Error(err, "failed to find webhook connection for project", "projectName", input.Params["projectName"]) + return nil, err + } + + // create the connection + logger.Debug("creating webhook connection for project %s", input.Params["projectName"]) + connection.Name = projectName + + // find the project and blueprint with which we will associate this connection + projectOutput, err := services.GetProject(projectName) + if err != nil { + logger.Error(err, "failed to find project for webhook connection", "projectName", projectName) + return nil, err + } + + if projectOutput == nil { + logger.Error(err, "project not found for webhook connection", "projectName", projectName) + return nil, errors.NotFound.New("project not found: " + projectName) + } + + if projectOutput.Blueprint == nil { + logger.Error(err, "unable to create webhook as the project has no blueprint", "projectName", projectName) + return nil, errors.BadInput.New("project has no blueprint: " + projectName) + } + + input = &plugin.ApiResourceInput{ + Params: map[string]string{ + "plugin": "webhook", + }, + Body: map[string]interface{}{ + "name": input.Params["projectName"], + }, + } + + err = connectionHelper.Create(connection, input) + if err != nil { + logger.Error(err, "failed to create webhook connection for project", "projectName", input.Params["projectName"]) + return nil, err + } + + // get the blueprint + blueprintId := projectOutput.Blueprint.ID + blueprint, err := services.GetBlueprint(blueprintId, true) + + if err != nil { + logger.Error(err, "failed to find blueprint for webhook connection", "blueprintId", blueprintId) + return nil, err + } + + // we need to associate this connection with the blueprint + blueprintConnection := &coremodels.BlueprintConnection{ + BlueprintId: blueprint.ID, + PluginName: pluginName, + ConnectionId: connection.ID, + } + + logger.Info("adding blueprint connection for blueprint %d and connection %d", blueprint.ID, connection.ID) + err = basicRes.GetDal().Create(blueprintConnection) + if err != nil { + logger.Error(err, "failed to create blueprint connection for project", "projectName", input.Params["projectName"]) return nil, err } } From 463f094b3e8677e5a906ed6ee4580be75bacbefb Mon Sep 17 00:00:00 2001 From: Marais van Zyl Date: Fri, 24 Oct 2025 16:42:11 +0100 Subject: [PATCH 09/11] refactor: create specific name for project deployment webhooks --- backend/plugins/webhook/api/deployments.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/plugins/webhook/api/deployments.go b/backend/plugins/webhook/api/deployments.go index 88ab5afe544..a550a3be586 100644 --- a/backend/plugins/webhook/api/deployments.go +++ b/backend/plugins/webhook/api/deployments.go @@ -129,6 +129,7 @@ func PostDeploymentsByProjectName(input *plugin.ApiResourceInput) (*plugin.ApiRe // find or create the connection for this project connection := &models.WebhookConnection{} projectName := input.Params["projectName"] + webhookName := fmt.Sprintf("%s_deployments", projectName) err := findByProjectName(connection, input.Params, pluginName) if err != nil { // if not found, we will attempt to create a new connection @@ -140,7 +141,7 @@ func PostDeploymentsByProjectName(input *plugin.ApiResourceInput) (*plugin.ApiRe // create the connection logger.Debug("creating webhook connection for project %s", input.Params["projectName"]) - connection.Name = projectName + connection.Name = webhookName // find the project and blueprint with which we will associate this connection projectOutput, err := services.GetProject(projectName) @@ -164,7 +165,7 @@ func PostDeploymentsByProjectName(input *plugin.ApiResourceInput) (*plugin.ApiRe "plugin": "webhook", }, Body: map[string]interface{}{ - "name": input.Params["projectName"], + "name": webhookName, }, } @@ -347,6 +348,7 @@ func GenerateDeploymentCommitId(connectionId uint64, deploymentId string, repoUr // findByProjectName finds the connection by project name and plugin name func findByProjectName(connection interface{}, params map[string]string, pluginName string) errors.Error { projectName := params["projectName"] + webhookName := fmt.Sprintf("%s_deployments", projectName) if projectName == "" { return errors.BadInput.New("missing projectName") } @@ -363,7 +365,7 @@ func findByProjectName(connection interface{}, params map[string]string, pluginN // FROM _tool_webhook_connections AS wc // JOIN _devlake_blueprint_connections AS bc ON wc.id = bc.connection_id AND bc.plugin_name = ? // JOIN _devlake_blueprints AS bp ON bc.blueprint_id = bp.id - // WHERE bp.project_name = ? + // WHERE bp.project_name = ? and _tool_webhook_connections.name = ? // LIMIT 1; basicRes.GetLogger().Debug("finding project webhook connection for project %s and plugin %s", projectName, pluginName) @@ -372,7 +374,7 @@ func findByProjectName(connection interface{}, params map[string]string, pluginN clauses = append(clauses, dal.Join("left join _devlake_blueprint_connections bc ON _tool_webhook_connections.id = bc.connection_id and bc.plugin_name = ?", pluginName), dal.Join("left join _devlake_blueprints bp ON bc.blueprint_id = bp.id"), - dal.Where("bp.project_name = ? and _tool_webhook_connections.name = ?", projectName, projectName), + dal.Where("bp.project_name = ? and _tool_webhook_connections.name = ?", projectName, webhookName), ) dal := basicRes.GetDal() From 7a05719cac313ff3d27559d456516472418e9152 Mon Sep 17 00:00:00 2001 From: Marais van Zyl Date: Mon, 10 Nov 2025 21:28:51 +0000 Subject: [PATCH 10/11] chore: revert change to webhook routing --- backend/server/api/router.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/backend/server/api/router.go b/backend/server/api/router.go index 33031573a38..721517cf59a 100644 --- a/backend/server/api/router.go +++ b/backend/server/api/router.go @@ -93,16 +93,6 @@ func RegisterRouter(r *gin.Engine, basicRes context.BasicRes) { } // mount all api resources for all plugins for pluginName, apiResources := range resources { - if pluginName == "webhook" { - // we need to register the project webhook endpoint first to avoid route conflict - const endpointName = "projects/:projectName/deployments" - if methodMap, ok := apiResources[endpointName]; ok { - if handler, ok := methodMap["POST"]; ok { - r.Handle("POST", fmt.Sprintf("/%s", endpointName), handlePluginCall(basicRes, pluginName, handler)) - delete(apiResources, endpointName) - } - } - } registerPluginEndpoints(r, basicRes, pluginName, apiResources) } } From d816e121c22d6e39542f0d8b42121f545ef7efc2 Mon Sep 17 00:00:00 2001 From: Marais van Zyl Date: Mon, 10 Nov 2025 21:30:59 +0000 Subject: [PATCH 11/11] chore: pass in webhook name --- backend/plugins/webhook/api/deployments.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/plugins/webhook/api/deployments.go b/backend/plugins/webhook/api/deployments.go index a550a3be586..3c739d1e1f5 100644 --- a/backend/plugins/webhook/api/deployments.go +++ b/backend/plugins/webhook/api/deployments.go @@ -130,7 +130,7 @@ func PostDeploymentsByProjectName(input *plugin.ApiResourceInput) (*plugin.ApiRe connection := &models.WebhookConnection{} projectName := input.Params["projectName"] webhookName := fmt.Sprintf("%s_deployments", projectName) - err := findByProjectName(connection, input.Params, pluginName) + err := findByProjectName(connection, input.Params, pluginName, webhookName) if err != nil { // if not found, we will attempt to create a new connection // Use direct comparison against the package sentinel; only treat other errors as fatal. @@ -346,9 +346,8 @@ func GenerateDeploymentCommitId(connectionId uint64, deploymentId string, repoUr } // findByProjectName finds the connection by project name and plugin name -func findByProjectName(connection interface{}, params map[string]string, pluginName string) errors.Error { +func findByProjectName(connection interface{}, params map[string]string, pluginName string, webhookName string) errors.Error { projectName := params["projectName"] - webhookName := fmt.Sprintf("%s_deployments", projectName) if projectName == "" { return errors.BadInput.New("missing projectName") }