preface

Monorepo NPM package release recently, reference (copy) Vite release method. However, when I finished the next day, Vite rewrote its release script (2022/2/11) 😭😭😭 so I revised my project again and wrote an article about it.

This paper will be divided into two parts:

  • Vite publishing from the user’s perspective
  • Vite release implementation

Vite publishing from the user’s perspective

After Vite publishing reconstruction, users can publish Vite through a visual interface.

The GitHub Action screen is shown below. You can publish vite and some official Vite plug-ins by manually triggering GitHub actions.

It is important to note that only project members can see and execute the Release workflow. If we want to experience running this workflow, we need to fork the Vite repository and then go to the Action screen

Where Package and type are optional, corresponding to the NPM package to be released and the generation method of version number (such as only adding minior revision number).

Once run, the workflow is automatically executed to publish vite to NPM.

Vite release implementation

Github workflows configuration files are stored in the repository under.github/workflows.

We are running the release workflow, so we need to see the lot/workflows/the yml configuration

release.yml

Let’s break release.yml down into several parts:

  • Defines parameters that the user can select
  • Run job and publish to NPM

Define optional parameters for the user

Optional parameters are as follows:

  • The branch that runs the publication, the main branch by default
  • The NPM package to publish, vite by default
  • Version number generation mode
name: Release

on:
  workflow_dispatch:
    inputs:
      branch:
        description: "branch"
        required: true
        type: string
        default: "main"
      package:
        description: "package"
        required: true
        type: choice
        options:
          - vite
          - plugin-legacy
          - plugin-vue
          - plugin-vue-jsx
          - plugin-react
          - create-vite
      type:
        description: "type"
        required: true
        type: choice
        options:
          - next
          - stable
          - minor-beta
          - major-beta
          - minor
          - major
Copy the code

The effect is as follows:

The form entry Use Workflow from, which is not defined in release.yml, is a built-in option when GitHub runs the workflow.

What it does is, which branch release.yml is read, because release.yml may be different from branch to branch.

If the selected branch does not have a release.yml file, these three options are no longer available and the pipeline cannot run.

Run job and publish to NPM

There are mainly the following steps:

  • Build using Ubuntu images
  • Pull git repository code and pull the selected branch
  • Using the 16
  • Install the PNPM
  • PNPM install, if there is a cache, then use the cache to speed up the installation
  • In the project of the package that needs to be published, executepnpm run release
jobs:
  release:
    # prevents this action from running on forks 
    # Avoid forking the repository to run this workflow
    if: github.repository = = 'vitejs/vite'
    name: Release
    runs-on: The ${{ matrix.os }}
    environment: Release
    strategy:
      matrix:
        # pseudo-matrix for convenience, NEVER use more than a single combination
        # Pseudo-matrix, for convenience (maybe vite developers copied code from elsewhere and made a few changes), don't use more than one combination
        Use the latest Ubuntu system and run the job using Node 16
        node: [16]
        os: [ubuntu-latest]
    steps:
    
    	Pull git code
      - name: checkout
        uses: actions/checkout@v2
        with:
		  # pull the corresponding branch
          ref: The ${{ github.event.inputs.branch }}
          # fetch-depth set to 0 to get all git commit histories and tags. If this parameter is not set, only the latest commit information is obtained by default.
          Get all Git commits to generate Changelog
          fetch-depth: 0
          
      	Use the previously defined Version of Node 16
      - uses: actions/setup-node@v2
        with:
          node-version: The ${{ matrix.node }}
          
      Set git user to commit code
      - run: git config user.name vitebot
      - run: git config user.email [email protected]
      
      Install PNPM and YARN. PNPM is used to install dependencies and YARN is used to publish NPM packages
      - run: npm i -g pnpm@6
      - run: npm i -g yarn # even if the repo is using pnpm, Vite still uses yarn v1 for publishing
      - run: yarn config set registry https://registry.npmjs.org # Yarn's default registry proxy doesn't work in CI
      
      # Use Node 16, again to specify the use of caching (PNPM not previously installed)
      # cache usage details see: https://github.com/actions/setup-node#caching-packages-dependencies
      - uses: actions/setup-node@v2
        with:
          node-version: The ${{ matrix.node }}
          cache: "pnpm"
          cache-dependency-path: "**/pnpm-lock.yaml"
      
      # install dependencies
      - name: install
        run: pnpm install --frozen-lockfile --prefer-offline
      
      # create.npmrc, used to store the secret key published by NPM, automatically read.npmrc secret key when published, avoiding user interaction such as password input
      - name: Creating .npmrc
        run: | cat << EOF > "$HOME/.npmrc" //registry.npmjs.org/:_authToken=$NPM_TOKEN EOF        env:
          NPM_TOKEN: The ${{ secrets.NPM_TOKEN }}
      
      # Run the release script in package.json under Packages and pass in the parameters
      # --quiet Skips command line interaction. The pipeline cannot interact
      # --type Specifies how the version number is generated
      - name: Release
        run: pnpm --dir packages/${{ github.event.inputs.package }} release -- --quiet --type The ${{ github.event.inputs.type }}
        env:
          GITHUB_TOKEN: The ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: The ${{ secrets.NPM_TOKEN }}
Copy the code

Release the script

Each package directory has a package.json with a release script, such as vite:

/ / of/packages/the vite/package. The json
{
  "name": "vite"."version": "2.8.1"."author": "Evan You"."scripts": {
    "release": "ts-node .. /.. /scripts/release.ts"}}Copy the code

You actually executed relex.ts in the scripts directory of the vite repository root. All packages in the Vite repository are distributed using this script

The document is 250 lines long, not much, so let’s take a look at the general structure:

async function main() :Promise<void> {
	// for now
}

main().catch((err) = > {
  console.error(err)
})
Copy the code

The entire file simply executes main and prints out any errors.

The main function consists of the following steps:

  1. Generate a new version number targetVersion
  2. Confirm the release again
  3. Update package.json version number
  4. Execute a build
  5. Generate the changelog
  6. Release NPM package
  7. Commit to making

Generate a new version number

If no version number is specified when executing the script, a new version number is generated

The generated rules are generated from the command line argument –type. The number of lines is quite large, in fact, most of them are some error handling and prompts, do not need to investigate. It is important to know that the NPM package semver was used to generate the version number.

const currentVersion = '1.0.0'
const inc: (i: ReleaseType) = > string = (i) = >
  semver.inc(currentVersion, i, 'beta')!

inc('major')  		// 2.0.0, if the second argument is not preXXX(premajor, etc.), the third argument is ignored
inc('premajor')		/ / 2.0.0 - beta. 0
inc('minor')		/ / 1.1.0
inc('preminor')		/ / 1.1.0 - beta. 0
inc('patch')		/ / 1.0.1
inc('prepatch')		/ / - beta 1.0.1. 0
inc('prerelease')	/ / - beta 1.0.1. 0
Copy the code

If –type is not passed, the version type is selected through command line interaction. This happens when you manually call PNPM Run Release in your project, which is how you publish before you publish refactoring. The cli interaction modes are as follows:

Here’s the code:

// args is the argument passed in when the script is executed using the command line
// Make the first parameter targetVersion
let targetVersion: string | undefined = args._[0]

// If no targetVersion is passed, it will be generated automatically
if(! targetVersion) {// Read the type argument from the command line, --type XXX
  const type: string | undefined = args.type
  // Generate a version number based on type
  if (type) {
    const currentBeta = currentVersion.includes('beta')
    if (type= = ='next') {
      targetVersion = inc(currentBeta ? 'prerelease' : 'patch')}else if (type= = ='stable') {
      // Out of beta
      if(! currentBeta) {throw new Error(
          `Current version: ${currentVersion} isn't a beta, stable can't be used`
        )
      }
      targetVersion = inc('patch')}else if (type= = ='minor-beta') {
      if (currentBeta) {
        throw new Error(
          `Current version: ${currentVersion} is already a beta, minor-beta can't be used`
        )
      }
      targetVersion = inc('preminor')}else if (type= = ='major-beta') {
      if (currentBeta) {
        throw new Error(
          `Current version: ${currentVersion} is already a beta, major-beta can't be used`
        )
      }
      targetVersion = inc('premajor')}else if (type= = ='minor') {
      if (currentBeta) {
        throw new Error(
          `Current version: ${currentVersion} is a beta, use stable to release it first`
        )
      }
      targetVersion = inc('minor')}else if (type= = ='major') {
      if (currentBeta) {
        throw new Error(
          `Current version: ${currentVersion} is a beta, use stable to release it first`
        )
      }
      targetVersion = inc('major')}else {
      throw new Error(
        `type: The ${type} isn't a valid type. Use stable, minor-beta, major-beta, or next`)}}else {
    // no explicit version or type, offer suggestions
    const { release }: { release: string } = await prompts({
      type: 'select'.name: 'release'.message: 'Select release type'.choices: versionIncrements
        .map((i) = > `${i} (${inc(i)}) `)
        .concat(['custom'])
        .map((i) = > ({ value: i, title: i }))
    })

    if (release === 'custom') {
      const res: { version: string } = await prompts({
        type: 'text'.name: 'version'.message: 'Input custom version'.initial: currentVersion
      })
      targetVersion = res.version
    } else {
      targetVersion = release.match(/ / / ((. *) \))! [1]}}}Copy the code

Confirm the release again

Confirm the release again, beta version needs to confirm again.

If the –quiet argument is passed, it will be skipped. This is used when the GitHub CI workflow is executed and the execution script will actively pass –quiet.

if(! args.quiet) {if (targetVersion.includes('beta') && !args.tag) {
    const { tagBeta }: { tagBeta: boolean } = await prompts({
      type: 'confirm'.name: 'tagBeta'.message: `Publish under dist-tag "beta"? `
    })

    if (tagBeta) args.tag = 'beta'
  }

  const { yes }: { yes: boolean } = await prompts({
    type: 'confirm'.name: 'yes'.message: `Releasing ${tag}. Confirm? `
  })

  if(! yes) {return}}else {
  if (targetVersion.includes('beta') && !args.tag) {
    args.tag = 'beta'}}Copy the code

Update package.json version number

step('\nUpdating package version... ')
updateVersion(targetVersion)
Copy the code

UpdateVersion overwrites the original package.json

function updateVersion(version: string) :void {
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
  pkg.version = version
  writeFileSync(pkgPath, JSON.stringify(pkg, null.2) + '\n')}Copy the code

Execute a build

Run PNPM run build

step('\nBuilding package... ')
if(! skipBuild && ! isDryRun) {await run('pnpm'['run'.'build'])}else {
  console.log(`(skipped)`)}Copy the code

If the — DRY parameter is passed, it indicates that the run is a DRY Run (also known as a trial run), often used for script debugging

In the dry run of relex.ts, the build and upload of the NPM package is not performed, but the command line statements to be executed are printed out on the command line

Generate the changelog

Run the PNPM run Changelog command. This article does not explain how to generate Changelog due to limited space.

step('\nGenerating changelog... ')
await run('pnpm'['run'.'changelog'])
Copy the code

Release NPM package

step('\nPublishing package... ')
await publishPackage(targetVersion, runIfNotDry)
Copy the code

The publishPackage implementation is as follows:

async function publishPackage(
  version: string,
  runIfNotDry: RunFn | DryRunFn
) :Promise<void> {
  // Parameters of YARN publish
  const publicArgs = [
    'publish'.'--no-git-tag-version'.'--new-version',
    version,
    '--access'.'public'
  ]
  if (args.tag) {
    publicArgs.push(`--tag`, args.tag)
  }
  try {
    // important: we still use Yarn 1 to publish since we rely on its specific
    // behavior
    // It is still published with YARN 1 and has not been optimized yet
    await runIfNotDry('yarn', publicArgs, {
      stdio: 'pipe'
    })
    console.log(colors.green(`Successfully published ${pkgName}@${version}`))}catch (e: any) {
    if (e.stderr.match(/previously published/)) {
      console.log(colors.red(`Skipping already published: ${pkgName}`))}else {
      throw e
    }
  }
}
Copy the code

RunIfNotDry implementation:

// Run the command line
const run: RunFn = (bin, args, opts = {}) = >
  execa(bin, args, { stdio: 'inherit'. opts })type DryRunFn = (bin: string, args: string[], opts? :any) = > void

// In dry run mode, only command line statements are output
const dryRun: DryRunFn = (bin, args, opts: any) = >
  console.log(colors.blue(`[dryrun] ${bin} ${args.join(' ')}`), opts)

const runIfNotDry = isDryRun ? dryRun : run
Copy the code

RunIfNotDry In Dry Run mode, the command line statements are output, but not executed, and only used for local debugging.

Commit to making

const tag = pkgName === 'vite' ? `v${targetVersion}` : `${pkgName}@${targetVersion}`

const { stdout } = await run('git'['diff'] and {stdio: 'pipe' })

// If there is a git discrepancy, commit and label it
// If the version number is changed, package.json is changed
if (stdout) {
  step('\nCommitting changes... ')
  await runIfNotDry('git'['add'.'-A'])
  await runIfNotDry('git'['commit'.'-m'.`release: ${tag}`])
  await runIfNotDry('git'['tag', tag])
} else {
  console.log('No changes to commit.')
}

step('\nPushing to GitHub... ')
await runIfNotDry('git'['push'.'origin'.`refs/tags/${tag}`])
await runIfNotDry('git'['push'])

if (isDryRun) {
  console.log(`\nDry run finished - run git diff to see package changes.`)}Copy the code

Tag generation rules:

For example, if targetVersion is 1.0.1, the tag is v1.0.1 if the package being released is Vite. If the published package is something else, such as plugin-vue, the tag is [email protected]

conclusion

We look at the source, need to have a purpose, for example this time, is to learn the vite source distribution. Looking at the source code with a purpose like this, you’ll see that it’s not that hard; Do not look at the source code warehouse from beginning to end, do not understand, and it is easy to lose the patience and confidence to learn.

Careful you, may find, in fact, a lot of times the source is not very perfect

  • For example, the annotations for release.yml explicitly state that the pseudo-matrix is for convenience, presumably copied from other configuration files with minor modifications.

  • For example, the NPM package is still published using YARN 1 (indicated in the comment). It is estimated that the package management tool of the Vite repository migrates PNPM from YARN, but the publishing mode has not been migrated.

If an open source library can become the mainstream, it must have its outstanding points, but after all, the time of developers is limited, so it cannot be perfect in every aspect. It is normal that such a release process does not affect the core code and does not have high priority for optimization.

Therefore, we are more to learn its essence from the open source code, improve and optimize it, and apply it to their own projects.

If this article is helpful to you, please help to point a thumbs-up πŸ‘, your encouragement is the biggest power on the way of my creation