@exadev/semantic-release-workspace 1.2.4 → 1.3.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/README.md CHANGED
@@ -68,6 +68,78 @@ Everything about *what* releases (topological order, forced-patch dependents, de
68
68
 
69
69
  `'single'` mode still runs each package's own configured publish plugins afterward (npm publish with provenance, GitHub releases), scoped per package exactly as `'per-package'` mode does — it just runs their `verifyConditions`/`publish`/`success` steps directly against the already-committed-and-tagged repository state, since by that point semantic-release's own top-level orchestrator would misread the tag this mode already created as an existing release. `addChannel` and `fail` are not called in this mode (pre-release channel promotion and posting an automated failure comment/issue, respectively) — a deliberate scope boundary, not a silent gap: raise an issue if your workflow needs them.
70
70
 
71
+ ## Gating publish
72
+
73
+ `commitStrategy` changes how a release is committed; `gatePublish` changes *when* it gets published relative to being tagged and pushed -- a different, orthogonal axis. With `gatePublish: true`, each due package is tagged and pushed via [@exadev/release-gate](https://www.npmjs.com/package/@exadev/release-gate) (a real dependency of this package -- no separate install needed) but never published: nothing calls `npm publish`, creates a GitHub Release, or runs any other configured publish step, until a separate `resume` step does so explicitly -- from the same process, or a completely different one (a later CI job, once a deploy or smoke test has confirmed the release should actually go out).
74
+
75
+ Rejected outright combined with `commitStrategy: 'single'`: that mode's tag/publish machinery is entirely bespoke and never goes through semantic-release's own `run()` (see [Commit strategies](#commit-strategies) above), so it has no insertion point for `release-gate`'s detach/resume primitives.
76
+
77
+ Two-step CI example -- tag in one job, publish once a gate has passed in a later one:
78
+
79
+ ```yaml
80
+ jobs:
81
+ tag:
82
+ runs-on: ubuntu-latest
83
+ permissions:
84
+ contents: write
85
+ id-token: write
86
+ steps:
87
+ - uses: actions/checkout@v4
88
+ with: { fetch-depth: 0 }
89
+ - uses: pnpm/action-setup@v4
90
+ - uses: actions/setup-node@v4
91
+ with: { node-version-file: .node-version }
92
+ - run: pnpm install --frozen-lockfile
93
+ - run: pnpm exec semantic-release-workspace release --gate-publish --gate-state-file gate-state.json
94
+ env:
95
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
96
+ - uses: actions/upload-artifact@v4
97
+ with: { name: gate-state, path: gate-state.json }
98
+
99
+ deploy-and-verify:
100
+ needs: tag
101
+ runs-on: ubuntu-latest
102
+ steps:
103
+ - run: ./deploy-and-smoke-test.sh # whatever the actual gate is
104
+
105
+ publish:
106
+ needs: deploy-and-verify
107
+ runs-on: ubuntu-latest
108
+ permissions:
109
+ contents: write
110
+ id-token: write
111
+ steps:
112
+ - uses: actions/checkout@v4
113
+ with: { fetch-depth: 0 }
114
+ - uses: pnpm/action-setup@v4
115
+ - uses: actions/setup-node@v4
116
+ with: { node-version-file: .node-version }
117
+ - run: pnpm install --frozen-lockfile
118
+ - uses: actions/download-artifact@v4
119
+ with: { name: gate-state }
120
+ - run: pnpm exec semantic-release-workspace resume --gate-state-file gate-state.json
121
+ env:
122
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
123
+ NPM_CONFIG_PROVENANCE: 'true'
124
+ ```
125
+
126
+ The programmatic equivalent, run in the same process or two separate ones:
127
+
128
+ ```ts
129
+ import { releaseWorkspace, resumeWorkspaceRelease } from '@exadev/semantic-release-workspace';
130
+
131
+ // Detach: tags and pushes, publishes nothing.
132
+ const { detached } = await releaseWorkspace({ root: process.cwd(), gatePublish: true });
133
+
134
+ // ...later, once whatever external gate needs to pass has passed, in this process or a fresh one:
135
+ const outcome = await resumeWorkspaceRelease({ root: process.cwd(), detached: detached ?? [] });
136
+ for (const pkg of outcome.packages) {
137
+ console.log(pkg.name, pkg.released ? `published ${pkg.gitTag}` : 'no release');
138
+ }
139
+ ```
140
+
141
+ `detached` (`WorkspaceReleaseOutcome.detached`, present only when `gatePublish: true`) carries everything `resumeWorkspaceRelease` needs -- one entry per package, each holding either the `@exadev/release-gate` state for a real release or `null` for a package with nothing to release. Its `relativeDirectory` field, not an absolute path, is what a resume pass resolves `cwd` from, so it works correctly even when the resume runs against a different checkout of the same repository than the one that ran the detach.
142
+
71
143
  ## Relationship to @qiwi/multi-semantic-release
72
144
 
73
145
  This tool exists because of [documents.js#664](https://github.com/ExaDev/documents.js/issues/664)'s research, which compared the third-party landscape — [@qiwi/multi-semantic-release](https://github.com/qiwi/multi-semantic-release) (itself a fork of [dhoulb's original](https://github.com/dhoulb/multi-semantic-release)), its successor [bulk-release](https://www.npmjs.com/package/bulk-release), and [Changesets](https://github.com/changesets/changesets) — against building in-house, and chose in-house: the org already maintains shared tooling config in exactly this shape, semantic-release's plugin lifecycle is well documented rather than proprietary, and the failure modes specific to cross-package version propagation were already understood from operating the ecosystem's existing automation ([background reading](https://dev.to/antongolub/the-chronicles-of-semantic-release-and-monorepos-5cfc)).
@@ -130,8 +202,17 @@ Run it from the workspace root (or pass `--root <directory>`). A dry run analyse
130
202
  | `--analyze-commits <json>` | Options for the wrapped @semantic-release/commit-analyzer (e.g. `'{"preset":"conventionalcommits","releaseRules":[...]}'`) |
131
203
  | `--generate-notes <json>` | Options for the wrapped @semantic-release/release-notes-generator |
132
204
  | `--commit-strategy <mode>` | `per-package` (default) or `single` — see [Commit strategies](#commit-strategies) |
205
+ | `--gate-publish` | Tag and push each due package, but defer publishing — see [Gating publish](#gating-publish). Requires `--gate-state-file`; rejected with `--commit-strategy single` |
206
+ | `--gate-state-file <path>` | With `--gate-publish`: where to write the state a later `resume` run needs |
133
207
  | `--config <file>` | A config file (`.json`, `.yaml`, `.yml`, `.js`, `.cjs`, or `.ts`, loaded via [cosmiconfig](https://github.com/cosmiconfig/cosmiconfig)) providing any of the above; explicit flags win |
134
208
 
209
+ `resume` (a separate subcommand, not a `release` flag) finishes publishing what a `--gate-publish` run tagged and pushed:
210
+
211
+ | Option | Meaning |
212
+ | --- | --- |
213
+ | `--root <directory>` | Workspace root holding `pnpm-workspace.yaml` in *this* checkout — may differ from the one that ran `release --gate-publish` (default: the process working directory) |
214
+ | `--gate-state-file <path>` | Required — the state file a `release --gate-publish` run wrote |
215
+
135
216
  Listing `@semantic-release/commit-analyzer` or `@semantic-release/release-notes-generator` as a `--plugin` is rejected: the orchestrator always provides those two steps itself (wrapped), so configuring them there would be a silent no-op — pass their options via `--analyze-commits`/`--generate-notes` instead. A real (non-dry) run under `commitStrategy: 'per-package'` (the default) must include `@semantic-release/git` in the pipeline, because without it nothing commits released manifests and changelogs back to the branch; `commitStrategy: 'single'` is the opposite — it rejects `@semantic-release/git` outright, since it does that committing itself (see [Commit strategies](#commit-strategies)).
136
217
 
137
218
  Note that the orchestrator sets `tagFormat`, `plugins`, `analyzeCommits`, and `generateNotes` explicitly on every per-package run, so those keys in any `release.config.*` found in the workspace are overridden by construction — configure the release through the orchestrator, not through a leftover single-package config.
@@ -168,7 +249,7 @@ const outcome = await releaseWorkspace({
168
249
  });
169
250
  ```
170
251
 
171
- Every stage is also exported individually — `discoverWorkspace`, `buildDependencyGraph`, `topologicalOrder`, `updateDependencyRange`, `createScopedPlugins`, `filterCommitsToDirectory` — along with the error hierarchy (`WorkspaceReleaseError` and friends) so embedders can distinguish orchestration failures from unexpected crashes.
252
+ Every stage is also exported individually — `discoverWorkspace`, `buildDependencyGraph`, `topologicalOrder`, `updateDependencyRange`, `createScopedPlugins`, `filterCommitsToDirectory`, `resumeWorkspaceRelease` — along with the error hierarchy (`WorkspaceReleaseError` and friends) so embedders can distinguish orchestration failures from unexpected crashes.
172
253
 
173
254
  ### Repository requirements
174
255
 
@@ -177,6 +258,7 @@ Every stage is also exported individually — `discoverWorkspace`, `buildDepende
177
258
  - A branch checkout, not a detached HEAD: dependency-bump commits are pushed to the current branch by name, so a detached HEAD stops the run with a `WorkspaceStateError` rather than pushing `HEAD:HEAD` at the remote. CI checkouts that default to a detached HEAD need the branch checked out explicitly.
178
259
  - A recognised CI environment for real runs (semantic-release refuses to publish from an unknown environment unless told otherwise); outside CI it falls back to dry-run behaviour.
179
260
  - Merge commits count for no package: `git log --name-only` lists no files for them, so their changes arrive through their parents, which the same range covers individually. Squash-merge workflows are unaffected, since a squash commit is an ordinary commit with a full file list.
261
+ - A `resume` run needs the same checkout the `release --gate-publish` run pushed to, or an equivalent one at the same commit and tags (a fresh `git clone`/checkout of the same repository at the same ref works fine) -- it resolves each package's directory from `pnpm-workspace.yaml` relative to its own `--root`, not from any absolute path recorded during the detach pass.
180
262
 
181
263
  ## Status
182
264