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,
};
};