📦 Size Limit: 从开源项目学习如何为你的业务增加检测报告

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

引言

相信大家或多或少在工作中都有接触过 AntDesign,不过大多数同学对于 AntDesign 更多只是停留在使用它来快速搭建我们的项目。

今天这篇文章中从一个角度使用 AntDesign 来为我们的项目服务:借鉴学习 Ant 中的 workflow 从而来为我们的项目中每一次 MR/PR 增加检测报告与尺寸限制。

SizeLimit 作用

下图为 Github 中 antDesign 中每一次 PullRequest 的自动检测:

看起来很酷对吧,AntDesign 团队正是通过这种自动化的方式,在必要的人为 Review 外通过 Github Action 来集成了多种自动化脚本来评估每一次 PR 的改动以及影响面。

上图中的 size-limit report 会在每一次 PR 创建时使用 github bot 自动创建一条评论。

评论的内容中会体现本次提交内容与目标分支内容的尺寸对比体积,从而帮助 Reviewers 更好的评估本次改动带来的体积影响。

接下来,这篇文中就来为大家解读 Antd 中是如何实现 SizeLimit Report 的过程,同时也会为大家提供如何在 Gitlab 中实现 Antd 中这一套 Action Checker。

前置知识

Github Action

所谓 Github Action 是 GitHub 提供的一项持续集成和部署服务。它允许开发者在代码仓库中配置和运行自动化的工作流程,以便在代码提交、拉取请求或其他事件发生时执行各种操作。

Github Action 中有几个常见的概念:

  • workflow (工作流程):workflow 表示一种可配置化的工作流程,一个 workflow 由一个或多个 job 组成。同时 workflow 可以在满足 repository 中的 Event 条件后触发运行整个工作流程,也可以选择手动触发或者按定义的时间进行定时触发。

workflow 在 Github 仓库中可以通过 .github/workworks 目录中进行定义,比如在 AntDesign 的存储库中 即通过多个 yml 文件定义了多种不同的工作流程。

常见的情况下,我们会在不同的情况下触发不同的 workflow 来进行自动化检查。

比如文章中的 SizeLimit Action 就是在仓库中存在新的 PullRequest 或者为已存在的 PullRequest 进行推送时会触发对应的 workflow 来进行自动化检查。

  • Event(事件): 所谓事件则是在满足某些条件下触发整个 workflow 的前置约束条件。

比如我们上边所说在每次创建新的 PR 时触发,创建 PR 就可以被称之为一次 Event 的触发。

  • Job (任务):job 是在同一个 runner 中执行工作流程(workflow)的一组步骤。

每个 job 可以是执行可执行的命令文件、比如 shell、node 等命令。同时每个 job 由于在统一 runner 中执行,所以彼此之间可以存在一定的执行顺序、数据缓存复用等关系。

  • action (动作):通常在工作流中一些比较复杂的操作我们可以使用 action 关键来复用这些繁琐的流程。

  • Runners(运行程序):运行程序是触发工作流时运行工作流的服务器。 每个运行器一次可以运行一个作业。

上边的概念没有接触过 Github Action 的同学乍一看多少会有些懵,其实我们完全可以将 Github Action 等价于 Gitlab 中的 Pipeline。

整个 workflow 可以对应为 Gitlab 中一次执行 pipeline 的过程,所谓 Job、Event、Action 之类大家都可以联合 Gitlab pipeline 来理解这一过程。

SizeLimit

siz-limit 是一款 JavaScript 性能工具,它可以通过简单的配置来帮助我们检查并计算文件的尺寸、加载时间的前后差异,从而站在真实用户的角度计算加载 JS 文件所需的实际成本,并且可以在超过配置的文件体积时抛出错误。

我在这里新建一个空的 Repo,这个 Repo 仅仅在 package.json 中配置了:

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 的配置稍做修改:

json 复制代码
  // ...
  "size-limit": [
    {
      "path": "./src/index.js",
      "limit": "10 ms"
    }
  ],
  // ...

再次运行 npx size-limit 后:

这次因为该文件的总时长 32ms 超过了我们配置的 10ms 所以 size-limit 执行失败得到了不一样的输出。

总的来说,size-limit 这个库的使用方式也比较简单。有兴趣的同学可以花个几分钟阅读一下它的文档,我相信对大家来说很容易就可以上手。

SizeLimit Action 实现思路

上面的章节中和大家简单介绍了 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 中为我们的代码进行自动化的体积检查。

接下来,我们就来和大家看看如何实现上述的流程。

实现 SizeLimit

作为前端工程师比起来其他脚本语言 NodeJs 的上手成本对于我们来说几乎是零成本,所以这里我们选择使用 nodejs 来实现我们的 Limit 逻辑.

首先,我们先来创建一个空的 Typescript Library 项目,这里我已经创建了一个空的 Typescript Library 模块项目,大家可以直接使用即可。

接下来的部分我会和大家一起一步一步实现和 Ant 中一模一样的 Size-Limit Action 。

参数准备

首先模板中的入口文件 src/main.ts 中我们先来聚焦在执行参数环节,对于一个成熟设计的 Action 来说往往需要在设计之初就考虑到适配到不同的项目。

Action 的参数设计往往对于整体的架构设计尤其重要,比如不同的项目使用的包管理工具可能会有所不同(npm、yarn、pnpm、bun 等等),又或者不同的项目构建命令又不一定是相同等等诸如此类。

在着手与开始编写代码前,我们对于 Size-Limit Action 的参数进行简单的架构设计。它会接受一下子参数:

  • github_token: github token一种GitHub App 安装访问令牌,既然我们要使用 Github Action 的自动化流程自然也需要接受外部传入的 github token 作为权限认证令牌。

  • build_script: 当前项目的构建打包命令,不同的项目存在不同的构建命令,Size-Limit Action 更多是针对构建后的 JavaScript 代码进行体积分析。比如某些项目构建命令为 npm run build 某些又为 npm run dist 等等...

  • clean_script: 构建完成后的删除上次构建产物的清除命令。

  • package_manager: 包管理工具,在进行项目安装时应该使用的包管理工具(pnpm、npm、yarn、bun 等)。

  • directory: 上述命令运行的工作目录环境,也就是我们应该在哪个目录下运行上述脚本。

ts 复制代码
// 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 中获取对应的环境变量。

Github Client

@actions/github 中提供了一系列方便我们进行 Github 操作的相关 Api。

我们仅仅需要在项目中安装 @actions/github 即可方便的访问 github workflow 的相关信息。

我们可以通过 @actions/github 中的 context 来判断当前执行环境是否在 pullRequest 操作中:

typescript 复制代码
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);
}

执行 Limit

在创建了 Github 实例后,按照最开始的流程图我们应该在当前分支以及目标分支下分别执行 size-limit 获得两次分支下构建产物的尺寸尺寸信息。

当然,无论是在当前分支还是目标分支执行 size-limit 操作的逻辑基本上是一致的。所以我们可以额外抽离一份公用的代码来单独作为单独执行 Size-Limit 的工具文件 src/SizeLimit.ts

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
    };
  }
}

上述的代码中大概分为以下几个步骤:

  • 判断当前项目使用的包管理工具。
  • 对于传入目标分支的项目拉取最新的代码后切换到传入的分支调用安装命令以及构建命令,否则未传入目标分支情况会在当前项目内进行安装以及构建。
  • 构建完成后则会使用 @actions/exec 调用 npx size-limit --json 命令,根据项目配置的 size-limit 配置执行 size-limit 获得 json 格式的 Limit 报告。
  • 最终,返回的 output 表示执行完成后 sizeLimit 的 json 报告内容。而 status 则表示执行 sizeLimit 命令时的 exit code ( 0 为正常状态)。

可复用的 Limit 逻辑我们已经编写完成。此时,我们再次聚焦到 src/main.ts 入口文件中。

我们需要做的即是在当前提交分支下执行 size-limit 获得报告以及在对应 PR 的 target 分支下执行获取报告内容:

ts 复制代码
// 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
  );
}

同样,上边的代码有一些需要留意的地方:

  • 首先两次 term.execSizeLimit 的执行唯一的区别是一个是在当前提交下执行 sizeLimit 获得报告而另一个则是在 PR 的 target 目标分支下执行获得 sizeLimit 报告。

  • 其次,pr.base.ref是指 Pull Request(PR)的基础分支的引用(ref)。它表示 PR 所基于的分支或提交的引用。比如我们在 GitHub 上创建一个 PR 时,需要会选择一个基础分支和一个要合并的分支。pr.base.ref会返回所选的基础分支的引用,通常是一个分支名称或提交的 SHA 。

此时,我们就可以获得 PR 中当前分支的目标分支的 SizeLimit Json 形式的报告了。

生成 Report

在获得 Json 形式的报告后,接下来的步骤我们需要根据获得的 Json 报告来生成一份前后对比易于阅读的 Markdown 内容。

首先,要生成 Markdown 形式的报告,我们需要先将生成的 size-limit Json 字符串格式化成为我们便于操作的 json 格式:

ts 复制代码
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;
  }
}
ts 复制代码
// 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 报告的形式为:

json 复制代码
[
  {
    "name": "library",
    "passed": true,
    "size": 0,
    "running": 0,
    "loading": 0
  }
]

我们需要将上边的 Json 格式格式化成为一个 object 形式:

json 复制代码
{
  library: { name: 'library', size: 0, running: 0, loading: 0, total: 0 }
}

此时,我们已经获得格式化后 source 分支以及 target 分支的 JSON 格式的报告,接下来我们只要按照对应的 JSON Object 进行格式化生成对应的 markdown 即可。

最终,我们期望通过前后两次 JSON 对象的对比来生成下面格式的 markdown 内容:

同样,关于数据格式化的方法我们仍然存放在 SizeLimit 这个 Class 中:

ts 复制代码
// 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 格式。

ts 复制代码
// 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 的判断:

  • 当执行的 size-limit 的项目中安装了 @size-limit/time 时,执行 size-limit 命令会输出相关 slow 3g 内容下的加载/执行时间,自然我们通过 parseResults 方法传入的内容会包含 total 字段。

  • 当执行的 size-limit 的项目中未安装了 @size-limit/time 时,仅安装了 @size-limit/file 时,执行 size-limit 命令并不会输出 loading 以及 running 字段。自然我们通过 parseResults 获得的数据结构中 total 会为 undefined 。

最终,这一步完毕我们已经可以生成前后对比下的 markdown 形式的 report 接下来我们仅需要将 markdown 形式的 report 通过之前初始化的 octokit 对象调用创建 PR 评论的 api 即可达到我们最终的效果。

创建/更新评论

上一步我们已经得到了在 PR 中的前后对比的 markdown table 形式的报告,这一步我们就来通过 github api 在每次 PR 中来创建对应的评论。

这一步之中我们首先需要做的则是判断当前 PR 中是否已有 SizeLimit 的 Report:

  • 当前 PR 已有对应 SizeLimit 的 Report,此时我们应该使用最新的 Report 来更新旧的报告。
  • 当前 PR 中还未存在对应的 SizeLimit Report,此时我们应该创建一条新的评论来展示本次 Report。

区分上边的场景的关键就在于当前 PR 的评论中是否已有 SizeLimit 的报告,自然我们通过 github api 只要获取到当前 PR 下所有的评论内容然后判断内容是否为我们在 src/main.ts 中定义的 SIZE_LIMIT_HEADING 开头的内容即可:

ts 复制代码
// 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 来进行创建/更新评论内容即可:

typescript 复制代码
// 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 判断为失败:

ts 复制代码
// 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.');
  }
}

Ending

原本是打算在 Github 中在编写一套 yml 脚本来和大家在自己的仓库中稍微把玩一下我们自己的 size-limit 流程。

这一套配置虽然并不麻烦但是稍微有些繁琐,有兴趣的同学可以直接参考 size-limit 的 Github Readme 说明,官方已经提供了对应的 size-limit Action 相关复用说明。

如果大家想要在 Github 中体验,仅需要在自己的 Github 项目中按照文档说明进行简单的 yml workflow 以及注入对应的 Github Environment Variable 即可。

文章中的源代码大家可以参阅这里,当然大家也可以直接参考 size-limit 的源代码。

其实文章中的内容更多是倾向于给大家带来一种实现思路,通过 size-limit 的源码内容来为大家认识 workflow 的过程并没有大家想象的那么遥不可及。

Gitlab Limit

上边的篇幅中和大家讲述了 size-limit 在 github 上的实现流程。

相信大多数同学在工作中更多是使用公司内部的 github 进行代码组织和管理而非直接使用 Gitbub 。

不过我相信在了解了上边代码的思路后,在 gitlab 中复刻一个 Gitlab 版的 sizeLimit 完全是信手拈来。

笔者也同样在自己公司中通过 SizeLimit Action 实现了一套类似的流程:

这里我就不在赘述如何在 Gilab 中这一套的实现流程,实际上完全和文章中上述的代码实现思路一模一样。

稍稍有些不同的是将 Github 的 Api 更换成了 github 的 Api,比如:

  • @actions/github 在 gitlab 中的平替 @gitbeaker/rest。
  • 同时,我们将触发时机从 PR 的创建/更新替换成为了 MR 的创建和更新。自然,SizeLimit Report 的评论也替换成为了 MR 下的评论。

当然,大家如果有兴趣在 Gitlab 中实现这一套机制有什么疑问的话我们可以在评论区进行交流。

结尾

无论是 Github 的 workflow 还是 Gitlab 的 pipeline 文章中的代码更多是想带来一种抛砖引玉的效果,通过 size-limit 的实现思路思考如何在日常业务项目中来借鉴开源的自动化工作流保障我们业务代码质量。

当然可借鉴的自动化流程远远不止一个 SizeLimit 这么简单,后续大家有兴趣的话我会为大家分享一些 UI 自动化回归、chatGPT CodeReview 等等相关的实践内容。

文章的内容到这里就告一段落了,希望文章中的内容可以真正帮助到大家。

相关推荐
旺旺大力包13 分钟前
【 Git 】git 的安装和使用
前端·笔记·git
PP东18 分钟前
ES学习class类用法(十一)
javascript·学习
海威的技术博客23 分钟前
JS中的原型与原型链
开发语言·javascript·原型模式
雪落满地香29 分钟前
前端:改变鼠标点击物体的颜色
前端
余生H1 小时前
前端Python应用指南(二)深入Flask:理解Flask的应用结构与模块化设计
前端·后端·python·flask·全栈
outstanding木槿1 小时前
JS中for循环里的ajax请求不数据
前端·javascript·react.js·ajax
酥饼~1 小时前
html固定头和第一列简单例子
前端·javascript·html
一只不会编程的猫1 小时前
高德地图自定义折线矢量图形
前端·vue.js·vue
所以经济危机就是没有新技术拉动增长了1 小时前
二、javascript的进阶知识
开发语言·javascript·ecmascript
m0_748250931 小时前
html 通用错误页面
前端·html