diff --git a/.vscode/settings.json b/.vscode/settings.json index 0cec1de..c7d29da 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,6 +3,7 @@ "bunx", "CEINTL", "Chakroun", + "cloudml", "commitlint", "dbaeumer", "devcontainers", @@ -11,6 +12,8 @@ "Gruntfuggly", "tseslint", "wlpbbgiky", + "wujiali", + "Wyob", "Yoav" ] } diff --git a/controllers/managePipelineEvent/index.ts b/controllers/managePipelineEvent/index.ts new file mode 100644 index 0000000..6fc0b91 --- /dev/null +++ b/controllers/managePipelineEvent/index.ts @@ -0,0 +1,121 @@ +import service from "../../service" +import netTool from "../../service/netTool" +import { Gitlab } from "../../types/gitlab" +import { Notify } from "../../types/notify" +import { sec2minStr } from "../../utils/timeTools" + +/** + * 判断是否是合并请求 + * @param pipeline + * @returns + */ +const checkIsMergeCommit = (pipeline: Gitlab.PipelineEvent) => { + const regex = /^Merge branch '.*' into '.*'$/ + return regex.test(pipeline.commit.title) +} + +/** + * 判断是否是成功的CI + * @param pipeline + * @returns + */ +const checkIsSuccess = (pipeline: Gitlab.PipelineEvent) => { + return pipeline.object_attributes.status === "success" +} + +/** + * 获取合并请求 + * @param pipeline + * @returns + */ +const getMergeRequest = async (pipeline: Gitlab.PipelineEvent) => { + if (!checkIsMergeCommit(pipeline)) return null + const res = await service.gitlab.commit.getMr( + pipeline.project.id, + pipeline.object_attributes.sha + ) + if (res.length === 0) return null + return res.find((mr) => mr.merge_commit_sha === pipeline.commit.id) || null +} + +/** + * 获取用户信息 + * @param pipeline + * @param mergeRequest + * @returns + */ +const getUserInfo = ( + pipeline: Gitlab.PipelineEvent, + mergeRequest: Gitlab.MergeRequest | null +) => { + let participant = pipeline.user.name + const receiver = [pipeline.user.username] + // 有MR且用户不同 + if (mergeRequest && mergeRequest.author.username !== pipeline.user.username) { + participant += `、${mergeRequest.author.name}` + receiver.push(mergeRequest.author.username) + } + return { participant, receiver } +} + +/** + * 获取机器人消息 + * @param variable 模板变量 + * @returns + */ +const getRobotMsg = async (variable: Notify.CardVariable) => + JSON.stringify({ + type: "template", + data: { + config: { + update_multi: true, + }, + template_id: "ctp_AA36QafWyob2", + template_variable: variable, + }, + }) + +/** + * 发送通知消息 + * @param pipeline + * @param apiKey + * @returns + */ +const sendNotifyMsg = async ( + pipeline: Gitlab.PipelineEvent, + apiKey: string +) => { + // 只处理成功的CICD + if (!checkIsSuccess(pipeline)) return netTool.ok() + // 获取对应的合并请求 + const mergeRequest = await getMergeRequest(pipeline) + // 获取用户信息 + const { participant, receiver } = getUserInfo(pipeline, mergeRequest) + // 获取模板变量 + const variable: Notify.CardVariable = { + project: pipeline.project.path_with_namespace, + project_link: pipeline.project.web_url, + pipeline: pipeline.object_attributes.id.toString(), + pipeline_link: `${pipeline.project.web_url}/-/pipelines`, + ref: pipeline.object_attributes.ref, + ref_link: `${pipeline.project.web_url}/-/commits/${pipeline.object_attributes.ref}`, + commit_user: pipeline.user.name, + duration: sec2minStr(pipeline.object_attributes.duration), + participant, + commit_message: pipeline.commit.title, + mr: mergeRequest ? mergeRequest.references.full : "无关联的MR", + mr_link: mergeRequest ? mergeRequest.web_url : "", + } + // 获取机器人消息 + const robotMsg = await getRobotMsg(variable) + // 发送消息 + await service.message.byUserIdList(receiver, robotMsg, apiKey) + // 返回成功 + return netTool.ok() +} + +const managePipelineEvent = { + sendNotifyMsg, +} + +export default managePipelineEvent diff --git a/index.ts b/index.ts index cfcf6d4..d0b3284 100644 --- a/index.ts +++ b/index.ts @@ -1,4 +1,5 @@ import { manageCIMonitorReq } from "./routes/ci" +import { manageGitlabEventReq } from "./routes/event" import initSchedule from "./schedule" import netTool from "./service/netTool" @@ -13,6 +14,8 @@ const server = Bun.serve({ if (url.pathname === "/") return netTool.ok("hello, glade to see you!") // CI 监控 if (url.pathname === "/ci") return manageCIMonitorReq(req) + // Gitlab 事件 + if (url.pathname === "/event") return manageGitlabEventReq(req) // 其他 return netTool.ok("hello, glade to see you!") } catch (error: any) { diff --git a/routes/ci/index.ts b/routes/ci/index.ts index e4d0e77..ea471ef 100644 --- a/routes/ci/index.ts +++ b/routes/ci/index.ts @@ -8,15 +8,8 @@ import netTool from "../../service/netTool" */ export const manageCIMonitorReq = (req: Request) => { const url = new URL(req.url) - if (url.pathname === "/ci") { - const chat_id = url.searchParams.get("chat_id") - if (!chat_id) return netTool.badRequest("chat_id is required!") - manageRobot.sendCIReportByChatId(chat_id) - return Response.json({ - code: 0, - msg: "success", - data: "reporting...", - }) - } - return netTool.ok() + const chat_id = url.searchParams.get("chat_id") + if (!chat_id) return netTool.badRequest("chat_id is required!") + manageRobot.sendCIReportByChatId(chat_id) + return netTool.ok("reporting...") } diff --git a/routes/event/index.ts b/routes/event/index.ts new file mode 100644 index 0000000..23b9d1a --- /dev/null +++ b/routes/event/index.ts @@ -0,0 +1,20 @@ +import managePipelineEvent from "../../controllers/managePipelineEvent" +import netTool from "../../service/netTool" +import { Gitlab } from "../../types/gitlab" + +/** + * 处理管理Gitlab事件的请求。 + * @param req - 请求对象。 + * @returns 响应对象。 + */ +export const manageGitlabEventReq = async (req: Request) => { + const apiKey = req.headers.get("x-gitlab-token") + if (!apiKey) return netTool.badRequest("x-gitlab-token is required!") + const eventType = req.headers.get("x-gitlab-event") + // 只处理流水线钩子 + if (eventType === "Pipeline Hook") { + const body = (await req.json()) as Gitlab.PipelineEvent + return managePipelineEvent.sendNotifyMsg(body, apiKey) + } + return netTool.ok() +} diff --git a/script/notify/mr.json b/script/notify/mr.json new file mode 100644 index 0000000..bdccb91 --- /dev/null +++ b/script/notify/mr.json @@ -0,0 +1,17 @@ +[ + { + "title": "feat: 修改错别字", + "target_branch": "preview", + "source_branch": "feat/pai", + "author": { + "username": "wujiali5", + "name": "伍嘉丽" + }, + "sha": "f5485b7eebf700476d6cfb27e50a85309dd144d1", + "merge_commit_sha": "b18338018b421918aa4f5dc4da3e0b50d728fd78", + "references": { + "full": "cloudml-visuals/fe/cloud-ml-fe!374" + }, + "web_url": "https://git.n.xiaomi.com/cloudml-visuals/fe/cloud-ml-fe/-/merge_requests/374" + } +] \ No newline at end of file diff --git a/script/notify/pipline.json b/script/notify/pipline.json new file mode 100644 index 0000000..d0fba7d --- /dev/null +++ b/script/notify/pipline.json @@ -0,0 +1,25 @@ +{ + "object_attributes": { + "id": 8778929, + "ref": "ci", + "sha": "fc0fdf57c3b662b296b89dc2a289798d130d1be1", + "status": "success", + "created_at": "2024-07-23 08:57:14 +0800", + "finished_at": "2024-07-23 08:57:24 +0800", + "duration": 5 + }, + "user": { + "name": "赵英博", + "username": "zhaoyingbo" + }, + "project": { + "id": 145623, + "web_url": "https://git.n.xiaomi.com/cloudml-visuals/fe/test", + "path_with_namespace": "cloudml-visuals/fe/test" + }, + "commit": { + "id": "fc0fdf57c3b662b296b89dc2a289798d130d1be1", + "title": "chore: Add .gitlab-ci.yml for printing \"Hello, world!\"", + "url": "https://git.n.xiaomi.com/cloudml-visuals/fe/test/-/commit/fc0fdf57c3b662b296b89dc2a289798d130d1be1" + } +} diff --git a/script/pipline/pending.json b/script/pipline/pending.json deleted file mode 100644 index 6262703..0000000 --- a/script/pipline/pending.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "object_kind": "pipeline", - "object_attributes": { - "id": 8779191, - "ref": "main", - "tag": false, - "sha": "66ac20f0f6a839f29753b17dcdf12deb91148fed", - "before_sha": "39a21d2cb3a0d39d2fb0df81a244ebeae8989ce4", - "source": "push", - "status": "pending", - "detailed_status": "pending", - "stages": ["print"], - "created_at": "2024-07-23 09:28:44 +0800", - "finished_at": null, - "duration": null, - "queued_duration": null, - "variables": [] - }, - "merge_request": null, - "user": { - "id": 30441, - "name": "伍嘉丽", - "username": "wujiali5", - "avatar_url": "https://git.n.xiaomi.com/uploads/-/system/user/avatar/30441/avatar.png", - "email": "[REDACTED]" - }, - "project": { - "id": 145623, - "name": "test", - "description": "", - "web_url": "https://git.n.xiaomi.com/cloudml-visuals/fe/test", - "avatar_url": null, - "git_ssh_url": "git@git.n.xiaomi.com:cloudml-visuals/fe/test.git", - "git_http_url": "https://git.n.xiaomi.com/cloudml-visuals/fe/test.git", - "namespace": "fe", - "visibility_level": 0, - "path_with_namespace": "cloudml-visuals/fe/test", - "default_branch": "main", - "ci_config_path": "" - }, - "commit": { - "id": "66ac20f0f6a839f29753b17dcdf12deb91148fed", - "message": "Merge branch 'ci' into 'main'\n\nchore: Update .gitlab-ci.yml to print \"Hello, world~\"\n\nSee merge request cloudml-visuals/fe/test!2", - "title": "Merge branch 'ci' into 'main'", - "timestamp": "2024-07-23T09:28:43+08:00", - "url": "https://git.n.xiaomi.com/cloudml-visuals/fe/test/-/commit/66ac20f0f6a839f29753b17dcdf12deb91148fed", - "author": { - "name": "伍嘉丽", - "email": "wujiali5@xiaomi.com" - } - }, - "builds": [ - { - "id": 25375438, - "stage": "print", - "name": "print", - "status": "pending", - "created_at": "2024-07-23 09:28:44 +0800", - "started_at": null, - "finished_at": null, - "duration": null, - "queued_duration": 3.506639987, - "when": "on_success", - "manual": false, - "allow_failure": false, - "user": { - "id": 30441, - "name": "伍嘉丽", - "username": "wujiali5", - "avatar_url": "https://git.n.xiaomi.com/uploads/-/system/user/avatar/30441/avatar.png", - "email": "[REDACTED]" - }, - "runner": null, - "artifacts_file": { - "filename": null, - "size": null - }, - "environment": null - } - ] -} diff --git a/script/pipline/success.json b/script/pipline/success.json deleted file mode 100644 index 886e5c3..0000000 --- a/script/pipline/success.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "object_kind": "pipeline", - "object_attributes": { - "id": 8778929, - "ref": "ci", - "tag": false, - "sha": "fc0fdf57c3b662b296b89dc2a289798d130d1be1", - "before_sha": "0000000000000000000000000000000000000000", - "source": "push", - "status": "success", - "detailed_status": "passed", - "stages": ["print"], - "created_at": "2024-07-23 08:57:14 +0800", - "finished_at": "2024-07-23 08:57:24 +0800", - "duration": 5, - "queued_duration": 3, - "variables": [] - }, - "merge_request": null, - "user": { - "id": 10011, - "name": "赵英博", - "username": "zhaoyingbo", - "avatar_url": "https://git.n.xiaomi.com/uploads/-/system/user/avatar/10011/avatar.png", - "email": "zhaoyingbo@live.cn" - }, - "project": { - "id": 145623, - "name": "test", - "description": "", - "web_url": "https://git.n.xiaomi.com/cloudml-visuals/fe/test", - "avatar_url": null, - "git_ssh_url": "git@git.n.xiaomi.com:cloudml-visuals/fe/test.git", - "git_http_url": "https://git.n.xiaomi.com/cloudml-visuals/fe/test.git", - "namespace": "fe", - "visibility_level": 0, - "path_with_namespace": "cloudml-visuals/fe/test", - "default_branch": "main", - "ci_config_path": "" - }, - "commit": { - "id": "fc0fdf57c3b662b296b89dc2a289798d130d1be1", - "message": "chore: Add .gitlab-ci.yml for printing \"Hello, world!\"\n", - "title": "chore: Add .gitlab-ci.yml for printing \"Hello, world!\"", - "timestamp": "2024-07-23T08:57:13+08:00", - "url": "https://git.n.xiaomi.com/cloudml-visuals/fe/test/-/commit/fc0fdf57c3b662b296b89dc2a289798d130d1be1", - "author": { - "name": "zhaoyingbo", - "email": "zhaoyingbo@xiaomi.com" - } - }, - "builds": [ - { - "id": 25374785, - "stage": "print", - "name": "print", - "status": "success", - "created_at": "2024-07-23 08:57:14 +0800", - "started_at": "2024-07-23 08:57:18 +0800", - "finished_at": "2024-07-23 08:57:24 +0800", - "duration": 5.914004, - "queued_duration": 3.550911, - "when": "on_success", - "manual": false, - "allow_failure": false, - "user": { - "id": 10011, - "name": "赵英博", - "username": "zhaoyingbo", - "avatar_url": "https://git.n.xiaomi.com/uploads/-/system/user/avatar/10011/avatar.png", - "email": "zhaoyingbo@live.cn" - }, - "runner": { - "id": 9134, - "description": "cloudml-fe-bj", - "runner_type": "group_type", - "active": true, - "is_shared": false, - "tags": ["cloudml-fe-bj"] - }, - "artifacts_file": { - "filename": null, - "size": null - }, - "environment": null - } - ] -} diff --git a/service/gitlab/commit.ts b/service/gitlab/commit.ts index a74d63d..ff794ec 100644 --- a/service/gitlab/commit.ts +++ b/service/gitlab/commit.ts @@ -1,3 +1,4 @@ +import { Gitlab } from "../../types/gitlab" import netTool from "../netTool" import { GITLAB_AUTH_HEADER, GITLAB_BASE_URL, gitlabReqWarp } from "./tools" @@ -9,7 +10,7 @@ import { GITLAB_AUTH_HEADER, GITLAB_BASE_URL, gitlabReqWarp } from "./tools" */ const getMr = async (project_id: number, sha: string) => { const URL = `${GITLAB_BASE_URL}/projects/${project_id}/repository/commits/${sha}/merge_requests` - return gitlabReqWarp( + return gitlabReqWarp( () => netTool.get(URL, {}, GITLAB_AUTH_HEADER), [] ) diff --git a/service/message/index.ts b/service/message/index.ts index c0b39d6..4f77ea8 100644 --- a/service/message/index.ts +++ b/service/message/index.ts @@ -41,4 +41,18 @@ message.byUserId = async (user_id: string, content: string) => { }) } +message.byUserIdList = async ( + user_id_list: string[], + content: string, + api_key?: string +) => { + return message({ + receive_id: user_id_list.join(","), + receive_id_type: "user_id", + msg_type: "interactive", + content, + api_key, + }) +} + export default message diff --git a/types/gitlab.ts b/types/gitlab.ts index 21cc92a..6e16e87 100644 --- a/types/gitlab.ts +++ b/types/gitlab.ts @@ -1,12 +1,12 @@ export namespace Gitlab { - /* 定义错误接口 */ + /* 错误 */ export interface Error { /** * 错误消息 */ message: string } - /* 定义用户接口 */ + /* 用户 */ export interface User { /** * 用户ID @@ -34,7 +34,7 @@ export namespace Gitlab { web_url: string } - /* 定义项目详情接口 */ + /* 项目详情 */ export interface ProjDetail { /** * 项目ID @@ -66,7 +66,7 @@ export namespace Gitlab { message?: string } - /* 定义管道详情接口 */ + /* 管道详情 */ export interface PipelineDetail { /** * 管道ID @@ -114,7 +114,7 @@ export namespace Gitlab { message?: string } - /* 定义管道接口 */ + /* 管道 */ export interface Pipeline { /** * 管道ID @@ -154,7 +154,7 @@ export namespace Gitlab { web_url: string } - /* 定义徽章接口 */ + /* 徽章 */ export interface Badge { /** * 徽章ID @@ -186,12 +186,16 @@ export namespace Gitlab { kind: "project" | "group" } - /* 定义管道事件接口 */ + /* 管道事件 */ export interface PipelineEvent { /** * 对象属性,包含了构建的详细信息 */ object_attributes: { + /** + * 管道ID + */ + id: number /** * 分支名 */ @@ -259,6 +263,60 @@ export namespace Gitlab { * 提交的标题 */ title: string + /** + * 提交的链接URL + */ + url: string } } + + /* 合并请求 */ + export interface MergeRequest { + /** + * 合并请求的标题 + */ + title: string + /** + * 目标分支 + */ + target_branch: string + /** + * 源分支 + */ + source_branch: string + /** + * 作者信息 + */ + author: { + /** + * 用户名 + */ + username: string + /** + * 用户姓名 + */ + name: string + } + /** + * 提交的SHA值 + */ + sha: string + /** + * 合并提交的SHA值 + */ + merge_commit_sha: string + /** + * 引用信息 + */ + references: { + /** + * 完整引用 + */ + full: string + } + /** + * 合并请求的web URL + */ + web_url: string + } } diff --git a/types/notify.ts b/types/notify.ts new file mode 100644 index 0000000..9ff955d --- /dev/null +++ b/types/notify.ts @@ -0,0 +1,16 @@ +export namespace Notify { + export interface CardVariable { + project: string + project_link: string + pipeline: string + pipeline_link: string + ref: string + ref_link: string + commit_user: string + duration: string + participant: string + commit_message: string + mr: string + mr_link: string + } +} diff --git a/utils/timeTools.ts b/utils/timeTools.ts index dff9c61..9552cdc 100644 --- a/utils/timeTools.ts +++ b/utils/timeTools.ts @@ -20,3 +20,12 @@ export const getPrevWeekWithYear = () => { export const sec2min = (sec: number) => { return (sec / 60).toFixed(1) } + +/** + * 秒转分钟,格式为 1m 30s + */ +export const sec2minStr = (sec: number) => { + const min = Math.floor(sec / 60) + const s = sec % 60 + return `${min}m ${s}s` +}