@williamthorsen/release-kit 14.0.0 → 14.2.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 (110) hide show
  1. package/.meta/changelog.json +173 -0
  2. package/.readyup/kits/default.js +64 -7
  3. package/.readyup/kits/npm-auto-publish.js +127 -75
  4. package/.readyup/manifest.json +22 -7
  5. package/CHANGELOG.md +47 -0
  6. package/README.md +18 -4
  7. package/dist/esm/assertReleasesOnNpm.d.ts +7 -0
  8. package/dist/esm/assertReleasesOnNpm.js +56 -0
  9. package/dist/esm/assertTaggedBaseline.d.ts +2 -2
  10. package/dist/esm/attachChangelogDiagnostics.d.ts +2 -2
  11. package/dist/esm/bin/release-kit.js +10 -6
  12. package/dist/esm/buildChangelogEntries.d.ts +3 -3
  13. package/dist/esm/buildChangelogEntries.js +3 -3
  14. package/dist/esm/buildDependencyGraph.d.ts +11 -11
  15. package/dist/esm/buildDependencyGraph.js +13 -13
  16. package/dist/esm/buildReleaseSummary.d.ts +3 -3
  17. package/dist/esm/buildReleaseSummary.js +4 -12
  18. package/dist/esm/changelogArchive.d.ts +14 -0
  19. package/dist/esm/changelogArchive.js +21 -0
  20. package/dist/esm/changelogJsonFile.d.ts +2 -2
  21. package/dist/esm/changelogOverrides.d.ts +9 -8
  22. package/dist/esm/changelogOverrides.js +3 -6
  23. package/dist/esm/createGithubReleaseCommand.d.ts +2 -1
  24. package/dist/esm/createGithubReleaseCommand.js +7 -4
  25. package/dist/esm/deriveDirectoryConfig.d.ts +13 -0
  26. package/dist/esm/deriveDirectoryConfig.js +59 -0
  27. package/dist/esm/deriveWorkspaceConfig.js +2 -3
  28. package/dist/esm/discoverWorkspaces.d.ts +3 -3
  29. package/dist/esm/enumerateReleaseWindows.d.ts +1 -1
  30. package/dist/esm/enumerateReleaseWindows.js +1 -3
  31. package/dist/esm/findTransitiveDependents.js +3 -3
  32. package/dist/esm/findUnroutedEntryScopes.d.ts +2 -2
  33. package/dist/esm/formatDirectoryPublishSkip.d.ts +3 -0
  34. package/dist/esm/formatDirectoryPublishSkip.js +3 -0
  35. package/dist/esm/formatExcludedSkip.d.ts +5 -2
  36. package/dist/esm/formatExcludedSkip.js +2 -2
  37. package/dist/esm/formatProjectPublishSkip.d.ts +3 -0
  38. package/dist/esm/formatProjectPublishSkip.js +3 -0
  39. package/dist/esm/generateChangelogs.d.ts +7 -7
  40. package/dist/esm/generateChangelogs.js +2 -2
  41. package/dist/esm/help/prepareHelp.d.ts +1 -1
  42. package/dist/esm/help/prepareHelp.js +7 -5
  43. package/dist/esm/init/hasPublishablePackage.d.ts +10 -0
  44. package/dist/esm/init/hasPublishablePackage.js +28 -0
  45. package/dist/esm/init/initCommand.js +23 -13
  46. package/dist/esm/init/scaffold.d.ts +6 -1
  47. package/dist/esm/init/scaffold.js +10 -7
  48. package/dist/esm/init/templates.js +1 -1
  49. package/dist/esm/init/updateTemplatesCommand.d.ts +2 -1
  50. package/dist/esm/init/updateTemplatesCommand.js +12 -6
  51. package/dist/esm/isPublishableManifest.d.ts +2 -0
  52. package/dist/esm/isPublishableManifest.js +5 -0
  53. package/dist/esm/loadConfig.d.ts +12 -12
  54. package/dist/esm/loadConfig.js +79 -30
  55. package/dist/esm/matchesTagPrefix.d.ts +2 -0
  56. package/dist/esm/matchesTagPrefix.js +4 -0
  57. package/dist/esm/npmRegistry.d.ts +35 -0
  58. package/dist/esm/npmRegistry.js +58 -0
  59. package/dist/esm/prepareCommand.d.ts +1 -1
  60. package/dist/esm/prepareCommand.js +50 -36
  61. package/dist/esm/prepareTarget.d.ts +103 -0
  62. package/dist/esm/prepareTarget.js +274 -0
  63. package/dist/esm/previewTagPrefixes.d.ts +5 -3
  64. package/dist/esm/previewTagPrefixes.js +60 -14
  65. package/dist/esm/propagateBumps.js +1 -1
  66. package/dist/esm/publishCommand.js +13 -1
  67. package/dist/esm/readChangelogSections.d.ts +6 -4
  68. package/dist/esm/readChangelogSections.js +18 -4
  69. package/dist/esm/releasePlan.d.ts +4 -2
  70. package/dist/esm/releasePlan.js +34 -12
  71. package/dist/esm/releasePrepareTargets.d.ts +47 -0
  72. package/dist/esm/releasePrepareTargets.js +335 -0
  73. package/dist/esm/renderChangelogMarkdown.d.ts +23 -0
  74. package/dist/esm/renderChangelogMarkdown.js +47 -11
  75. package/dist/esm/reportPrepare.d.ts +2 -3
  76. package/dist/esm/reportPrepare.js +24 -12
  77. package/dist/esm/resolveCommandTags.d.ts +3 -1
  78. package/dist/esm/resolveCommandTags.js +15 -22
  79. package/dist/esm/resolveReleaseDeclaration.d.ts +45 -0
  80. package/dist/esm/resolveReleaseDeclaration.js +153 -0
  81. package/dist/esm/resolveReleaseTags.d.ts +14 -3
  82. package/dist/esm/resolveReleaseTags.js +36 -15
  83. package/dist/esm/resolveReleaseTargets.d.ts +29 -0
  84. package/dist/esm/resolveReleaseTargets.js +131 -0
  85. package/dist/esm/sync-labels/generateCommand.js +2 -1
  86. package/dist/esm/sync-labels/resolveLabels.d.ts +4 -3
  87. package/dist/esm/sync-labels/resolveLabels.js +30 -1
  88. package/dist/esm/sync-labels/templates.js +1 -1
  89. package/dist/esm/sync-labels/types.d.ts +2 -0
  90. package/dist/esm/types.d.ts +251 -167
  91. package/dist/esm/types.js +22 -0
  92. package/dist/esm/validateConfig.js +80 -21
  93. package/dist/esm/validateOverridesCommand.d.ts +1 -1
  94. package/dist/esm/validateOverridesCommand.js +17 -28
  95. package/docs/api.md +25 -58
  96. package/docs/changelogs.md +26 -6
  97. package/docs/configuration.md +80 -17
  98. package/docs/labels.md +30 -5
  99. package/docs/project-releases.md +42 -2
  100. package/docs/publishing.md +3 -3
  101. package/docs/releasing.md +12 -4
  102. package/docs/work-types.md +1 -1
  103. package/package.json +6 -6
  104. package/presets/labels/common.yaml +11 -0
  105. package/dist/esm/releasePrepare.d.ts +0 -36
  106. package/dist/esm/releasePrepare.js +0 -230
  107. package/dist/esm/releasePrepareMono.d.ts +0 -29
  108. package/dist/esm/releasePrepareMono.js +0 -503
  109. package/dist/esm/releasePrepareProject.d.ts +0 -47
  110. package/dist/esm/releasePrepareProject.js +0 -164
package/docs/labels.md CHANGED
@@ -36,11 +36,13 @@ The block declares the repository's label registry: the set of labels defined on
36
36
 
37
37
  The `common` preset defines a label for every work type in change-grammar's taxonomy, named by the type's `trackerLabel`, in addition to the labels in its bundled YAML file.
38
38
 
39
+ The bundled file's `confidence:` family grades how advisable a recommendation is, for example one that a triage of spike findings turns into a ticket: `confidence:high` is clearly worth doing, `confidence:medium` is worth doing but arguable, and `confidence:low` is not worth a ticket now.
40
+
39
41
  Names match case-insensitively, as GitHub matches them: `Bug` in the `labels` record replaces a preset's `bug`, and `Bug: null` removes it. A label takes the spelling of the last layer to write it.
40
42
 
41
- `description` is optional throughout, in a preset as in the `labels` record, and `sync-labels init` generates none for a scope label. The generated file spells an absent description `''` because `github-label-sync` reads an omitted description as "leave the label's current one alone"; the empty form clears it.
43
+ `description` is optional throughout, in a preset as in the `labels` record, and `sync-labels init` generates none for a scope label. The generated file spells an absent description `''` because `github-label-sync` reads an omitted description as "leave the label's current one alone"; the empty form clears it. Because a scope label without a description groups compactly in GitHub's label list, release-kit's readyup kit warns about each `scope:` label in the generated file that has one.
42
44
 
43
- Overlaps between layers are never errors; order resolves them, and the committed `.github/labels.yaml` diff shows any unexpected change at review. Two misstatements are config errors, because neither is visible in the output diff: a dangling `null`, which removes a name not defined by any preset, and `labels` keys that differ only in case, all but one of which the fold would discard.
45
+ Overlaps between layers are never errors; order resolves them, and the committed `.github/labels.yaml` diff shows any unexpected change at review. Three misstatements are config errors, because none is visible in the output diff: a dangling `null`, which removes a name not defined by any preset; `labels` keys that differ only in case, all but one of which the fold would discard; and an alias that names a resolved label or repeats another alias, which would make the sync merge one label into another and delete it (see [Renaming a label](#renaming-a-label)).
44
46
 
45
47
  ### Retiring a label
46
48
 
@@ -52,10 +54,31 @@ labels: {
52
54
  },
53
55
  ```
54
56
 
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 instead deletes the label, as the sync deletes every label that `.github/labels.yaml` does not declare, and the label disappears from every issue and pull request that had it.
57
+ 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
58
 
57
59
  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
60
 
61
+ ### Renaming a label
62
+
63
+ List a label's old names in `aliases` to rename it:
64
+
65
+ ```typescript
66
+ labels: {
67
+ 'scope:core': { color: '00ff96', aliases: ['scope:basic'] }, // rename `scope:basic` to `scope:core`
68
+ },
69
+ ```
70
+
71
+ The sync renames an existing label that an alias names, in place, rather than deleting it and creating a new one, so the label stays on the issues and pull requests that have it. The dry-run **Label diff** lists the rename. When the repository has both the old name and the new one, the sync adds the new label to every issue and pull request that has the old one, then deletes the old label. A label that an alias names is not reported as undeclared.
72
+
73
+ `aliases` is set only in the `labels` record, not in a preset, and it takes at least one name. Names match case-insensitively. An alias must not name a label that the config resolves, including the label that declares it, and must not appear twice: Either would make the sync merge one declared label into another and delete it. To rename a preset's label, remove it with `null` and alias it from the new entry:
74
+
75
+ ```typescript
76
+ labels: {
77
+ bug: null,
78
+ 'type:bug': { color: 'd73a4a', aliases: ['bug'] },
79
+ },
80
+ ```
81
+
59
82
  ### When labels are applied
60
83
 
61
84
  The scaffolded workflow declares three triggers:
@@ -68,15 +91,17 @@ The scaffolded workflow declares three triggers:
68
91
 
69
92
  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
93
 
71
- The `check` job runs the same sync in dry-run. Its **Label diff** log group lists every create, edit, rename, and deletion that the sync would perform, including deletions of labels that the file does not declare: the destructive edits that a diff of `.github/labels.yaml` alone cannot show, because a label created by hand or by another workflow is absent from the file both before and after. Read that log; the check reports success whether or not the diff is destructive, so a green check is not evidence that nothing will be deleted.
94
+ 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
95
 
73
96
  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
97
 
98
+ 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.
99
+
75
100
  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
101
 
77
102
  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
103
 
79
- Manual dispatch is not a preview. It matches the `sync` job, so it applies the labels, deletions included; only the pull-request path runs in dry-run.
104
+ 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
105
 
81
106
  ### Dry-run checks on fork pull requests
82
107
 
@@ -1,6 +1,6 @@
1
1
  # Project releases
2
2
 
3
- How to add a project-level release to a monorepo, what the `project` block accepts, and how that release interacts with `prepare`'s flags.
3
+ How to add a project-level release to a monorepo, what the `project` block accepts, how that release interacts with `prepare`'s flags, and how to declare it as a `releases` composite instead.
4
4
 
5
5
  Some monorepos release a single combined deliverable, such as a Chrome extension, a CLI binary, or a packaged desktop app, for which the per-workspace tags and changelogs alone do not describe what the user actually receives. Declare the optional `project` block to add a project-level release stage that runs alongside the per-workspace pipeline.
6
6
 
@@ -69,6 +69,46 @@ CLI flag interactions:
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
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.
72
+ - `--set-version` is rejected with an error when `project` or a `releases` composite 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
+
74
+ A `releases` composite has a name, so `--only` can select it: `--only suite` prepares the composite alone, from its own window, and `--only nmr,suite` prepares `nmr`, its dependents, and the composite. A run narrowed by `--only` that doesn't name the composite skips it with a warning.
73
75
 
74
76
  How `--bump` and `--force` combine for every target is in [Releasing](releasing.md#release-kit-prepare).
77
+
78
+ ## Moving to `releases`
79
+
80
+ The [`releases`](configuration.md#releases) block declares the same targets by name, and every setting of the `workspaces` and `project` blocks has a counterpart in it, except `project.paths`. A config uses one surface or the other; tag prefixes are unchanged either way.
81
+
82
+ | Legacy setting | `releases` entry |
83
+ | ------------------------------------------------- | ----------------------------------------------------- |
84
+ | `workspaces: [{ dir: 'x', shouldExclude: true }]` | `x: false` |
85
+ | `workspaces: [{ dir: 'x', legacyIdentities }]` | `x: { path: 'packages/x', legacyIdentities }` |
86
+ | `project: {}` | `<name>: { includes: [...every retained workspace] }` |
87
+ | `project: { tagPrefix: 'app-v' }` | `<name>: { includes: [...], tagPrefix: 'app-v' }` |
88
+ | `project: { paths }` | No counterpart; keep the `project` block |
89
+
90
+ Unlike a `project` block, a composite releases only its listed members' commits, and it has a name that `--only` can select. A workspace left out of `includes` still releases on its own.
91
+
92
+ A `project` block whose window covers the whole repo only because every workspace is excluded maps to the [repo-root target](configuration.md#the-repo-root), `<name>: { path: '.' }`, together with a `false` entry for each excluded workspace. The root target reads the whole repo by declaration, so a workspace added later doesn't narrow it.
93
+
94
+ ## Publishing and GitHub Releases
95
+
96
+ `push`, `publish`, and `create-github-release` resolve the project tag on HEAD through the `tagPrefix` of the `project` block or the `releases` composite, alongside the workspace tags:
97
+
98
+ - `release-kit push` pushes the project tag with the release commit, including when the project tag is the only release tag on HEAD.
99
+ - `release-kit publish` never publishes the project tag, because the project does not have an npm package. When `--tags` names it, `publish` warns `Skipping v0.13.0: a project tag is not an npm package.` and publishes nothing for it.
100
+ - `release-kit create-github-release` creates a GitHub Release for the project tag from `./.meta/changelog.json`, whatever the root `package.json#private` says. A private workspace's tag is still skipped, as [Publishing](publishing.md#publishability-filter) describes.
101
+
102
+ The `create-github-release.yaml` workflow that `release-kit init` scaffolds for a monorepo triggers only on `'*-v[0-9]*.[0-9]*.[0-9]*'`, which requires a literal `-v` and does not match a `v<version>` project tag. Add the project pattern to its tag filter:
103
+
104
+ ```yaml
105
+ on:
106
+ push:
107
+ tags:
108
+ - '*-v[0-9]*.[0-9]*.[0-9]*'
109
+ - 'v[0-9]*.[0-9]*.[0-9]*'
110
+ ```
111
+
112
+ A GitHub tag filter matches from the start of the tag, so `'v[0-9]*.[0-9]*.[0-9]*'` does not match a workspace tag such as `core-v1.0.0`. A custom `tagPrefix` needs its own pattern, such as `'app-v[0-9]*.[0-9]*.[0-9]*'` for `tagPrefix: 'app-v'`. `publish.yaml` does not need the project pattern, because `publish` skips the project tag.
113
+
114
+ When every workspace sets `private: true`, `init` does not create `create-github-release.yaml` (see [Releasing](releasing.md)). A project-release repo of that shape adds the workflow itself.
@@ -8,14 +8,14 @@ Publishes packages that have release tags on HEAD. The publish workflow's reusab
8
8
 
9
9
  ### Publishability filter
10
10
 
11
- `publish` and `create-github-release` operate only on workspaces whose `package.json#private` is absent or `false`. A workspace marked `private: true` is "versioned but not published": It can still be tagged by `release-kit tag` and get a `CHANGELOG.md` entry, but it is skipped by both `release-kit publish` (no registry publish) and `release-kit create-github-release` (no GitHub Release). `private: true` alone excludes a workspace from publish and announce; it doesn't require any extra release config. Other commands ignore this filter and operate on private workspaces unchanged.
11
+ `publish` and `create-github-release` operate only on workspaces whose `package.json#private` is absent or `false`. A workspace marked `private: true` is "versioned but not published": It can still be tagged by `release-kit tag` and get a `CHANGELOG.md` entry, but it is skipped by both `release-kit publish` (no registry publish) and `release-kit create-github-release` (no GitHub Release). `private: true` alone excludes a workspace from publish and announce; it doesn't require any extra release config. Other commands ignore this filter and operate on private workspaces unchanged. A project tag is the exception: `publish` never publishes it, and `create-github-release` creates a Release for it whatever the root `package.json#private` says (see [Project releases](project-releases.md#publishing-and-github-releases)).
12
12
 
13
13
  An unpublishable tag is always skipped cleanly, never fatal. For `release-kit publish`, whether the skip is announced depends on how the tag was resolved:
14
14
 
15
15
  - **Without `--tags`** (implicit resolution): Unpublishable tags on HEAD are silently filtered. The pre-publish listing shows only the publishable subset. If the filter empties the set, `release-kit publish` prints `Nothing to publish.` and exits 0.
16
16
  - **With `--tags`** (explicit naming): Each named tag pointing at an unpublishable workspace is skipped with a warning, and any publishable tags named in the same command are still published.
17
17
 
18
- `release-kit create-github-release` skips unpublishable workspaces with a warning regardless of `--tags`; an all-private tag set is a clean no-op.
18
+ `release-kit create-github-release` skips unpublishable workspaces with a warning regardless of `--tags`; an all-private tag set is a clean no-op. A project tag is not skipped.
19
19
 
20
20
  Example warning when an explicit tag is unpublishable:
21
21
 
@@ -25,6 +25,6 @@ Skipping basic-v1.0.0 (packages/basic): package.json#private is true.
25
25
 
26
26
  ## `release-kit create-github-release`
27
27
 
28
- Creates GitHub Releases from `changelog.json` for tags on HEAD. Independent of `npm publish`: Invoking this command creates Releases for publishable packages whether or not they were published to a registry. Private workspaces are skipped (see [Publishability filter](#publishability-filter)).
28
+ Creates GitHub Releases from `changelog.json` for tags on HEAD. Independent of `npm publish`: Invoking this command creates Releases for publishable packages whether or not they were published to a registry. Private workspaces are skipped, and a project tag gets a Release (see [Publishability filter](#publishability-filter)).
29
29
 
30
30
  When `--tags` is omitted, every release tag pointing at HEAD is processed. The CLI requires the `gh` CLI on `PATH` and `contents: write` permission. The bundled `create-github-release.reusable.yaml` GitHub Actions workflow runs this command in CI.
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. A repo with a `project` block is the exception: `create-github-release` still creates a Release for its project tag, so that repo adds `create-github-release.yaml` itself (see [Project releases](project-releases.md#publishing-and-github-releases)). 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
- `--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.
34
+ `--only` names the release targets from which the release starts, by target name: a workspace's directory name (e.g., `arrays`, `release-kit`) unless a [`releases`](configuration.md#releases) entry renames it, or a `releases` composite's name. A named composite releases from its own window, whether or not the run includes its members; a run narrowed by `--only` skips every composite that it doesn't name. 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,7 +88,7 @@ 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
 
85
- 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`.
91
+ In a single-package repo, the value is the version: `--set-version 1.0.0`. In a monorepo, it names the workspace or [directory target](configuration.md#directory-targets) with each version, as comma-separated `<name>@N.N.N` entries: `--set-version arrays@1.0.0,strings@2.0.0`. Each named target releases at its version whether or not it has commits, and starts the release like a target named by `--only`.
86
92
 
87
93
  The flag validates that:
88
94
 
@@ -120,12 +126,14 @@ The `init` command scaffolds a release workflow at `.github/workflows/release.ya
120
126
 
121
127
  | Input | Type | Description |
122
128
  | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
123
- | `only` | string | Workspaces to release, with their dependents (comma-separated, leave empty for all) |
129
+ | `only` | string | Release targets to release, with their dependents (comma-separated, leave empty for all) |
124
130
  | `bump` | choice | Bump type: `auto` (default, detects it from commits), `patch`, `minor`, or `major` |
125
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) |
126
132
 
127
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.
128
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
+
129
137
  ### Triggering a release
130
138
 
131
139
  ```sh
@@ -51,7 +51,7 @@ Each work-type has a `breakingPolicy` value:
51
51
 
52
52
  A `BREAKING CHANGE:` body footer on a `forbidden`-policy type triggers the same warning path as the prefix `!` does: A type that cannot be breaking is parsed as non-breaking whichever surface has the signal. The warning is all that a footer triggers; it doesn't raise a bump (see [`🚨 **Breaking:**` bullet marker](#-breaking-bullet-marker)).
53
53
 
54
- The release-prepare orchestrators (`releasePrepare`, `releasePrepareMono`, `releasePrepareProject`) apply `DEFAULT_BREAKING_POLICIES` automatically. Violations in the titles and change-record entries of each workspace's or project's unreleased window are collected onto the corresponding result's `policyViolations` field, on a skipped result as on a released one, and rendered under the section in the prepare report:
54
+ The release-prepare orchestrator (`releasePrepareTargets`) applies `DEFAULT_BREAKING_POLICIES` automatically. Violations in the titles and change-record entries of each workspace's or project's unreleased window are collected onto the corresponding result's `policyViolations` field, on a skipped result as on a released one, and rendered under the section in the prepare report:
55
55
 
56
56
  ```
57
57
  arrays
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williamthorsen/release-kit",
3
- "version": "14.0.0",
3
+ "version": "14.2.0",
4
4
  "description": "Version-bumping and changelog-generation toolkit for release workflows",
5
5
  "keywords": [
6
6
  "changelog",
@@ -51,8 +51,8 @@
51
51
  ],
52
52
  "dependencies": {
53
53
  "@williamthorsen/change-grammar": "0.2.0",
54
- "@williamthorsen/nmr-core": "0.13.2",
55
- "@williamthorsen/toolbelt.errors": "0.8.2",
54
+ "@williamthorsen/nmr-core": "0.13.3",
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,9 +60,9 @@
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/semver": "7.8.0",
63
- "@williamthorsen/toolbelt.filesystem": "0.9.2",
64
- "@williamthorsen/toolbelt.testing": "0.6.2",
65
- "@williamthorsen/toolbelt.vitest": "0.10.2",
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
68
  "readyup": "0.40.0",
@@ -53,6 +53,17 @@
53
53
  - name: 'value:questionable'
54
54
  color: c5def5
55
55
 
56
+ # Confidence
57
+ - name: 'confidence:high'
58
+ color: 0b7285
59
+ description: Clearly worth doing
60
+ - name: 'confidence:medium'
61
+ color: 3bc9db
62
+ description: Worth doing, but arguable
63
+ - name: 'confidence:low'
64
+ color: 99e9f2
65
+ description: Not worth a ticket now
66
+
56
67
  # Triage
57
68
  - name: duplicate
58
69
  color: cfd3d7
@@ -1,36 +0,0 @@
1
- import type { ReleasePlan } from './releasePlan.js';
2
- import type { PrepareConfig, ReleaseType } from './types.js';
3
- /** Options for the release preparation workflow. */
4
- export interface ReleasePrepareOptions {
5
- /**
6
- * Release even when the range since the last tag doesn't contain any commits or any bump-worthy commits.
7
- * Orthogonal to `bumpOverride`: When `bumpOverride` is not given, the release falls back to `patch`.
8
- */
9
- force?: boolean;
10
- /** Level of the release, in place of the level that the changelog items call for; it doesn't trigger a release. */
11
- bumpOverride?: ReleaseType;
12
- /**
13
- * Explicit target version (canonical `N.N.N`) that bypasses commit-derived bump logic and takes precedence over
14
- * `bumpOverride` and `force`.
15
- */
16
- setVersion?: string;
17
- /**
18
- * If true, plan per-workspace release-notes previews under `{workspacePath}/docs/`
19
- * (`README.v{version}.md` and `RELEASE_NOTES.v{version}.md`) from each workspace's
20
- * changelog entries. Requires `config.changelogJson.enabled`; when disabled,
21
- * the planner records a warning on the plan and doesn't plan any previews.
22
- */
23
- withReleaseNotes?: boolean;
24
- }
25
- /**
26
- * Orchestrates the release preparation workflow for a single package.
27
- *
28
- * 1. Reads the release history once, and stops when the current version is recorded but untagged.
29
- * 2. Decides the release from the history's bump, `--force`, and `--bump` (or takes `--set-version`).
30
- * 3. Plans the bump of every configured `package.json` version field.
31
- * 4. Plans the changelogs from the same history, and the release-notes previews when requested.
32
- * 5. Renders the optional format command.
33
- *
34
- * Returns the plan without writing any file.
35
- */
36
- export declare function releasePrepare(config: PrepareConfig, options: ReleasePrepareOptions): ReleasePlan;
@@ -1,230 +0,0 @@
1
- import { join as joinPath } from 'node:path';
2
- import { assertTaggedBaseline, findUntaggedBaseline } from "./assertTaggedBaseline.js";
3
- import { attachChangelogDiagnostics } from "./attachChangelogDiagnostics.js";
4
- import { readReleaseHistory, toReleaseEntries } from "./buildChangelogEntries.js";
5
- import { buildReleaseSummary } from "./buildReleaseSummary.js";
6
- import { mergeChangelogEntriesWithDisk, renderChangelogJson, resolveChangelogJsonPath } from "./changelogJsonFile.js";
7
- import { applyChangelogOverrides, formatStaleOverrideKeyWarning, loadOverridesForScopes, } from "./changelogOverrides.js";
8
- import { decideRelease } from "./decideRelease.js";
9
- import { deriveSectionOrder } from "./deriveReleaseNotesConfig.js";
10
- import { hasPrettierConfig } from "./hasPrettierConfig.js";
11
- import { resolveWorkTypes } from "./loadConfig.js";
12
- import { planReleaseNotesPreviews } from "./planReleaseNotesPreviews.js";
13
- import { planVersionBump, planVersionSet } from "./planVersionBump.js";
14
- import { planPreservedSections } from "./readChangelogSections.js";
15
- import { renderChangelogMarkdown } from "./renderChangelogMarkdown.js";
16
- export function releasePrepare(config, options) {
17
- const { bumpOverride, force, setVersion, withReleaseNotes } = options;
18
- const writes = [];
19
- const overridesResult = loadOverridesForScopes({ project: '.' });
20
- if (overridesResult.errors.length > 0) {
21
- throw new Error(`Failed to load changelog overrides:\n - ${overridesResult.errors.join('\n - ')}`);
22
- }
23
- const overrides = overridesResult.project;
24
- const history = readReleaseHistory(config, { tagPrefixes: [config.tagPrefix] });
25
- assertTaggedBaseline([
26
- findUntaggedBaseline({
27
- label: 'package',
28
- packageFiles: config.packageFiles,
29
- changelogPaths: config.changelogPaths,
30
- tagPrefixes: [config.tagPrefix],
31
- previousTag: history.previousTag,
32
- }, config),
33
- ]);
34
- const { commits } = history.unreleased;
35
- const tag = history.previousTag;
36
- const since = tag === undefined ? '(no previous release found)' : `since ${tag}`;
37
- let releaseType;
38
- let bump;
39
- if (setVersion === undefined) {
40
- const decision = decideRelease({
41
- naturalBump: history.unreleased.bump,
42
- commitCount: commits.length,
43
- force,
44
- bumpOverride,
45
- skipReasons: {
46
- noCommits: `No commits ${since}. Pass --force to release at patch. Skipping.`,
47
- noBumpWorthy: `No bump-worthy commits ${since}. Pass --force to release at patch (or --force --bump=X for a different level). Skipping.`,
48
- },
49
- });
50
- if (decision.outcome === 'skip') {
51
- const skipped = buildSkippedSinglePackage(history, decision.skipReason);
52
- return {
53
- workspaces: [skipped],
54
- tags: [],
55
- writes: [],
56
- summary: '',
57
- formatCommand: undefined,
58
- };
59
- }
60
- releaseType = decision.releaseType;
61
- bump = planVersionBump(config.packageFiles, releaseType);
62
- }
63
- else {
64
- bump = planVersionSet(config.packageFiles, setVersion);
65
- }
66
- writes.push(...bump.writes);
67
- const newTag = `${config.tagPrefix}${bump.newVersion}`;
68
- const planWarnings = [];
69
- const changelogs = planSinglePackageChangelogs({
70
- config,
71
- history,
72
- newTag,
73
- overrides,
74
- overrideWarnings: planWarnings,
75
- });
76
- const { changelogFiles, changelogJsonFiles, changelogPreservation } = changelogs;
77
- const previewWrites = planSinglePackagePreviews(withReleaseNotes === true, config, newTag, changelogs.entries, planWarnings);
78
- writes.push(...changelogs.writes, ...previewWrites);
79
- const formatCommandStr = config.formatCommand ?? (hasPrettierConfig() ? 'npx prettier --write' : undefined);
80
- let formatCommand;
81
- if (formatCommandStr !== undefined) {
82
- const modifiedFiles = [
83
- ...config.packageFiles,
84
- ...config.changelogPaths.map((changelogPath) => joinPath(changelogPath, 'CHANGELOG.md')),
85
- ...changelogJsonFiles,
86
- ];
87
- formatCommand = { command: `${formatCommandStr} ${modifiedFiles.join(' ')}`, files: modifiedFiles };
88
- }
89
- const released = buildReleasedSinglePackage({
90
- history,
91
- bump,
92
- newTag,
93
- changelogFiles,
94
- changelogPreservation,
95
- releaseType,
96
- bumpOverride: setVersion === undefined ? bumpOverride : undefined,
97
- setVersion,
98
- previewFiles: previewWrites.map((write) => write.path),
99
- });
100
- const plan = {
101
- workspaces: [released],
102
- tags: [newTag],
103
- writes,
104
- summary: buildReleaseSummary({ workspaces: [released] }),
105
- formatCommand,
106
- };
107
- if (planWarnings.length > 0) {
108
- plan.warnings = planWarnings;
109
- }
110
- return plan;
111
- }
112
- function buildSkippedSinglePackage(history, skipReason) {
113
- const { unreleased } = history;
114
- const skipped = {
115
- status: 'skipped',
116
- commitCount: unreleased.commits.length,
117
- parsedCommitCount: unreleased.parsedCommitCount,
118
- skipReason,
119
- };
120
- if (history.previousTag !== undefined) {
121
- skipped.previousTag = history.previousTag;
122
- }
123
- if (unreleased.unparseableCommits !== undefined) {
124
- skipped.unparseableCommits = unreleased.unparseableCommits;
125
- }
126
- attachChangelogDiagnostics(skipped, unreleased.diagnostics);
127
- return skipped;
128
- }
129
- function buildReleasedSinglePackage(args) {
130
- const { history, bump, newTag, changelogFiles, releaseType, bumpOverride, setVersion } = args;
131
- const { commits, diagnostics, parsedCommitCount, unparseableCommits } = history.unreleased;
132
- const released = {
133
- status: 'released',
134
- commitCount: commits.length,
135
- currentVersion: bump.currentVersion,
136
- newVersion: bump.newVersion,
137
- tag: newTag,
138
- bumpedFiles: bump.writes.map((write) => write.path),
139
- changelogFiles,
140
- commits,
141
- };
142
- if (history.previousTag !== undefined) {
143
- released.previousTag = history.previousTag;
144
- }
145
- if (releaseType !== undefined) {
146
- released.releaseType = releaseType;
147
- released.parsedCommitCount = parsedCommitCount;
148
- if (unparseableCommits !== undefined) {
149
- released.unparseableCommits = unparseableCommits;
150
- }
151
- }
152
- if (bumpOverride !== undefined) {
153
- released.bumpOverride = bumpOverride;
154
- }
155
- attachChangelogDiagnostics(released, diagnostics);
156
- if (setVersion !== undefined) {
157
- released.setVersion = setVersion;
158
- }
159
- if (args.previewFiles.length > 0) {
160
- released.previewFiles = args.previewFiles;
161
- }
162
- if (args.changelogPreservation.length > 0) {
163
- released.changelogPreservation = args.changelogPreservation;
164
- }
165
- return released;
166
- }
167
- function planSinglePackageChangelogs(args) {
168
- const { config, history, newTag, overrides, overrideWarnings } = args;
169
- const today = new Date().toISOString().slice(0, 10);
170
- const builtEntries = toReleaseEntries(history, newTag, today);
171
- const applied = applyChangelogOverrides(builtEntries, overrides);
172
- if (applied.errors.length > 0) {
173
- throw new Error(`Changelog override application failed:\n - ${applied.errors.join('\n - ')}`);
174
- }
175
- overrideWarnings.push(...applied.warnings);
176
- const matchedSet = new Set(applied.matchedKeys);
177
- for (const overrideKey of overrides.keys()) {
178
- if (!matchedSet.has(overrideKey)) {
179
- overrideWarnings.push(formatStaleOverrideKeyWarning(overrideKey));
180
- }
181
- }
182
- const sectionOrder = deriveSectionOrder(resolveWorkTypes(config.workTypes));
183
- const changelogFiles = [];
184
- const changelogJsonFiles = [];
185
- const changelogPreservation = [];
186
- const writes = [];
187
- let firstMergedEntries = [];
188
- for (const changelogPath of config.changelogPaths) {
189
- const jsonPath = resolveChangelogJsonPath(config, changelogPath);
190
- const mergedEntries = mergeChangelogEntriesWithDisk(jsonPath, applied.entries);
191
- if (changelogFiles.length === 0) {
192
- firstMergedEntries = mergedEntries;
193
- }
194
- if (config.changelogJson.enabled) {
195
- writes.push({ path: jsonPath, content: renderChangelogJson(mergedEntries) });
196
- changelogJsonFiles.push(jsonPath);
197
- }
198
- const changelogFile = joinPath(changelogPath, 'CHANGELOG.md');
199
- const preserved = planPreservedSections(changelogFile, mergedEntries);
200
- writes.push({
201
- path: changelogFile,
202
- content: renderChangelogMarkdown(mergedEntries, { sectionOrder, preservedSections: preserved.sections }),
203
- });
204
- changelogFiles.push(changelogFile);
205
- if (preserved.preservation !== undefined) {
206
- changelogPreservation.push(preserved.preservation);
207
- }
208
- }
209
- return { changelogFiles, changelogJsonFiles, changelogPreservation, entries: firstMergedEntries, writes };
210
- }
211
- function planSinglePackagePreviews(withReleaseNotes, config, newTag, entries, warnings) {
212
- if (!withReleaseNotes) {
213
- return [];
214
- }
215
- if (!config.changelogJson.enabled) {
216
- warnings.push('--with-release-notes requires changelogJson.enabled; skipping preview generation');
217
- return [];
218
- }
219
- if (config.changelogPaths.length === 0) {
220
- return [];
221
- }
222
- const previews = planReleaseNotesPreviews({
223
- workspacePath: '.',
224
- tag: newTag,
225
- entries,
226
- sectionOrder: deriveSectionOrder(resolveWorkTypes(config.workTypes)),
227
- });
228
- warnings.push(...previews.warnings);
229
- return previews.writes;
230
- }
@@ -1,29 +0,0 @@
1
- import type { ReleasePlan } from './releasePlan.js';
2
- import type { ReleasePrepareOptions } from './releasePrepare.js';
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
- }
18
- /**
19
- * Orchestrates release preparation for a monorepo with multiple workspaces.
20
- *
21
- * Phase 0: Selects the planned workspaces: those from which the release starts and their transitive dependents.
22
- * Phase 1: Determines direct bumps from commits for each workspace.
23
- * Phase 2: Propagates bumps to dependents.
24
- * Phase 2b: Sorts the full release set topologically.
25
- * Phase 3: Plans bumps and changelogs in dependency order.
26
- * Phase 3b: Plans the project release.
27
- * Phase 4: Renders the format command.
28
- */
29
- export declare function releasePrepareMono(fullConfig: MonorepoPrepareConfig, options: MonorepoPrepareOptions): ReleasePlan;