@williamthorsen/release-kit 13.1.0 → 14.1.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/.meta/changelog.json +118 -9
- package/.readyup/kits/default.js +4 -4
- package/.readyup/kits/npm-auto-publish.js +81 -41
- package/.readyup/manifest.json +19 -9
- package/CHANGELOG.md +38 -6
- package/README.md +14 -6
- package/dist/esm/assertReleasesOnNpm.d.ts +7 -0
- package/dist/esm/assertReleasesOnNpm.js +56 -0
- package/dist/esm/bin/release-kit.js +5 -2
- package/dist/esm/deriveWorkspaceConfig.js +2 -2
- package/dist/esm/enumerateReleaseWindows.d.ts +1 -1
- package/dist/esm/enumerateReleaseWindows.js +1 -3
- package/dist/esm/findTransitiveDependents.d.ts +3 -0
- package/dist/esm/findTransitiveDependents.js +20 -0
- package/dist/esm/help/prepareHelp.d.ts +1 -1
- package/dist/esm/help/prepareHelp.js +4 -2
- package/dist/esm/init/hasPublishablePackage.d.ts +10 -0
- package/dist/esm/init/hasPublishablePackage.js +28 -0
- package/dist/esm/init/initCommand.js +23 -13
- package/dist/esm/init/scaffold.d.ts +6 -1
- package/dist/esm/init/scaffold.js +10 -7
- package/dist/esm/init/templates.js +1 -1
- package/dist/esm/init/updateTemplatesCommand.d.ts +2 -1
- package/dist/esm/init/updateTemplatesCommand.js +12 -6
- package/dist/esm/isPublishableManifest.d.ts +2 -0
- package/dist/esm/isPublishableManifest.js +5 -0
- package/dist/esm/matchesTagPrefix.d.ts +2 -0
- package/dist/esm/matchesTagPrefix.js +4 -0
- package/dist/esm/npmRegistry.d.ts +35 -0
- package/dist/esm/npmRegistry.js +58 -0
- package/dist/esm/prepareCommand.d.ts +1 -0
- package/dist/esm/prepareCommand.js +52 -61
- package/dist/esm/previewTagPrefixes.js +2 -1
- package/dist/esm/releasePrepare.d.ts +2 -16
- package/dist/esm/releasePrepareMono.d.ts +17 -2
- package/dist/esm/releasePrepareMono.js +23 -11
- package/dist/esm/sync-labels/templates.js +1 -1
- package/docs/configuration.md +1 -1
- package/docs/labels.md +5 -3
- package/docs/project-releases.md +2 -2
- package/docs/readiness-checks.md +2 -2
- package/docs/releasing.md +20 -10
- package/package.json +8 -8
- package/dist/esm/validateOnlyExcludesStrandedDependents.d.ts +0 -37
- package/dist/esm/validateOnlyExcludesStrandedDependents.js +0 -115
|
@@ -5,6 +5,7 @@ import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
|
5
5
|
import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
|
|
6
6
|
import { detectUndeclaredTagPrefixes } from "./detectUndeclaredTagPrefixes.js";
|
|
7
7
|
import { describeEmptyWorkspace, discoverWorkspaces } from "./discoverWorkspaces.js";
|
|
8
|
+
import { matchesTagPrefix } from "./matchesTagPrefix.js";
|
|
8
9
|
export function previewTagPrefixes(config) {
|
|
9
10
|
const workspace = discoverWorkspaces();
|
|
10
11
|
if (workspace.kind === 'empty') {
|
|
@@ -62,7 +63,7 @@ function countTagsMatching(prefix) {
|
|
|
62
63
|
maxBuffer: GIT_OUTPUT_LIMIT,
|
|
63
64
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
64
65
|
});
|
|
65
|
-
return output.split('\n').filter((line) => line.trim()
|
|
66
|
+
return output.split('\n').filter((line) => matchesTagPrefix(line.trim(), [prefix])).length;
|
|
66
67
|
}
|
|
67
68
|
catch {
|
|
68
69
|
return 0;
|
|
@@ -10,24 +10,10 @@ export interface ReleasePrepareOptions {
|
|
|
10
10
|
/** Level of the release, in place of the level that the changelog items call for; it doesn't trigger a release. */
|
|
11
11
|
bumpOverride?: ReleaseType;
|
|
12
12
|
/**
|
|
13
|
-
* Explicit target version (canonical `N.N.N`) that bypasses commit-derived bump logic
|
|
14
|
-
*
|
|
15
|
-
* `config.workspaces` to a single workspace before invoking.
|
|
13
|
+
* Explicit target version (canonical `N.N.N`) that bypasses commit-derived bump logic and takes precedence over
|
|
14
|
+
* `bumpOverride` and `force`.
|
|
16
15
|
*/
|
|
17
16
|
setVersion?: string;
|
|
18
|
-
/**
|
|
19
|
-
* Workspace directories to which `--only` narrowed the run (monorepo only), with
|
|
20
|
-
* `config.workspaces` already filtered to match. Present only for a narrowed run, which
|
|
21
|
-
* skips the project release: The project tier rolls up every contributing workspace, and
|
|
22
|
-
* the narrowing has changed which workspaces those are.
|
|
23
|
-
*/
|
|
24
|
-
only?: string[];
|
|
25
|
-
/**
|
|
26
|
-
* Every configured workspace `dir`, taken before `--only` narrowed `config.workspaces`; the unrouted-scope report
|
|
27
|
-
* does not report a scope naming one of them that the run did not read. Defaults to the `dir`s of
|
|
28
|
-
* `config.workspaces`.
|
|
29
|
-
*/
|
|
30
|
-
configuredWorkspaceDirs?: readonly string[];
|
|
31
17
|
/**
|
|
32
18
|
* If true, plan per-workspace release-notes previews under `{workspacePath}/docs/`
|
|
33
19
|
* (`README.v{version}.md` and `RELEASE_NOTES.v{version}.md`) from each workspace's
|
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
import type { ReleasePlan } from './releasePlan.js';
|
|
2
2
|
import type { ReleasePrepareOptions } from './releasePrepare.js';
|
|
3
3
|
import type { MonorepoPrepareConfig } from './types.js';
|
|
4
|
+
/** Options for monorepo release preparation. */
|
|
5
|
+
export interface MonorepoPrepareOptions extends Omit<ReleasePrepareOptions, 'setVersion'> {
|
|
6
|
+
/**
|
|
7
|
+
* Workspace `dir`s from which the release starts; the run plans them and their transitive dependents. Present only
|
|
8
|
+
* for a narrowed run, which skips the project release: The project tier rolls up every contributing workspace, and
|
|
9
|
+
* the narrowing has changed which workspaces those are.
|
|
10
|
+
*/
|
|
11
|
+
only?: readonly string[];
|
|
12
|
+
/**
|
|
13
|
+
* Explicit target versions (canonical `N.N.N`), keyed by workspace `dir`. Each named workspace starts the release
|
|
14
|
+
* and releases at its version, which takes precedence over `bumpOverride` and `force`.
|
|
15
|
+
*/
|
|
16
|
+
setVersions?: ReadonlyMap<string, string>;
|
|
17
|
+
}
|
|
4
18
|
/**
|
|
5
19
|
* Orchestrates release preparation for a monorepo with multiple workspaces.
|
|
6
20
|
*
|
|
21
|
+
* Phase 0: Selects the planned workspaces: those from which the release starts and their transitive dependents.
|
|
7
22
|
* Phase 1: Determines direct bumps from commits for each workspace.
|
|
8
|
-
* Phase 2:
|
|
23
|
+
* Phase 2: Propagates bumps to dependents.
|
|
9
24
|
* Phase 2b: Sorts the full release set topologically.
|
|
10
25
|
* Phase 3: Plans bumps and changelogs in dependency order.
|
|
11
26
|
* Phase 3b: Plans the project release.
|
|
12
27
|
* Phase 4: Renders the format command.
|
|
13
28
|
*/
|
|
14
|
-
export declare function releasePrepareMono(
|
|
29
|
+
export declare function releasePrepareMono(fullConfig: MonorepoPrepareConfig, options: MonorepoPrepareOptions): ReleasePlan;
|
|
@@ -11,6 +11,7 @@ import { applyWorkspaceOverrides, createOverrideContext, formatStaleOverrideKeyW
|
|
|
11
11
|
import { decideRelease } from "./decideRelease.js";
|
|
12
12
|
import { deriveDependenciesHeader, deriveSectionOrder } from "./deriveReleaseNotesConfig.js";
|
|
13
13
|
import { detectUndeclaredTagPrefixes } from "./detectUndeclaredTagPrefixes.js";
|
|
14
|
+
import { findTransitiveDependents } from "./findTransitiveDependents.js";
|
|
14
15
|
import { findUnroutedEntryScopes } from "./findUnroutedEntryScopes.js";
|
|
15
16
|
import { getAllTagPrefixes } from "./generateChangelogs.js";
|
|
16
17
|
import { hasPrettierConfig } from "./hasPrettierConfig.js";
|
|
@@ -21,10 +22,17 @@ import { propagateBumps } from "./propagateBumps.js";
|
|
|
21
22
|
import { planPreservedSections } from "./readChangelogSections.js";
|
|
22
23
|
import { releasePrepareProject } from "./releasePrepareProject.js";
|
|
23
24
|
import { renderChangelogMarkdown } from "./renderChangelogMarkdown.js";
|
|
24
|
-
export function releasePrepareMono(
|
|
25
|
+
export function releasePrepareMono(fullConfig, options) {
|
|
25
26
|
const { only, withReleaseNotes } = options;
|
|
26
27
|
const writes = [];
|
|
27
28
|
const warnings = [];
|
|
29
|
+
const graph = buildDependencyGraph(fullConfig.workspaces);
|
|
30
|
+
const startDirs = selectStartDirs(fullConfig.workspaces, options);
|
|
31
|
+
const plannedDirs = findTransitiveDependents(graph, startDirs);
|
|
32
|
+
const config = {
|
|
33
|
+
...fullConfig,
|
|
34
|
+
workspaces: fullConfig.workspaces.filter((workspace) => plannedDirs.has(workspace.dir)),
|
|
35
|
+
};
|
|
28
36
|
if (withReleaseNotes === true && !config.changelogJson.enabled) {
|
|
29
37
|
warnings.push('--with-release-notes requires changelogJson.enabled; skipping preview generation');
|
|
30
38
|
}
|
|
@@ -32,10 +40,9 @@ export function releasePrepareMono(config, options) {
|
|
|
32
40
|
const sectionOrder = deriveSectionOrder(workTypes);
|
|
33
41
|
const dependenciesHeader = deriveDependenciesHeader(workTypes);
|
|
34
42
|
const overrideContext = createOverrideContext(config.workspaces);
|
|
35
|
-
const { directBumps, directResults, skippedResults, untaggedBaselines } = determineDirectBumps(config, options);
|
|
36
|
-
reportUnroutedEntryScopes([...directResults.values(), ...skippedResults], config,
|
|
43
|
+
const { directBumps, directResults, skippedResults, untaggedBaselines } = determineDirectBumps(config, options, startDirs);
|
|
44
|
+
reportUnroutedEntryScopes([...directResults.values(), ...skippedResults], config, fullConfig.workspaces.map((workspace) => workspace.dir));
|
|
37
45
|
const skippedHistories = new Map(skippedResults.map((skipped) => [skipped.workspace.dir, skipped.history]));
|
|
38
|
-
const graph = buildDependencyGraph(config.workspaces);
|
|
39
46
|
const fullReleaseSet = propagateBumps(directBumps, graph);
|
|
40
47
|
assertTaggedBaseline([...untaggedBaselines]
|
|
41
48
|
.filter(([dir]) => directBumps.has(dir) || !fullReleaseSet.has(dir))
|
|
@@ -107,11 +114,15 @@ export function releasePrepareMono(config, options) {
|
|
|
107
114
|
...(project !== undefined && { project }),
|
|
108
115
|
};
|
|
109
116
|
}
|
|
110
|
-
function
|
|
111
|
-
const {
|
|
112
|
-
if (
|
|
113
|
-
|
|
117
|
+
function selectStartDirs(workspaces, options) {
|
|
118
|
+
const { only, setVersions } = options;
|
|
119
|
+
if (only === undefined) {
|
|
120
|
+
return new Set(workspaces.map((workspace) => workspace.dir));
|
|
114
121
|
}
|
|
122
|
+
return new Set([...only, ...(setVersions?.keys() ?? [])]);
|
|
123
|
+
}
|
|
124
|
+
function determineDirectBumps(config, options, startDirs) {
|
|
125
|
+
const { force, bumpOverride, setVersions } = options;
|
|
115
126
|
const directBumps = new Map();
|
|
116
127
|
const directResults = new Map();
|
|
117
128
|
const skippedResults = [];
|
|
@@ -138,6 +149,7 @@ function determineDirectBumps(config, options) {
|
|
|
138
149
|
if (tag === undefined) {
|
|
139
150
|
maybeEmitBaselineHint(workspace, knownPrefixes, hintState);
|
|
140
151
|
}
|
|
152
|
+
const setVersion = setVersions?.get(workspace.dir);
|
|
141
153
|
if (setVersion !== undefined) {
|
|
142
154
|
directBumps.set(workspace.dir, { releaseType: 'patch', newVersionOverride: setVersion });
|
|
143
155
|
directResults.set(workspace.dir, {
|
|
@@ -152,7 +164,7 @@ function determineDirectBumps(config, options) {
|
|
|
152
164
|
const decision = decideRelease({
|
|
153
165
|
naturalBump: history.unreleased.bump,
|
|
154
166
|
commitCount: history.unreleased.commits.length,
|
|
155
|
-
force,
|
|
167
|
+
force: force === true && startDirs.has(workspace.dir),
|
|
156
168
|
bumpOverride,
|
|
157
169
|
skipReasons: {
|
|
158
170
|
noCommits: `No commits for ${name} ${since}. Pass --force to release at patch. Skipping.`,
|
|
@@ -173,8 +185,8 @@ function determineDirectBumps(config, options) {
|
|
|
173
185
|
}
|
|
174
186
|
return { directBumps, directResults, skippedResults, untaggedBaselines };
|
|
175
187
|
}
|
|
176
|
-
function reportUnroutedEntryScopes(results, config,
|
|
177
|
-
const findings = findUnroutedEntryScopes(results.map(({ workspace, history }) => ({ dir: workspace.dir, commits: history.unreleased.commits })),
|
|
188
|
+
function reportUnroutedEntryScopes(results, config, configuredWorkspaceDirs) {
|
|
189
|
+
const findings = findUnroutedEntryScopes(results.map(({ workspace, history }) => ({ dir: workspace.dir, commits: history.unreleased.commits })), configuredWorkspaceDirs, config.scopeAliases ?? {});
|
|
178
190
|
for (const { workspace, history } of results) {
|
|
179
191
|
history.unreleased.diagnostics.unroutedEntryScopes = findings.get(workspace.dir) ?? [];
|
|
180
192
|
}
|
|
@@ -11,7 +11,7 @@ on:
|
|
|
11
11
|
paths:
|
|
12
12
|
- .github/labels.yaml
|
|
13
13
|
|
|
14
|
-
# Preview on review: The check's log has
|
|
14
|
+
# Preview on review: The check's log has the computed diff, and the check warns about each label not declared by the file.
|
|
15
15
|
pull_request:
|
|
16
16
|
paths:
|
|
17
17
|
- .github/labels.yaml
|
package/docs/configuration.md
CHANGED
|
@@ -90,7 +90,7 @@ Tag prefixes from distinct owners must not be identical or be a strict prefix of
|
|
|
90
90
|
- A `retiredPackages[]` entry (one owner per entry).
|
|
91
91
|
- The `project` block, when configured.
|
|
92
92
|
|
|
93
|
-
Because release-kit matches a tag to a prefix when the tag name
|
|
93
|
+
Because release-kit matches a tag to a prefix when the tag name is the prefix followed by a complete SemVer version, a strict-prefix overlap between distinct owners can match the wrong owner's tags. For example, a retired prefix of `pkg-` collides with a workspace prefix of `pkg-1.0.0-v`, since `pkg-1.0.0-v1.2.3` is `pkg-` followed by the pre-release version `1.0.0-v1.2.3`. The rule rejects every strict-prefix overlap, including one such as `v` and `vue-helpers-v`, whose shorter prefix can't match any tag of the longer one.
|
|
94
94
|
|
|
95
95
|
The rule is enforced at config load; the resulting error identifies both colliding declarations.
|
|
96
96
|
|
package/docs/labels.md
CHANGED
|
@@ -52,7 +52,7 @@ labels: {
|
|
|
52
52
|
},
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
The sync workflow archives a marked label rather than deleting it. An archived label stays on the issues and pull requests that have it, but it cannot be added to new ones and does not appear in the label picker. Removing the mark unarchives the label on the next sync. Removing the entry
|
|
55
|
+
The sync workflow archives a marked label rather than deleting it. An archived label stays on the issues and pull requests that have it, but it cannot be added to new ones and does not appear in the label picker. Removing the mark unarchives the label on the next sync. Removing the entry leaves the label in place: The sync does not delete a label that `.github/labels.yaml` does not declare, and it reports each such label as a warning. To delete a label, run `gh label delete <name>`; the label disappears from every issue and pull request that had it.
|
|
56
56
|
|
|
57
57
|
The mark is set only in the `labels` record, not in a preset. To retire a preset's label, replace it with an entry that restates its color and adds the mark.
|
|
58
58
|
|
|
@@ -68,15 +68,17 @@ The scaffolded workflow declares three triggers:
|
|
|
68
68
|
|
|
69
69
|
Applying on merge closes the window in which a regenerated `.github/labels.yaml` remains unapplied. In that window, a later manual dispatch would apply an unreviewed label set.
|
|
70
70
|
|
|
71
|
-
The `check` job runs the same sync in dry-run. Its **Label diff** log group lists every create, edit,
|
|
71
|
+
The `check` job runs the same sync in dry-run. Its **Label diff** log group lists every create, edit, and rename that the sync would perform.
|
|
72
72
|
|
|
73
73
|
Its **Archive state** log group lists each label that the sync would archive or unarchive. Archiving runs after the rest of the sync, so a label that the file both adds and marks is created and then archived.
|
|
74
74
|
|
|
75
|
+
Both jobs end with an **Undeclared labels** log group, and they report each repository label that the file does not declare as a warning annotation, which appears on the run and on the pull request's check. A label created by hand, for example to put it on the ticket that will declare it, therefore survives every sync until the file declares it or someone deletes it.
|
|
76
|
+
|
|
75
77
|
The two jobs are split because a job's permissions are fixed when the run is created and cannot vary by trigger. The split keeps a write-scoped token out of pull-request runs.
|
|
76
78
|
|
|
77
79
|
The push trigger filters on the path alone; the `sync` job compares `github.ref_name` against the repository's default branch. The workflow therefore doesn't need a per-repo edit, whatever that branch is named, and a push touching `.github/labels.yaml` on any other branch produces a run whose jobs all skip.
|
|
78
80
|
|
|
79
|
-
Manual dispatch is not a preview. It matches the `sync` job, so it applies the labels
|
|
81
|
+
Manual dispatch is not a preview. It matches the `sync` job, so it applies the labels; only the pull-request path runs in dry-run.
|
|
80
82
|
|
|
81
83
|
### Dry-run checks on fork pull requests
|
|
82
84
|
|
package/docs/project-releases.md
CHANGED
|
@@ -68,7 +68,7 @@ CLI flag interactions:
|
|
|
68
68
|
- `--dry-run` previews project artifacts alongside workspace artifacts; it doesn't write any files.
|
|
69
69
|
- `--bump=major|minor|patch` propagates to the project release as a level chooser. It does not trigger a release on its own when the window doesn't have any commits or any bump-worthy commits.
|
|
70
70
|
- `--force` runs the project release even when the history since the last project tag doesn't have any commits or any bump-worthy commits. Defaults to patch when `--bump` is not given; combine with `--bump=X` to release at a different level.
|
|
71
|
-
- `--only` narrows the run to the named workspaces and skips the project release, which is reported as a warning. The project release rolls up every contributing workspace, and `--only` changes which workspaces those are, so rolling up a narrowed set would release a project version covering work that the run excluded. Run a full `prepare` (without `--only`) to include the project release.
|
|
72
|
-
- `--set-version` is rejected with an error when `project` is configured. `--set-version`
|
|
71
|
+
- `--only` narrows the run to the named workspaces and their dependents and skips the project release, which is reported as a warning. The project release rolls up every contributing workspace, and `--only` changes which workspaces those are, so rolling up a narrowed set would release a project version covering work that the run excluded. Run a full `prepare` (without `--only`) to include the project release.
|
|
72
|
+
- `--set-version` is rejected with an error when `project` is configured. `--set-version` sets individual workspaces' versions, but a project release rolls up every contributing workspace; the two semantics don't compose. To use `--set-version`, run on a config without a `project` block.
|
|
73
73
|
|
|
74
74
|
How `--bump` and `--force` combine for every target is in [Releasing](releasing.md#release-kit-prepare).
|
package/docs/readiness-checks.md
CHANGED
|
@@ -7,12 +7,12 @@ release-kit publishes two [readyup](https://www.npmjs.com/package/readyup) kits
|
|
|
7
7
|
The [README](../README.md#readiness-checks) shows how to name release-kit in the readyup config.
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
rdy run --
|
|
10
|
+
rdy run --sources # every kit each listed package publishes
|
|
11
11
|
rdy run --from npm:@williamthorsen/release-kit # the default kit alone
|
|
12
12
|
rdy run --from npm:@williamthorsen/release-kit npm-auto-publish # the npm-auto-publish kit
|
|
13
13
|
rdy list --from npm:@williamthorsen/release-kit # what release-kit publishes
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
`--
|
|
16
|
+
`--sources` is the form that keeps working when release-kit publishes further kits.
|
|
17
17
|
|
|
18
18
|
Because `npm-auto-publish` queries the npm registry for each package's trusted publisher, it needs an npm session elevated by two-factor authentication, and is meant to be invoked deliberately rather than included in an unattended run. A session that cannot answer those queries, whether it is missing a login, missing the elevation, or facing an unreachable registry, is reported once by the `npm session can answer trust queries` gate. The rows that would repeat the failed query are then skipped: the trusted-publisher rows in every case, and `published to npm` when the registry itself is unreachable. Each package's `package.json` checks report throughout, since they don't read a registry.
|
package/docs/releasing.md
CHANGED
|
@@ -15,13 +15,15 @@ Scaffolded files:
|
|
|
15
15
|
- `.github/workflows/release.yaml`: Workflow that delegates to a reusable release workflow
|
|
16
16
|
- `.config/release-kit.config.ts`: Starter config with commented-out customization examples (with `--with-config`)
|
|
17
17
|
|
|
18
|
+
When every workspace, or the single package, sets `private: true`, `init` skips `create-github-release.yaml` and `publish.yaml`, `--force` included, and reports each skip: `publish` and `create-github-release` skip private packages, so neither workflow would do anything. Its next steps then leave out the provenance and trusted-publisher steps.
|
|
19
|
+
|
|
18
20
|
## `release-kit update-templates`
|
|
19
21
|
|
|
20
22
|
Updates the scaffolded workflows to the templates of the installed release-kit version, and prints what changed in each file. Run it after upgrading release-kit; `--dry-run` previews the changes without writing them.
|
|
21
23
|
|
|
22
24
|
The workflows are managed files: release-kit renders them, and a change made by hand is overwritten on the next update. The config files are the repository's own, and the command never reads or writes them.
|
|
23
25
|
|
|
24
|
-
- It creates a missing workflow that `init` would create.
|
|
26
|
+
- It creates a missing workflow that `init` would create. In a repo whose packages are all private, `init` does not create `create-github-release.yaml` or `publish.yaml`, so the command does not create either one when it is missing and reports the skip; it still refreshes either one that exists.
|
|
25
27
|
- It refreshes `.github/workflows/sync-labels.yaml` only when the file exists, because `sync-labels init` scaffolds it on request.
|
|
26
28
|
- It keeps `publish.yaml`'s provenance setting: When the file does not set `provenance: true`, as in a private repository, the updated file does not either.
|
|
27
29
|
|
|
@@ -29,7 +31,7 @@ The workflows are managed files: release-kit renders them, and a change made by
|
|
|
29
31
|
|
|
30
32
|
Runs release preparation with automatic workspace discovery.
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
`--only` names the workspaces from which the release starts, by package directory name (e.g., `arrays`, `release-kit`). The run also releases every workspace that depends on one of them, directly or transitively: A dependent with commits of its own releases at its own bump, and one without releases at a propagated patch bump, so that it publishes against the new version. Workspaces outside that set are left out of the run.
|
|
33
35
|
|
|
34
36
|
`prepare` reads each target's history once and builds the changelog items of its unreleased commits. The bump is the highest level that those items call for, under [`versionPatterns`](configuration.md#versionpatterns), so the bump and the changelog describe the same release: A `feat` entry in a [change-record block](changelogs.md#change-record-blocks) bumps minor under a `docs:` title, and a breaking item bumps major.
|
|
35
37
|
|
|
@@ -57,6 +59,10 @@ release-kit prepare --force --bump=minor
|
|
|
57
59
|
release-kit prepare --bump=minor
|
|
58
60
|
```
|
|
59
61
|
|
|
62
|
+
### Packages that npm doesn't have yet
|
|
63
|
+
|
|
64
|
+
Before it writes any file, and in a dry run as well, `prepare` asks npm about each publishable package that the release includes, querying the registry that the package's `publishConfig.registry` names when it sets one. OIDC trusted publishing can't publish a package's first version, so `prepare` refuses to release a package that npm doesn't list, and names each one with its fix: Run `npm publish --access public` from the package's directory once, then register the package as a trusted publisher. The `npm-auto-publish` [readiness kit](readiness-checks.md) prints the `npm trust` command. `prepare` also refuses when it can't look a package up, such as when the registry is unreachable, and reports npm's error for that package.
|
|
65
|
+
|
|
60
66
|
### Previewing release notes with `--with-release-notes`
|
|
61
67
|
|
|
62
68
|
`--with-release-notes` writes two versioned files per workspace after each workspace's `changelog.json` is produced:
|
|
@@ -82,18 +88,20 @@ docs/*.v*.md
|
|
|
82
88
|
|
|
83
89
|
The `--set-version` flag is a first-class escape hatch for the cases in which the derived bump produces the wrong version, most notably when promoting a pre-1.0 package to 1.0.0. Pre-1.0 packages collapse a `feat!` breaking change to a minor bump (matching semantic-release's `initialMajor: false` and release-please's `bump-minor-pre-major`), so a deliberate promotion to 1.0.0 must be requested explicitly.
|
|
84
90
|
|
|
91
|
+
In a single-package repo, the value is the version: `--set-version 1.0.0`. In a monorepo, it names the workspace with each version, as comma-separated `<workspace>@N.N.N` entries: `--set-version arrays@1.0.0,strings@2.0.0`. Each named workspace releases at its version whether or not it has commits, and starts the release like a workspace named by `--only`.
|
|
92
|
+
|
|
85
93
|
The flag validates that:
|
|
86
94
|
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
-
|
|
95
|
+
- Each version is canonical `N.N.N` semver (pre-release suffixes are rejected).
|
|
96
|
+
- In a monorepo, each entry names a known workspace, and none names it twice.
|
|
97
|
+
- Each version is strictly greater than the workspace's current version (numeric comparison on each component).
|
|
90
98
|
|
|
91
|
-
|
|
99
|
+
The rest of the pipeline (changelog generation, tag creation, commit summary, propagation to dependents) runs unchanged, so each dependent receives a propagated patch bump that records the explicit version. Without `--only`, the run is a full run with the explicit versions applied. `--bump` and `--force` combine with `--set-version` and govern every other workspace.
|
|
92
100
|
|
|
93
|
-
Promoting a pre-1.0 package to 1.0.0 in a monorepo:
|
|
101
|
+
Promoting a pre-1.0 package to 1.0.0 in a monorepo, which also releases its dependents:
|
|
94
102
|
|
|
95
103
|
```sh
|
|
96
|
-
release-kit prepare --
|
|
104
|
+
release-kit prepare --set-version arrays@1.0.0 --only arrays
|
|
97
105
|
```
|
|
98
106
|
|
|
99
107
|
An empty changelog section is expected for a bare promotion, because the changelog is generated from commits since the last tag. To include a narrative entry, merge a descriptive release commit (e.g., a `feat!` describing the stable API) before running `prepare`.
|
|
@@ -118,19 +126,21 @@ The `init` command scaffolds a release workflow at `.github/workflows/release.ya
|
|
|
118
126
|
|
|
119
127
|
| Input | Type | Description |
|
|
120
128
|
| ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
121
|
-
| `only` | string | Workspaces to release (comma-separated, leave empty for all)
|
|
129
|
+
| `only` | string | Workspaces to release, with their dependents (comma-separated, leave empty for all) |
|
|
122
130
|
| `bump` | choice | Bump type: `auto` (default, detects it from commits), `patch`, `minor`, or `major` |
|
|
123
131
|
| `force` | boolean | Release even when the window doesn't have any commits or any bump-worthy commits (defaults to patch; combine with `bump` for a different level) |
|
|
124
132
|
|
|
125
133
|
For repos that need a self-contained workflow instead of the reusable one, the scaffolded file can be expanded. The key steps are: checkout with full history (`fetch-depth: 0`), run `release-kit prepare` with optional `--only`, `--bump`, and `--force` flags, check for changes, read tags from `tmp/.release-tags`, then commit, tag, and push.
|
|
126
134
|
|
|
135
|
+
The reusable `release`, `create-github-release`, and `sync-labels` workflows run on `ubuntu-latest` unless the repository's `CI_RUNS_ON` variable names another runner; `publish` always runs on `ubuntu-latest`. See [Runner selection](https://github.com/williamthorsen/node-monorepo-tools/blob/main/.github/workflows/README.md#runner-selection).
|
|
136
|
+
|
|
127
137
|
### Triggering a release
|
|
128
138
|
|
|
129
139
|
```sh
|
|
130
140
|
# All workspaces
|
|
131
141
|
gh workflow run release.yaml
|
|
132
142
|
|
|
133
|
-
# Specific workspace(s)
|
|
143
|
+
# Specific workspace(s), with their dependents
|
|
134
144
|
gh workflow run release.yaml -f only=arrays
|
|
135
145
|
gh workflow run release.yaml -f only=arrays,strings -f bump=minor
|
|
136
146
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamthorsen/release-kit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "14.1.0",
|
|
4
4
|
"description": "Version-bumping and changelog-generation toolkit for release workflows",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"changelog",
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"schemas/**"
|
|
51
51
|
],
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@williamthorsen/change-grammar": "0.
|
|
54
|
-
"@williamthorsen/nmr-core": "0.13.
|
|
55
|
-
"@williamthorsen/toolbelt.errors": "0.8.
|
|
53
|
+
"@williamthorsen/change-grammar": "0.2.0",
|
|
54
|
+
"@williamthorsen/nmr-core": "0.13.2",
|
|
55
|
+
"@williamthorsen/toolbelt.errors": "0.8.3",
|
|
56
56
|
"json-stringify-pretty-compact": "4.0.0",
|
|
57
57
|
"semver": "7.8.5",
|
|
58
58
|
"yaml": "2.9.1",
|
|
@@ -60,12 +60,12 @@
|
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/semver": "7.8.0",
|
|
63
|
-
"@williamthorsen/toolbelt.filesystem": "0.9.
|
|
64
|
-
"@williamthorsen/toolbelt.testing": "0.6.
|
|
65
|
-
"@williamthorsen/toolbelt.vitest": "0.10.
|
|
63
|
+
"@williamthorsen/toolbelt.filesystem": "0.9.3",
|
|
64
|
+
"@williamthorsen/toolbelt.testing": "0.6.3",
|
|
65
|
+
"@williamthorsen/toolbelt.vitest": "0.10.3",
|
|
66
66
|
"actionlint": "2.0.6",
|
|
67
67
|
"esbuild": "0.28.2",
|
|
68
|
-
"readyup": "0.
|
|
68
|
+
"readyup": "0.40.0",
|
|
69
69
|
"smol-toml": "1.9.0",
|
|
70
70
|
"vitest": "4.1.11"
|
|
71
71
|
},
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { DependencyGraph } from './buildDependencyGraph.js';
|
|
2
|
-
import type { WorkspaceConfig } from './types.js';
|
|
3
|
-
/** Result of probing a workspace's commits since its last tag. */
|
|
4
|
-
export interface CommitsProbeResult {
|
|
5
|
-
/** Whether the workspace has any commits since its last tag (excluding `release:` commits). */
|
|
6
|
-
has: boolean;
|
|
7
|
-
/**
|
|
8
|
-
* The baseline tag from which the commits were measured; `undefined` when the workspace does not have a prior tag.
|
|
9
|
-
*/
|
|
10
|
-
tag: string | undefined;
|
|
11
|
-
}
|
|
12
|
-
/** A single excluded workspace whose changes would be stranded by the `--only` invocation. */
|
|
13
|
-
export interface StrandedDependentViolation {
|
|
14
|
-
/** The excluded workspace's `dir`. */
|
|
15
|
-
dir: string;
|
|
16
|
-
/** The `dir` of the released (or anticipated-release) dependency whose release republishes this workspace. */
|
|
17
|
-
downstreamOf: string;
|
|
18
|
-
/**
|
|
19
|
-
* The baseline tag from which this workspace's commits were counted; `undefined` if the workspace does not have a
|
|
20
|
-
* prior tag.
|
|
21
|
-
*/
|
|
22
|
-
tag: string | undefined;
|
|
23
|
-
}
|
|
24
|
-
/** Probe callback that returns whether a workspace has commits since its last tag. */
|
|
25
|
-
type CommitsProbe = (workspace: WorkspaceConfig) => CommitsProbeResult;
|
|
26
|
-
/**
|
|
27
|
-
* Detects `--only` invocations that would silently strand changes in excluded internal dependents.
|
|
28
|
-
*
|
|
29
|
-
* First computes R, the `--only` workspaces that will release, then walks the reverse-dependency graph breadth-first
|
|
30
|
-
* from R and records each excluded workspace with its own commits as a violation. The walk continues through a
|
|
31
|
-
* violation, as though the user had added it to `--only`, which reports deeper violations in the same pass. It stops
|
|
32
|
-
* at an excluded workspace without commits, which does not republish, so its dependents are unaffected by R.
|
|
33
|
-
*
|
|
34
|
-
* Returns `undefined` when the walk does not find any violation, or the violations sorted by `dir`.
|
|
35
|
-
*/
|
|
36
|
-
export declare function validateOnlyExcludesStrandedDependents(workspaces: readonly WorkspaceConfig[], only: readonly string[], graph: DependencyGraph, hasCommits: CommitsProbe): StrandedDependentViolation[] | undefined;
|
|
37
|
-
export {};
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
export function validateOnlyExcludesStrandedDependents(workspaces, only, graph, hasCommits) {
|
|
2
|
-
const probeCommits = memoizeCommitsProbe(hasCommits);
|
|
3
|
-
const workspaceByDir = new Map(workspaces.map((w) => [w.dir, w]));
|
|
4
|
-
const released = computeReleasedSet(only, workspaceByDir, graph, probeCommits);
|
|
5
|
-
const violations = collectStrandedViolations(released, new Set(only), graph, probeCommits);
|
|
6
|
-
if (violations.length === 0)
|
|
7
|
-
return undefined;
|
|
8
|
-
violations.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
9
|
-
return violations;
|
|
10
|
-
}
|
|
11
|
-
function memoizeCommitsProbe(probe) {
|
|
12
|
-
const cache = new Map();
|
|
13
|
-
return (workspace) => {
|
|
14
|
-
const cached = cache.get(workspace.dir);
|
|
15
|
-
if (cached !== undefined)
|
|
16
|
-
return cached;
|
|
17
|
-
const result = probe(workspace);
|
|
18
|
-
cache.set(workspace.dir, result);
|
|
19
|
-
return result;
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
function computeReleasedSet(only, workspaceByDir, graph, probeCommits) {
|
|
23
|
-
const released = new Set();
|
|
24
|
-
for (const dir of only) {
|
|
25
|
-
const workspace = workspaceByDir.get(dir);
|
|
26
|
-
if (workspace !== undefined && probeCommits(workspace).has) {
|
|
27
|
-
released.add(dir);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
let changed = true;
|
|
31
|
-
while (changed) {
|
|
32
|
-
changed = false;
|
|
33
|
-
for (const dir of only) {
|
|
34
|
-
if (released.has(dir))
|
|
35
|
-
continue;
|
|
36
|
-
if (hasDependencyIn(dir, graph, released)) {
|
|
37
|
-
released.add(dir);
|
|
38
|
-
changed = true;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return released;
|
|
43
|
-
}
|
|
44
|
-
function hasDependencyIn(dir, graph, released) {
|
|
45
|
-
const forwardDeps = graph.dependenciesOf.get(dir);
|
|
46
|
-
if (forwardDeps === undefined)
|
|
47
|
-
return false;
|
|
48
|
-
for (const depPackageName of forwardDeps) {
|
|
49
|
-
const depDir = graph.packageNameToDir.get(depPackageName);
|
|
50
|
-
if (depDir !== undefined && released.has(depDir))
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
function collectStrandedViolations(released, onlySet, graph, probeCommits) {
|
|
56
|
-
const violations = [];
|
|
57
|
-
const violationDirs = new Set();
|
|
58
|
-
const visited = new Set();
|
|
59
|
-
const queue = [];
|
|
60
|
-
for (const dir of released) {
|
|
61
|
-
const packageName = graph.dirToPackageName.get(dir);
|
|
62
|
-
if (packageName !== undefined) {
|
|
63
|
-
queue.push({ packageName, attributionRoot: dir });
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
while (queue.length > 0) {
|
|
67
|
-
const item = queue.shift();
|
|
68
|
-
if (item === undefined)
|
|
69
|
-
break;
|
|
70
|
-
if (visited.has(item.packageName))
|
|
71
|
-
continue;
|
|
72
|
-
visited.add(item.packageName);
|
|
73
|
-
const dependents = graph.dependentsOf.get(item.packageName);
|
|
74
|
-
if (dependents === undefined)
|
|
75
|
-
continue;
|
|
76
|
-
for (const dependent of dependents) {
|
|
77
|
-
visitDependent(dependent, item.attributionRoot, {
|
|
78
|
-
released,
|
|
79
|
-
onlySet,
|
|
80
|
-
graph,
|
|
81
|
-
probeCommits,
|
|
82
|
-
queue,
|
|
83
|
-
violations,
|
|
84
|
-
violationDirs,
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return violations;
|
|
89
|
-
}
|
|
90
|
-
function visitDependent(dependent, attributionRoot, ctx) {
|
|
91
|
-
if (ctx.onlySet.has(dependent.dir)) {
|
|
92
|
-
if (ctx.released.has(dependent.dir)) {
|
|
93
|
-
enqueueDependent(dependent, ctx);
|
|
94
|
-
}
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
const probe = ctx.probeCommits(dependent);
|
|
98
|
-
if (!probe.has)
|
|
99
|
-
return;
|
|
100
|
-
if (!ctx.violationDirs.has(dependent.dir)) {
|
|
101
|
-
ctx.violationDirs.add(dependent.dir);
|
|
102
|
-
ctx.violations.push({
|
|
103
|
-
dir: dependent.dir,
|
|
104
|
-
downstreamOf: attributionRoot,
|
|
105
|
-
tag: probe.tag,
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
enqueueDependent(dependent, ctx);
|
|
109
|
-
}
|
|
110
|
-
function enqueueDependent(dependent, ctx) {
|
|
111
|
-
const packageName = ctx.graph.dirToPackageName.get(dependent.dir);
|
|
112
|
-
if (packageName !== undefined) {
|
|
113
|
-
ctx.queue.push({ packageName, attributionRoot: dependent.dir });
|
|
114
|
-
}
|
|
115
|
-
}
|