Monorepo从构建到自动化发布

3,005 阅读4分钟

前言

最近一直在捣鼓团队的脚手架开发,一个脚手架中往往包含多个库,所以就想着用monorepo来实现。当然程序员首要任务就是偷懒,所以顺便把自动化构建和发布都搞了,顺便水一篇文章(第一次发文章,有不足处多多讨论)

构建Monorepo项目

在管理monorepo项目时,pnpm的优势非常明显,就如官方文档介绍所说的,pnpm是一个能够提高安装速度和节省磁盘空间的工具,能够更好的处理多个项目间的依赖关系,并且解决了很多令人诟病的问题

1. 安装pnpm

npm i pnpm -g

2. 创建仓库

mkdir monorepo && cd monorepo
pnpm init
git init

3. 添加工作区说明文件(在根目录创建pnpm-workspace.yaml文件)

packages:
  - 'packages/**'

4. 创建项目

mkdir packages && cd packages
<!-- 创建main项目 -->
mkdir main && cd main
pnpm init
修改项目package.json的name为: cx-monorepo-main

<!-- 创建utils项目 -->
mkdir ../utils && cd ../utils
pnpm init
修改项目package.json的name为: cx-monorepo-utils

5. pnpm怎么安装依赖

  • 安装全局依赖

    pnpm i typescript -w
    
  • 安装局部依赖

    pnpm i typescript --filter cx-monorepo-xxx
    或者
    cd packages/main && pnpm i typescript
    
  • 安装项目内依赖

    与安装局部依赖一致,包名为package的name。 安装后会产生一条workspace的依赖,在publish之后会转变为真实路径依赖

6. 安装依赖

typescirpt用于编译代码,@changesets/cli用于包版本更新、changelog生成、包的发布

pnpm i typescript @changesets/cli -w -D

7. 写点代码

  • utils

    新增文件index.ts

    // packages/utils/index.ts
    export function sum(n: number, m: number) {
      return n + m
    }
    
  • main

    安装utils依赖

    pnpm i cx-monorepo-utils --filter cx-monorepo-main
    

    新增文件index.ts

    // packages/main/index.ts
    import { sum } from 'cx-monorepo-utils'
    
    sum(1, 2)
    

    packages/utils/package.jsonpackages/main/package.json添加脚本

    {
      "scripts": {
        "build": "tsc index.ts"
      }
    }
    
  • 根目录

    package.json添加脚本,执行成功后,我们在gitlab创建仓库,并将master分支先推送上去

    {
      "scripts": {
        "build": "pnpm --filter=./packages/* build"
      }
    }
    

8. changeset

  • 初始化changeset

    <!--在根目录执行-->
    pnpm changeset init
    
    <!--编辑.changeset/config.json-->
    将access选项改为public
    baseBranch选项改为master,并提交代码
    
    <!--tips:以@开头的包默认是私有包,需要付费,如果包名是@开头又不想付费,命名规则就必须为@npm用户名/包名-->
    
  • 提交变更记录

    追加脚本并执行

    {
      "scripts": {
        "changeset": "changeset"
      }
    }
    
  • 更新依赖版本并生成changelog

    追加脚本并执行

    {
      "scripts": {
        "update:version": "changeset version && pnpm i --frozen-lockfile=false"
      }
    }
    
  • 发布到npm

    先将npm源切换到npm,然后追加脚本并执行pnpm run publish

    {
      "scripts": {
        "publish": "pnpm build && pnpm release:only",
        "release:only": "changeset publish"
      }
    }
    

自动化发布

上文我们介绍了monorepo的构建以及发布,接下来我们探索一下如何在CI/CD中自动发布

1. NPM TOKEN

由于我们在之前执行publish的时候还需要验证登录,但在自动化构建过程中如果还需要执行登录流程,那这个自动化程度显然并不高,那么我们要怎么跳过这个流程呢,答案就是token

  • 创建token

    1. 首先在npm官网登录,在用户菜单选择Access Tokens
    2. 选择Classic Token
    3. 因为要用在自动化构建,需要避开双因素验证(2FA),所以选择Automation类型,创建完记得保存token
  • 使用token

    1. 我们创建token是为了在用于npm发包的时候可以避开登录,这个token我们可以配置在.nrmrc文件中,对应的key是_authToken

    2. 编辑.npmrc,追加以下内容

      //registry.npmjs.org/:always-auth=true
      //registry.npmjs.org/:_authToken=刚刚保存的token
      
    3. 先执行npm logout,保证现在npm是退出状态,然后执行publish脚本,如果推送成功说明配置有效

      tips:如果之前使用了 token 发布,然后执行 npm logout ,会导致 token 失效

2. CI/CD

  • 安装依赖

    pnpm i changesets-gitlab -w -D
    
  • 前置环境变量

    • 由于使用changesets-gitlab插件,所以需要提供以下几个环境变量
    • GITLAB_HOST:如果是私有化部署的话需要提供
    • GITLAB_TOKEN:gitlab账号token
    • GITLAB_CI_USER_NAME:gitlab账号
  • 创建gitlab token

    • gitlab首页,在右上角用户信息,选择Settings
    • 选择Access Tokens ,然后创建token
  • 配置环境变量

    1. 进入monorepo项目仓库,选择Settings→ CI/CD
    2. 展开Variables
    3. 添加以下变量
      • key: GITLAB_CI_USER_NAME
      • value: gitlab账号
      • key: GITLAB_HOST
      • value: 因为我们是私有化的,所以填上https://gitlab.xxx.cn
      • key: GITLAB_TOKEN
      • value: 刚刚生成的gitlab token
      • key: NPM_TOKEN
      • value: npm重新生成token,刚刚生成的token已经在本地使用过了。由于token是比较私密的数据,而.nrmrc是会提交到仓库的,所以我们需要使用变量,然后在在CI/CD执行发布包之前再写入token
  • .npmrc文件以下内容删除

    //registry.npmjs.org/:always-auth=true
    //registry.npmjs.org/:_authToken=刚刚保存的token
    
  • 在根目录新增.gitlab-ci.yml文件

    stages:
      - check:changeset
      - update:version
      - publish
    before_script:
      - npm config set registry https://registry.npmmirror.com/
      - npm i pnpm -g
      - pnpm i
    
    检查是否有执行changeset:
      stage: check:changeset
      image: node:16.1.0
      script:
        - pnpm changesets-gitlab comment
      rules:
        - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_COMMIT_REF_NAME != "changeset-release/master"
    
    版本更新:
      stage: update:version
      image: node:16.1.0
      script:
        - pnpm changesets-gitlab
      variables:
        INPUT_VERSION: pnpm run update:version
        INPUT_COMMIT: 'build: 版本更新'
        INPUT_TITLE: '版本更新'
    
      rules:
        - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH && $CI_COMMIT_TITLE != "Merge branch 'changeset-release/master' into 'master'"
          when: manual
    
    发布包:
      stage: publish
      image: node:16.1.0
      script:
        - pnpm run build
        - npm config set registry https://registry.npmjs.org/
        - echo -e  "\n//registry.npmjs.org/:always-auth=true \n//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> .npmrc
        - pnpm changesets-gitlab
      variables:
        INPUT_PUBLISH: pnpm run publish
      rules:
        - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH && $CI_COMMIT_TITLE == "Merge branch 'changeset-release/master' into 'master'"
          changes:
            - packages/**/package.json
    

结语

通过学习和阅读本文内容,我们实现了从零构建一个monorepo项目,并通过changeset进行版本管理和CHANGELOG更新,同时也掌握了如何优雅的发布一个npm包。但这些还是远远不够的,比如eslint配置commitlintlint-staged等等都还需要去完善,gitlab-ci.yml配置方面也是一笔带过,又或者某些包没有改动过,但是每次发布都执行打包命令,这些都值得大家去继续探索优化。

问题排查

  • 执行publish时报错npm adduser

    • 如果是在本地终端报错,首先排查npm是否已经登录,随后排查npm源地址是否是https://registry.npmjs.org/
    • CI/CD环境报错,需要排查有没有创建NPM_TOKEN变量、NPM_TOKEN是否正确、NPM_TOKEN类型是否有避开2FA验证、npm源地址是否正确
  • 执行pnpm changesets-gitlab comment报错 Response code 401 (Unauthorized)

    • 检查GITLAB_CI_USER_NAMEGITLAB_HOSTGITLAB_TOKEN 是否有配置
  • 子仓库独立eslintvscodeeslint插件报错

    • 添加vscode配置
      // .vscode/setting.json
      {
        "eslint.workingDirectories": [{ "mode": "auto" }]
      }
      
  • 使用私有化部署npm方案,无法创建npm token

    • 由于无法创建token,并且CI/CD中又无法输入账号密码,所以需要依赖npm-cli-login帮我们在CI/CD中完成登录的过程

    • 修改.gitlab.ci.yml部分内容

      发布包:
        stage: publish
        image: node:16.1.0
        script:
          - pnpm run build
          - NPM_RC_PATH=.npmrc NPM_REGISTRY=$NPM_REGISTRY NPM_USER=$NPM_USER  NPM_PASS=$NPM_PASS NPM_EMAIL=$NPM_EMAIL  pnpm dlx npm-cli-login
          - pnpm changesets-gitlab
        variables:
          INPUT_PUBLISH: pnpm run publish
        rules:
          - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH && $CI_COMMIT_TITLE == "Merge branch 'changeset-release/master' into 'master'"
            when: manual
            changes:
              - packages/**/package.json
      
    • 创建gitlab环境变量

      • NPM_REGISTRY: 私有化npm地址,例如:http://npm.xxx.cn
      • NPM_USER:私有化npm用户名
      • NPM_PASS:密码
      • NPM_EMAIL:邮箱

文档