关于npm:如何利用极狐GitLab-轻松管理NPM依赖发布与更新

2次阅读

共计 11735 个字符,预计需要花费 30 分钟才能阅读完成。

本文来自:
任治桐 极狐(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 公布更新更及时,让研发工作更高效!爱钻研的小明,浅尝到了精英效力的微妙味道。😁

正文完
 0