前言
公司内部持续集成用的Jenkins,办公通讯用的钉钉,代码维护用的GitLab。
持续集成的构建详情在日常开发中需要频繁查看,过程是否报错,提交的概要。
但是旧有的版本流程,只有记录了合入主干的时侯,输出一个签入签出的文本,
所以有时候还需要借助运维大佬帮忙找为毛失败,很浪费大伙的时间,成本太高。
所以我就在想,如何简化这个过程,让效率更高。
当看到钉钉支持卡片化和 markdown 化推送信息的时侯,我就知道游戏开始了。
效果图
实现的流程
旧的查阅定位
在这个东西出来之前,构建信息都需要走这么几个步骤。
当前的流程
一步到位,不需要去关注其他,也不用占用其他人的时间来帮你定位一些很基础的信息。
实现的功能
- 提供跳转到对应的 gitlab 仓库(包含issue 这些)
- 判断根目录是否有 changelog,有则提供跳转 gitlab 对应的 changelog 文件
- 谁推送了,推送的行为
- 快速跳转到对应的 jenkins-job,查看构建过程
- 输出仓库的概要信息
- 输出仓库的名字
- 获取最近五次提交的 commit 概要,忽略 merge request 这些的
- 展示构建的那次 commit 并支持跳转到 gitlab 查看该 commit 的变动记录
- 展示构建的分支并支持跳转到 gitlab 的分支
- 支持文档链接传入
- 支持同时把信息推送给多个群
没有用到第三方库,都是用 node 的内置 api 实现文件读取操作及 http 请求。
实现的过程
配置读取
如何读取执行根目录的配置文件呢?
主要用到了 process.cwd
查询执行路径, 实现读取 package.json 和独立配置文件的参数
const fs = require("fs"); const path = require("path"); const process = require("process"); const jk2dtFile = path.resolve(process.cwd(), "./jk2dtrc.js"); const pkgFile = require(path.resolve(process.cwd(), "./package.json")); let importConfig = {}; if (fs.existsSync(jk2dtFile)) { process.stdout.write("jk2dt配置文件存在 \n"); const config = require(jk2dtFile); importConfig = config; } else { if (fs.existsSync(pkgFile)) { process.stdout.write("jk2dt配置文件不存在,尝试从 package.json 读取 \n"); if (pkgFile.jk2dt && typeof pkgFile.jk2dt === "object") { importConfig = pkgFile.jk2dt; } else { process.stdout.write("package.json也没有对应配置项,采用默认配置 \n"); } } } module.exports = importConfig;
markdown自定义模版转换
在 markdown 里面提供一些占位符,来达到定制化的效果,最简单粗暴的模板替换的姿势
- us-msg.md
{{TIPS_BANNER}} {{GitRepoDesc}} ### --- {{GitRepoName}} --- **构建分支:** {{GitRepoBranchUrl}} {{PkgVersion}} {{GitBuildCommitLink}} {{GitRepoChangeLog}} {{RepoRecentTitle}} {{RepoRecentCommitMsg}} {{GitRepoActionType}} ### --- Jenkins --- **执行人:** {{PushBy}} **构建任务:** {{JK_JOBS_NAME}} **构建日志:** {{JK_JOBS_CONSOLE}} **构建状态:** {{JK_JOBS_STATUS}} **构建时间:** {{JK_JOBS_TIME}}
- covert-md-2-str
const fs = require("fs"); const path = require("path"); function mdTemplateStr({ TipsBanner, TemplateName, PkgVersion, JobInfo: { JOB_NAME, JOB_BUILD_DISPLAY_NAME, JOB_BUILD_URL, JOB_STATUS, JOB_END_TIME }, GitInfo: { RepoUrl, RepoBranch, RepoName, RepoDesc, RepoBranchUrl, RepoChangeLog, RepoPushMan, RepoActionType, RepoRecentCommitMsg, BuildCommitMDLink } }) { const PlacehoderVar = { "{{TIPS_BANNER}}": TipsBanner, "{{JK_JOBS_NAME}}": JOB_NAME, "{{JK_JOBS_TIME}}": JOB_END_TIME, "{{JK_JOBS_CONSOLE}}": `[${JOB_BUILD_DISPLAY_NAME}](${JOB_BUILD_URL})`, "{{JK_JOBS_STATUS}}": JOB_STATUS, "{{GitRepoName}}": RepoName, "{{GitRepoDesc}}": RepoDesc, "{{GitRepoBranch}}": RepoBranch, "{{GitRepoBranchUrl}}": RepoBranchUrl, "{{PkgVersion}}": PkgVersion ? `**打包版本:** ${PkgVersion}` : "", "{{RepoRecentTitle}}": RepoRecentCommitMsg ? "**提交概要:**" : "", "{{RepoRecentCommitMsg}}": RepoRecentCommitMsg, "{{GitRepoChangeLog}}": RepoBranch === "master" ? `**变更日志:** [CHANGELOG](${RepoChangeLog})` : "", "{{GitBuildCommitLink}}": BuildCommitMDLink ? `**构建提交:** ${BuildCommitMDLink}` : "", "{{GitRepoActionType}}": RepoActionType ? `**推送行为:** ${RepoActionType}` : "", "{{PushBy}}": RepoPushMan }; let mdStr = fs.readFileSync( path.join(__dirname, `../template/${TemplateName}.md`) ); mdStr = mdStr.toString(); for (const [k, v] of Object.entries(PlacehoderVar)) { const re = new RegExp(k, "g"); mdStr = mdStr.replace(re, v); } return mdStr; } module.exports = mdTemplateStr;
查询 changelog 文件是否存在
- 先判断项目根目录是否存在对应的CHANGELOG.md
- 没有再用 Linux 的
grep
查询 changlog.md(忽略大小写),用execSync
同步执行 shell
const path = require("path"); const pkgFile = require(path.resolve(process.cwd(), "./package.json")); const projectExecShellPath = process.cwd(); const fs = require("fs"); const { execSync } = require("child_process"); function rootExistChangelogFile() { const CHANGELOG = path.resolve(process.cwd(), "./CHANGELOG.md"); try { if (fs.existsSync(CHANGELOG)) { return true; } else { return !!execSync( `ls -l ${projectExecShellPath} | grep -i "changelog.md"` ).toString(); } return false; } catch (error) { return false; } }
查询npm包的版本
- 先判断是否包含 package.name 或者 main(主入口)是否存在,这是包的必要因素
- 然后判断是否我们考虑的分支范围
- 最后 shell 去查询
/** * 获取包的dist-tags */ function getPackageDistTag(branch) { if ( !pkgFile || !pkgFile.name || !pkgFile.main || ["master", "dev", "develop", "next"].indexOf(branch) === -1 ) { return ""; } let distTag; switch (branch) { case "master": distTag = "latest"; break; case "dev": distTag = "dev"; break; case "develop": distTag = "dev"; break; case "next": distTag = "next"; break; default: distTag = "dev"; break; } const execShell = `npm show ${pkgFile.name} dist-tags.${distTag} 2>/dev/null`; try { return execSync(execShell).toString(); } catch (error) { return ""; } }
获取最近的五次提交概要
- 小于等于0的默认及分支不存在当做不查询
- 用
grep
忽略包含 "lerna|into|merge"词汇的提交概要 - 再用 sed 来改造字符串,输出一个带换行的 markdown 格式字符串
function getLastNCommit(n = 5, branch) { if (n <= 0 || !branch) { return ""; } const readLineFilterResult = branch === "master" ? 'grep -E -i -v "lerna"' : 'grep -E -i -v "lerna|into|merge"'; const lineModify = "sed 's/^/> /g' |sed 's/$/\\\n/g' "; const execShell = `git log --oneline -${n} ${branch}| ${readLineFilterResult} | ${lineModify} `; try { return execSync(execShell).toString(); } catch (error) { return ""; } }
判断accessToken是否有效
支持字符串和数组,强校验
function findValidAK(dict) { let tempObj = {}; for (let [k, v] of Object.entries(dict)) { if (isType.isString(v) && v) { tempObj[k] = v; } if ( isType.isObj(v) && Array.isArray(v.success) && Array.isArray(v.error) && v.success.length > 0 && v.error.length > 0 ) { tempObj[k] = v; } } return tempObj; }
纯粹的类型判断
/** * * @param {*} obj - 对象 * @return {boolean} - 布尔值 * @description - 判断是否为Promise */ function isThenable(obj) { return ( !!obj && (typeof obj === "object" || typeof obj === "function") && typeof obj.then === "function" ); } function isString(o) { //是否字符串 return Object.prototype.toString.call(o).slice(8, -1) === "String"; } function isNumber(o) { //是否数字 return Object.prototype.toString.call(o).slice(8, -1) === "Number"; } function isObj(o) { //是否对象 return Object.prototype.toString.call(o).slice(8, -1) === "Object"; } module.exports = { isThenable, isString, isObj, isNumber };
源代码及npm包
注意事项
- 仅支持linux/unix/macos,调用了一些很常见的命令行, 如 grep,ls 。
- 和 jenkins 高度耦合,很多基础信息都是从 jenkins 内置临时环境变量拿的
- 仓库信息基本所有基础信息支持覆写,默认从 jenkins 提供的临时环境变量构建的Git仓库信息