头图
本文来自:
任治桐 极狐(GitLab)  前端工程师

NPM 是 Node.js 的包管理工具,用来安装各种 Node.js 的扩展。本文将分享如何通过极狐GitLab,让 NPM 依赖发布更新更加快速和自动化,让你轻松管理依赖,拥有更多时间专注于核心工作!

少年小明之烦恼

在开发团队日常工作中,不可避免的会依赖大量第三方模块;同时,团队内部也会发布一些公共模块到内部或外部源中,方便跨团队复用。但往往会遇到和小明一样的烦恼。

小明同学负责内部公共 NPM 模块发布和升级工作。他每天的工作是这样的:

1. 开发同学通知小明某个公共模块代码有更新;

2. 小明打包内部公共模块;

3. 发布到 NPM 源;

4. 在开发群里通知各个团队升级到最新版本。

渐渐的,每个团队都被繁琐小事缠身,低效:

  • 开发同学需要经常检查依赖的 NPM 模块是否有更新;
  • 公共模块的维护同学更新代码后需要决定是否发布版本;
  • 版本更新后还需要通知各个团队;
  • 各个团队还是容易出现更新不及时、容易遗漏等问题。

终于有一天,小明同学灵机一动:如果以后更新公共模块代码能够根据一定规则,自动更新版本号→自动发布日志→自动发布到 NPM 源→自动将公司内所有依赖该模块的代码库更新为最新版本,岂不乐哉?

经过一番探索,小明发现,通过极狐GitLab CI 和第三方工具结合,就可以达到目的。一起实践吧!

NPM 自动发布-操作指南

我们知道,NPM 包版本规范为 Semantic Versioning ,即为 major.minor.patch 格式数字组成。

那么,如果我们可以识别开发人员的 git commit message ,通过对提交信息进行形式化约定,就可以自动生成新的符合 Semantic Versioning 的版本号。然后,将新版本号更新到我们的 package.json 文件中,最后发布到 NPM 源中即可。

commitlint

首先,我们需要通过 commitlint 或类似工具,强制规范化团队的 git commit message ,通过如下命令将 commitlint 安装到项目中:

yarn add --dev @commitlint/cli @commitlint/config-conventional

# 设置 commitlint 配置文件使用 conventional config
echo "module.exports = { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js

我们采用 @commitlint/config-conventional 作为常规配置,它默认采用 Angular Commit Message 规范,也可自行修改。默认 git commit message 格式如下:

<type>(<scope>): <short summary> 
  │
  └─⫸ 可选类型: build|ci|docs|feat|fix|perf|refactor|test|chore

流水线配置

之后,需要在极狐GitLab CI 流水线中执行 commitlint ,在项目根目录新建一个 .gitlab-ci.yml 文件,添加如下代码:

# .gitlab-ci.yml

stages:
  - lint

workflow:
  rules:
    - if: $CI_MERGE_REQUEST_IID
    - if: $CI_COMMIT_TAG
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

default:
  image: node:16
  cache:
    paths:
      - node_modules/
      - .yarn

.yarn_install:
  before_script:
    - yarn install --frozen-lockfile --check-files --cache-folder .yarn

lint:commit:
  extends: .yarn_install
  stage: lint
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
  script:
    - yarn commitlint --from ${CI_MERGE_REQUEST_DIFF_BASE_SHA} --to HEAD --verbose

如果有不符合 commitlint 规范的 git commit message,极狐GitLab CI 就会阻止该 MR 合入。

这样,我们就拥有了符合约定的提交信息。

semantic-release

Semantic-release 是一个可以根据约定提交信息类型生成新版本号、极狐GitLab Changelog,还提供了大量插件用于发布 NPM 的工具。

首先安装 semantic-release 到我们的项目:

yarn add --dev semantic-release

与此同时,我们还想同步 publish 到 NPM 源,接着将更新的版本号推送到主分支,最后创建极狐GitLab Changelog。所以还需要安装如下 semantic-release plugins:

yarn add --dev @semantic-release/changelog @semantic-release/git @semantic-release/gitlab
由于 @semantic-release/npm 插件已经是 semantic-release 的一部分,所以不需要单独安装,参见文档 。

需要注意的是,通常情况下,我们只需针对主分支流水线执行:

  • 更新 package.json 版本号;
  • 推送 NPM 源;
  • 生成 changelog。

而在 MR 流水线中,我们只需确保能够正常生成 changelog 即可。为此,针对 MR 执行 --dry-run 模式,然后在 semantic-release 配置文件中区分执行步骤。

默认情况下,我们的提交信息会按照如下规则更新版本号:

完整的匹配规则参见 default-release-rules.js :

/**
 * Default `releaseRules` rules for common commit formats, following conventions.
 *
 * @type {Array}
 */
module.exports = [
  {breaking: true, release: 'major'},
  {revert: true, release: 'patch'},
  // Angular
  {type: 'feat', release: 'minor'},
  {type: 'fix', release: 'patch'},
  {type: 'perf', release: 'patch'},
  // Atom
  {emoji: ':racehorse:', release: 'patch'},
  {emoji: ':bug:', release: 'patch'},
  {emoji: ':penguin:', release: 'patch'},
  {emoji: ':apple:', release: 'patch'},
  {emoji: ':checkered_flag:', release: 'patch'},
  // Ember
  {tag: 'BUGFIX', release: 'patch'},
  {tag: 'FEATURE', release: 'minor'},
  {tag: 'SECURITY', release: 'patch'},
  // ESLint
  {tag: 'Breaking', release: 'major'},
  {tag: 'Fix', release: 'patch'},
  {tag: 'Update', release: 'minor'},
  {tag: 'New', release: 'minor'},
  // Express
  {component: 'perf', release: 'patch'},
  {component: 'deps', release: 'patch'},
  // JSHint
  {type: 'FEAT', release: 'minor'},
  {type: 'FIX', release: 'patch'},
];

如需修改上述规则,就需要添加 semantic-release 配置文件,新建 release.config.js 文件到项目根目录下:

const { execSync } = require('child_process');

const isDryRun = () => {
  return process.argv.includes('--dry-run'); // 通过命令行参数判断当前模式
};

const getCurrentBranch = () => {
  return execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
};

// MR运行配置
const getDryRunConfig = () => {
  return {
    branches: getCurrentBranch(),
    plugins: [
      [
        '@semantic-release/commit-analyzer',
        {
          preset: 'conventionalCommits',
          releaseRules: [
            { type: 'feat', release: 'minor' },
            { type: 'revert', release: 'patch' },
            { type: 'docs', release: 'patch' },
            { type: 'style', release: 'patch' },
            { type: 'chore', release: 'patch' },
            { type: 'refactor', release: 'patch' },
            { type: 'test', release: 'patch' },
            { type: 'build', release: 'patch' },
            { type: 'ci', release: 'patch' },
            { type: 'improvement', release: 'patch' },
          ],
        },
      ],
      [
        '@semantic-release/release-notes-generator',
        {
          preset: 'conventionalCommits',
          presetConfig: {
            types: [
              { type: 'feat', section: 'Features' },
              { type: 'fix', section: 'Bug Fixes' },
              { type: 'perf', section: 'Performance Improvements' },
              { type: 'revert', section: 'Reverts' },
              { type: 'docs', section: 'Documentation', hidden: false },
              { type: 'style', section: 'Styles', hidden: true },
              { type: 'chore', section: 'Miscellaneous Chores' },
              { type: 'refactor', section: 'Code Refactors', hidden: false },
              { type: 'test', section: 'Tests', hidden: true },
              { type: 'build', section: 'Build System', hidden: false },
              { type: 'ci', section: 'CI/CD', hidden: false },
              { type: 'improvement', section: 'Improvements', hidden: false },
            ],
          },
        },
      ],
    ],
  };
};

// 主分支运行配置
const defaultConfig = {
  branches: ['main'],
  plugins: [
    [
      '@semantic-release/commit-analyzer',
      {
        preset: 'conventionalCommits',
        releaseRules: [
          { type: 'feat', release: 'minor' },
          { type: 'revert', release: 'patch' },
          { type: 'docs', release: 'patch' },
          { type: 'style', release: 'patch' },
          { type: 'chore', release: 'patch' },
          { type: 'refactor', release: 'patch' },
          { type: 'test', release: 'patch' },
          { type: 'build', release: 'patch' },
          { type: 'ci', release: 'patch' },
          { type: 'improvement', release: 'patch' },
        ],
      },
    ],
    [
      '@semantic-release/release-notes-generator',
      {
        preset: 'conventionalCommits',
        presetConfig: {
          types: [
            { type: 'feat', section: 'Features' },
            { type: 'fix', section: 'Bug Fixes' },
            { type: 'perf', section: 'Performance Improvements' },
            { type: 'revert', section: 'Reverts' },
            { type: 'docs', section: 'Documentation', hidden: false },
            { type: 'style', section: 'Styles', hidden: true },
            { type: 'chore', section: 'Miscellaneous Chores' },
            { type: 'refactor', section: 'Code Refactors', hidden: false },
            { type: 'test', section: 'Tests', hidden: true },
            { type: 'build', section: 'Build System', hidden: false },
            { type: 'ci', section: 'CI/CD', hidden: false },
            { type: 'improvement', section: 'Improvements', hidden: false },
          ],
        },
      },
    ],
    '@semantic-release/changelog', // 仅有主分支需要更新极狐GitLab changelog
    '@semantic-release/npm', // 仅有主分支需要npm publish
    '@semantic-release/git',
    ['@semantic-release/gitlab', { gitlabUrl: 'https://jihulab.com' }], // 需要指定gitlabUrl 为极狐GitLab地址
  ],
  success: false,
  fail: false,
};

module.exports = isDryRun() ? getDryRunConfig() : defaultConfig;

流水线配置

此时,就可以进入上文提到的 .gitlab-ci.yml 文件,添加 semantic-release 相关的 stage:

stages:
  - lint
  - test
  - release

workflow:
  rules:
    - if: $CI_MERGE_REQUEST_IID
    - if: $CI_COMMIT_TAG
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

default:
  image: node:16
  cache:
    paths:
      - node_modules/
      - .yarn

.yarn_install:
  before_script:
    - yarn install --frozen-lockfile --check-files --cache-folder .yarn

lint:commit:
  extends: .yarn_install
  stage: lint
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
  script:
    - yarn commitlint --from ${CI_MERGE_REQUEST_DIFF_BASE_SHA} --to HEAD --verbose

lint:prettier:
  extends: .yarn_install
  stage: lint
  script:
    - yarn lint:prettier

jest:test:
  extends: .yarn_install
  stage: test
  script:
    - yarn test

semantic-release:
  extends: .yarn_install
  stage: release
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  script:
    - git config --global http.emptyAuth true
    - yarn semantic-release

# Run a dry run on Merge Requests
semantic-release-dry-run:
  needs: ['jest:test']
  script:
    - git config --global http.emptyAuth true
    - yarn semantic-release --branches $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME --dry-run --no-ci
  rules:
    - if: $CI_MERGE_REQUEST_IID

下面,我们就需要进入极狐GitLab CI 设置页面,添加对应的变量。

首先前往 https://www.npmjs.com/ 生成 access token 用来发布 NPM 模块:

然后,进入极狐GitLab 项目设置 → 访问令牌生成项目 access token,用来推送版本号更新,生成 release note 等:

最后,前往极狐GitLab 项目设置 → CI/CD → 变量新增  NPM_TOKEN,GITLAB_TOKEN 两个变量,分别输入刚才生成的 npm access token 和 项目 access token 。切记设为保护和隐藏变量,否则会存在 token 泄露的风险。

此时,我们所有准备工作都完成了。下面就提交一个 MR 试验一下:

该 MR 包含了一个 chore 类型的 commit message ,等待流水线通过后,合入该 MR。此时可以看到创建了一条主分支流水线,并且执行了 semantic-release 将版本号更新到 13.3.2 后 publish 到 NPM 源:

此时进入项目发布页面,可以看到 semantic-release 根据提交信息生成的极狐GitLab release-note:

🎉 至此,通过极狐GitLab CI 和第三方工具的结合,我们实现了:

  • 根据提交信息自动更新版本号;
  • 自动发布 NPM 模块;
  • 自动生成极狐GitLab release-note。

新的 NPM 模块发布流程,如下图所示:

下面让我们继续尝试,优化项目依赖 NPM 模块版本更新流程。

NPM 模块自动更新-操作指南

随着我们依赖的 NPM 模块增多,频繁手动更新版本也是一件麻烦事。下面就来介绍,如何通过极狐GitLab + renovate bot 自动更新第三方依赖版本号。

Renovate Bot

Renovate bot 是一款能够自动更新依赖版本的工具,不仅适用于 NPM 依赖,同样适用于 Docker、Ruby gem 等多种依赖。

Renovate bot 官方提供了一个 renovate-runner 项目,来帮助我们托管自己的 renovate bot。由于官方的 renovate-runner 位于 GitLab.com 。为了方便大家访问,我将这个项目镜像到了极狐GitLab 里,镜像项目地址位于:https://jihulab.com/gitlab-cn/frontend/renovate-runner

那么如何基于 renovate-runner 项目托管自己的 renovate 机器人呢?

1. 我们需要创建一个自己的 my-renovate-bot 项目。项目名字可自拟。

2. 在 my-renovate-bot 创建 .gitlab-ci.yml 文件:

include:
    - project: 'gitlab-cn/frontend/renovate-runner'
      file: '/templates/renovate-dind.gitlab-ci.yml'

variables:
  RENOVATE_ONBOARDING: 'false'
  RENOVATE_REQUIRE_CONFIG: 'ignored'

配置项含义如下:

  • project 配置为 renovate-runner 项目地址;
  • file 为我们需要使用流水线模板;
  • variables 可以对模板中变量的默认值进行覆盖。

3. 在 my-renovate-bot 下创建一个 renovate bot 配置文件 config.js :

module.exports = {
    endpoint: 'https://jihulab.com/api/v4/',
    platform: 'gitlab',
    labels: ['renovate', 'dependencies', 'automated'],
    includeForks: true,
    extends: ['config:base'],
    rangeStrategy: 'pin',
    enabledManagers: ["npm", "regex"],
    repositories: [{ 
        repository: 'your project path', 
        bumpVersion: true,
        internalChecksFilter: "strict",
        stabilityDays: 30,
        reviewersFromCodeOwners: true,
    }]
}

其中 repositories 的配置有两种方式,如上文所示,添加在 config.js 中可以针对不同项目使用不同的配置项。如果不需要针对各个项目单独定制,则可以在 my-renovate-bot 项目设置→ CI/CD 变量中添加 RENOVATE_EXTRA_FLAGS 配置项,以空格分隔多个项目名称即可:

4. 生成 RENOVATE_TOKEN 来方便 renovate bot 访问项目、创建 MR 。如果是个人使用,可以前往个人设置页面创建 Access Token 即可:

5. 为了方便 renovate bot 创建 MR 时携带上本次更新的 changelog,我们还需要前往 GitHub 生成一个 GITHUB_COM_TOKEN 。

设置定时任务

至此,我们的配置准备工作已经完成。下一步前往 my-renovate-bot 项目 → CI/CD → 计划 页面设置定时任务。我们可以选择每天凌晨自动执行 renovate bot ,它会自动扫描我们的 package.json 文件,获取最新依赖版本,然后创建更新版本号的 MR 。

注意:如果我们选择自定义执行时间,需要按照 Cron 语法 输入。大家可以使用这个工具 方便生成自己的 Cron 表达式。

到这里,我们所有的配置工作已经完成。下面就尝试触发 renovate bot 进行测试。前往我们在 config.js 或 RENOVATE_EXTRA_FLAGS 中配置的项目 MR 页面,可以看到 renovate-bot 已经帮助我们创建了三个 MR ,分别进行了固定版本号,升级 path 版本,升级 major 版本的操作。

进入 MR 详情,可以看到,renovate bot 帮助我们抓取了详细的 changelog 。表格中的字段含义分别是:

  • Age - 该版本发布至今的时间;
  • Adoption - 该版本在使用 renovate bot 的项目中接受安装的比例;
  • Passing - 该版本通过测试的更新的百分比;
  • Confidence - 该版本的可信度。

最后,我们只需要查看最新的 MR,选择是否需要合并即可。🎉

总结

如上图所示,我们已经实现了通过极狐GitLab 和 renovate bot 结合自动更新 NPM 依赖版本号的功能。

通过和第一部分所述 NPM 自动发布相结合,就可以实现不需要人工介入的 NPM 包自动发布 + 自动更新的工作流了,很大程度上节约开发时间,减少沟通成本,让 NPM 发布更新更及时,让研发工作更高效!爱钻研的小明,浅尝到了精英效能的奇妙滋味。😁


极狐GitLab
64 声望36 粉丝

极狐(GitLab) 以“核心开放”为原则,面向中国市场,提供开箱即用的开放式一体化安全DevOps平台——极狐GitLab。通过业界领先的优先级管理、安全、风险和合规性功能,实现产品、开发、QA、安全和运维团队间的高效协同...