2.15.10 • Published 2 days ago

bulk-release v2.15.10

Weekly downloads
-
License
MIT
Repository
github
Last release
2 days ago

zx-bulk-release

zx-based alternative for multi-semantic-release

CI Maintainability Test Coverage npm (tag)

Key features

  • Conventional commits trigger semantic releases.
  • Automated cross-pkg version bumping.
  • Predictable toposort-driven flow.
  • No default branch blocking (no release commits).
  • Pkg changelogs go to changelog branch (configurable).
  • Docs are published to gh-pages branch (configurable).
  • No extra builds. The required deps are fetched from the pkg registry (npmFetch config opt).

Roadmap

  • Store release metrics to meta.
  • Self-repair. Restore broken/missing metadata from external registries (npm, pypi, m2). Tags should be the only source of truth
  • Multistack. Add support for java/kt/py.
  • Semaphore. Let several release agents to serve the monorepo at the same time.

Requirements

  • macOS / linux
  • Node.js >= 16.0.0
  • npm >=7 / yarn >= 3
  • wget
  • tar
  • git

Usage

Install

yarn add zx-bulk-release

CLI

GH_TOKEN=ghtoken GH_USER=username NPM_TOKEN=npmtoken npx zx-bulk-release [opts]
FlagDescriptionDefault
--ignorePackages to ignore: a, b
--include-privateInclude private packagesfalse
--concurrencybuild/publish threads limitos.cpus.length
--no-buildSkip buildCmd invoke
--no-testDisable testCmd run
--no-npm-fetchDisable npm artifacts fetching
--only-workspace-depsRecognize only workspace: deps as graph edges
--dry-run / --no-publishDisable any publish logic
--reportPersist release state to file
--snapshotDisable any publishing steps except of npm and publishCmd (if defined), then push packages to the snapshot channel
--debugEnable zx verbose mode
--version / -vPrint own version

JS API

import { run } from 'zx-bulk-release'

const cwd = '/foo/bar'
const env = {GH_TOKEN: 'foo', NPM_TOKEN: 'bar'}
const flags = {dryRun: true}

await run({
  cwd,    // Defaults to process.cwd()
  flags,  // Defaults to process.env
  env     // Defaults to minimist-parsed `process.argv.slice(2)`
})

Config

cosmiconfig

Any cosmiconfig compliant format: .releaserc, .release.json, .release.yaml, etc in the package root or in the repo root dir.

{
  "cmd": "yarn && yarn build && yarn test",
  "npmFetch": true,
  "changelog": "changelog",
  "ghPages": "gh-pages"
}

env vars

export const parseEnv = (env = process.env) => {
  const {GH_USER, GH_USERNAME, GITHUB_USER, GITHUB_USERNAME, GH_TOKEN, GITHUB_TOKEN, NPM_TOKEN, NPM_REGISTRY, NPMRC, NPM_USERCONFIG, NPM_CONFIG_USERCONFIG, NPM_PROVENANCE, GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL} = env

  return {
    ghUser:             GH_USER || GH_USERNAME || GITHUB_USER || GITHUB_USERNAME,
    ghToken:            GH_TOKEN || GITHUB_TOKEN,
    npmToken:           NPM_TOKEN,
    // npmConfig suppresses npmToken
    npmConfig:          NPMRC || NPM_USERCONFIG || NPM_CONFIG_USERCONFIG,
    npmRegistry:        NPM_REGISTRY || 'https://registry.npmjs.org',
    npmProvenance:      NPM_PROVENANCE,
    gitCommitterName:   GIT_COMMITTER_NAME || 'Semrel Extra Bot',
    gitCommitterEmail:  GIT_COMMITTER_EMAIL || 'semrel-extra-bot@hotmail.com',
  }
}

Demo

Implementation notes

Flow

try {
  const {packages, queue, root} = await topo({cwd, flags})
  console.log('queue:', queue)

  for (let name of queue) {
    const pkg = packages[name]

    await analyze(pkg, packages, root)

    if (pkg.changes.length === 0) continue

    await build(pkg, packages)

    if (flags.dryRun) continue

    await publish(pkg)
  }
} catch (e) {
  console.error(e)
  throw e
}

topo

Toposort is used to resolve the pkg release queue. By default, it omits the packages marked as private. You can override this by setting the --include-private flag.

analyze

Determines pkg changes, release type, next version etc.

export const analyze = async (pkg, packages, root) => {
  pkg.config = await getPkgConfig(pkg.absPath, root.absPath)
  pkg.latest = await getLatest(pkg)

  const semanticChanges = await getSemanticChanges(pkg.absPath, pkg.latest.tag?.ref)
  const depsChanges = await updateDeps(pkg, packages)
  const changes = [...semanticChanges, ...depsChanges]

  pkg.changes = changes
  pkg.version = resolvePkgVersion(changes, pkg.latest.tag?.version || pkg.manifest.version)
  pkg.manifest.version = pkg.version

  console.log(`[${pkg.name}] semantic changes`, changes)
}

Set config.releaseRules to override the default rules preset:

[
  {group: 'Features', releaseType: 'minor', prefixes: ['feat']},
  {group: 'Fixes & improvements', releaseType: 'patch', prefixes: ['fix', 'perf', 'refactor', 'docs', 'patch']},
  {group: 'BREAKING CHANGES', releaseType: 'major', keywords: ['BREAKING CHANGE', 'BREAKING CHANGES']},
]

build

Applies config.cmd to build pkg assets: bundles, docs, etc.

export const build = async (pkg, packages) => {
  // ...
  if (!pkg.fetched && config.cmd) {
    console.log(`[${pkg.name}] run cmd '${config.cmd}'`)
    await $.o({cwd: pkg.absPath, quote: v => v})`${config.cmd}`
  }
  // ...
}

publish

Publish the pkg to git, npm, gh-pages, gh-release, etc.

export const publish = async (pkg) => {
  await fs.writeJson(pkg.manifestPath, pkg.manifest, {spaces: 2})
  await pushTag(pkg)
  await pushMeta(pkg)
  await pushChangelog(pkg)
  await npmPublish(pkg)
  await ghRelease(pkg)
  await ghPages(pkg)
}

Tags

Lerna tags (like @pkg/name@v1.0.0-beta.0) are suitable for monorepos, but they don’t follow semver spec. Therefore, we propose another contract:

'2022.6.13-optional-org.pkg-name.v1.0.0-beta.1+sha.1-f0'
// date    name                  version             format

Note, npm-package-name charset is wider than semver, so we need a pinch of base64url magic for some cases.

'2022.6.13-examplecom.v1.0.0.ZXhhbXBsZS5jb20-f1'
// date    name       ver    b64             format

Anyway, it's still possible to override the default config by tagFormat option:

tagFormatExample
f02022.6.22-qiwi.pijma-native.v1.0.0-beta.0+foo.bar-f0
f12022.6.13-examplecom.v1.0.0.ZXhhbXBsZS5jb20-f1
lerna@qiwi/pijma-ssr@1.1.12
pure1.2.3-my.package

Meta

Each release gathers its own meta. It is recommended to store the data somehow to ensure flow reliability.:

  • Set meta: {type: 'asset'} to persist as gh asset.
  • If set meta: {type: null} the required data will be fetched from the npm artifact.
  • Otherwise, it will be pushed as a regular git commit to the meta branch (default behaviour).

2022-6-26-semrel-extra-zxbr-test-c-1-3-1-f0.json

{
  "META_VERSION": "1",
  "name": "@semrel-extra/zxbr-test-c",
  "hash": "07b7df33f0159f674c940bd7bbb2652cdaef5207",
  "version": "1.3.1",
  "dependencies": {
    "@semrel-extra/zxbr-test-a": "^1.4.0",
    "@semrel-extra/zxbr-test-d": "~1.2.0"
  }
}

Report

Release process state is reported to the console and to a file if --report flag is set to /some/path/release-report.json, for example.

{
  status: 'success',            // 'sucess' | 'failure' | 'pending'
  error: null,                  // null or Error
  queue: ['a', 'b', 'c', 'd']   // release queue
  packages: [{
    name: 'a',
    version: '1.1.0',
    path: '/pkg/abs/path',
    relPath: 'pkg/rel/path',
    config: {                   // pkg config
      changelog: 'changelog',
      npmFetch: true
    },                 
    changes: [{                 // semantic changes
      group: 'Features',
      releaseType: 'minor',
      change: 'feat: add feat',
      subj: 'feat: add feat',
      body: '',
      short: '792512c',
      hash: '792512cccd69c6345d9d32d3d73e2591ea1776b5'
    }],                  
    tag: {
      version: 'v1.1.0',
      name: 'a',
      ref: '2022.6.22-a.v1.1.0-f0'
    },
    releaseType: 'minor',       // 'major' | 'minor' | 'patch'
    prevVersion: '1.0.0'        // previous version or null
  }, {
    name: 'b',
    // ...
  }],
  events: [
    {msg: ['zx-bulk-release'], scope:'~', date: 1665839585488, level: 'info'},
    {msg: ['queue:',['a','b']], scope:'~', date: 1665839585493, level: 'info'},
    {msg: ["run buildCmd 'yarn && yarn build && yarn test'"], scope: 'a', date: 1665839585719, level:'info'},
    // ...
  ]
}

Output

Compact and clear logs

Run npm_config_yes=true npx zx-bulk-release
zx-bulk-release
[@semrel-extra/zxbr-test-a] semantic changes [
  {
    group: 'Fixes & improvements',
    releaseType: 'patch',
    change: 'fix(a): random',
    subj: 'fix(a): random',
    body: '',
    short: '6ff25bd',
    hash: '6ff25bd421755b929ef2b58f35c727670fd93849'
  }
]
[@semrel-extra/zxbr-test-a] run cmd 'yarn && yarn build && yarn test'
[@semrel-extra/zxbr-test-a] push release tag 2022.6.27-semrel-extra.zxbr-test-a.1.8.1-f0
[@semrel-extra/zxbr-test-a] push artifact to branch 'meta'
[@semrel-extra/zxbr-test-a] push changelog
[@semrel-extra/zxbr-test-a] publish npm package @semrel-extra/zxbr-test-a 1.8.1 to https://registry.npmjs.org
[@semrel-extra/zxbr-test-a] create gh release
[@semrel-extra/zxbr-test-b] semantic changes [
  {
    group: 'Dependencies',
    releaseType: 'patch',
    change: 'perf',
    subj: 'perf: @semrel-extra/zxbr-test-a updated to 1.8.1'
  }
]
[@semrel-extra/zxbr-test-b] run cmd 'yarn && yarn build && yarn test'
[@semrel-extra/zxbr-test-b] push release tag 2022.6.27-semrel-extra.zxbr-test-b.1.3.5-f0
[@semrel-extra/zxbr-test-b] push artifact to branch 'meta'
[@semrel-extra/zxbr-test-b] push changelog
[@semrel-extra/zxbr-test-b] publish npm package @semrel-extra/zxbr-test-b 1.3.5 to https://registry.npmjs.org
[@semrel-extra/zxbr-test-b] create gh release
[@semrel-extra/zxbr-test-d] semantic changes [

References

License

MIT

2.15.10

2 days ago

2.15.9

2 days ago

2.15.8

23 days ago

2.15.7

24 days ago

2.15.6

1 month ago

2.15.5

1 month ago

2.15.4

2 months ago

2.15.2

3 months ago

2.15.3

3 months ago

2.15.1

3 months ago

2.15.0

3 months ago

2.13.2

5 months ago

2.13.3

5 months ago

2.14.0

5 months ago

2.13.1

5 months ago

2.13.0

5 months ago

2.12.0

8 months ago

2.11.6

8 months ago

2.11.7

8 months ago

2.11.5

10 months ago

2.12.1

6 months ago

2.12.2

6 months ago

2.11.4

11 months ago

2.11.0

11 months ago

2.11.1

11 months ago

2.10.0

11 months ago

2.9.3

11 months ago

2.11.2

11 months ago

2.11.3

11 months ago

2.9.2

11 months ago

2.9.1

11 months ago

2.9.0

12 months ago

2.8.0

12 months ago

2.7.0

1 year ago

2.6.1

1 year ago

2.6.0

1 year ago

2.5.0

1 year ago

2.4.1

1 year ago

2.3.2

1 year ago

2.4.0

1 year ago

2.3.1

1 year ago

2.5.2

1 year ago

2.5.1

1 year ago

2.4.2

1 year ago

2.3.3

1 year ago

2.5.3

1 year ago

2.3.0

1 year ago

2.2.17

1 year ago

2.2.16

1 year ago

2.2.15

1 year ago

2.2.14

1 year ago

2.2.13

1 year ago