@williamthorsen/release-kit 10.5.0 → 10.7.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.
Files changed (44) hide show
  1. package/.meta/changelog.json +1570 -0
  2. package/.readyup/kits/default.js +127 -4
  3. package/.readyup/kits/npm-auto-publish.js +87 -23
  4. package/.readyup/manifest.json +14 -9
  5. package/CHANGELOG.md +83 -0
  6. package/README.md +68 -963
  7. package/dist/esm/bin/release-kit.js +4 -3
  8. package/dist/esm/buildChangelogEntries.js +5 -0
  9. package/dist/esm/buildDependencyGraph.d.ts +6 -5
  10. package/dist/esm/buildDependencyGraph.js +5 -1
  11. package/dist/esm/changelogJsonUtils.d.ts +5 -7
  12. package/dist/esm/changelogJsonUtils.js +0 -13
  13. package/dist/esm/changelogOverrides.js +8 -0
  14. package/dist/esm/extractMigration.d.ts +14 -0
  15. package/dist/esm/extractMigration.js +18 -0
  16. package/dist/esm/init/scaffold.js +14 -6
  17. package/dist/esm/packsPath.d.ts +15 -0
  18. package/dist/esm/packsPath.js +47 -0
  19. package/dist/esm/planVersionBump.d.ts +2 -1
  20. package/dist/esm/planVersionBump.js +4 -0
  21. package/dist/esm/propagateBumps.d.ts +1 -3
  22. package/dist/esm/propagateBumps.js +2 -2
  23. package/dist/esm/releasePrepare.js +0 -16
  24. package/dist/esm/releasePrepareMono.js +3 -24
  25. package/dist/esm/reportPrepare.js +4 -2
  26. package/dist/esm/runGitCliff.d.ts +16 -23
  27. package/dist/esm/runGitCliff.js +4 -10
  28. package/dist/esm/sync-labels/resolveLabels.d.ts +5 -3
  29. package/dist/esm/sync-labels/resolveLabels.js +21 -6
  30. package/dist/esm/types.d.ts +13 -2
  31. package/dist/esm/validateConfig.js +1 -1
  32. package/docs/api.md +73 -0
  33. package/docs/changelogs.md +70 -0
  34. package/docs/configuration.md +105 -0
  35. package/docs/editorial-overrides.md +144 -0
  36. package/docs/labels.md +83 -0
  37. package/docs/project-releases.md +74 -0
  38. package/docs/publishing.md +30 -0
  39. package/docs/readiness-checks.md +18 -0
  40. package/docs/releasing.md +108 -0
  41. package/docs/work-types.md +121 -0
  42. package/package.json +16 -12
  43. package/dist/esm/readCurrentVersion.d.ts +0 -2
  44. package/dist/esm/readCurrentVersion.js +0 -18
@@ -35,8 +35,9 @@ Commands:
35
35
  work-types Check for or sync work-type taxonomy drift against the upstream canonical
36
36
 
37
37
  Options:
38
- --dry-run Preview changes without writing files
39
- --help, -h Show this help message
38
+ --dry-run Preview changes without writing files
39
+ --help, -h Show this help message
40
+ --version, -V Show the version number
40
41
  `);
41
42
  }
42
43
  function showSyncLabelsHelp() {
@@ -95,7 +96,7 @@ function showInitHelp() {
95
96
  Usage: release-kit init [options]
96
97
 
97
98
  Initialize release-kit in the current repository.
98
- By default, scaffolds only the GitHub Actions workflow file.
99
+ By default, scaffolds only the GitHub Actions workflow files.
99
100
 
100
101
  Options:
101
102
  --with-config Also scaffold .config/release-kit.config.ts and .config/git-cliff.toml
@@ -1,6 +1,7 @@
1
1
  import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
2
2
  import { extractVersion } from "./changelogJsonUtils.js";
3
3
  import { DEFAULT_WORK_TYPES } from "./defaults.js";
4
+ import { extractMigration } from "./extractMigration.js";
4
5
  import { COMMIT_PREPROCESSOR_PATTERNS, PIPE_SCOPE_SOURCE } from "./parseCommitMessage.js";
5
6
  import { resolveCliffConfigPath } from "./resolveCliffConfigPath.js";
6
7
  import { runGitCliff } from "./runGitCliff.js";
@@ -101,6 +102,10 @@ function transformReleases(releases, devOnlySections) {
101
102
  if (breaking) {
102
103
  item.breaking = true;
103
104
  }
105
+ const migration = extractMigration(body);
106
+ if (migration !== undefined) {
107
+ item.migration = migration;
108
+ }
104
109
  if (commit.id !== undefined && commit.id !== '') {
105
110
  item.hash = commit.id;
106
111
  }
@@ -1,10 +1,12 @@
1
1
  import type { WorkspaceConfig } from './types.js';
2
- /** Reverse adjacency map from package names to their workspace dependents. */
2
+ /** Identities, current versions, and `workspace:` dependency edges of the workspaces' packages. */
3
3
  export interface DependencyGraph {
4
4
  /** Resolve a package name to its workspace `dir`. */
5
5
  packageNameToDir: Map<string, string>;
6
6
  /** Resolve a workspace `dir` to its package name (inverse of `packageNameToDir`). */
7
7
  dirToPackageName: Map<string, string>;
8
+ /** Resolve a workspace `dir` to its current version; absent when its `package.json` has no string `version`. */
9
+ dirToVersion: Map<string, string>;
8
10
  /** Map a package name to the workspaces that depend on it. */
9
11
  dependentsOf: Map<string, WorkspaceConfig[]>;
10
12
  /**
@@ -14,11 +16,10 @@ export interface DependencyGraph {
14
16
  dependenciesOf: Map<string, Set<string>>;
15
17
  }
16
18
  /**
17
- * Build a reverse dependency graph from the workspaces' `package.json` files.
19
+ * Builds the dependency graph from the workspaces' `package.json` files.
18
20
  *
19
21
  * Reads each workspace's primary `package.json` (first entry in `packageFiles`) to discover
20
- * `workspace:` references in `dependencies` and `peerDependencies`. Returns a map from each
21
- * package name to the workspaces that depend on it, enabling upward traversal from a bumped
22
- * package to all its dependents.
22
+ * `workspace:` references in `dependencies` and `peerDependencies`, and to record its current
23
+ * version.
23
24
  */
24
25
  export declare function buildDependencyGraph(workspaces: readonly WorkspaceConfig[]): DependencyGraph;
@@ -6,6 +6,7 @@ function isPackageJsonSubset(value) {
6
6
  export function buildDependencyGraph(workspaces) {
7
7
  const packageNameToDir = new Map();
8
8
  const dirToPackageName = new Map();
9
+ const dirToVersion = new Map();
9
10
  const dependentsOf = new Map();
10
11
  const dependenciesOf = new Map();
11
12
  const workspacePackages = new Map();
@@ -16,6 +17,9 @@ export function buildDependencyGraph(workspaces) {
16
17
  }
17
18
  const pkg = readPackageJsonSubset(primaryPackageFile);
18
19
  workspacePackages.set(workspace, pkg);
20
+ if (typeof pkg.version === 'string') {
21
+ dirToVersion.set(workspace.dir, pkg.version);
22
+ }
19
23
  if (pkg.name === undefined) {
20
24
  continue;
21
25
  }
@@ -44,7 +48,7 @@ export function buildDependencyGraph(workspaces) {
44
48
  }
45
49
  }
46
50
  }
47
- return { packageNameToDir, dirToPackageName, dependentsOf, dependenciesOf };
51
+ return { packageNameToDir, dirToPackageName, dirToVersion, dependentsOf, dependenciesOf };
48
52
  }
49
53
  function readPackageJsonSubset(filePath) {
50
54
  let content;
@@ -1,13 +1,11 @@
1
- import type { ChangelogEntry, ChangelogItem } from './types.js';
1
+ import type { ChangelogEntry } from './types.js';
2
2
  /**
3
- * Type guard for `ChangelogItem` values parsed from JSON.
3
+ * Type guard for `ChangelogEntry` values parsed from JSON.
4
4
  *
5
- * Accepts the required `description` plus optional `body`, `breaking`, and `hash` fields. The
6
- * `hash` field is shape-checked but not required — synthetic propagation entries omit it, and
7
- * pre-hash `changelog.json` files are still considered valid.
5
+ * Checks the entry's top level only: `sections` is verified to be an array, and its contents pass
6
+ * through unvalidated. `.meta/changelog.json` is written by release-kit and read tolerantly; strict
7
+ * validation applies to the human-authored `.meta/changelog-overrides.json`, in `changelogOverrides.ts`.
8
8
  */
9
- export declare function isChangelogItem(value: unknown): value is ChangelogItem;
10
- /** Type guard for `ChangelogEntry` values parsed from JSON. */
11
9
  export declare function isChangelogEntry(value: unknown): value is ChangelogEntry;
12
10
  /** Extract the version from a tag by stripping the prefix up to the first digit. */
13
11
  export declare function extractVersion(tag: string): string;
@@ -1,18 +1,5 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { isRecord, isUnknownArray } from "./typeGuards.js";
3
- export function isChangelogItem(value) {
4
- if (!isRecord(value))
5
- return false;
6
- if (typeof value['description'] !== 'string')
7
- return false;
8
- if (value['body'] !== undefined && typeof value['body'] !== 'string')
9
- return false;
10
- if (value['breaking'] !== undefined && typeof value['breaking'] !== 'boolean')
11
- return false;
12
- if (value['hash'] !== undefined && typeof value['hash'] !== 'string')
13
- return false;
14
- return true;
15
- }
16
3
  export function isChangelogEntry(value) {
17
4
  return (isRecord(value) &&
18
5
  typeof value['version'] === 'string' &&
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { describeError } from '@williamthorsen/toolbelt.errors';
4
+ import { extractMigration } from "./extractMigration.js";
4
5
  import { isRecord } from "./typeGuards.js";
5
6
  const OVERRIDES_FILENAME = '.meta/changelog-overrides.json';
6
7
  const VALID_AUDIENCE_VALUES = new Set(['all', 'dev', 'skip']);
@@ -206,6 +207,13 @@ function applyOverrideToItem(item, override) {
206
207
  }
207
208
  if (override.body !== undefined) {
208
209
  result.body = override.body;
210
+ const migration = extractMigration(override.body);
211
+ if (migration === undefined) {
212
+ delete result.migration;
213
+ }
214
+ else {
215
+ result.migration = migration;
216
+ }
209
217
  }
210
218
  if (override.breaking !== undefined) {
211
219
  result.breaking = override.breaking;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Extracts the instruction from a commit body's `Migration:` paragraph.
3
+ *
4
+ * The first paragraph opening with the label wins: the lede doctrine specifies one labeled
5
+ * paragraph per body, so a second is ignored rather than appended. The match is anchored and
6
+ * case-sensitive, so `migration:` and `**Migration:**` yield nothing; the doctrine calls the
7
+ * label literal, and tolerating variants would let the form drift.
8
+ *
9
+ * Newlines inside the paragraph are preserved. The first character is capitalized, which is
10
+ * inert for the imperative the doctrine requires and repairs the lowercase continuation of a
11
+ * paragraph written before it. Returns `undefined` when no paragraph carries the label, or when
12
+ * the label is followed by nothing.
13
+ */
14
+ export declare function extractMigration(body: string | undefined): string | undefined;
@@ -0,0 +1,18 @@
1
+ const PARAGRAPH_SEPARATOR = /\n(?:[ \t]*\n)+/;
2
+ const MIGRATION_LABEL = /^Migration:[ \t]*/;
3
+ export function extractMigration(body) {
4
+ if (body === undefined) {
5
+ return undefined;
6
+ }
7
+ for (const paragraph of body.split(PARAGRAPH_SEPARATOR)) {
8
+ if (!MIGRATION_LABEL.test(paragraph)) {
9
+ continue;
10
+ }
11
+ const instruction = paragraph.replace(MIGRATION_LABEL, '').trim();
12
+ if (instruction.length === 0) {
13
+ return undefined;
14
+ }
15
+ return instruction.charAt(0).toUpperCase() + instruction.slice(1);
16
+ }
17
+ return undefined;
18
+ }
@@ -1,22 +1,30 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { readFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
  import { findPackageRoot, writeFileWithCheck } from '@williamthorsen/nmr-core';
4
4
  import { describeError } from '@williamthorsen/toolbelt.errors';
5
5
  import { createGithubReleaseWorkflow, publishWorkflow, releaseConfigScript, releaseWorkflow } from "./templates.js";
6
6
  export function copyCliffTemplate(dryRun, overwrite) {
7
7
  const destPath = '.config/git-cliff.toml';
8
- const root = findPackageRoot(import.meta.url);
9
- const templatePath = resolve(root, 'cliff.toml.template');
10
- if (!existsSync(templatePath)) {
11
- return { filePath: destPath, outcome: 'failed', error: `Could not find bundled template at ${templatePath}` };
8
+ let root;
9
+ try {
10
+ root = findPackageRoot(import.meta.url);
11
+ }
12
+ catch (error) {
13
+ const message = describeError(error);
14
+ return { filePath: destPath, outcome: 'failed', error: `Failed to resolve package root: ${message}` };
12
15
  }
16
+ const templatePath = resolve(root, 'cliff.toml.template');
13
17
  let content;
14
18
  try {
15
19
  content = readFileSync(templatePath, 'utf8');
16
20
  }
17
21
  catch (error) {
18
22
  const message = describeError(error);
19
- return { filePath: destPath, outcome: 'failed', error: `Failed to read template ${templatePath}: ${message}` };
23
+ return {
24
+ filePath: destPath,
25
+ outcome: 'failed',
26
+ error: `Failed to read bundled template at ${templatePath}: ${message}`,
27
+ };
20
28
  }
21
29
  return writeFileWithCheck(destPath, content, { dryRun, overwrite });
22
30
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Reports whether npm would place `path` in the published tarball, given a package.json `files` value.
3
+ *
4
+ * An absent or malformed `files` value packs everything, so it reports `true`; an empty array packs nothing
5
+ * beyond the files npm always includes, so it reports `false`. An entry naming an ancestor directory covers
6
+ * everything beneath it, which is how `files: ["dist/*"]` ships `dist/esm/index.js`, so every ancestor prefix
7
+ * of `path` is tested rather than `path` alone.
8
+ *
9
+ * Two approximations of npm's packing rules remain, both failing toward a silent pass: a negation entry
10
+ * (`!dist/*.map`) never counts toward inclusion, because gitignore-style ordered negation is not modeled; and a
11
+ * repo controlling its tarball through `.npmignore` alone declares no `files` field, so it reports `true`
12
+ * without the ignore file being read. For a check that warns about an omission, a missed report costs less than
13
+ * a false one.
14
+ */
15
+ export declare function packsPath(filesField: unknown, path: string): boolean;
@@ -0,0 +1,47 @@
1
+ export function packsPath(filesField, path) {
2
+ if (!isStringArray(filesField))
3
+ return true;
4
+ const prefixes = listAncestorPrefixes(path);
5
+ return filesField.some((entry) => {
6
+ const pattern = compileEntry(entry);
7
+ return pattern !== undefined && prefixes.some((prefix) => pattern.test(prefix));
8
+ });
9
+ }
10
+ const GLOB_TOKEN_PATTERN = /\*\*\/|\*\*|\*|\?|[^*?]+/g;
11
+ function compileEntry(entry) {
12
+ if (entry.startsWith('!'))
13
+ return undefined;
14
+ const normalized = entry.replace(/^\.?\//, '').replace(/\/+$/, '');
15
+ if (normalized === '')
16
+ return undefined;
17
+ return new RegExp(`^${translateGlob(normalized)}$`);
18
+ }
19
+ function isStringArray(value) {
20
+ return Array.isArray(value) && value.every((entry) => typeof entry === 'string');
21
+ }
22
+ function listAncestorPrefixes(path) {
23
+ const segments = path.split('/');
24
+ return segments.map((_segment, index) => segments.slice(0, index + 1).join('/'));
25
+ }
26
+ function translateGlob(pattern) {
27
+ let source = '';
28
+ for (const [token] of pattern.matchAll(GLOB_TOKEN_PATTERN)) {
29
+ switch (token) {
30
+ case '**/':
31
+ source += String.raw `(?:[^/]*\/)*`;
32
+ break;
33
+ case '**':
34
+ source += '.*';
35
+ break;
36
+ case '*':
37
+ source += '[^/]*';
38
+ break;
39
+ case '?':
40
+ source += '[^/]';
41
+ break;
42
+ default:
43
+ source += RegExp.escape(token);
44
+ }
45
+ }
46
+ return source;
47
+ }
@@ -16,6 +16,7 @@ export interface VersionBumpPlan {
16
16
  export declare function planVersionBump(packageFiles: readonly string[], releaseType: ReleaseType): VersionBumpPlan;
17
17
  /**
18
18
  * Computes an explicit version assignment for a workspace's package files without writing them,
19
- * bypassing commit-derived bump logic. Backs the `--set-version` flag.
19
+ * bypassing commit-derived bump logic. Backs the `--set-version` flag, and so rejects a
20
+ * `newVersion` that is not greater than the current version.
20
21
  */
21
22
  export declare function planVersionSet(packageFiles: readonly string[], newVersion: string): VersionBumpPlan;
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
3
3
  import { bumpVersion } from "./bumpVersion.js";
4
+ import { isForwardVersion } from "./compareVersions.js";
4
5
  function isPackageJson(value) {
5
6
  return typeof value === 'object' && value !== null && 'version' in value && typeof value.version === 'string';
6
7
  }
@@ -13,6 +14,9 @@ export function planVersionBump(packageFiles, releaseType) {
13
14
  export function planVersionSet(packageFiles, newVersion) {
14
15
  const { firstFile, firstPkg } = readPrimaryPackage(packageFiles);
15
16
  const currentVersion = firstPkg.version;
17
+ if (!isForwardVersion(currentVersion, newVersion)) {
18
+ throw new Error(`--set-version ${newVersion} is not greater than current version ${currentVersion}`);
19
+ }
16
20
  return { currentVersion, newVersion, writes: renderVersionWrites(packageFiles, firstFile, firstPkg, newVersion) };
17
21
  }
18
22
  function readPrimaryPackage(packageFiles) {
@@ -12,8 +12,6 @@ export interface ReleaseEntry {
12
12
  */
13
13
  newVersionOverride?: string;
14
14
  }
15
- /** Map from workspace `dir` to its current version string (read from package.json). */
16
- export type CurrentVersions = Map<string, string>;
17
15
  /**
18
16
  * Walk upward through the dependency graph via BFS, adding `patch` bumps for dependents
19
17
  * not already in the release set with a higher bump.
@@ -21,4 +19,4 @@ export type CurrentVersions = Map<string, string>;
21
19
  * Returns the full release set (direct + propagated). Direct entries that also have a
22
20
  * propagated dependency get `propagatedFrom` metadata without changing their bump type.
23
21
  */
24
- export declare function propagateBumps(directBumps: Map<string, ReleaseEntry>, graph: DependencyGraph, currentVersions: CurrentVersions): Map<string, ReleaseEntry>;
22
+ export declare function propagateBumps(directBumps: Map<string, ReleaseEntry>, graph: DependencyGraph): Map<string, ReleaseEntry>;
@@ -1,5 +1,5 @@
1
1
  import { bumpVersion } from "./bumpVersion.js";
2
- export function propagateBumps(directBumps, graph, currentVersions) {
2
+ export function propagateBumps(directBumps, graph) {
3
3
  const result = new Map();
4
4
  for (const [dir, entry] of directBumps) {
5
5
  result.set(dir, { ...entry });
@@ -19,7 +19,7 @@ export function propagateBumps(directBumps, graph, currentVersions) {
19
19
  if (packageName === undefined) {
20
20
  continue;
21
21
  }
22
- const currentVersion = currentVersions.get(dir);
22
+ const currentVersion = graph.dirToVersion.get(dir);
23
23
  const entry = result.get(dir);
24
24
  if (currentVersion === undefined || entry === undefined) {
25
25
  continue;
@@ -5,7 +5,6 @@ import { buildReleaseSummary } from "./buildReleaseSummary.js";
5
5
  import { mergeChangelogEntriesWithDisk, renderChangelogJson, resolveChangelogJsonPath } from "./changelogJsonFile.js";
6
6
  import { applyChangelogOverrides, formatStaleOverrideKeyWarning, loadOverridesForScopes, } from "./changelogOverrides.js";
7
7
  import { createPolicyViolationCollector } from "./collectPolicyViolations.js";
8
- import { isForwardVersion } from "./compareVersions.js";
9
8
  import { DEFAULT_BREAKING_POLICIES, DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
10
9
  import { determineBumpFromCommits } from "./determineBumpFromCommits.js";
11
10
  import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
@@ -13,10 +12,8 @@ import { hasPrettierConfig } from "./hasPrettierConfig.js";
13
12
  import { resolveWorkTypes } from "./loadConfig.js";
14
13
  import { planReleaseNotesPreviews } from "./planReleaseNotesPreviews.js";
15
14
  import { planVersionBump, planVersionSet } from "./planVersionBump.js";
16
- import { readCurrentVersion } from "./readCurrentVersion.js";
17
15
  import { renderChangelogMarkdown } from "./renderChangelogMarkdown.js";
18
16
  import { deriveSectionOrder } from "./resolveReleaseNotesConfig.js";
19
- import { refreshGitCliffCache } from "./runGitCliff.js";
20
17
  export function releasePrepare(config, options) {
21
18
  const { bumpOverride, setVersion, withReleaseNotes } = options;
22
19
  const writes = [];
@@ -35,18 +32,6 @@ export function releasePrepare(config, options) {
35
32
  const collector = createPolicyViolationCollector();
36
33
  let bump;
37
34
  if (setVersion !== undefined) {
38
- const primaryPackageFile = config.packageFiles[0];
39
- if (primaryPackageFile === undefined) {
40
- throw new Error('No package files specified');
41
- }
42
- const currentVersion = readCurrentVersion(primaryPackageFile);
43
- if (currentVersion === undefined) {
44
- throw new Error(`Cannot validate --set-version: failed to read current version from ${primaryPackageFile}`);
45
- }
46
- if (!isForwardVersion(currentVersion, setVersion)) {
47
- throw new Error(`--set-version ${setVersion} is not greater than current version ${currentVersion}`);
48
- }
49
- refreshGitCliffCache();
50
35
  bump = planVersionSet(config.packageFiles, setVersion);
51
36
  }
52
37
  else {
@@ -78,7 +63,6 @@ export function releasePrepare(config, options) {
78
63
  formatCommand: undefined,
79
64
  };
80
65
  }
81
- refreshGitCliffCache();
82
66
  bump = planVersionBump(config.packageFiles, releaseType);
83
67
  }
84
68
  writes.push(...bump.writes);
@@ -8,7 +8,6 @@ import { buildSyntheticChangelogEntry } from "./buildSyntheticChangelogEntry.js"
8
8
  import { mergeChangelogEntriesWithDisk, renderChangelogJson, resolveChangelogJsonPath } from "./changelogJsonFile.js";
9
9
  import { applyWorkspaceOverrides, createOverrideContext, formatStaleOverrideKeyWarning, } from "./changelogOverrides.js";
10
10
  import { createPolicyViolationCollector } from "./collectPolicyViolations.js";
11
- import { isForwardVersion } from "./compareVersions.js";
12
11
  import { decideRelease } from "./decideRelease.js";
13
12
  import { DEFAULT_BREAKING_POLICIES, DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
14
13
  import { detectUndeclaredTagPrefixes } from "./detectUndeclaredTagPrefixes.js";
@@ -19,11 +18,9 @@ import { resolveWorkTypes } from "./loadConfig.js";
19
18
  import { planReleaseNotesPreviews } from "./planReleaseNotesPreviews.js";
20
19
  import { planVersionBump, planVersionSet } from "./planVersionBump.js";
21
20
  import { propagateBumps } from "./propagateBumps.js";
22
- import { readCurrentVersion } from "./readCurrentVersion.js";
23
21
  import { releasePrepareProject } from "./releasePrepareProject.js";
24
22
  import { renderChangelogMarkdown } from "./renderChangelogMarkdown.js";
25
23
  import { deriveSectionOrder } from "./resolveReleaseNotesConfig.js";
26
- import { refreshGitCliffCache } from "./runGitCliff.js";
27
24
  export function releasePrepareMono(config, options) {
28
25
  const { only, withReleaseNotes } = options;
29
26
  const writes = [];
@@ -33,7 +30,7 @@ export function releasePrepareMono(config, options) {
33
30
  }
34
31
  const sectionOrder = deriveSectionOrder(resolveWorkTypes(config.workTypes));
35
32
  const overrideContext = createOverrideContext(config.workspaces);
36
- const { directBumps, directResults, skippedResults, currentVersions } = determineDirectBumps(config, options);
33
+ const { directBumps, directResults, skippedResults } = determineDirectBumps(config, options);
37
34
  const previousTags = new Map();
38
35
  for (const result of directResults.values()) {
39
36
  previousTags.set(result.workspace.dir, result.tag);
@@ -42,7 +39,7 @@ export function releasePrepareMono(config, options) {
42
39
  previousTags.set(skipped.workspace.dir, skipped.tag);
43
40
  }
44
41
  const graph = buildDependencyGraph(config.workspaces);
45
- const fullReleaseSet = propagateBumps(directBumps, graph, currentVersions);
42
+ const fullReleaseSet = propagateBumps(directBumps, graph);
46
43
  const { sorted: sortedDirs, cyclicDirs } = topologicalSort(fullReleaseSet, graph);
47
44
  if (cyclicDirs.length > 0) {
48
45
  warnings.push(`Circular workspace dependencies detected among: ${cyclicDirs.join(', ')}. ` +
@@ -53,9 +50,6 @@ export function releasePrepareMono(config, options) {
53
50
  enabled: withReleaseNotes === true && config.changelogJson.enabled,
54
51
  sectionOrder,
55
52
  };
56
- if (fullReleaseSet.size > 0) {
57
- refreshGitCliffCache();
58
- }
59
53
  const { tags, modifiedFiles } = executeReleaseSet({
60
54
  sortedDirs,
61
55
  fullReleaseSet,
@@ -123,7 +117,6 @@ function determineDirectBumps(config, options) {
123
117
  const directBumps = new Map();
124
118
  const directResults = new Map();
125
119
  const skippedResults = [];
126
- const currentVersions = new Map();
127
120
  const hintState = { emitted: false };
128
121
  const knownPrefixes = config.workspaces.flatMap(getAllTagPrefixes);
129
122
  for (const workspace of config.workspaces) {
@@ -134,21 +127,7 @@ function determineDirectBumps(config, options) {
134
127
  if (tag === undefined) {
135
128
  maybeEmitBaselineHint(workspace, knownPrefixes, hintState);
136
129
  }
137
- const primaryPackageFile = workspace.packageFiles[0];
138
- if (primaryPackageFile !== undefined) {
139
- const currentVersion = tryStage(stageLabel, () => readCurrentVersion(primaryPackageFile));
140
- if (currentVersion !== undefined) {
141
- currentVersions.set(workspace.dir, currentVersion);
142
- }
143
- }
144
130
  if (setVersion !== undefined) {
145
- const currentVersion = currentVersions.get(workspace.dir);
146
- if (currentVersion === undefined) {
147
- throw new Error(`Cannot validate --set-version: failed to read current version from ${primaryPackageFile ?? '(no package file)'}`);
148
- }
149
- if (!isForwardVersion(currentVersion, setVersion)) {
150
- throw new Error(`--set-version ${setVersion} is not greater than current version ${currentVersion}`);
151
- }
152
131
  directBumps.set(workspace.dir, { releaseType: 'patch', newVersionOverride: setVersion });
153
132
  directResults.set(workspace.dir, {
154
133
  workspace,
@@ -203,7 +182,7 @@ function determineDirectBumps(config, options) {
203
182
  bumpOverride,
204
183
  });
205
184
  }
206
- return { directBumps, directResults, skippedResults, currentVersions };
185
+ return { directBumps, directResults, skippedResults };
207
186
  }
208
187
  function collectSkippedWorkspaces(skippedResults, fullReleaseSet) {
209
188
  const workspaces = [];
@@ -1,4 +1,6 @@
1
1
  import { bold, dim, sectionHeader } from "./format.js";
2
+ import { GIT_CLIFF_NPX_ARGS } from "./runGitCliff.js";
3
+ const CLIFF_DRY_RUN_COMMAND = `npx ${GIT_CLIFF_NPX_ARGS.join(' ')} ...`;
2
4
  export function reportPrepare(plan, options) {
3
5
  const isMultiWorkspace = plan.workspaces.some((w) => w.name !== undefined) || plan.project !== undefined;
4
6
  if (isMultiWorkspace) {
@@ -103,7 +105,7 @@ function formatProjectSection(lines, project, dryRun) {
103
105
  lines.push(dim(' Generating changelogs...'));
104
106
  for (const file of project.changelogFiles) {
105
107
  if (dryRun) {
106
- lines.push(dim(` [dry-run] Would run: npx --yes git-cliff ... --output ${file}`));
108
+ lines.push(dim(` [dry-run] Would run: ${CLIFF_DRY_RUN_COMMAND} --output ${file}`));
107
109
  }
108
110
  else {
109
111
  lines.push(dim(` Generating changelog: ${file}`));
@@ -195,7 +197,7 @@ function formatBumpFiles(lines, workspace, dryRun, indent = '') {
195
197
  function formatChangelogFiles(lines, workspace, dryRun, indent = '') {
196
198
  for (const file of workspace.changelogFiles) {
197
199
  if (dryRun) {
198
- lines.push(dim(`${indent} [dry-run] Would run: npx --yes git-cliff ... --output ${file}`));
200
+ lines.push(dim(`${indent} [dry-run] Would run: ${CLIFF_DRY_RUN_COMMAND} --output ${file}`));
199
201
  }
200
202
  else {
201
203
  lines.push(dim(`${indent} Generating changelog: ${file}`));
@@ -1,3 +1,12 @@
1
+ /**
2
+ * The exact `git-cliff` version every invocation resolves.
3
+ *
4
+ * npm's `git-cliff` trails the upstream release, so this pin normally sits behind the newest version cliff
5
+ * itself reports. Bumping it is an edit to this line, once npm publishes the version you want.
6
+ */
7
+ export declare const GIT_CLIFF_VERSION = "2.13.1";
8
+ /** The npx arguments that precede the cliff args at every invocation, and that the dry-run report renders. */
9
+ export declare const GIT_CLIFF_NPX_ARGS: readonly string[];
1
10
  /**
2
11
  * Invokes `git-cliff` via `npx` and returns the output it produced.
3
12
  *
@@ -9,15 +18,14 @@
9
18
  * entire matching tag history on every run, so its size grows without bound with the repo;
10
19
  * a file accommodates that, while a pipe is bounded by `maxBuffer`.
11
20
  *
12
- * The `--prefer-offline` flag and `npm_config_progress=false` env entry are deliberate: `--prefer-offline` skips
13
- * npx's per-call npm-registry revalidation HTTP round-trip (~2.5 s per invocation on a warm cache), and
14
- * `npm_config_progress=false` suppresses npx's animated stderr spinner, which otherwise renders as a transient flicker.
21
+ * The npx spec names an exact version, which leaves npm no staleness to check, so `--prefer-offline` skips the
22
+ * per-call registry revalidation round-trip (~2.5 s per invocation on a warm cache) without stranding the run
23
+ * on whatever the cache happens to hold.
15
24
  *
16
- * Because `--prefer-offline` also suppresses npx's cache-staleness check, the cached `git-cliff` binary
17
- * would otherwise drift further and further behind upstream, with each cliff invocation re-emitting the
18
- * "A new version of git-cliff is available" notice and the local cache never updating.
19
- * `refreshGitCliffCache` (below) revalidates the cache once at the top of each `prepare` run so that
20
- * subsequent `--prefer-offline` calls run against a current binary.
25
+ * Two env entries are set for the child. `npm_config_progress=false` suppresses npx's animated stderr spinner,
26
+ * which otherwise renders as a transient flicker. `RUST_LOG=warn` drops cliff's INFO lines, among them the
27
+ * crates.io update notice it emits on every invocation while npm trails upstream, and leaves warnings and errors
28
+ * in place; an inherited `RUST_LOG` wins, so raising the level for debugging still works.
21
29
  *
22
30
  * The helper injects `--config <path>` and `--output <path>` itself — callers must NOT include either in `cliffArgs`.
23
31
  * `--output` is appended last so a caller that passes one anyway cannot redirect the output the helper then reads.
@@ -28,18 +36,3 @@
28
36
  * The temp-dir cleanup runs in a `finally` so it still happens on throw.
29
37
  */
30
38
  export declare function runGitCliff(cliffConfigPath: string, cliffArgs: readonly string[]): string;
31
- /**
32
- * Revalidate npx's cache for `git-cliff` once per `prepare` run.
33
- *
34
- * Spawns `npx --yes git-cliff --version` *without* `--prefer-offline`, so that npm performs its normal
35
- * registry-staleness check and refreshes the cached binary if a newer version is available.
36
- * Subsequent `runGitCliff` calls within the same run keep `--prefer-offline` (preserving the per-call perf win)
37
- * but now run against an up-to-date cache, so git-cliff's own self-update notice no longer fires repeatedly.
38
- *
39
- * Stdio: stdin ignored, stdout piped (the version line is suppressed as noise), stderr inherited (npm errors and
40
- * any rare upgrade notice surface, but only once per run). `maxBuffer` is declared even though the piped output
41
- * is a single version line, because every call that pipes stdout declares one.
42
- *
43
- * Errors propagate unchanged — a failed cache refresh fails the prepare run loudly rather than silently degrading.
44
- */
45
- export declare function refreshGitCliffCache(): void;
@@ -2,7 +2,8 @@ import { execFileSync } from 'node:child_process';
2
2
  import { copyFileSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
- import { GIT_OUTPUT_LIMIT } from '@williamthorsen/nmr-core';
5
+ export const GIT_CLIFF_VERSION = '2.13.1';
6
+ export const GIT_CLIFF_NPX_ARGS = ['--prefer-offline', '--yes', `git-cliff@${GIT_CLIFF_VERSION}`];
6
7
  export function runGitCliff(cliffConfigPath, cliffArgs) {
7
8
  const tempDir = mkdtempSync(join(tmpdir(), 'cliff-'));
8
9
  try {
@@ -12,9 +13,9 @@ export function runGitCliff(cliffConfigPath, cliffArgs) {
12
13
  copyFileSync(cliffConfigPath, configPath);
13
14
  }
14
15
  const outputPath = join(tempDir, 'output.json');
15
- execFileSync('npx', ['--prefer-offline', '--yes', 'git-cliff', '--config', configPath, ...cliffArgs, '--output', outputPath], {
16
+ execFileSync('npx', [...GIT_CLIFF_NPX_ARGS, '--config', configPath, ...cliffArgs, '--output', outputPath], {
16
17
  stdio: ['ignore', 'ignore', 'inherit'],
17
- env: { ...process.env, npm_config_progress: 'false' },
18
+ env: { ...process.env, npm_config_progress: 'false', RUST_LOG: process.env['RUST_LOG'] ?? 'warn' },
18
19
  });
19
20
  return readFileSync(outputPath, 'utf8');
20
21
  }
@@ -22,10 +23,3 @@ export function runGitCliff(cliffConfigPath, cliffArgs) {
22
23
  rmSync(tempDir, { recursive: true, force: true });
23
24
  }
24
25
  }
25
- export function refreshGitCliffCache() {
26
- execFileSync('npx', ['--yes', 'git-cliff', '--version'], {
27
- maxBuffer: GIT_OUTPUT_LIMIT,
28
- stdio: ['ignore', 'pipe', 'inherit'],
29
- env: { ...process.env, npm_config_progress: 'false' },
30
- });
31
- }
@@ -5,9 +5,11 @@ import type { LabelDefinition } from './types.js';
5
5
  *
6
6
  * Resolution is an ordered fold with last-writer-wins: presets are loaded in `extends` order (a later preset replaces
7
7
  * an earlier preset's label of the same name), then the `labels` record is applied: An entry adds a label, replaces
8
- * one an earlier layer defined, or removes it (`null`). Replacement is wholesale, so an entry omitting `description`
8
+ * one an earlier layer defined, or removes it (`null`). Names match case-insensitively, as GitHub matches them, and the
9
+ * replacing layer's spelling becomes the label's name. Replacement is wholesale, so an entry omitting `description`
9
10
  * drops the one an earlier layer supplied rather than inheriting it.
10
- * Throws only on a dangling `null`: a removal naming a label no preset defined, which is a stale reference that
11
- * would otherwise produce no output diff to review.
11
+ * Throws on two misstatements that would otherwise leave no trace in the output diff: keys in the `labels` record that
12
+ * differ only in case, all but one of which the fold would discard, and a dangling `null`, a removal naming a label that
13
+ * no preset defined.
12
14
  */
13
15
  export declare function resolveLabels(config: RepoLabelsConfig): LabelDefinition[];