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 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" diff --git a/backend/plugins/webhook/api/deployments.go b/backend/plugins/webhook/api/deployments.go index bbfb2bb83cc..3c739d1e1f5 100644 --- a/backend/plugins/webhook/api/deployments.go +++ b/backend/plugins/webhook/api/deployments.go @@ -26,11 +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" @@ -109,6 +112,96 @@ 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{} + projectName := input.Params["projectName"] + webhookName := fmt.Sprintf("%s_deployments", projectName) + 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. + 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 = 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 + } + + 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": webhookName, + }, + } + + 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 + } + } + + 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 @@ -251,3 +344,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 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/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, }; };