本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
相信大家或多或少在工作中都有接触过 AntDesign,不过大多数同学对于 AntDesign 更多只是停留在使用它来快速搭建我们的项目。
今天这篇文章中从另一个角度使用 AntDesign 来为我们的项目服务:借鉴学习 Ant 中的 workflow 从而来为我们的项目中每一次 MR/PR 增加检测报告与尺寸限制。
下图为 Github 中 antDesign 中每一次 PullRequest 的自动检测:
看起来很酷对吧,AntDesign 团队正是通过这种自动化的方式,在必要的人为 Review 外通过 Github Action 来集成了多种自动化脚本来评估每一次 PR 的改动以及影响面。
上图中的 size-limit report 会在每一次 PR 创建时使用 github bot 自动创建一条评论。
评论的内容中会体现本次提交内容与目标分支内容的尺寸对比体积,从而帮助 Reviewers 更好的评估本次改动带来的体积影响。
接下来,这篇文中就来为大家解读 Antd 中是如何在 workflow 中实现 SizeLimit Report ,同时也会为大家分享在 Gitlab Pipeline 中复刻这一套自动化 Action Checker 的思路。
所谓 Github Action 是 GitHub 提供的一项持续集成和部署服务。它允许开发者在代码仓库中配置和运行自动化的工作流程,以便在代码提交、拉取请求或其他事件发生时执行各种操作。
Github Action 中有几个常见的概念:
workflow 在 Github 仓库中可以通过 .github/workworks 目录中进行定义,比如在 AntDesign 的存储库中 即通过多个 yml 文件定义了多种不同的工作流程。
常见的情况下,我们会在不同的情况下触发不同的 workflow 来进行自动化检查。
比如文章中的 SizeLimit Action 就是在仓库中存在新的 PullRequest 或者为已存在的 PullRequest 进行推送时会触发对应的 workflow 来进行自动化检查。
比如我们上边所说在每次创建新的 PR 时触发,创建 PR 就可以被称之为一次 Event 的触发。
每个 job 可以是执行可执行的命令文件、比如 shell、node 等命令。同时每个 job 由于在统一 runner 中执行,所以彼此之间可以存在一定的执行顺序、数据缓存复用等关系。
上边的概念没有接触过 Github Action 的同学乍一看多少会有些懵,其实我们完全可以将 Github Action 等价于 Gitlab 中的 Pipeline。
整个 workflow 可以对应为 Gitlab 中一次执行 pipeline 的过程,所谓 Job、Event、Action 之类大家都可以联合 Gitlab pipeline 来理解这一过程。
siz-limit 是一款 JavaScript 性能工具,它可以通过简单的配置来帮助我们检查并计算文件的尺寸、加载时间的前后差异,从而站在真实用户的角度计算加载 JS 文件所需的实际成本,并且可以在超过配置的文件体积时抛出错误。
我在这里新建一个空的 Repo,这个 Repo 仅仅在 package.json 中配置了:
// ...
"size-limit": [
{
"path": "./src/index.js",
"limit": "50 ms"
}
],
// ...
这里我们配置了 size-limit 去检测 ./src/index.js
文件,同时指定了这个文件最大的加载/执行时间为 50ms。
之后,通过运行 npx size-limit 就可以在控制台中得到一段输出:
在运行 npx size-limit 后,size-limit 会通过在无头浏览器中运行加载我们在 package.json 中配置的文件从而计算出对应文件的 gzip 之后的体积以及加载/执行时间。
同样我们也可以对于 size-limit 的配置稍做修改:
// ...
"size-limit": [
{
"path": "./src/index.js",
"limit": "10 ms"
}
],
// ...
再次运行 npx size-limit 后:
这次因为该文件的总时长 32ms 超过了我们配置的 10ms 所以 size-limit 执行失败得到了不一样的输出。
总的来说,size-limit 这个库的使用方式也比较简单。有兴趣的同学可以花个几分钟阅读一下它的文档,我相信对大家来说很容易就可以上手。
上面的章节中和大家简单介绍了 Github Action 的概念以及 Size Limit 的用法。
回到文章的开头,我们可以看到 AntDesign 的每一次 PullRequest 中存在一个 github-actions 的机器人来评论本次 Size Report。
我们可以直观的通过 size report 来看到本次 pullrequest 的中关闭文件体积的变化,接下来我们就来聊聊如何实现 AntDesign 中一模一样的功能。
首先,我们刚刚也提到过 Github Action 中的 Event,所谓 Event 即是表示在满足某些条件下触发整个 workflow 的前置约束条件。
顺着这个思路,我们可以想到在每一次新的 PullRequest 创建时我们应该在整个 workflow 中增加一个 size-limit 的 job 来进行尺寸检测。
其次,在某些时候比如已有 PR 的情况下再次提价。此时我们也应该根据最新一次的提交重新运行 Size-Limit 的 job 更新已有 Report:
截止目前看起来一切都很简单对吧,不过细心的同学可能会发现在上边 Antd 的报告是存在一些红色的箭头:
没错,所谓红色的箭头既是表示本次提交相较于目标分支代码体积有所上升,后方的括号中也标识了本次提交上升的代码体积。
当然,如果本次提交有代码体积下降的话也会有对应的蓝色 🔽 箭头来说明,以及同样会标明下降的体积。
要实现这样的效果,单单通过在 workflow 中运行 size-limit 是肯定不够的。
所以我们需要调整一下 size-limit job 的内容,需要在触发 PR 时对比前后分支的两次 limit 报告内容从而实现 bot report 的评论,整个 report 的流程如下:
上图为整个 Github Size-Limit 的执行流程,我们可以使用 Github Action 以及 Size-Limit 来实现上述的流程为每一次 PullReqeuest 中为我们的代码进行自动化的体积检查。
接下来,我们就来和大家看看如何实现上述的流程。
作为前端工程师比起来其他脚本语言 NodeJs 的上手成本对于我们来说几乎是零成本,所以这里我们选择使用 nodejs 来实现我们的 Limit 逻辑.
首先,我们先来创建一个空的 Typescript Library 项目,这里我已经创建了一个空的 Typescript Library 模块项目,大家可以直接使用即可。
接下来的部分我会和大家一起一步一步实现和 Ant 中一模一样的 Size-Limit Action 。
首先模板中的入口文件 src/main.ts
中我们先来聚焦在执行参数环节,对于一个成熟设计的 Action 来说往往需要在设计之初就考虑到适配到不同的项目。
Action 的参数设计往往对于整体的架构设计尤其重要,比如不同的项目使用的包管理工具可能会有所不同(npm、yarn、pnpm、bun 等等),又或者不同的项目构建命令又不一定是相同等等诸如此类。
在着手与开始编写代码前,我们对于 Size-Limit Action 的参数进行简单的架构设计。它会接受一下子参数:
npm run build
某些又为 npm run dist
等等...
// src/main.js
import { getInput } from "@actions/core";
function run() {
// 用 getInput 方法中获取需要外部传入的变量
const token = getInput('github_token');
const skipStep = getInput('skip_step');
const buildScript = getInput('build_script');
const cleanScript = getInput('clean_script');
const packageManager = getInput('package_manager');
const directory = getInput('directory') || process.cwd();
}
run();
上边的代码中我们在 main.ts 中通过 @actions/core getInput 方法获取外部传入的参数。
本质上 @actions/core 中的 getInput 参数同样也是从 process.env 中获取对应的环境变量。
@actions/github 中提供了一系列方便我们进行 Github 操作的相关 Api。
我们仅仅需要在项目中安装 @actions/github 即可方便的访问 github workflow 的相关信息。
我们可以通过 @actions/github 中的 context 来判断当前执行环境是否在 pullRequest 操作中:
import { context, getOctokit } from '@actions/github';
// ...
function run() {
const { payload, repo } = context;
// 获取本次 pr 相关信息
const pr = payload.pull_request;
// 当前未非 PullRequest 操作下
if (!pr) {
throw new Error('No PR found. Only pull_request workflows are supported.');
}
const token = getInput('github_token');
const skipStep = getInput('skip_step');
const buildScript = getInput('build_script');
const cleanScript = getInput('clean_script');
const packageManager = getInput('package_manager');
const directory = getInput('directory') || process.cwd();
// PullRequest 环境下 初始化 github 操作实例
const octokit = getOctokit(token);
}
在创建了 Github 实例后,按照最开始的流程图我们应该在当前分支以及目标分支下分别执行 size-limit 获得两次分支下构建产物的尺寸尺寸信息。
当然,无论是在当前分支还是目标分支执行 size-limit 操作的逻辑基本上是一致的。所以我们可以额外抽离一份公用的代码来单独作为单独执行 Size-Limit 的工具文件 src/SizeLimit.ts
:
// src/Term.ts
import { exec } from '@actions/exec';
import hasYarn from 'has-yarn';
import hasPNPM from 'has-pnpm';
import fs from 'node:fs';
import path from 'node:path';
function hasBun(cwd = process.cwd()) {
return fs.existsSync(path.resolve(cwd, 'bun.lockb'));
}
export class Term {
/**
* Autodetects and gets the current package manager for the current directory, either yarn, pnpm, bun,
* or npm. Default is `npm`.
*
* @param directory The current directory
* @returns The detected package manager in use, one of `yarn`, `pnpm`, `npm`, `bun`
*/
getPackageManager(directory?: string): string {
return hasYarn(directory)
? 'yarn'
: hasPNPM(directory)
? 'pnpm'
: hasBun(directory)
? 'bun'
: 'npm';
}
async execSizeLimit(
branch?: string,
buildScript?: string,
cleanScript?: string,
directory?: string,
packageManager?: string
): Promise<{ status: number; output: string }> {
// 获取当前项目的包管理工具
const manager = packageManager || this.getPackageManager(directory);
// SizeLimit 执行结果
let output = '';
// 如果传入分支,需要切换到对应目标分支
if (branch) {
try {
await exec(`git fetch origin ${branch} --depth=1`);
} catch (error) {
console.log('Fetch failed', (error as any).message);
}
await exec(`git checkout -f ${branch}`);
}
// 调用包管理工具安装当前项目
await exec(`${manager} install`, [], {
cwd: directory
});
// 运行构建命令,获得当前项目构建产物
const script = buildScript || 'build';
await exec(`${manager} run ${script}`, [], {
cwd: directory
});
// 调用 size-limit --json 命令获得当前项目下 package.json 配置的 size-limit 文件执行后的信息
const status = await exec('npx size-limit --json', [], {
ignoreReturnCode: true,
listeners: {
stdout: (data: Buffer) => {
output += data.toString();
}
},
cwd: directory
});
// 如果传入了 cleanScript,执行清空命令
if (cleanScript) {
await exec(`${manager} run ${cleanScript}`, [], {
cwd: directory
});
}
return {
status,
output
};
}
}
上述的代码中大概分为以下几个步骤:
npx size-limit --json
命令,根据项目配置的 size-limit 配置执行 size-limit 获得 json 格式的 Limit 报告。可复用的 Limit 逻辑我们已经编写完成。此时,我们再次聚焦到 src/main.ts
入口文件中。
我们需要做的即是在当前提交分支下执行 size-limit 获得报告以及在对应 PR 的 target 分支下执行获取报告内容:
// src/main.ts
async function run() {
// ...
// 当前提交分支下获取 size-limit 报告
const { status, output } = await term.execSizeLimit(
null,
buildScript,
cleanScript,
directory,
packageManager
);
// pr target 分支下获取 size-limit 报告
const { output: baseOutput } = await term.execSizeLimit(
pr.base.ref, // 需要切换到的目标分支
buildScript,
cleanScript,
directory,
packageManager
);
}
同样,上边的代码有一些需要留意的地方:
pr.base.ref
是指 Pull Request(PR)的基础分支的引用(ref)。它表示 PR 所基于的分支或提交的引用。比如我们在 GitHub 上创建一个 PR 时,需要会选择一个基础分支和一个要合并的分支。pr.base.ref
会返回所选的基础分支的引用,通常是一个分支名称或提交的 SHA 。
此时,我们就可以获得 PR 中当前分支的目标分支的 SizeLimit Json 形式的报告了。
在获得 Json 形式的报告后,接下来的步骤我们需要根据获得的 Json 报告来生成一份前后对比易于阅读的 Markdown 内容。
首先,要生成 Markdown 形式的报告,我们需要先将生成的 size-limit Json 字符串格式化成为我们便于操作的 json 格式:
import { SizeLimit } from './SizeLimit';
// src/main.ts
async function run() {
// ...
// 同样,创建一个单独可复用的 SizeLimit Class 来单独管理数据格式化以及生成 markdown 的工具操作
const limit = new SizeLimit();
let base;
let current;
try {
// 将 base 和 current 的报告格式化成为便于操作的 json object
base = limit.parseResults(baseOutput);
current = limit.parseResults(output);
} catch (error) {
console.log(
'Error parsing size-limit output. The output should be a json.'
);
throw error;
}
}
// src/SizeLimit.ts
class SizeLimit {
parseResults(output: string): { [name: string]: IResult } {
// 格式化 sizeLimit 输出的字符串
const results = JSON.parse(output);
// 格式化 sizeLimit Array ,输出为期望的 object 形式
return results.reduce(
(current: { [name: string]: IResult }, result: any) => {
let time = {};
if (result.loading !== undefined && result.running !== undefined) {
const loading = +result.loading;
const running = +result.running;
time = {
running,
loading,
// 总共的时间(加载+执行)
total: loading + running
};
}
return {
// 原始报告内容
...current,
// 增加后的内容
[result.name]: {
name: result.name,
size: +result.size,
...time
}
};
},
{}
);
}
}
export { SizeLimit };
简单来说,上一步输出的 Limit Stringify Array 报告的形式为:
[
{
"name": "library",
"passed": true,
"size": 0,
"running": 0,
"loading": 0
}
]
我们需要将上边的 Json 格式格式化成为一个 object 形式:
{
library: { name: 'library', size: 0, running: 0, loading: 0, total: 0 }
}
此时,我们已经获得格式化后 source 分支以及 target 分支的 JSON 格式的报告,接下来我们只要按照对应的 JSON Object 进行格式化生成对应的 markdown 即可。
最终,我们期望通过前后两次 JSON 对象的对比来生成下面格式的 markdown 内容:
同样,关于数据格式化的方法我们仍然存放在 SizeLimit 这个 Class 中:
// src/main.ts
import { markdownTable as table } from 'markdown-table';
const SIZE_LIMIT_HEADING = `## size-limit report 📦 `;
async function run() {
// ...
const limit = new SizeLimit();
let base;
let current;
try {
// 将 base 和 current 的报告格式化成为便于操作的 json object
base = limit.parseResults(baseOutput);
current = limit.parseResults(output);
} catch (error) {
console.log(
'Error parsing size-limit output. The output should be a json.'
);
throw error;
}
const body = [
SIZE_LIMIT_HEADING,
// 生成 size-limit markdown table 形式的前后对比报告
table(limit.formatResults(base, current))
].join('\r\n');
}
关于 markdown-table 的用法大家可以直接参考它的说明文档,它的用法非常简单。这里我们使用 markdown-table 来将 limit.formatResults(base, current)
生成的前后对比 Json 生成对应的 markdown table 格式。
// src/SizeLimit.ts
// @ts-ignore
import bytes from 'bytes';
interface IResult {
name: string;
size: number;
running?: number;
loading?: number;
total?: number;
}
const EmptyResult = {
name: '-',
size: 0,
running: 0,
loading: 0,
total: 0
};
class SizeLimit {
static SIZE_RESULTS_HEADER = ['Path', 'Size'];
static TIME_RESULTS_HEADER = [
'Path',
'Size',
'Loading time (3g)',
'Running time (snapdragon)',
'Total time'
];
private formatBytes(size: number): string {
return bytes.format(size, { unitSeparator: ' ' });
}
private formatTime(seconds: number): string {
if (seconds >= 1) {
return `${Math.ceil(seconds * 10) / 10} s`;
}
return `${Math.ceil(seconds * 1000)} ms`;
}
private formatSizeChange(base: number = 0, current: number = 0): string {
const value = current - base;
if (value > 0) {
return `+${this.formatBytes(value)} 🔺`;
}
if (value < 0) {
return `${this.formatBytes(value)} 🔽`;
}
return '';
}
private formatChange(base: number = 0, current: number = 0): string {
if (base === 0) {
return '+100% 🔺';
}
const value = ((current - base) / base) * 100;
const formatted =
(Math.sign(value) * Math.ceil(Math.abs(value) * 100)) / 100;
if (value > 0) {
return `+${formatted}% 🔺`;
}
if (value === 0) {
return `${formatted}%`;
}
return `${formatted}% 🔽`;
}
private formatLine(value: string, change: string) {
if (!change) {
return value;
}
return `${value} (${change})`;
}
private formatSizeResult(
name: string,
base: IResult,
current: IResult
): Array<string> {
return [
`\`${name}\``,
this.formatLine(
this.formatBytes(current.size),
this.formatSizeChange(base.size, current.size)
)
];
}
private formatTimeResult(
name: string,
base: IResult,
current: IResult
): Array<string> {
return [
`\`${name}\``,
this.formatLine(
this.formatBytes(current.size),
this.formatSizeChange(base.size, current.size)
),
this.formatLine(
this.formatTime(current.loading),
this.formatChange(base.loading, current.loading)
),
this.formatLine(
this.formatTime(current.running),
this.formatChange(base.running, current.running)
),
this.formatTime(current.total)
];
}
parseResults(output: string): { [name: string]: IResult } {
const results = JSON.parse(output);
return results.reduce(
(current: { [name: string]: IResult }, result: any) => {
let time = {};
if (result.loading !== undefined && result.running !== undefined) {
const loading = +result.loading;
const running = +result.running;
time = {
running,
loading,
total: loading + running
};
}
return {
...current,
[result.name]: {
name: result.name,
size: +result.size,
...time
}
};
},
{}
);
}
formatResults(
base: { [name: string]: IResult },
current: { [name: string]: IResult }
): Array<Array<string>> {
// 将前后数据的 key 进行合并去重
const names = [...new Set([...Object.keys(base), ...Object.keys(current)])];
// 项目中未安装 @size-limit/time package 的情况下则 total 字段会是 undefined
// 判断当前报告是否仅包含 Size 报告(不包含 time )
const isSize = names.some(
(name: string) => current[name] && current[name].total === undefined
);
// 根据不同的类型来生成不同的 table header
const header = isSize
? SizeLimit.SIZE_RESULTS_HEADER
: SizeLimit.TIME_RESULTS_HEADER;
// 对比 names 中每一个字段生成前后对比的表格内容
const fields = names.map((name: string) => {
const baseResult = base[name] || EmptyResult;
const currentResult = current[name] || EmptyResult;
if (isSize) {
return this.formatSizeResult(name, baseResult, currentResult);
}
return this.formatTimeResult(name, baseResult, currentResult);
});
// 返回对应的 markdown table 格式的 Array 数据
return [header, ...fields];
}
}
export { SizeLimit };
上边的代码即为,完整的 SizeLimit 的 Util Class 内容。
我们使用 formatResults 方法来生成前后 size-limit 报告的 markdown 形式的 table 对比报告。
具体的代码这里我就不一一和大家进行解读,具体的用法大家直接参考 markdown-table 文档中的说明即可,这里的代码更多是比较繁琐的 JSON to Markdown 的格式化并没有太多的难度。
唯一需要注意的是 isSize 的判断:
最终,这一步完毕我们已经可以生成前后对比下的 markdown 形式的 report 接下来我们仅需要将 markdown 形式的 report 通过之前初始化的 octokit 对象调用创建 PR 评论的 api 即可达到我们最终的效果。
上一步我们已经得到了在 PR 中的前后对比的 markdown table 形式的报告,这一步我们就来通过 github api 在每次 PR 中来创建对应的评论。
这一步之中我们首先需要做的则是判断当前 PR 中是否已有 SizeLimit 的 Report:
区分上边的场景的关键就在于当前 PR 的评论中是否已有 SizeLimit 的报告,自然我们通过 github api 只要获取到当前 PR 下所有的评论内容然后判断内容是否为我们在 src/main.ts
中定义的 SIZE_LIMIT_HEADING 开头的内容即可:
// src/main.ts
async function run() {
// ...
const body = [
SIZE_LIMIT_HEADING,
// 生成 size-limit markdown table 形式的前后对比报告
table(limit.formatResults(base, current))
].join('\r\n');
// 获取当前 PR 下的所有评论
const commentLists = await octokit.paginate(
'GET /repos/:owner/:repo/issues/:issue_number/comments',
{
...repo,
issue_number: pr.number
}
);
// 判断当前 PR 下所有评论内容是否包含 size-limit 报告
const sizeLimitComment = commentLists.find((comment) =>
(comment as any).body.startsWith(SIZE_LIMIT_HEADING)
);
const comment = !sizeLimitComment ? null : sizeLimitComment;
}
上述的代码中我们使用了 octokit.paginate 方法来获得当前 PR 下的所有 comments,从而对比 comments 的 body 字符串内容来判断当前 PR 是否已有 size-limit report。
接下来,我们已可以获得当前 PR 下是否已存在 size-limit 的 report 。我们只需要根据是否已存在 report 来进行创建/更新评论内容即可:
// src/main.ts
// ...
async function run() {
// ...
const comment = !sizeLimitComment ? null : sizeLimitComment;
if (!sizeLimitComment) {
try {
// 为 PR 关联的 issues 创建一条评论,内容为 size-limit 报告
// 该条评论会同步在 PullRequest 下
await octokit.rest.issues.createComment({
...repo,
// eslint-disable-next-line camelcase
issue_number: pr.number,
body
});
} catch (error) {
console.log(
"Error creating comment. This can happen for PR's originating from a fork without write permissions."
);
}
} else {
try {
// 为 PR 关联的 issues 更新 size-limit 评论内容
await octokit.rest.issues.updateComment({
...repo,
// eslint-disable-next-line camelcase
comment_id: (comment as any).id,
body
});
} catch (error) {
console.log(
"Error updating comment. This can happen for PR's originating from a fork without write permissions."
);
}
}
}
上述的代码我们通过 octokit.rest.issues.createComment/octokit.rest.issues.updateComment
来更新/创建关于 sizeLimit report 的内容。
需要留意的是在 Antd 中每一条 PR 创建时是需要关联 issue 的,自然我们通过 issues 相关的评论操作是会同步到对应 PR 下的评论。
截止到这里关于 SizeLimit Report 的核心流程我们已经基本完毕了,不过细心的小伙伴还会发现我们在当前分支中运行的 execSizeLimit 返回的 status 还没有被使用到。
所谓 status 上边我们提到过,它表示执行 npx size limit 命令时程序的 exit code。自然,如果 exit code 大于 0 时子进程非正常退出,则表示本次提交下的 size-limit 执行失败。
当 size-limit 执行失败时(超过项目配置的 limit 字段限制),此时我们需要将本次 job 判断为失败:
// src/main.ts
async function run() {
// ...
// 当前提交分支下获取 size-limit 报告
const { status, output } = await term.execSizeLimit(
null,
buildScript,
cleanScript,
directory,
packageManager
);
// ...
if (status > 0) {
setFailed('Size limit has been exceeded.');
}
}
原本是打算在 Github 中在编写一套 yml 脚本来和大家在自己的仓库中稍微把玩一下我们自己的 size-limit 流程。
这一套配置虽然并不麻烦但是稍微有些繁琐,有兴趣的同学可以直接参考 size-limit 的 Github Readme 说明,官方已经提供了对应的 size-limit Action 相关复用说明。
如果大家想要在 Github 中体验,仅需要在自己的 Github 项目中按照文档说明进行简单的 yml workflow 以及注入对应的 Github Environment Variable 即可。
文章中的源代码大家可以参阅这里,当然大家也可以直接参考 size-limit 的源代码。
其实文章中的内容更多是倾向于给大家带来一种实现思路,通过 size-limit 的源码内容来为大家认识 workflow 的过程并没有大家想象的那么遥不可及。
上边的篇幅中和大家讲述了 size-limit 在 github 上的实现流程。
相信大多数同学在工作中更多是使用公司内部的 github 进行代码组织和管理而非直接使用 Gitbub 。
不过我相信在了解了上边代码的思路后,在 gitlab 中复刻一个 Gitlab 版的 sizeLimit 完全是信手拈来。
笔者也同样在自己公司中通过 SizeLimit Action 实现了一套类似的流程:
这里我就不在赘述如何在 Gilab 中这一套的实现流程,实际上完全和文章中上述的代码实现思路一模一样。
稍稍有些不同的是将 Github 的 Api 更换成了 github 的 Api,比如:
当然,大家如果有兴趣在 Gitlab 中实现这一套机制有什么疑问的话我们可以在评论区进行交流。
无论是 Github 的 workflow 还是 Gitlab 的 pipeline 文章中的代码更多是想带来一种抛砖引玉的效果,通过 size-limit 的实现思路思考如何在日常业务项目中来借鉴开源的自动化工作流保障我们业务代码质量。
当然可借鉴的自动化流程远远不止一个 SizeLimit 这么简单,后续大家有兴趣的话我会为大家分享一些 UI 自动化回归、chatGPT CodeReview 等等相关的实践内容。
文章的内容到这里就告一段落了,希望文章中的内容可以真正帮助到大家。