diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c4785db..7eae02c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,33 +1,24 @@ { "name": "ci_monitor", - "image": "git.yingbo.im:333/zhaoyingbo/dev:bun", - "remoteUser": "bun", - "containerUser": "bun", + "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04", "customizations": { "vscode": { "settings": { "files.autoSave": "afterDelay", "editor.guides.bracketPairs": true, "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.fixAll.stylelint": true - } + "editor.formatOnSave": true }, "extensions": [ + "eamodio.gitlens", + "Gruntfuggly.todo-tree", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", - "eamodio.gitlens", - "unifiedjs.vscode-mdx", - "oderwat.indent-rainbow", - "jock.svg", "ChakrounAnas.turbo-console-log", - "Gruntfuggly.todo-tree", - "MS-CEINTL.vscode-language-pack-zh-hans", - "stylelint.vscode-stylelint", - "GitHub.copilot", - "streetsidesoftware.code-spell-checker" + "streetsidesoftware.code-spell-checker", + "MS-CEINTL.vscode-language-pack-zh-hans" ] } - } + }, + "onCreateCommand": "curl -fsSL https://bun.sh/install | bash" } diff --git a/controllers/managePipeLine/index.ts b/controllers/managePipeLine/index.ts index dc6c1f6..8f65b05 100644 --- a/controllers/managePipeLine/index.ts +++ b/controllers/managePipeLine/index.ts @@ -1,13 +1,13 @@ import db from "../../db"; -import { PipelineRecordModel } from "../../db/pipeline"; -import { ProjectRecordModel } from "../../db/project"; import service from "../../service"; import moment from "moment"; +import { Gitlab } from "../../types/gitlab"; +import { DB } from "../../types/db"; /** * 获取全部的pipeline列表 */ -const getFullPipelineList = async (project: ProjectRecordModel) => { +const getFullPipelineList = async (project: DB.Project) => { // 先获取最新的pipelineID const latestOne = await db.pipeline.getLatestOne(project.id); // 获取本次数据获取的截止时间,如果没有,则获取从20240101到现在所有pipeline信息 @@ -15,11 +15,11 @@ const getFullPipelineList = async (project: ProjectRecordModel) => { latestOne?.created_at || "2024-01-01T00:00:00.000+08:00" ); // 获取pipeline列表并保存 - const fullPipelineList: GitlabPipeline[] = []; + const fullPipelineList: Gitlab.Pipeline[] = []; let page = 1; let hasBeforeLatestTime = false; while (!hasBeforeLatestTime) { - const pipelines = await service.gitlab.fetchPipelines( + const pipelines = await service.gitlab.pipeline.getList( project.project_id, page++ ); @@ -37,20 +37,20 @@ const getFullPipelineList = async (project: ProjectRecordModel) => { } const fullPipelineDetailList = await Promise.all( fullPipelineList.map(({ project_id, id, created_at }) => - service.gitlab.fetchPipelineDetails(project_id, id, created_at) + service.gitlab.pipeline.getDetail(project_id, id, created_at) ) ); - return fullPipelineDetailList.filter( - (v) => v - ) as GitlabPipelineDetailWithCreateAt[]; + return fullPipelineDetailList.filter((v) => v) as (Gitlab.PipelineDetail & { + created_at: string; + })[]; }; const insertFullPipelineList = async ( - fullPipelineList: GitlabPipelineDetailWithCreateAt[][], + fullPipelineList: (Gitlab.PipelineDetail & { created_at: string })[][], fullUserMap: Record, fullProjectMap: Record ) => { - const dbPipelineList: Partial = []; + const dbPipelineList: Partial = []; fullPipelineList.forEach((pipelineList) => { pipelineList.forEach((pipeline) => { @@ -73,9 +73,7 @@ const insertFullPipelineList = async ( }); }); await Promise.all( - dbPipelineList.map((v: Partial) => - db.pipeline.create(v) - ) + dbPipelineList.map((v: Partial) => db.pipeline.create(v)) ); }; diff --git a/controllers/manageProject/index.ts b/controllers/manageProject/index.ts index 2035217..ddfb713 100644 --- a/controllers/manageProject/index.ts +++ b/controllers/manageProject/index.ts @@ -1,18 +1,16 @@ import db from "../../db"; -import { ProjectRecordModel } from "../../db/project"; import service from "../../service"; +import { DB } from "../../types/db"; /** * 填充项目信息 */ -const fillProj = async (project: ProjectRecordModel) => { - const projDetail = await service.gitlab.fetchProjectDetails( - project.project_id - ); +const fillProj = async (project: DB.Project) => { + const projDetail = await service.gitlab.project.getDetail(project.project_id); if (!projDetail) { return project; } - const useFulParams: Partial = { + const useFulParams: Partial = { ...project, avatar_url: projDetail.avatar_url, description: projDetail.description, @@ -40,7 +38,7 @@ const getFullProjList = async () => { return filledFullProjList; }; -const getFullProjectMap = (fullProjList: ProjectRecordModel[]) => { +const getFullProjectMap = (fullProjList: DB.Project[]) => { const fullProjectMap: Record = {}; fullProjList.forEach((item) => { fullProjectMap[item.project_id] = item.id; diff --git a/controllers/manageRobot/index.ts b/controllers/manageRobot/index.ts index ad1155b..62aa70b 100644 --- a/controllers/manageRobot/index.ts +++ b/controllers/manageRobot/index.ts @@ -121,7 +121,7 @@ const getRobotMsg = async () => * @param chat_id */ const sendCIReportByChatId = async (chat_id: string) => { - await service.sendMessage.byChatId(chat_id, await getRobotMsg()); + await service.message.byChatId(chat_id, await getRobotMsg()); }; /** @@ -129,7 +129,7 @@ const sendCIReportByChatId = async (chat_id: string) => { * @returns */ const sendCIReportByCron = async () => - await service.sendMessage.byGroupId("52usf3w8l6z4vs1", await getRobotMsg()); + await service.message.byGroupId("52usf3w8l6z4vs1", await getRobotMsg()); const manageRobot = { sendCIReportByChatId, diff --git a/controllers/manageUser/index.ts b/controllers/manageUser/index.ts index 066e605..5f24e2d 100644 --- a/controllers/manageUser/index.ts +++ b/controllers/manageUser/index.ts @@ -1,7 +1,8 @@ import db from "../../db"; +import { Gitlab } from "../../types/gitlab"; -const getFullUserMap = async (fullPipelineList: GitlabPipelineDetail[][]) => { - const userList: GitlabUser[] = []; +const getFullUserMap = async (fullPipelineList: Gitlab.PipelineDetail[][]) => { + const userList: Gitlab.User[] = []; fullPipelineList.forEach((fullPipeline) => { fullPipeline.forEach((item) => { if (item.user && !userList.find((v) => v.id === item.user?.id)) { diff --git a/db/pipeline/index.ts b/db/pipeline/index.ts index ff969ac..32bbc1c 100644 --- a/db/pipeline/index.ts +++ b/db/pipeline/index.ts @@ -1,33 +1,24 @@ -import { RecordModel } from "pocketbase"; import pbClient from "../pbClient"; import { managePb404 } from "../../utils/pbTools"; +import { DB } from "../../types/db"; -export interface PipelineRecordModel extends RecordModel { - project_id: string; - user_id: string; - pipeline_id: number; - ref: string; - status: string; - web_url: string; - // 2024-03-06 02:53:59.509Z - created_at: string; - started_at: string; - finished_at: string; - duration: number; - queued_duration: number; -} - +/** + * 通过其 ID 检索一个管道。 + * @param id 管道的 ID。 + * @returns 一个解析为管道对象的 promise。 + */ const getOne = (id: string) => - managePb404( + managePb404( async () => await pbClient.collection("pipeline").getOne(id) ); /** - * 获取项目最新一次构建 - * @param project_id 项目id + * 检索项目的最新管道。 + * @param project_id 项目的 ID。 + * @returns 一个解析为最新管道对象的 promise。 */ const getLatestOne = (project_id: string) => { - return managePb404( + return managePb404( async () => await pbClient .collection("pipeline") @@ -37,12 +28,18 @@ const getLatestOne = (project_id: string) => { ); }; -const create = async (data: Partial) => - await pbClient.collection("pipeline").create(data); +/** + * 创建一个新的管道。 + * @param data 新管道的数据。 + * @returns 一个解析为创建的管道对象的 promise。 + */ +const create = async (data: Partial) => + await pbClient.collection("pipeline").create(data); const pipeline = { create, getOne, getLatestOne, }; + export default pipeline; diff --git a/db/project/index.ts b/db/project/index.ts index bafd737..cd008d6 100644 --- a/db/project/index.ts +++ b/db/project/index.ts @@ -1,28 +1,36 @@ -import { RecordModel } from "pocketbase"; import { managePb404 } from "../../utils/pbTools"; import pbClient from "../pbClient"; +import { DB } from "../../types/db"; -export interface ProjectRecordModel extends RecordModel { - project_id: number; - description: string; - name: string; - path_with_namespace: string; - web_url: string; - avatar_url: string; - has_new_cicd: boolean; -} - +/** + * 通过其 ID 检索单个项目。 + * @param id - 项目的 ID。 + * @returns 一个解析为项目对象的 promise。 + */ const getOne = (id: string) => - managePb404( + managePb404( async () => await pbClient.collection("project").getOne(id) ); +/** + * 检索项目的完整列表。 + * @returns 一个解析为项目对象数组的 promise。 + */ const getFullList = async () => - await pbClient.collection("project").getFullList(); + await pbClient.collection("project").getFullList(); -const update = async (id: string, data: Partial) => - await pbClient.collection("project").update(id, data); +/** + * 使用新数据更新项目。 + * @param id - 要更新的项目的 ID。 + * @param data - 用于更新项目的部分数据。 + * @returns 一个解析为更新后的项目对象的 promise。 + */ +const update = async (id: string, data: Partial) => + await pbClient.collection("project").update(id, data); +/** + * 用于管理项目的函数集合。 + */ const project = { getFullList, getOne, diff --git a/db/user/index.ts b/db/user/index.ts index 389a9ec..b003673 100644 --- a/db/user/index.ts +++ b/db/user/index.ts @@ -1,22 +1,24 @@ -import { RecordModel } from "pocketbase"; import { managePb404 } from "../../utils/pbTools"; import pbClient from "../pbClient"; +import { DB } from "../../types/db"; -export interface UserRecordModel extends RecordModel { - user_id: number; - username: string; - name: string; - avatar_url: string; - web_url: string; -} - +/** + * 根据提供的ID从数据库检索单个用户。 + * @param id 要检索的用户的ID。 + * @returns 如果找到,返回解析为用户对象的promise;如果未找到,抛出404错误。 + */ const getOne = (id: string) => - managePb404( + managePb404( async () => await pbClient.collection("user").getOne(id) ); +/** + * 根据提供的用户ID从数据库检索单个用户。 + * @param user_id 要检索的用户的用户ID。 + * @returns 如果找到,返回解析为用户对象的promise;如果未找到,抛出404错误。 + */ const getOneByUserId = (user_id: number) => { - return managePb404( + return managePb404( async () => await pbClient .collection("user") @@ -26,16 +28,32 @@ const getOneByUserId = (user_id: number) => { ); }; -const create = async (data: Partial) => - await pbClient.collection("user").create(data); +/** + * 在数据库中创建一个新用户。 + * @param data 新用户的数据。 + * @returns 返回解析为已创建用户对象的promise。 + */ +const create = async (data: Partial) => + await pbClient.collection("user").create(data); -const upsert = async (data: Partial) => { +/** + * 在数据库中插入或更新一个用户。 + * 如果具有相同用户ID的用户已存在,则更新现有用户。 + * 如果具有相同用户ID的用户不存在,则创建一个新用户。 + * @param data 要插入或更新的用户数据。 + * @returns 返回解析为插入或更新的用户对象的promise。 + * 如果数据不包含用户ID,则返回null。 + */ +const upsert = async (data: Partial) => { if (!data.user_id) return null; const userInfo = await getOneByUserId(data.user_id); if (userInfo) return userInfo; return await create(data); }; +/** + * 用户模块提供了与数据库中的用户集合交互的函数。 + */ const user = { create, upsert, diff --git a/db/view/index.ts b/db/view/index.ts index 7a48a75..4c0fb9f 100644 --- a/db/view/index.ts +++ b/db/view/index.ts @@ -1,25 +1,14 @@ -import { RecordModel } from "pocketbase"; import { managePb404 } from "../../utils/pbTools"; import pbClient from "../pbClient"; +import { DB } from "../../types/db"; -export interface StatisticsPerWeekRecordModel extends RecordModel { - week: string; - total_count: number; - failed_count: number; - success_count: number; - success_rate: number; - duration: number; -} - -export interface StatisticsPerProjRecordModel extends RecordModel { - week: string; - name: string; - duration: number; - ref: string; -} - +/** + * 根据给定的周来检索完整的统计信息。 + * @param week - 需要检索统计信息的周。 + * @returns 一个解析为指定周的完整统计信息的promise。 + */ const getFullStatisticsByWeek = (week: string) => { - return managePb404( + return managePb404( async () => await pbClient .collection("statisticsPerWeek") @@ -27,8 +16,13 @@ const getFullStatisticsByWeek = (week: string) => { ); }; +/** + * 根据给定的周来检索项目统计信息。 + * @param week - 需要检索统计信息的周。 + * @returns 一个解析为指定周的项目统计信息数组的promise。 + */ const getProjStatisticsByWeek = (week: string) => { - return managePb404( + return managePb404( async () => await pbClient .collection("statisticsPerProj") diff --git a/index.ts b/index.ts index ea4d48e..4b7ba66 100644 --- a/index.ts +++ b/index.ts @@ -1,4 +1,4 @@ -import manageRobot from "./controllers/manageRobot"; +import { manageCIMonitorReq } from "./routes/ci"; import initSchedule from "./schedule"; import netTool from "./service/netTool"; @@ -7,18 +7,19 @@ initSchedule(); const server = Bun.serve({ async fetch(req) { - 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...", - }); + try { + const url = new URL(req.url); + // 根路由 + if (url.pathname === "/") return netTool.ok("hello, glade to see you!"); + // CI 监控 + if (url.pathname === "/ci") return manageCIMonitorReq(req); + // 其他 + return netTool.ok("hello, glade to see you!"); + } catch (error: any) { + // 错误处理 + console.error("🚀 ~ serve ~ error", error); + return netTool.serverError(error.message || "server error"); } - return netTool.ok(); }, port: 3000, }); diff --git a/routes/ci/index.ts b/routes/ci/index.ts new file mode 100644 index 0000000..4225965 --- /dev/null +++ b/routes/ci/index.ts @@ -0,0 +1,22 @@ +import manageRobot from "../../controllers/manageRobot"; +import netTool from "../../service/netTool"; + +/** + * 处理管理CI监视器的请求。 + * @param req - 请求对象。 + * @returns 响应对象。 + */ +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(); +}; diff --git a/badge/cloudml.ts b/script/badge/cloudml.ts similarity index 90% rename from badge/cloudml.ts rename to script/badge/cloudml.ts index 98f1489..ff8e08e 100644 --- a/badge/cloudml.ts +++ b/script/badge/cloudml.ts @@ -1,4 +1,6 @@ -import service from "../service"; +import service from "../../service"; +import { BadgeSetParams } from "../../service/gitlab/badge"; +import { Gitlab } from "../../types/gitlab"; const projectList = [ // "cloud-ml/cloudml-maas", @@ -23,14 +25,14 @@ const projectList = [ ]; const getNewProjectBadge = async ( - projectDetail: GitlabProjDetail -): Promise => { + projectDetail: Gitlab.ProjDetail +): Promise => { // 项目路径 cloud-ml/cloudml-dev const projectPath = projectDetail.path_with_namespace; // 根据项目路径获取sonarqubeId 类似于 cloud-ml/cloudml-dev -> cloud-ml:cloudml-dev const sonarqubeId = projectPath.replace("/", ":"); // 获取项目的badges - const badges: GitlabBadge[] = await service.gitlab.fetchProjectBadges( + const badges: Gitlab.Badge[] = await service.gitlab.badge.get( projectDetail.id ); // 对badges进行补全,可能只有 name: "sonarqube_coverage" 的情况,把剩下的补全 @@ -44,7 +46,7 @@ const getNewProjectBadge = async ( "sonarqube_quality_gate", ]; const diff = [...badgeNameList].filter((x) => !badgeNameSet.has(x)); - const newBadges: GitlabBadgeSetParams[] = diff.map((name) => { + const newBadges: BadgeSetParams[] = diff.map((name) => { const link_url = encodeURI( `https://sonarqube.mioffice.cn/dashboard?id=${sonarqubeId}` ); @@ -65,16 +67,12 @@ const getNewProjectBadge = async ( return newBadges; }; -const addNewProjectBadge = async ( - badgeSetParamsList: GitlabBadgeSetParams[] -) => { +const addNewProjectBadge = async (badgeSetParamsList: BadgeSetParams[]) => { const chunkSize = 5; // 每次并发请求的数量 for (let i = 0; i < badgeSetParamsList.length; i += chunkSize) { const chunk = badgeSetParamsList.slice(i, i + chunkSize); const res = await Promise.all( - chunk.map((badgeSetParams) => - service.gitlab.addProjectBadge(badgeSetParams) - ) + chunk.map((badgeSetParams) => service.gitlab.badge.add(badgeSetParams)) ); console.log(res); } @@ -84,9 +82,7 @@ const main = async () => { const errorList: string[] = []; const badgeAddParamsList = await Promise.all( projectList.map(async (projectName) => { - const projectDetail = await service.gitlab.fetchProjectDetails( - projectName - ); + const projectDetail = await service.gitlab.project.getDetail(projectName); if (!projectDetail) { errorList.push(projectName); return []; diff --git a/badge/miai.ts b/script/badge/miai.ts similarity index 84% rename from badge/miai.ts rename to script/badge/miai.ts index 6c52af1..b5f9e66 100644 --- a/badge/miai.ts +++ b/script/badge/miai.ts @@ -1,4 +1,6 @@ -import service from "../service"; +import service from "../../service"; +import { BadgeSetParams } from "../../service/gitlab/badge"; +import { Gitlab } from "../../types/gitlab"; const projectList = [ "miai-fe/fe/ai-admin-fe", @@ -54,16 +56,14 @@ const projectList = [ ]; const getProjectId = async (projectName: string) => { - const res = await service.gitlab.fetchProjectDetails(projectName); + const res = await service.gitlab.project.getDetail(projectName); return res?.id; }; const getNewProjectBadge = async ( projectId: number -): Promise => { - const badges: GitlabBadge[] = await service.gitlab.fetchProjectBadges( - projectId - ); +): Promise => { + const badges: Gitlab.Badge[] = await service.gitlab.badge.get(projectId); const replacePath = (value: string) => value.replace( @@ -71,7 +71,7 @@ const getNewProjectBadge = async ( "http://scan.sonarqube.xiaomi.srv" ); - return badges.map((badge: GitlabBadge) => ({ + return badges.map((badge: Gitlab.Badge) => ({ id: projectId, badge_id: badge.id, link_url: replacePath(badge.link_url), @@ -81,16 +81,12 @@ const getNewProjectBadge = async ( })); }; -const setNewProjectBadge = async ( - badgeSetParamsList: GitlabBadgeSetParams[] -) => { +const setNewProjectBadge = async (badgeSetParamsList: BadgeSetParams[]) => { const chunkSize = 5; // 每次并发请求的数量 for (let i = 0; i < badgeSetParamsList.length; i += chunkSize) { const chunk = badgeSetParamsList.slice(i, i + chunkSize); const res = await Promise.all( - chunk.map((badgeSetParams) => - service.gitlab.setProjectBadge(badgeSetParams) - ) + chunk.map((badgeSetParams) => service.gitlab.badge.set(badgeSetParams)) ); console.log(res); } diff --git a/script/pipline/pending.json b/script/pipline/pending.json new file mode 100644 index 0000000..c299626 --- /dev/null +++ b/script/pipline/pending.json @@ -0,0 +1,83 @@ +{ + "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 + } + ] +} \ No newline at end of file diff --git a/script/pipline/running.json b/script/pipline/running.json new file mode 100644 index 0000000..6ac9831 --- /dev/null +++ b/script/pipline/running.json @@ -0,0 +1,92 @@ +{ + "object_kind": "pipeline", + "object_attributes": { + "id": 8778929, + "ref": "ci", + "tag": false, + "sha": "fc0fdf57c3b662b296b89dc2a289798d130d1be1", + "before_sha": "0000000000000000000000000000000000000000", + "source": "push", + "status": "running", + "detailed_status": "running", + "stages": [ + "print" + ], + "created_at": "2024-07-23 08:57:14 +0800", + "finished_at": null, + "duration": null, + "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": "running", + "created_at": "2024-07-23 08:57:14 +0800", + "started_at": "2024-07-23 08:57:18 +0800", + "finished_at": null, + "duration": 2.977211601, + "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 + } + ] +} \ No newline at end of file diff --git a/script/pipline/success.json b/script/pipline/success.json new file mode 100644 index 0000000..c94e969 --- /dev/null +++ b/script/pipline/success.json @@ -0,0 +1,92 @@ +{ + "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 + } + ] +} \ No newline at end of file diff --git a/service/gitlab.ts b/service/gitlab.ts deleted file mode 100644 index be6d0c7..0000000 --- a/service/gitlab.ts +++ /dev/null @@ -1,116 +0,0 @@ -import netTool from "./netTool"; - -const AUTH_HEADER = { "PRIVATE-TOKEN": "Zd1UASPcMwVox5tNS6ep" }; - -const BASE_URL = "https://git.n.xiaomi.com/api/v4"; - -const gitlabReqWarp = async ( - func: Function, - default_value: any -): Promise => { - try { - let response = {} as T & GitlabError; - response = (await func()) as T & GitlabError; - if (response.message === "404 Project Not Found") return default_value; - return response; - } catch { - return default_value; - } -}; - -/** - * 获取项目详情 - * @param id - * @returns - */ -const fetchProjectDetails = async (id: number | string) => { - if (typeof id === "string") id = encodeURIComponent(id); - const URL = `${BASE_URL}/projects/${id}`; - return gitlabReqWarp( - () => netTool.get(URL, {}, AUTH_HEADER), - null - ); -}; - -/** - * 获取流水线列表 - * @param project_id - * @param page - * @returns - */ -const fetchPipelines = async (project_id: number, page = 1) => { - const URL = `${BASE_URL}/projects/${project_id}/pipelines`; - const params = { scope: "finished", per_page: 100, page }; - return gitlabReqWarp( - () => netTool.get(URL, params, AUTH_HEADER), - [] - ); -}; - -/** - * 获取流水线详情 - * @param project_id - * @param pipeline_id - * @param created_at - * @returns - */ -const fetchPipelineDetails = async ( - project_id: number, - pipeline_id: number, - created_at: string -) => { - const URL = `${BASE_URL}/projects/${project_id}/pipelines/${pipeline_id}`; - const res = await gitlabReqWarp( - () => netTool.get(URL, {}, AUTH_HEADER), - null - ); - if (res === null) return null; - return { ...res, created_at }; -}; - -/** - * 获取项目的所有徽章 - * @param project_id - */ -const fetchProjectBadges = async (project_id: number) => { - const URL = `${BASE_URL}/projects/${project_id}/badges`; - return gitlabReqWarp( - () => netTool.get(URL, {}, AUTH_HEADER), - [] - ); -}; - -/** - * 设置徽章 - * @param badge - */ -const setProjectBadge = async (badge: GitlabBadgeSetParams) => { - const URL = `${BASE_URL}/projects/${badge.id}/badges/${badge.badge_id}`; - return gitlabReqWarp( - () => netTool.put(URL, badge, AUTH_HEADER), - null - ); -}; - -/** - * 添加徽章 - * @param badge - */ -const addProjectBadge = async (badge: GitlabBadgeSetParams) => { - const URL = `${BASE_URL}/projects/${badge.id}/badges`; - return gitlabReqWarp( - () => netTool.post(URL, badge, {}, AUTH_HEADER), - null - ); -}; - -const gitlab = { - fetchPipelines, - setProjectBadge, - addProjectBadge, - fetchProjectBadges, - fetchProjectDetails, - fetchPipelineDetails, -}; - -export default gitlab; diff --git a/service/gitlab/badge.ts b/service/gitlab/badge.ts new file mode 100644 index 0000000..619594b --- /dev/null +++ b/service/gitlab/badge.ts @@ -0,0 +1,60 @@ +import { Gitlab } from "../../types/gitlab"; +import netTool from "../netTool"; +import { GITLAB_AUTH_HEADER, GITLAB_BASE_URL, gitlabReqWarp } from "./tools"; + +/** + * 代表设置 GitLab 徽章的参数。 + */ +export type BadgeSetParams = Omit & { + badge_id?: number; +}; + +/** + * 为特定项目检索 GitLab 徽章。 + * @param project_id 项目的 ID。 + * @returns 一个承诺,解析为 GitLab 徽章的数组。 + */ +const get = async (project_id: number) => { + const URL = `${GITLAB_BASE_URL}/projects/${project_id}/badges`; + return gitlabReqWarp( + () => netTool.get(URL, {}, GITLAB_AUTH_HEADER), + [] + ); +}; + +/** + * 设置 GitLab 徽章。 + * @param badge 徽章参数。 + * @returns 一个承诺,解析为更新后的徽章。 + */ +const set = async (badge: BadgeSetParams) => { + const URL = `${GITLAB_BASE_URL}/projects/${badge.id}/badges/${badge.badge_id}`; + return gitlabReqWarp( + () => netTool.put(URL, badge, {}, GITLAB_AUTH_HEADER), + null + ); +}; + +/** + * 添加 GitLab 徽章。 + * @param badge 徽章参数。 + * @returns 一个承诺,解析为添加的徽章。 + */ +const add = async (badge: BadgeSetParams) => { + const URL = `${GITLAB_BASE_URL}/projects/${badge.id}/badges`; + return gitlabReqWarp( + () => netTool.post(URL, badge, {}, GITLAB_AUTH_HEADER), + null + ); +}; + +/** + * 代表一系列与徽章相关的函数。 + */ +const badge = { + get, + set, + add, +}; + +export default badge; diff --git a/service/gitlab/commit.ts b/service/gitlab/commit.ts new file mode 100644 index 0000000..ec567f7 --- /dev/null +++ b/service/gitlab/commit.ts @@ -0,0 +1,22 @@ +import netTool from "../netTool"; +import { GITLAB_AUTH_HEADER, GITLAB_BASE_URL, gitlabReqWarp } from "./tools"; + +/** + * 检索与特定提交关联的合并请求。 + * @param project_id - 项目的ID。 + * @param sha - 提交的SHA。 + * @returns 一个解析为合并请求数组的promise。 + */ +const getMr = async (project_id: number, sha: string) => { + const URL = `${GITLAB_BASE_URL}/projects/${project_id}/repository/commits/${sha}/merge_requests`; + return gitlabReqWarp( + () => netTool.get(URL, {}, GITLAB_AUTH_HEADER), + [] + ); +}; + +const commit = { + getMr, +}; + +export default commit; diff --git a/service/gitlab/index.ts b/service/gitlab/index.ts new file mode 100644 index 0000000..2e1a61b --- /dev/null +++ b/service/gitlab/index.ts @@ -0,0 +1,14 @@ + +import badge from "./badge"; +import commit from "./commit"; +import pipeline from "./pipeline"; +import project from "./project"; + +const gitlab = { + project, + badge, + commit, + pipeline, +}; + +export default gitlab; diff --git a/service/gitlab/pipeline.ts b/service/gitlab/pipeline.ts new file mode 100644 index 0000000..dc8d579 --- /dev/null +++ b/service/gitlab/pipeline.ts @@ -0,0 +1,46 @@ +import { Gitlab } from "../../types/gitlab"; +import netTool from "../netTool"; +import { GITLAB_AUTH_HEADER, GITLAB_BASE_URL, gitlabReqWarp } from "./tools"; + +/** + * 获取特定GitLab流水线的详细信息。 + * @param project_id - 项目的ID。 + * @param pipeline_id - 流水线的ID。 + * @param created_at - 流水线的创建日期。 + * @returns 一个解析为流水线详细信息的promise。 + */ +const getDetail = async ( + project_id: number, + pipeline_id: number, + created_at: string +) => { + const URL = `${GITLAB_BASE_URL}/projects/${project_id}/pipelines/${pipeline_id}`; + const res = await gitlabReqWarp( + () => netTool.get(URL, {}, GITLAB_AUTH_HEADER), + null + ); + if (res === null) return null; + return { ...res, created_at }; +}; + +/** + * 获取特定项目的GitLab流水线列表。 + * @param project_id - 项目的ID。 + * @param page - 结果的页码(默认值:1)。 + * @returns 一个解析为流水线数组的promise。 + */ +const getList = async (project_id: number, page = 1) => { + const URL = `${GITLAB_BASE_URL}/projects/${project_id}/pipelines`; + const params = { scope: "finished", per_page: 100, page }; + return gitlabReqWarp( + () => netTool.get(URL, params, GITLAB_AUTH_HEADER), + [] + ); +}; + +const pipeline = { + getDetail, + getList, +}; + +export default pipeline; diff --git a/service/gitlab/project.ts b/service/gitlab/project.ts new file mode 100644 index 0000000..842f65e --- /dev/null +++ b/service/gitlab/project.ts @@ -0,0 +1,24 @@ +import { Gitlab } from "../../types/gitlab"; +import netTool from "../netTool"; +import { GITLAB_AUTH_HEADER, GITLAB_BASE_URL, gitlabReqWarp } from "./tools"; + +/** + * 获取 GitLab 项目的详细信息。 + * @param project_id - 项目的 ID 或 URL-encoded 路径。 + * @returns 一个解析为项目详细信息的 promise。 + */ +const getDetail = async (project_id: number | string) => { + if (typeof project_id === "string") + project_id = encodeURIComponent(project_id); + const URL = `${GITLAB_BASE_URL}/projects/${project_id}`; + return gitlabReqWarp( + () => netTool.get(URL, {}, GITLAB_AUTH_HEADER), + null + ); +}; + +const project = { + getDetail, +}; + +export default project; diff --git a/service/gitlab/tools.ts b/service/gitlab/tools.ts new file mode 100644 index 0000000..622706b --- /dev/null +++ b/service/gitlab/tools.ts @@ -0,0 +1,19 @@ +import { Gitlab } from "../../types/gitlab"; + +export const gitlabReqWarp = async ( + func: Function, + default_value: any +): Promise => { + try { + let response = {} as T & Gitlab.Error; + response = (await func()) as T & Gitlab.Error; + if (response.message === "404 Project Not Found") return default_value; + return response; + } catch { + return default_value; + } +}; + +export const GITLAB_BASE_URL = "https://git.n.xiaomi.com/api/v4"; + +export const GITLAB_AUTH_HEADER = { "PRIVATE-TOKEN": "Zd1UASPcMwVox5tNS6ep" }; diff --git a/service/index.ts b/service/index.ts index bf18c74..3ef1ac5 100644 --- a/service/index.ts +++ b/service/index.ts @@ -1,9 +1,9 @@ -import sendMessage from "./sendMessage"; import gitlab from "./gitlab"; +import message from "./message"; const service = { gitlab, - sendMessage, + message, }; export default service; diff --git a/service/message/index.ts b/service/message/index.ts new file mode 100644 index 0000000..8bf8e5c --- /dev/null +++ b/service/message/index.ts @@ -0,0 +1,44 @@ +import netTool from "../netTool"; + +const API_KEY = "1dfz4wlpbbgiky0"; +const URL = "https://egg.imoaix.cn/message"; + +const message = async (body: any) => { + try { + const res = await netTool.post(URL, { + api_key: API_KEY, + ...body, + }); + return res; + } catch { + return null; + } +}; + +message.byGroupId = async (group_id: string, content: string) => { + return message({ + group_id, + msg_type: "interactive", + content, + }); +}; + +message.byChatId = async (chat_id: string, content: string) => { + return message({ + receive_id: chat_id, + receive_id_type: "chat_id", + msg_type: "interactive", + content, + }); +}; + +message.byUserId = async (user_id: string, content: string) => { + return message({ + receive_id: user_id, + receive_id_type: "user_id", + msg_type: "interactive", + content, + }); +}; + +export default message; diff --git a/service/sendMessage.ts b/service/sendMessage.ts deleted file mode 100644 index c81bc4b..0000000 --- a/service/sendMessage.ts +++ /dev/null @@ -1,35 +0,0 @@ -import netTool from "./netTool"; - -const API_KEY = "1dfz4wlpbbgiky0"; - -const sendMessage = async (body: any) => { - const URL = "https://egg.imoaix.cn/message"; - try { - const res = await netTool.post(URL, { - api_key: API_KEY, - ...body, - }); - return res; - } catch { - return null; - } -}; - -sendMessage.byGroupId = async (group_id: string, content: string) => { - return sendMessage({ - group_id, - msg_type: "interactive", - content, - }); -}; - -sendMessage.byChatId = async (chat_id: string, content: string) => { - return sendMessage({ - receive_id: chat_id, - receive_id_type: "chat_id", - msg_type: "interactive", - content, - }); -}; - -export default sendMessage; diff --git a/service/typings.d.ts b/service/typings.d.ts deleted file mode 100644 index 1db3e0d..0000000 --- a/service/typings.d.ts +++ /dev/null @@ -1,71 +0,0 @@ -interface GitlabProjDetail { - id: number; - description: string; - name: string; - path_with_namespace: string; - web_url: string; - avatar_url?: any; - message?: string; -} - -interface GitlabPipeline { - id: number; - project_id: number; - sha: string; - ref: string; - status: string; - source: string; - created_at: string; - updated_at: string; - web_url: string; -} - -interface GitlabError { - message: string; -} - -interface GitlabUser { - id: number; - username: string; - name: string; - state: string; - avatar_url: string; - web_url: string; -} - -interface GitlabPipelineDetail { - id: number; - project_id: number; - ref: string; - status: string; - web_url: "https://git.n.xiaomi.com/miai-fe/fe/ai-scene-review-fe/-/pipelines/7646046"; - user: GitlabUser; - started_at: string; - finished_at: string; - duration: number; - queued_duration: number; - message?: string; -} - -interface GitlabPipelineDetailWithCreateAt extends GitlabPipelineDetail { - created_at: string; -} - -interface GitlabBadge { - id: number; - name: string; - link_url: string; - image_url: string; - rendered_link_url: string; - rendered_image_url: string; - kind: "project" | "group"; -} - -interface GitlabBadgeSetParams { - id: number; - badge_id?: number; - link_url: string; - image_url: string; - rendered_link_url: string; - rendered_image_url: string; -} diff --git a/types/db.ts b/types/db.ts new file mode 100644 index 0000000..a4df2a5 --- /dev/null +++ b/types/db.ts @@ -0,0 +1,52 @@ +import { RecordModel } from "pocketbase"; + +export namespace DB { + export interface Pipeline extends RecordModel { + project_id: string; + user_id: string; + pipeline_id: number; + ref: string; + status: string; + web_url: string; + // 2024-03-06 02:53:59.509Z + created_at: string; + started_at: string; + finished_at: string; + duration: number; + queued_duration: number; + } + + export interface Project extends RecordModel { + project_id: number; + description: string; + name: string; + path_with_namespace: string; + web_url: string; + avatar_url: string; + has_new_cicd: boolean; + } + + export interface User extends RecordModel { + user_id: number; + username: string; + name: string; + avatar_url: string; + web_url: string; + } + + export interface StatisticsPerWeek extends RecordModel { + week: string; + total_count: number; + failed_count: number; + success_count: number; + success_rate: number; + duration: number; + } + + export interface StatisticsPerProj extends RecordModel { + week: string; + name: string; + duration: number; + ref: string; + } +} diff --git a/types/gitlab.ts b/types/gitlab.ts new file mode 100644 index 0000000..0b6bdc3 --- /dev/null +++ b/types/gitlab.ts @@ -0,0 +1,59 @@ +export namespace Gitlab { + export interface Error { + message: string; + } + export interface User { + id: number; + username: string; + name: string; + state: string; + avatar_url: string; + web_url: string; + } + + export interface ProjDetail { + id: number; + description: string; + name: string; + path_with_namespace: string; + web_url: string; + avatar_url?: any; + message?: string; + } + + export interface PipelineDetail { + id: number; + project_id: number; + ref: string; + status: string; + web_url: "https://git.n.xiaomi.com/miai-fe/fe/ai-scene-review-fe/-/pipelines/7646046"; + user: User; + started_at: string; + finished_at: string; + duration: number; + queued_duration: number; + message?: string; + } + + export interface Pipeline { + id: number; + project_id: number; + sha: string; + ref: string; + status: string; + source: string; + created_at: string; + updated_at: string; + web_url: string; + } + + export interface Badge { + id: number; + name: string; + link_url: string; + image_url: string; + rendered_link_url: string; + rendered_image_url: string; + kind: "project" | "group"; + } +}