This is an automated email from the ASF dual-hosted git repository.

lynwee pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/main by this push:
     new de7be9509 feat: add support for creating deployments by project name 
(#8611)
de7be9509 is described below

commit de7be9509848738b7f2a86353439e018f3157b27
Author: Marais van Zyl <[email protected]>
AuthorDate: Tue Dec 2 04:08:32 2025 +0000

    feat: add support for creating deployments by project name (#8611)
    
    * feat: add support for creating deployments by project name and retrieving 
connections by project and plugin name
    
    * refactor: replace FirstByProjectName with findByProjectName for improved 
clarity and maintainability
    
    * chore: add gitattributes to help windows developers
    
    * fix(webhook): add project deployments endpoint for webhook plugin
    
    * chore(build): add .gitattributes to paths-ignore in .licenserc.yaml
    
    * fix: create connection if not exist
    
    * fix: allow for null apiKeys
    
    * feat: create webhook and blueprint connection if not exist
    
    * refactor: create specific name for project deployment webhooks
    
    * chore: revert change to webhook routing
    
    * chore: pass in webhook name
    
    * chore: improve logging for webhook connection creation by using 
projectName variable
    
    * refactor: replace gorm error check with custom error handling for webhook 
connection lookup
    
    * refactor: enhance connection handling in PostDeploymentsByProjectName 
function
    
    ---------
    
    Co-authored-by: Lynwee <[email protected]>
---
 .gitattributes                              |  34 +++++++
 .licenserc.yaml                             |   1 +
 backend/plugins/webhook/api/deployments.go  | 136 ++++++++++++++++++++++++++++
 backend/plugins/webhook/impl/impl.go        |   3 +
 config-ui/src/features/connections/utils.ts |   2 +-
 5 files changed, 175 insertions(+), 1 deletion(-)

diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 000000000..ddd9cd3e7
--- /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
diff --git a/.licenserc.yaml b/.licenserc.yaml
index 0e5becbd8..0014d1947 100644
--- a/.licenserc.yaml
+++ b/.licenserc.yaml
@@ -35,6 +35,7 @@ header:
     - "**/*.svg"
     - "**/*.png"
     - ".editorconfig"
+    - "**/.gitattributes"
     - "**/.gitignore"
     - "**/.helmignore"
     - "**/.dockerignore"
diff --git a/backend/plugins/webhook/api/deployments.go 
b/backend/plugins/webhook/api/deployments.go
index bbfb2bb83..fc2a463ef 100644
--- a/backend/plugins/webhook/api/deployments.go
+++ b/backend/plugins/webhook/api/deployments.go
@@ -26,11 +26,13 @@ import (
 
        "github.com/apache/incubator-devlake/core/dal"
        "github.com/apache/incubator-devlake/core/log"
+       "github.com/apache/incubator-devlake/server/services"
 
        "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"
@@ -109,6 +111,105 @@ 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.<br/>
+// @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"}<br/>
+// @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, err, shouldReturn := getOrCreateConnection(input)
+       if shouldReturn {
+               return nil, err
+       }
+
+       return postDeployments(input, connection, err)
+}
+
+func getOrCreateConnection(input *plugin.ApiResourceInput) 
(*models.WebhookConnection, errors.Error, bool) {
+       connection := &models.WebhookConnection{}
+       projectName := input.Params["projectName"]
+       webhookName := fmt.Sprintf("%s_deployments", projectName)
+       err := findByProjectName(connection, input.Params, pluginName, 
webhookName)
+       dal := basicRes.GetDal()
+       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 !dal.IsErrorNotFound(err) {
+                       logger.Error(err, "failed to find webhook connection 
for project", "projectName", projectName)
+                       return nil, err, true
+               }
+
+               // create the connection
+               logger.Debug("creating webhook connection for project %s", 
projectName)
+               connection.Name = webhookName
+
+               // 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, true
+               }
+
+               if projectOutput == nil {
+                       logger.Error(err, "project not found for webhook 
connection", "projectName", projectName)
+                       return nil, errors.NotFound.New("project not found: " + 
projectName), true
+               }
+
+               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), true
+               }
+
+               connectionInput := &plugin.ApiResourceInput{
+                       Params: map[string]string{
+                               "plugin": "webhook",
+                       },
+                       Body: map[string]interface{}{
+                               "name": webhookName,
+                       },
+               }
+
+               err = connectionHelper.Create(connection, connectionInput)
+               if err != nil {
+                       logger.Error(err, "failed to create webhook connection 
for project", "projectName", projectName)
+                       return nil, err, true
+               }
+
+               // 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, true
+               }
+
+               // 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 = dal.Create(blueprintConnection)
+               if err != nil {
+                       logger.Error(err, "failed to create blueprint 
connection for project", "projectName", projectName)
+                       return nil, err, true
+               }
+       }
+       return connection, err, false
+}
+
 func postDeployments(input *plugin.ApiResourceInput, connection 
*models.WebhookConnection, err errors.Error) (*plugin.ApiResourceOutput, 
errors.Error) {
        if err != nil {
                return nil, err
@@ -251,3 +352,38 @@ 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, webhookName 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 = ? and _tool_webhook_connections.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 = ? and 
_tool_webhook_connections.name = ?", projectName, webhookName),
+       )
+
+       dal := basicRes.GetDal()
+       return dal.First(connection, clauses...)
+}
diff --git a/backend/plugins/webhook/impl/impl.go 
b/backend/plugins/webhook/impl/impl.go
index 880cd726a..9a6768358 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/config-ui/src/features/connections/utils.ts 
b/config-ui/src/features/connections/utils.ts
index e64e32d0b..792213817 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,
   };
 };

Reply via email to