@wpelevator/wp-release 0.11.0 → 0.13.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 (51) hide show
  1. package/README.md +95 -13
  2. package/dist/changeset-config.d.ts +86 -2
  3. package/dist/changeset-config.js +101 -6
  4. package/dist/changeset-dependents.d.ts +19 -8
  5. package/dist/changeset-dependents.js +96 -34
  6. package/dist/changeset-files.d.ts +6 -5
  7. package/dist/changeset-files.js +7 -5
  8. package/dist/changeset-packages.d.ts +45 -8
  9. package/dist/changeset-packages.js +73 -14
  10. package/dist/changeset-prompt.d.ts +119 -0
  11. package/dist/changeset-prompt.js +203 -0
  12. package/dist/changeset-release.d.ts +38 -3
  13. package/dist/changeset-release.js +88 -11
  14. package/dist/changesets.d.ts +6 -2
  15. package/dist/changesets.js +18 -12
  16. package/dist/cli.d.ts +7 -1
  17. package/dist/cli.js +18 -5
  18. package/dist/commands/changeset-add.d.ts +5 -2
  19. package/dist/commands/changeset-add.js +108 -9
  20. package/dist/commands/changeset-args.d.ts +24 -5
  21. package/dist/commands/changeset-args.js +57 -14
  22. package/dist/commands/changeset-status.d.ts +2 -0
  23. package/dist/commands/changeset-tag.js +2 -1
  24. package/dist/commands/changeset-version.d.ts +6 -0
  25. package/dist/commands/changeset-version.js +15 -3
  26. package/dist/commands/command.d.ts +3 -0
  27. package/dist/commands/deploy-git.d.ts +24 -4
  28. package/dist/commands/deploy-git.js +56 -3
  29. package/dist/commands/deploy-svn.d.ts +24 -4
  30. package/dist/commands/deploy-svn.js +56 -2
  31. package/dist/commands/package-deploys.d.ts +78 -0
  32. package/dist/commands/package-deploys.js +108 -0
  33. package/dist/git-deploy.d.ts +32 -0
  34. package/dist/git-deploy.js +57 -1
  35. package/dist/index.d.ts +5 -4
  36. package/dist/index.js +2 -2
  37. package/dist/package-deploy.d.ts +105 -0
  38. package/dist/package-deploy.js +288 -0
  39. package/dist/release-repository.d.ts +14 -2
  40. package/dist/release-repository.js +46 -7
  41. package/dist/release-tags.d.ts +3 -1
  42. package/dist/release-tags.js +5 -2
  43. package/dist/tag-format.d.ts +3 -3
  44. package/dist/tag-format.js +1 -1
  45. package/dist/version-files.d.ts +2 -1
  46. package/dist/version-files.js +12 -7
  47. package/dist/versions.d.ts +28 -3
  48. package/dist/versions.js +84 -9
  49. package/package.json +1 -1
  50. package/dist/github.d.ts +0 -24
  51. package/dist/github.js +0 -30
package/README.md CHANGED
@@ -34,6 +34,8 @@ npx wp-release --help
34
34
  | `wp-release version-check [<source-dir>] [options]` | Check that the `Version` header, readme `Stable tag`, PHP constants and git tag agree. |
35
35
  | `wp-release deploy-svn [<source-dir>] [options]` | Deploy a plugin to the WordPress.org plugin SVN repository. |
36
36
  | `wp-release deploy-git [<source-dir>] --git-url <url> [options]` | Deploy the files of a plugin, theme or package to a Git repository as a release commit. |
37
+ | `wp-release deploy-git --release-ref <ref> [--package <name>] [--list]` | Deploy the configured packages of a release tag or branch to their Git targets. |
38
+ | `wp-release deploy-svn --release-ref <ref> [--package <name>] [--list]` | Deploy the configured plugin of a release tag to WordPress.org. |
37
39
  | `wp-release changeset-add [options]` | Record the release impact of a pull request. |
38
40
  | `wp-release changeset-status [options]` | List pending releases and check changed packages for changesets. |
39
41
  | `wp-release changeset-version [options]` | Apply version bumps and changelog entries. |
@@ -68,6 +70,7 @@ Each command also adds a section to the [job summary](https://docs.github.com/en
68
70
  | `sign` | Signature files and the public key. |
69
71
  | `verify` | That all files match, or the counts and a table of the changed, missing and extra files. |
70
72
  | `version-check` | The matching version, or a table of the mismatches with their files and lines. |
73
+ | `changeset-status` | Each changed package with whether it has a changeset. |
71
74
  | `changeset-version` | A table of the package releases with their version changes and bumps. |
72
75
  | `changeset-tag` | Each tag with its previous tag and status, and its release notes in a collapsed section. |
73
76
 
@@ -77,7 +80,7 @@ Use global `--config <path>` to select a config; relative paths resolve from the
77
80
 
78
81
  ### Changesets support (experimental)
79
82
 
80
- Changesets support is experimental. It reads Changesets-style Markdown and loads JavaScript configs with descriptors for npm and Composer manifests, plugin and theme headers, readme Stable tags, PHP constants, and Git tags. Use ESM (`export default`) or CommonJS (`module.exports`); TypeScript configs are not supported. Frontmatter accepts `package-name: patch|minor|major` entries; advanced YAML and prerelease mode are unsupported.
83
+ Changesets support is experimental. It reads Changesets-style Markdown and loads JavaScript configs with descriptors for npm and Composer manifests, plugin and theme headers, readme Stable tags, PHP constants, and Git tags. Use ESM (`export default`) or CommonJS (`module.exports`); TypeScript configs are not supported. Frontmatter accepts `package-name: patch|minor|major` entries; advanced YAML and the Changesets pre mode are unsupported (see [pre-releases](#pre-releases) for the alternative).
81
84
 
82
85
  Typical release flow, from a feature pull request to the publish workflows triggered by release tags:
83
86
 
@@ -124,22 +127,25 @@ export default defineConfig( {
124
127
 
125
128
  `version.from` selects the current version; `version.to` lists release targets. Package globs support `*`, `?` and `**`; `{path}` in tag templates is relative to `root`. Templates also accept `{type}`, `{name}` and `{slug}`, while `tagType` customizes the type label. Use `phpConstantVersion( { file, constant } )` to sync a PHP constant.
126
129
 
127
- The default strategy requires stable `x.y.z` versions. Configure `nextVersion( { package, bump, currentVersion, changesets } )` for other formats; changeset bumps remain `patch`, `minor` or `major`, and prerelease syntax is unsupported.
130
+ The default strategy bumps stable `x.y.z` versions with SemVer rules, and the `x.y` and `x.y.z.w` versions common in WordPress plugins the same way: the bumped part is raised, the parts after it reset to `0`, and the version keeps its number of parts. A minor bump of `2.1` is `2.2` and a major bump is `3.0`, while a patch bump adds the third part, `2.1.1`. A patch bump of `1.2.3.4` is `1.2.4.0`, so raising only the fourth part needs a custom strategy. Configure `nextVersion( { package, bump, currentVersion, changesets } )` for other formats; changeset bumps remain `patch`, `minor` or `major`. Pre-releases are added to the next version with `--prerelease`, described below.
128
131
 
129
132
  Without `--since`, `changeset-status` compares against `origin/<baseBranch>`; fetch that ref or pass `--since` when using a fork or different upstream. `loadChangesetConfig()` discovers a conventional config at the repository root, unless `--config <path>` selects one explicitly.
130
133
 
131
- Add release notes in a feature PR with `npx wp-release changeset-add --package @vendor/plugin --bump patch --summary "Fix the checkout validation."`. Repeat package and bump flags for multiple packages, or use `--empty` when no release is needed. In CI, `changeset-status --since origin/main` requires a changeset added by the PR; changesets already on the base do not count, while an empty PR changeset records an explicit no-release decision. Missing-change annotations identify an affected file.
134
+ Add release notes in a feature PR with `npx wp-release changeset-add`. In a terminal, it lists the packages, marks the ones that changed since `origin/<baseBranch>` (or `--since <ref>`) without a changeset, and asks for the packages to release, a `patch`, `minor` or `major` bump for each, and the release note. Choose packages by number, identifier or path, or press Enter to release the changed ones; `none` adds an empty changeset. Changed files include uncommitted and untracked ones, since changesets are usually added before committing.
135
+
136
+ Without a terminal, or for scripts, pass the changeset as options: `npx wp-release changeset-add --package @vendor/plugin --bump patch --summary "Fix the checkout validation."`. Repeat package and bump flags for multiple packages, or use `--empty` when no release is needed. A package can also be named by its path under `root`, like `plugins/alpha`, and the changeset records its identifier; `changeset-version --package` and `changeset-tag --package` accept paths too. A single `--bump` without `--package` releases every changed package without a changeset, like `changeset-add --bump patch --summary "…"` to fix a failing `changeset-status` check. `--json` and `--github-output` never ask.
137
+
138
+ In CI, `changeset-status --since origin/main` requires a changeset added by the PR; changesets already on the base do not count, while an empty PR changeset records an explicit no-release decision. Missing-change annotations identify an affected file.
132
139
 
133
140
  On the release branch, preview with `changeset-version --dry-run`, then run `changeset-version` to update version targets and changelogs. By default, new Markdown entries go before the first version heading; `changelogInsertAfter` sets a literal insertion marker for `CHANGELOG.md` and, with `readmeChangelog`, the plugin readme. Put the marker in both files; a missing marker is an error. Without a marker, plugin readmes use the `== Changelog ==` heading. Failed updates are rolled back for safe retries.
134
141
 
135
- Configure `changelogEntry( { package, changeset, commit } )` to format the changelog entry of each changeset, where `commit` is the `{ hash, shortHash, subject }` of the commit that added the changeset file, if it's committed. The formatted entries go into `CHANGELOG.md`, the `changelog` of each release in the `changeset-version` result, and so the release notes; plugin readmes keep the plain summaries. `getGitHubPullRequest( { cwd, commit } )` finds the `{ number, url, author }` of the pull request that merged a commit with the authenticated `gh` CLI, for example to credit it:
142
+ Configure `changelogEntry( { package, changeset, commit } )` to format the changelog entry of each changeset, where `commit` is the `{ hash, shortHash, subject }` of the commit that added the changeset file, if it's committed. The formatted entries go into `CHANGELOG.md`, the `changelog` of each release in the `changeset-version` result, and so the release notes; plugin readmes keep the plain summaries. Without the callback, entries are the plain summaries. For example, to link each entry to its commit:
136
143
 
137
144
  ```js
138
- changelogEntry: async ( { changeset, commit } ) => {
139
- const pullRequest = commit && ( await getGitHubPullRequest( { cwd: import.meta.dirname, commit: commit.hash } ) );
140
-
141
- return pullRequest ? `${ changeset.summary } (#${ pullRequest.number } by @${ pullRequest.author })` : changeset.summary;
142
- },
145
+ changelogEntry: ( { changeset, commit } ) =>
146
+ commit
147
+ ? `${ changeset.summary } ([\`${ commit.shortHash }\`](https://github.com/me/repo/commit/${ commit.hash }))`
148
+ : changeset.summary,
143
149
  ```
144
150
 
145
151
  Configure `dependents` to re-release packages that bundle a package when it releases. The dependents come from what the packages declare: a package is a dependent of a source when the spec that it declares in its npm `dependencies` or `optionalDependencies` (or the sections that the policy adds) resolves the current version of the source or points at the workspace, and it re-releases when the next version of the source falls outside the declared range, or always with `when: 'always'`. A pin to an older version selects a registry release instead and `changeset-status` warns about the drift until the pin resolves the package again. Versioning a dependent rewrites its declared specs of the source to the released version, keeping their range prefixes, so an exact pin matches the released workspace again and the built bundle matches the changelog. Wildcards and unpinned ranges accept the release without one, like the `devDependencies` of a package. The derived dependents appear in `changeset-status`, `changeset-version` and `changeset-tag` like any other release, and a dependent that releases for its own reasons keeps its own bump and appends the bundle note:
@@ -153,6 +159,8 @@ dependents: {
153
159
 
154
160
  Excluded packages still release with their own changesets and stay out of the automatic re-releases.
155
161
 
162
+ When the changesets of a pull request release a source, `changeset-status` requires no changeset for a dependent whose only change is the spec of that source in its `package.json`, since versioning rewrites the spec and the bundle note announces it. Any other change to the dependent, like another manifest field or dependency, documentation or code, needs its own changeset.
163
+
156
164
  The bundle changelog entry defaults to `Bundle <source> <version>.` and is formatted with `dependents.changelog( { package, source } )`:
157
165
 
158
166
  ```js
@@ -204,10 +212,37 @@ To release a single package, for example from a manual workflow, pass `--package
204
212
  prerelease: ${{ fromJSON( steps.tag.outputs.result ).tags[0].prerelease }}
205
213
  ```
206
214
 
207
- The release notes are the version's `CHANGELOG.md` section, or without one the commits that changed the package since its previous tag, the highest version below the released one. Configure `releaseNotes( { package, version, tag, previousTag, changelog, commits, notes } )` to format them, for example to add links; `notes` is the default text and `commits` lists `{ hash, shortHash, subject }`, newest first. `changeset-tag --package <name> --dry-run` prints them without creating the tag.
215
+ The release notes are the version's `CHANGELOG.md` section, or without one the commits that changed the package since its previous tag, the highest version below the released one, skipping pre-releases for a stable version. Configure `releaseNotes( { package, version, tag, previousTag, changelog, commits, notes } )` to format them, for example to add links; `notes` is the default text and `commits` lists `{ hash, shortHash, subject }`, newest first. `changeset-tag --package <name> --dry-run` prints them without creating the tag.
208
216
 
209
217
  For local development, run the TypeScript source with `npm run wp-release -- <command>` (for example, `npm run wp-release -- changeset-status --since origin/main`). GitHub workflows use the same CLI for versioning and tagging. `SUBTREE_DEPLOY_TOKEN` lets them update the release PR and push tags, and the `Tag Release` workflow passes it to `softprops/action-gh-release` to create the GitHub Releases.
210
218
 
219
+ #### Pre-releases
220
+
221
+ `changeset-version --prerelease <id>` (the `prerelease` option of `versionChangesets()`) releases pre-releases of the next versions, like `1.3.0-rc.1` for a minor bump of `1.2.0`. The identifier, like `alpha`, `beta` or `rc`, can have a number, like `rc.2`, which is used as is and fails when the version is already tagged. Without a number, the number is one above the highest existing tag of that identifier and next stable version, read with the `tagFormat` of the package, so the first one is `rc.1`, and the numbering starts again at `.1` when later changesets raise the next version. The dependents of a source become pre-releases too, so that they bundle its pre-release, unless they're left out with `--no-dependents`.
222
+
223
+ A pre-release writes every version target except the readme `Stable tag`, which keeps the last stable version since WordPress.org offers it to every site, and `changeset-tag` skips the `Stable tag` when it verifies a pre-release. The changelog gets a section for the pre-release, like `## 1.3.0-rc.1 (2026-10-12)`, so that `changeset-tag` finds its release notes, and the changesets are consumed like in any release.
224
+
225
+ Since a pre-release consumes the changesets that the stable release needs, run it on a branch that isn't merged, and tag that branch commit:
226
+
227
+ ```sh
228
+ git switch --force-create prerelease/plugins/alpha
229
+ npx wp-release changeset-version --package plugins/alpha --prerelease rc
230
+ git add --all
231
+ git commit --message "Prerelease plugins/alpha"
232
+ npx wp-release changeset-tag --package plugins/alpha --version 1.3.0-rc.1 --push
233
+ ```
234
+
235
+ The base branch keeps its stable versions and pending changesets, so the stable release later versions them as usual, in one changelog section. The tag keeps the pre-release commit reachable after the branch is reset or deleted. The release notes of a stable release compare with the previous stable release, skipping the pre-releases before it, and the `git-tag` version source ignores pre-release tags, so packages versioned by git tags keep building on their last stable version.
236
+
237
+ #### Snapshots
238
+
239
+ `changeset-version --snapshot <id>` (the `snapshot` option of `versionChangesets()`) releases snapshots of the next versions for test builds, like `1.3.0-pr-251.20261012183005` for a pull request: the next version with the identifier, like `pr-251` or `pr-251.0abc123` with a commit hash, and the UTC time, so that every snapshot is unique and they sort by time. Snapshots aren't numbered from tags and can't be combined with `--prerelease`. Like a pre-release, a snapshot writes every version target except the readme `Stable tag`. Build and publish the versioned files without committing them, for example under an npm dist-tag like `pr-251`, since a snapshot consumes the changesets:
240
+
241
+ ```sh
242
+ npx wp-release changeset-version --package @vendor/library --snapshot pr-251
243
+ npm publish --workspace packages/library --tag pr-251
244
+ ```
245
+
211
246
  ### Naming convention
212
247
 
213
248
  `@wpelevator/wp-release` is the suite of release tools and `wp-release` is its CLI. "Release" is the whole workflow; each command is one operation in it. Commands are flat: a single word or dashed words like `version-check` and `deploy-svn`, without command groups or aliases. Each GitHub Action is named `wp-release-` followed by the command name, so the CLI command and the `uses:` name always match:
@@ -457,7 +492,54 @@ How a deploy works:
457
492
 
458
493
  To release a tag again, for example after a release with the wrong files, delete the tag in the repository (`git push <url> :refs/tags/<tag>`) and run the deploy again. There's no flag for replacing a tag, since installs, Composer and GitHub Actions pins trust that a published tag doesn't change.
459
494
 
460
- `--dry-run` runs everything except the push. `--message` sets the commit message, which defaults to `Release <tag or branch> from <source commit>`. Credentials come from git, like a token in the URL or an SSH key, and credentials in URLs are never printed. The commit needs a git identity (`user.name` and `user.email`).
495
+ `--dry-run` runs everything except the push. `--message` sets the commit message, which defaults to `Release <tag or branch> from <source commit>`. Credentials come from git, like a credential helper or an SSH key, and credentials in URLs are never printed. For an HTTPS repository on `github.com`, or on the host of `GITHUB_SERVER_URL` like GitHub Enterprise Server, the `GITHUB_TOKEN` environment variable is sent as an authorization header, like `actions/checkout` does, so the token stays out of the URL, the git arguments and the repository config, and it replaces the header that `actions/checkout` persists. Pass `token` to `resolveGitDeployOptions()` for another HTTPS host. The commit needs a git identity (`user.name` and `user.email`).
496
+
497
+ ### Deploy configured packages
498
+
499
+ `deploy-git --release-ref <ref>` and `deploy-svn --release-ref <ref>` deploy the packages of a monorepo, or of a single-package repository, to the targets in the `deploy` config of their package groups, like the release repository of each package or its WordPress.org plugin. The ref decides what's deployed: a release tag of a package, like `refs/tags/plugins/alpha/1.2.0` read with the `tagFormat`, deploys that package and version, and a branch, like `refs/heads/main`, deploys every package with a target for that branch. A name without `refs/` is a release tag when it matches the tag format of a package, otherwise a branch, and in GitHub Actions `$GITHUB_REF` is the pushed ref.
500
+
501
+ ```js
502
+ packages: [
503
+ {
504
+ glob: 'plugins/*',
505
+ version: { from: wordpressPluginVersion(), to: [ wordpressPluginVersion(), wordpressStableTag() ] },
506
+ // Files that the deploys ship, instead of each package's .distignore.
507
+ files: { distignore: false, exclude: [ '.*', 'node_modules/', '/tests/' ] },
508
+ deploy: {
509
+ git: [
510
+ // The release repository of each plugin, with its history.
511
+ { repository: 'https://github.com/me/{slug}.git', message: 'Release {path} {ref} from {revision}' },
512
+ // A mirror that only gets release tags, named v1.2.0.
513
+ { repository: ( { package: pkg } ) => `https://gitlab.com/me/${ pkg.path }.git`, branches: false, tag: 'v{version}' },
514
+ ],
515
+ svn: { slug: 'me-{slug}', assetsDir: '.wordpress-org' },
516
+ },
517
+ },
518
+ // A package that isn't versioned, like a Docker image, only deploys branches.
519
+ { glob: 'docker/php-fpm', deploy: { git: { repository: 'https://github.com/me/php-fpm.git' } } },
520
+ ],
521
+ ```
522
+
523
+ | Mode | Command | Use |
524
+ |---|---|---|
525
+ | All | `deploy-git --release-ref <ref>` | Deploy every package of the ref to each of its targets, one at a time, in one log group for each package. A failed deploy doesn't stop the others, and the command exits with `1` at the end. |
526
+ | One package | `deploy-git --release-ref <ref> --package <name>` | Deploy only that package (repeatable), by identifier or path, for example to retry a failed one or in a CI job for each package with its own credentials. |
527
+ | Plan | `deploy-git --release-ref <ref> --list` | List the deploys without running them, with each package, its version or branch and the targets with the tag or branch they get. With `--json` or `--github-output` the result is `{ packages: [ … ] }`, for a CI matrix with `include: ${{ fromJSON( steps.plan.outputs.result ).packages }}`. |
528
+ | Single directory | `deploy-git <dir> --git-url <url> [options]` | Deploy one directory without a config, as described above. |
529
+
530
+ `deploy-svn` has the same modes. `--release-ref` can't be combined with the options of a single target, like `--git-url`, `--tag` or `--svn-tag`, which come from the config.
531
+
532
+ - **Git targets** take a `repository` and release both branches and tags by default. `branches` limits the branches, like `[ 'main' ]`, or turns them off with `false`, and `tag` is the tag template in the repository, `{version}` by default, or `false` to not release tags. A release tag is created on top of the default branch of the repository, and a branch is released to the same branch. `history: false` uses the source commit instead of the package history from `git subtree split`, and `message` sets the release commit message, `Release {ref} from {revision}` by default.
533
+ - **SVN targets** only deploy release tags: a stable release syncs `trunk/`, creates its tag and syncs the `assetsDir`, and a pre-release only creates its tag. `slug` defaults to `{slug}`, `url` to the WordPress.org plugin repository, and `message` sets the commit message. Credentials come from `SVN_USERNAME` and `SVN_PASSWORD`, or `--svn-username`.
534
+ - **Several targets**: `git` and `svn` each take one target or a list, like a release repository and a mirror on another host. Each target is a separate deploy.
535
+ - **Credentials**: a GitHub repository URL needs no token in it, since `GITHUB_TOKEN` is sent as an authorization header (see above). A callback can still add credentials for other hosts.
536
+ - **Templates and callbacks**: `repository`, `slug` and `message` are templates with `{name}`, `{slug}`, `{path}` and `{type}`, like the `tagFormat`, and `{version}`, `{branch}`, `{ref}` (the tag or branch that the deploy writes in the target) and `{revision}` (the source commit), or callbacks that receive `{ package, version, branch, ref, revision }`, where `package` has the `name`, `directory`, `path` and `type`. A callback can choose another host for some packages, read the environment, or return nothing to skip the target for that package or ref; an empty `message` uses the default one. Credentials in URLs are never printed or listed.
537
+ - **Packages without a version**: a package entry without `version`, like a single directory with a `glob` of its path, isn't released with changesets, so changes to it don't need one, and it only deploys branches, since it has no release tags.
538
+ - **Files**: `files.exclude` adds gitignore-style patterns relative to the package directory, and `files.distignore` sets the ignore file relative to it or turns it off with `false`. A `files` at the root of the config applies to every package group, for the rules that all packages share, and the `files` of a group add their patterns after it and can replace its ignore file. Without `files`, each package's `.distignore` applies, like for `zip`; there's no repository-wide `.distignore`, since each one is relative to its package. `getDeployPackages()` lists the packages with their file selection, for example to build a ZIP with the same files.
539
+
540
+ A deploy releases the files of the working tree, so build the packages first. One job that builds once and runs `deploy-git --release-ref "$GITHUB_REF"` is the simplest setup, also for a branch that releases every package. A matrix with a job for each package from `--list` would build every package in every job, so build once and pass the built tree to the jobs as an artifact if you need one, for example for separate credentials.
541
+
542
+ Other monorepo tools split the same way with less of the release in the config: [splitsh-lite](https://github.com/splitsh/lite), [Symplify MonorepoBuilder](https://github.com/symplify/monorepo-builder) and [`danharrin/monorepo-split-github-action`](https://github.com/danharrin/monorepo-split-github-action) push the source history of each package directory, with the packages and repositories listed in the workflow or a matrix, while `deploy-git` releases the built files on top of that history, so `vendor/` and `build/` reach Composer and WordPress installs. Like the publishers of [GoReleaser](https://goreleaser.com/), targets are lists with templates, and pre-releases skip the targets where they would replace the stable version.
461
543
 
462
544
  ### Exit codes
463
545
 
@@ -642,7 +724,7 @@ The changeset release modules, from the entry point down:
642
724
  |---|---|
643
725
  | `changeset-release.ts` | `ChangesetRelease`, which plans releases and runs `add()`, `getStatus()`, `version()` and `tag()`, and the function form of each operation. |
644
726
  | `changeset-config.ts` | Loading and validating `wp-release.config.js`, and the version source and target helpers. |
645
- | `changeset-packages.ts` | Discovering the packages of the configured globs and finding the changed ones. |
727
+ | `changeset-packages.ts` | Discovering the packages of the configured globs, with or without a version, and finding the changed ones. |
646
728
  | `changeset-files.ts` | Reading, writing and consuming changeset files. |
647
729
  | `changeset-dependents.ts` | The dependents policy: which packages re-release with their sources, and rewriting their dependency specs. |
648
730
  | `changesets.ts` | Parsing and formatting changesets, and the release types. |
@@ -650,8 +732,8 @@ The changeset release modules, from the entry point down:
650
732
  | `changelog.ts` | Changelog entries, and the `CHANGELOG.md` and readme changelog sections. |
651
733
  | `release-tags.ts` | Choosing the version and previous tag of a release, and building its release notes. |
652
734
  | `tag-format.ts` | Formatting tag names from the tag format. |
735
+ | `package-deploy.ts` | Resolving the configured deploys of a ref: the released packages, their targets and file selections, for `deploy-git` and `deploy-svn`. |
653
736
  | `release-repository.ts` | `ReleaseRepository`, the git commands of a release: tags, commits and changed files. |
654
- | `github.ts` | Finding the pull request of a commit with the GitHub CLI, for changelog formatters. |
655
737
  | `file-journal.ts` | `FileJournal`, which restores every file a failed `changeset-version` run changed. |
656
738
  | `versions.ts` | Version checks and SemVer precedence. |
657
739
 
@@ -66,8 +66,83 @@ export interface ChangesetPackageGroup {
66
66
  tagType?: string;
67
67
  /** Manifest field used as the changeset name; defaults to `path`. */
68
68
  idFrom?: PackageIdSource;
69
- /** Version source and files to update for every discovered package. */
70
- version: PackageVersionConfig;
69
+ /**
70
+ * Version source and files to update for every discovered package. Without
71
+ * it, the packages aren't released with changesets and only deploy branches.
72
+ */
73
+ version?: PackageVersionConfig;
74
+ /** Files that the deploys of the packages ship. */
75
+ files?: PackageFilesConfig;
76
+ /** Where `deploy-git` and `deploy-svn --release-ref` deploy the packages. */
77
+ deploy?: PackageDeployConfig;
78
+ }
79
+ /** Files that the deploys of a package ship. */
80
+ export interface PackageFilesConfig {
81
+ /** Ignore file relative to the package directory, or false to not read one; defaults to .distignore when present. */
82
+ distignore?: string | false;
83
+ /** Extra gitignore-style patterns relative to the package directory. */
84
+ exclude?: string[];
85
+ }
86
+ /** A package and the release that a deploy target is resolved for. */
87
+ export interface DeployTargetInput {
88
+ /** Deployed package. */
89
+ package: DeployPackage;
90
+ /** Released version, for a release tag. */
91
+ version?: string;
92
+ /** Deployed branch, for a branch. */
93
+ branch?: string;
94
+ /** Tag or branch that the deploy writes in the target: the tag of a release, or the branch. */
95
+ ref: string;
96
+ /** Source commit of the deploy. */
97
+ revision: string;
98
+ }
99
+ /** A configured package that a deploy target receives. */
100
+ export interface DeployPackage {
101
+ /** Package identifier. */
102
+ name: string;
103
+ /** Absolute package directory. */
104
+ directory: string;
105
+ /** Package path relative to the configured root. */
106
+ path: string;
107
+ type: ChangesetPackage['type'];
108
+ }
109
+ /**
110
+ * A deploy setting: a template with {name}, {slug}, {path}, {type},
111
+ * {version}, {branch}, {ref} and {revision}, or a callback that receives the
112
+ * package and its release. An empty repository or slug skips the target, and
113
+ * an empty message uses the default one.
114
+ */
115
+ export type DeployTemplate = string | ((input: DeployTargetInput) => string | undefined | Promise<string | undefined>);
116
+ /** A Git repository that `deploy-git` releases a package to. */
117
+ export interface GitDeployTarget {
118
+ /** Repository URL, like `https://github.com/me/{slug}.git`. */
119
+ repository: DeployTemplate;
120
+ /** Branches released to the same branch of the repository: all with true (the default), none with false, or the listed ones. */
121
+ branches?: boolean | string[];
122
+ /** Tag of a release in the repository, with {version} and the package placeholders, defaults to `{version}`; false to not release tags. */
123
+ tag?: string | false;
124
+ /** Whether the package history from git subtree split is the source history of the releases; defaults to true. */
125
+ history?: boolean;
126
+ /** Release commit message, like `Release {path} {ref} from {revision}`; defaults to `Release {ref} from {revision}`. */
127
+ message?: DeployTemplate;
128
+ }
129
+ /** A WordPress.org plugin that `deploy-svn` releases a package to. */
130
+ export interface SvnDeployTarget {
131
+ /** Plugin slug, defaults to `{slug}`. */
132
+ slug?: DeployTemplate;
133
+ /** SVN repository URL with {slug}, defaults to WordPress.org. */
134
+ url?: string;
135
+ /** Directory with banners, icons and screenshots relative to the package, synced to assets/ with stable releases. */
136
+ assetsDir?: string;
137
+ /** Readme file relative to the package, defaults to readme.txt. */
138
+ readme?: string;
139
+ /** SVN commit message, like `Release {version}`. */
140
+ message?: DeployTemplate;
141
+ }
142
+ /** Deploy targets of a package group, each one target or a list of them. */
143
+ export interface PackageDeployConfig {
144
+ git?: GitDeployTarget | GitDeployTarget[];
145
+ svn?: SvnDeployTarget | SvnDeployTarget[];
71
146
  }
72
147
  /** Inputs supplied to a dependents changelog formatter. */
73
148
  export interface DependentsChangelogInput {
@@ -166,6 +241,8 @@ export interface ChangesetConfig {
166
241
  changelogEntry?: (input: ChangelogEntryInput) => string | Promise<string>;
167
242
  /** Groups of packages re-released whenever one of their source packages releases. */
168
243
  dependents?: DependentsConfig;
244
+ /** Files that the deploys of every package group ship; the `files` of a group add their patterns and can replace the ignore file. */
245
+ files?: PackageFilesConfig;
169
246
  }
170
247
  /** Inputs supplied to a custom next-version strategy. */
171
248
  export interface NextVersionInput {
@@ -293,3 +370,10 @@ export declare function loadChangesetConfig(input?: LoadChangesetConfigInput): P
293
370
  config: ChangesetConfig;
294
371
  file: string;
295
372
  } | undefined>;
373
+ /**
374
+ * Normalize a deploy target setting to a list of targets.
375
+ *
376
+ * @param value One target, a list of them or undefined.
377
+ * @return Targets.
378
+ */
379
+ export declare function toTargetList<Target>(value: Target | Target[] | undefined): Target[];
@@ -250,13 +250,15 @@ function validateChangesetConfig(config, configFile) {
250
250
  'function' !== typeof candidate.releaseNotes) {
251
251
  throw new ValidationError(`wp-release config ${configFile}.releaseNotes must be a function.`);
252
252
  }
253
+ validateFilesConfig(candidate.files, `${configFile}.files`);
253
254
  candidate.packages.forEach((group, index) => {
254
255
  if ('object' !== typeof group ||
255
256
  null === group ||
256
- 'object' !== typeof group.version ||
257
- null === group.version ||
258
- 'object' !== typeof group.version.from ||
259
- !Array.isArray(group.version.to)) {
257
+ (undefined !== group.version &&
258
+ ('object' !== typeof group.version ||
259
+ null === group.version ||
260
+ 'object' !== typeof group.version.from ||
261
+ !Array.isArray(group.version.to)))) {
260
262
  throw new ValidationError(`wp-release config ${configFile} has an invalid packages[${index}] entry.`);
261
263
  }
262
264
  if (undefined !== group.idFrom &&
@@ -275,9 +277,102 @@ function validateChangesetConfig(config, configFile) {
275
277
  group.exclude.some((item) => 'string' !== typeof item))) {
276
278
  throw new ValidationError(`wp-release config ${configFile} packages[${index}].exclude must be an array of strings.`);
277
279
  }
278
- validateVersionDescriptor(group.version.from, `${configFile}.packages[${index}].version.from`, false);
279
- group.version.to.forEach((target, targetIndex) => validateVersionDescriptor(target, `${configFile}.packages[${index}].version.to[${targetIndex}]`, true));
280
+ if (group.version) {
281
+ validateVersionDescriptor(group.version.from, `${configFile}.packages[${index}].version.from`, false);
282
+ group.version.to.forEach((target, targetIndex) => validateVersionDescriptor(target, `${configFile}.packages[${index}].version.to[${targetIndex}]`, true));
283
+ }
284
+ validateFilesConfig(group.files, `${configFile}.packages[${index}].files`);
285
+ validateDeployConfig(group.deploy, `${configFile}.packages[${index}].deploy`);
286
+ });
287
+ }
288
+ function isObject(value) {
289
+ return ('object' === typeof value && null !== value && !Array.isArray(value));
290
+ }
291
+ function validateFilesConfig(files, location) {
292
+ if (undefined !== files && !isObject(files)) {
293
+ throw new ValidationError(`wp-release config ${location} must be an object.`);
294
+ }
295
+ const { distignore, exclude } = (files ?? {});
296
+ if (undefined !== distignore &&
297
+ false !== distignore &&
298
+ ('string' !== typeof distignore || !distignore)) {
299
+ throw new ValidationError(`wp-release config ${location}.distignore must be a file path or false.`);
300
+ }
301
+ if (undefined !== exclude &&
302
+ (!Array.isArray(exclude) ||
303
+ exclude.some((item) => 'string' !== typeof item))) {
304
+ throw new ValidationError(`wp-release config ${location}.exclude must be an array of strings.`);
305
+ }
306
+ }
307
+ function validateDeployConfig(deploy, location) {
308
+ if (undefined !== deploy && !isObject(deploy)) {
309
+ throw new ValidationError(`wp-release config ${location} must be an object.`);
310
+ }
311
+ const targets = (deploy ?? {});
312
+ Object.keys(targets).forEach((key) => {
313
+ if (!['git', 'svn'].includes(key)) {
314
+ throw new ValidationError(`wp-release config ${location}.${key} is not a deploy target; use git or svn.`);
315
+ }
280
316
  });
317
+ toTargetList(targets.git).forEach((target, index) => validateGitTarget(target, `${location}.git[${index}]`));
318
+ toTargetList(targets.svn).forEach((target, index) => validateSvnTarget(target, `${location}.svn[${index}]`));
319
+ }
320
+ /**
321
+ * Normalize a deploy target setting to a list of targets.
322
+ *
323
+ * @param value One target, a list of them or undefined.
324
+ * @return Targets.
325
+ */
326
+ export function toTargetList(value) {
327
+ if (undefined === value) {
328
+ return [];
329
+ }
330
+ return Array.isArray(value) ? value : [value];
331
+ }
332
+ function isTemplate(value) {
333
+ return (('string' === typeof value && '' !== value) ||
334
+ 'function' === typeof value);
335
+ }
336
+ function validateGitTarget(target, location) {
337
+ if (!isObject(target) || !isTemplate(target.repository)) {
338
+ throw new ValidationError(`wp-release config ${location} must be an object with a repository URL template or callback.`);
339
+ }
340
+ if (undefined !== target.branches &&
341
+ 'boolean' !== typeof target.branches &&
342
+ (!Array.isArray(target.branches) ||
343
+ target.branches.some((item) => 'string' !== typeof item))) {
344
+ throw new ValidationError(`wp-release config ${location}.branches must be a boolean or an array of branch names.`);
345
+ }
346
+ if (undefined !== target.tag &&
347
+ false !== target.tag &&
348
+ ('string' !== typeof target.tag ||
349
+ !target.tag.includes('{version}'))) {
350
+ throw new ValidationError(`wp-release config ${location}.tag must be a template with {version}, or false.`);
351
+ }
352
+ validateMessage(target.message, location);
353
+ if (undefined !== target.history && 'boolean' !== typeof target.history) {
354
+ throw new ValidationError(`wp-release config ${location}.history must be a boolean.`);
355
+ }
356
+ }
357
+ function validateMessage(message, location) {
358
+ if (undefined !== message && !isTemplate(message)) {
359
+ throw new ValidationError(`wp-release config ${location}.message must be a template or a callback.`);
360
+ }
361
+ }
362
+ function validateSvnTarget(target, location) {
363
+ if (!isObject(target)) {
364
+ throw new ValidationError(`wp-release config ${location} must be an object.`);
365
+ }
366
+ validateMessage(target.message, location);
367
+ if (undefined !== target.slug && !isTemplate(target.slug)) {
368
+ throw new ValidationError(`wp-release config ${location}.slug must be a template or a callback.`);
369
+ }
370
+ for (const key of ['url', 'assetsDir', 'readme']) {
371
+ if (undefined !== target[key] &&
372
+ ('string' !== typeof target[key] || !target[key])) {
373
+ throw new ValidationError(`wp-release config ${location}.${key} must be a non-empty string.`);
374
+ }
375
+ }
281
376
  }
282
377
  function validateVersionDescriptor(descriptor, location, isTarget) {
283
378
  if ('object' !== typeof descriptor || null === descriptor) {
@@ -2,6 +2,7 @@ import type { ChangesetConfig, DependentsConfig } from './changeset-config.ts';
2
2
  import type { ChangedPackages, ConfiguredPackage } from './changeset-packages.ts';
3
3
  import type { Changeset, ChangesetPackage, ChangesetReleasePlan, PackageRelease } from './changesets.ts';
4
4
  import type { FileJournal } from './file-journal.ts';
5
+ import type { ReleaseRepository } from './release-repository.ts';
5
6
  /**
6
7
  * A declared dependency of a package on another configured package.
7
8
  */
@@ -67,16 +68,26 @@ export declare function assertDependentsSourcesReleased(plan: ChangesetReleasePl
67
68
  * with its sources, which the release pull request already announces when
68
69
  * the changesets of the diff release one of them. Exempting its package
69
70
  * manifest pin change keeps the check from demanding an entry for what
70
- * versioning rewrites anyway; documentation and code changes still need
71
- * their own changesets.
71
+ * versioning rewrites anyway. Any other change, like documentation, code or
72
+ * another manifest field or dependency, still needs its own changeset.
72
73
  *
73
- * @param changed Changed packages and their files.
74
- * @param changesetsInDiff Changesets added or changed by the diff.
75
- * @param packages Discovered packages.
76
- * @param config Repository release configuration.
77
- * @return Names of the packages whose only changes are manifests of released dependents.
74
+ * @param input Changes of the diff and the repository.
75
+ * @param input.changed Changed packages and their files.
76
+ * @param input.changesetsInDiff Changesets added or changed by the diff.
77
+ * @param input.packages Discovered packages.
78
+ * @param input.config Repository release configuration.
79
+ * @param input.repository Repository to read the base manifests from.
80
+ * @param input.since Base ref of the diff, like origin/main.
81
+ * @return Names of the packages whose only changes are pins of released sources.
78
82
  */
79
- export declare function findBundledPinChanges(changed: ChangedPackages, changesetsInDiff: Changeset[], packages: ConfiguredPackage[], config: ChangesetConfig): Promise<Set<string>>;
83
+ export declare function findBundledPinChanges(input: {
84
+ changed: ChangedPackages;
85
+ changesetsInDiff: Changeset[];
86
+ packages: ConfiguredPackage[];
87
+ config: ChangesetConfig;
88
+ repository: ReleaseRepository;
89
+ since: string;
90
+ }): Promise<Set<string>>;
80
91
  /**
81
92
  * Check whether a changeset names a package.
82
93
  *
@@ -1,5 +1,6 @@
1
1
  import { readFile, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { isDeepStrictEqual } from 'node:util';
3
4
  import semverSatisfies from 'semver/functions/satisfies.js';
4
5
  import validRange from 'semver/ranges/valid.js';
5
6
  import { assertKnownPackages, globRegExp } from "./changeset-packages.js";
@@ -208,57 +209,118 @@ export async function assertDependentsSourcesReleased(plan, fullPlan, packages,
208
209
  * with its sources, which the release pull request already announces when
209
210
  * the changesets of the diff release one of them. Exempting its package
210
211
  * manifest pin change keeps the check from demanding an entry for what
211
- * versioning rewrites anyway; documentation and code changes still need
212
- * their own changesets.
212
+ * versioning rewrites anyway. Any other change, like documentation, code or
213
+ * another manifest field or dependency, still needs its own changeset.
213
214
  *
214
- * @param changed Changed packages and their files.
215
- * @param changesetsInDiff Changesets added or changed by the diff.
216
- * @param packages Discovered packages.
217
- * @param config Repository release configuration.
218
- * @return Names of the packages whose only changes are manifests of released dependents.
215
+ * @param input Changes of the diff and the repository.
216
+ * @param input.changed Changed packages and their files.
217
+ * @param input.changesetsInDiff Changesets added or changed by the diff.
218
+ * @param input.packages Discovered packages.
219
+ * @param input.config Repository release configuration.
220
+ * @param input.repository Repository to read the base manifests from.
221
+ * @param input.since Base ref of the diff, like origin/main.
222
+ * @return Names of the packages whose only changes are pins of released sources.
219
223
  */
220
- export async function findBundledPinChanges(changed, changesetsInDiff, packages, config) {
224
+ export async function findBundledPinChanges(input) {
225
+ const { changed, changesetsInDiff, packages, config, repository, since } = input;
221
226
  const candidates = changed.packages.filter((pkg) => undefined !== config.dependents &&
222
227
  !hasChangeset(changesetsInDiff, pkg.name) &&
223
- (changed.filesByPackage[pkg.name] ?? []).every((file) => 'package.json' === path.basename(file)));
228
+ (changed.filesByPackage[pkg.name] ?? []).every((file) => repository.relativePath(manifestFile(pkg)) === file));
224
229
  const bundled = await Promise.all(candidates.map(async (pkg) => {
225
- const sources = await collectDeclarationNames(pkg, packages, config);
226
- return sources.some((source) => hasChangeset(changesetsInDiff, source))
227
- ? pkg.name
228
- : undefined;
230
+ const identifiers = await collectReleasedIdentifiers(pkg, packages, config, changesetsInDiff);
231
+ const pinsOnly = identifiers.length > 0 &&
232
+ (await changesOnlyPins({
233
+ pkg,
234
+ identifiers,
235
+ sections: dependentsSections(config.dependents),
236
+ repository,
237
+ since,
238
+ }));
239
+ return pinsOnly ? pkg.name : undefined;
229
240
  }));
230
241
  return new Set(bundled.filter((name) => undefined !== name));
231
242
  }
232
243
  /**
233
- * Check whether a changeset names a package.
244
+ * Collect the identifiers that a package declares for the configured sources
245
+ * released by the changesets, by the dependency sections of the policy,
246
+ * without resolving the specs against the versions. The changeset check uses
247
+ * this to see through a pin that the versioning will rewrite after the
248
+ * source releases.
234
249
  *
235
- * @param changesets Changesets.
236
- * @param name Package name.
237
- * @return Whether one of the changesets names the package.
250
+ * @param pkg Planned package.
251
+ * @param packages Discovered packages.
252
+ * @param config Repository release configuration.
253
+ * @param changesets Changesets that release the sources.
254
+ * @return Declared identifiers of the released sources, like npm package names.
238
255
  */
239
- export function hasChangeset(changesets, name) {
240
- return changesets.some((changeset) => Object.hasOwn(changeset.packages, name));
256
+ async function collectReleasedIdentifiers(pkg, packages, config, changesets) {
257
+ const identifiers = [];
258
+ for (const source of packages) {
259
+ const specs = pkg.name !== source.name && hasChangeset(changesets, source.name)
260
+ ? await findNpmSpecs(pkg, source, dependentsSections(config.dependents))
261
+ : [];
262
+ identifiers.push(...specs.map(({ name }) => name));
263
+ }
264
+ return identifiers;
241
265
  }
242
266
  /**
243
- * Collect the names of the configured sources that a package declares, by the
244
- * dependency sections of the policy, without resolving the specs against the
245
- * versions. The changeset check uses this to see through a pin that the
246
- * versioning will rewrite after the source releases.
267
+ * Check whether the package manifest differs from the base only in the specs
268
+ * of the given identifiers. A manifest that the diff adds has no base and
269
+ * always counts as a change of its own.
247
270
  *
248
- * @param pkg Planned package.
249
- * @param packages Discovered packages.
250
- * @param config Repository release configuration.
251
- * @return Release package names the package declares.
271
+ * @param input Package, identifiers and the repository.
272
+ * @param input.pkg Changed package.
273
+ * @param input.identifiers Declared identifiers whose specs may differ.
274
+ * @param input.sections Dependency sections of the policy.
275
+ * @param input.repository Repository to read the base manifest from.
276
+ * @param input.since Base ref of the diff, like origin/main.
277
+ * @return Whether only the specs of the identifiers changed.
252
278
  */
253
- async function collectDeclarationNames(pkg, packages, config) {
254
- const names = [];
255
- for (const source of packages) {
256
- if (pkg.name !== source.name &&
257
- (await findNpmSpecs(pkg, source, dependentsSections(config.dependents))).length > 0) {
258
- names.push(source.name);
279
+ async function changesOnlyPins(input) {
280
+ const { pkg, identifiers, sections, repository, since } = input;
281
+ const base = await repository.readFileAtMergeBase(since, manifestFile(pkg));
282
+ const head = await readJsonIfExists(manifestFile(pkg));
283
+ return (undefined !== base &&
284
+ undefined !== head &&
285
+ isDeepStrictEqual(withoutSpecs(JSON.parse(base), sections, identifiers), withoutSpecs(head, sections, identifiers)));
286
+ }
287
+ /**
288
+ * Copy a manifest without the specs of the given identifiers in the
289
+ * dependency sections.
290
+ *
291
+ * @param manifest Parsed package manifest.
292
+ * @param sections Dependency sections.
293
+ * @param identifiers Declared identifiers to drop.
294
+ * @return Manifest copy without those specs.
295
+ */
296
+ function withoutSpecs(manifest, sections, identifiers) {
297
+ const stripped = { ...manifest };
298
+ for (const section of sections) {
299
+ const specs = manifest[section];
300
+ if ('object' === typeof specs && null !== specs) {
301
+ stripped[section] = Object.fromEntries(Object.entries(specs).filter(([name]) => !identifiers.includes(name)));
259
302
  }
260
303
  }
261
- return names;
304
+ return stripped;
305
+ }
306
+ /**
307
+ * The npm manifest of a package.
308
+ *
309
+ * @param pkg Package.
310
+ * @return Absolute manifest path.
311
+ */
312
+ function manifestFile(pkg) {
313
+ return path.join(pkg.directory, 'package.json');
314
+ }
315
+ /**
316
+ * Check whether a changeset names a package.
317
+ *
318
+ * @param changesets Changesets.
319
+ * @param name Package name.
320
+ * @return Whether one of the changesets names the package.
321
+ */
322
+ export function hasChangeset(changesets, name) {
323
+ return changesets.some((changeset) => Object.hasOwn(changeset.packages, name));
262
324
  }
263
325
  /**
264
326
  * Collect the names of the configured sources that a package builds against,