bulk-release 2.16.2 → 2.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## [2.18.0](https://github.com/semrel-extra/zx-bulk-release/compare/v2.17.0...v2.18.0) (2026-04-05)
2
+
3
+ ### Features
4
+ * feat: introduce recovery mode ([2f15fd8](https://github.com/semrel-extra/zx-bulk-release/commit/2f15fd83cfb41a4a55a4ab12d880c00e24c5c717))
5
+
6
+ ## [2.17.0](https://github.com/semrel-extra/zx-bulk-release/compare/v2.16.2...v2.17.0) (2026-04-05)
7
+
8
+ ### Features
9
+ * feat: rollback git tags on npm push issues ([3d55439](https://github.com/semrel-extra/zx-bulk-release/commit/3d55439a150f08bf2c0cf3d1edae9f327abce0e4))
10
+
1
11
  ## [2.16.2](https://github.com/semrel-extra/zx-bulk-release/compare/v2.16.1...v2.16.2) (2026-04-05)
2
12
 
3
13
  ### Fixes & improvements
package/README.md CHANGED
@@ -50,6 +50,7 @@ GH_TOKEN=ghtoken GH_USER=username NPM_TOKEN=npmtoken npx zx-bulk-release [opts]
50
50
  | `--dry-run` / `--no-publish` | Disable any publish logic | |
51
51
  | `--report` | Persist release state to file | |
52
52
  | `--snapshot` | Disable any publishing steps except of `npm` and `publishCmd` (if defined), then push packages to the `snapshot` channel | |
53
+ | `--recover` | Remove orphan git tags (tag pushed but npm publish failed) and retry release | |
53
54
  | `--debug` | Enable [zx](https://github.com/google/zx#verbose) verbose mode | |
54
55
  | `--version` / `-v` | Print own version | |
55
56
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bulk-release",
3
3
  "alias": "bulk-release",
4
- "version": "2.16.2",
4
+ "version": "2.18.0",
5
5
  "description": "zx-based alternative for multi-semantic-release",
6
6
  "type": "module",
7
7
  "exports": {
@@ -109,11 +109,18 @@ export const getTags = async (cwd, ref) =>
109
109
 
110
110
  export const pushTag = async ({cwd, tag, gitCommitterName, gitCommitterEmail}) => {
111
111
  await setUserConfig(cwd, gitCommitterName, gitCommitterEmail)
112
+
112
113
  await $({cwd})`
113
114
  git tag -m ${tag} ${tag} &&
114
115
  git push origin ${tag}`
115
116
  }
116
117
 
118
+ export const deleteRemoteTag = async ({cwd, tag}) => {
119
+ log()(`rolling back remote tag '${tag}'`)
120
+ await $({cwd, nothrow: true})`git push origin :refs/tags/${tag}`
121
+ await $({cwd, nothrow: true})`git tag -d ${tag}`
122
+ }
123
+
117
124
  // Memoize prevents .git/config lock
118
125
  // https://github.com/qiwi/packasso/actions/runs/4539987310/jobs/8000403413#step:7:282
119
126
  export const setUserConfig = memoizeBy(async(cwd, gitCommitterName, gitCommitterEmail) => $({cwd})`
@@ -1,12 +1,19 @@
1
1
  import {getPkgConfig} from '../config.js'
2
2
  import {getLatest} from '../processor/meta.js'
3
- import {getRoot, getSha} from '../api/git.js'
3
+ import {getRoot, getSha, deleteRemoteTag} from '../api/git.js'
4
+ import {fetchManifest} from '../api/npm.js'
5
+ import {log} from '../log.js'
4
6
  import {$} from 'zx-extra'
5
7
 
6
8
  // Inspired by https://docs.github.com/en/actions/learn-github-actions/contexts
7
9
  export const contextify = async (pkg, {packages, root, flags, env}) => {
8
10
  pkg.config = await getPkgConfig([pkg.absPath, root.absPath], env)
9
11
  pkg.latest = await getLatest(pkg)
12
+
13
+ if (flags.recover && pkg.latest.tag) {
14
+ await recover(pkg)
15
+ }
16
+
10
17
  pkg.context = {
11
18
  git: {
12
19
  sha: await getSha(pkg.absPath),
@@ -17,3 +24,25 @@ export const contextify = async (pkg, {packages, root, flags, env}) => {
17
24
  packages
18
25
  }
19
26
  }
27
+
28
+ // Verify that the latest tagged version actually exists on npm.
29
+ // If not (tag was pushed but npm publish failed), delete the orphan tag
30
+ // so the next analyze phase can re-detect changes and retry the release.
31
+ const recover = async (pkg) => {
32
+ const needsNpm = !pkg.manifest.private && pkg.config.npmPublish !== false
33
+ if (!needsNpm) return
34
+
35
+ const {tag} = pkg.latest
36
+ const manifest = await fetchManifest({
37
+ name: pkg.name,
38
+ version: tag.version,
39
+ config: pkg.config,
40
+ }, {nothrow: true})
41
+
42
+ if (!manifest) {
43
+ const cwd = await getRoot(pkg.absPath)
44
+ log({pkg})(`recover: tag '${tag.ref}' exists but ${pkg.name}@${tag.version} not found on npm, removing orphan tag`)
45
+ await deleteRemoteTag({cwd, tag: tag.ref})
46
+ pkg.latest = await getLatest(pkg)
47
+ }
48
+ }
@@ -5,14 +5,11 @@ import {npmPersist, npmPublish} from '../api/npm.js'
5
5
  import {prepareMeta, pushMeta, pushReleaseTag} from '../processor/meta.js'
6
6
  import {pushChangelog} from '../api/changelog.js'
7
7
  import {ghPages, ghRelease} from '../api/gh.js'
8
+ import {deleteRemoteTag} from '../api/git.js'
8
9
 
9
10
  export const publish = memoizeBy(async (pkg, run = exec) => within(async () => {
10
11
  $.scope = pkg.name
11
12
 
12
- // Debug
13
- // https://www.npmjs.com/package/@packasso/preset-ts-tsc-uvu/v/0.0.0?activeTab=code
14
- // https://github.com/qiwi/packasso/actions/runs/4514909191/jobs/7951564982#step:7:817
15
- // https://github.com/qiwi/packasso/blob/meta/2023-3-24-packasso-preset-ts-tsc-uvu-0-21-0-f0.json
16
13
  if (pkg.version !== pkg.manifest.version) {
17
14
  throw new Error('package.json version not synced')
18
15
  }
@@ -27,14 +24,26 @@ export const publish = memoizeBy(async (pkg, run = exec) => within(async () => {
27
24
  ])
28
25
  } else {
29
26
  await pushReleaseTag(pkg)
30
- await Promise.all([
31
- pushMeta(pkg),
32
- pushChangelog(pkg),
33
- npmPublish(pkg),
34
- ghRelease(pkg),
35
- ghPages(pkg),
36
- run(pkg, 'publishCmd')
37
- ])
27
+ try {
28
+ await Promise.all([
29
+ pushMeta(pkg),
30
+ pushChangelog(pkg),
31
+ npmPublish(pkg),
32
+ ghRelease(pkg),
33
+ ghPages(pkg),
34
+ run(pkg, 'publishCmd')
35
+ ])
36
+ } catch (e) {
37
+ // Rollback the tag only for npm-published packages so the next run can retry.
38
+ // Git-tag-only packages (private or npmPublish: false) keep their tag — it IS the release.
39
+ const needsNpm = !pkg.manifest.private && pkg.config.npmPublish !== false
40
+ const cwd = pkg.context.git.root
41
+ const tag = pkg.context.git.tag
42
+ if (tag && needsNpm) {
43
+ await deleteRemoteTag({cwd, tag})
44
+ }
45
+ throw e
46
+ }
38
47
  }
39
48
  pkg.published = true
40
49
  }))