@wpelevator/wp-release 0.12.0 → 0.14.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 (78) hide show
  1. package/README.md +150 -19
  2. package/dist/changeset-config.d.ts +91 -5
  3. package/dist/changeset-config.js +108 -11
  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 +55 -8
  9. package/dist/changeset-packages.js +85 -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 +43 -5
  13. package/dist/changeset-release.js +99 -16
  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 +51 -14
  18. package/dist/commands/args.d.ts +0 -21
  19. package/dist/commands/args.js +1 -35
  20. package/dist/commands/changeset-add.d.ts +5 -2
  21. package/dist/commands/changeset-add.js +109 -9
  22. package/dist/commands/changeset-args.d.ts +24 -5
  23. package/dist/commands/changeset-args.js +57 -14
  24. package/dist/commands/changeset-status.d.ts +3 -1
  25. package/dist/commands/changeset-status.js +2 -1
  26. package/dist/commands/changeset-tag.d.ts +1 -1
  27. package/dist/commands/changeset-tag.js +6 -3
  28. package/dist/commands/changeset-version.d.ts +7 -1
  29. package/dist/commands/changeset-version.js +17 -4
  30. package/dist/commands/checksums.js +2 -2
  31. package/dist/commands/command.d.ts +5 -0
  32. package/dist/commands/deploy-git.d.ts +24 -4
  33. package/dist/commands/deploy-git.js +57 -3
  34. package/dist/commands/deploy-svn.d.ts +24 -4
  35. package/dist/commands/deploy-svn.js +57 -2
  36. package/dist/commands/keygen.d.ts +3 -0
  37. package/dist/commands/keygen.js +6 -1
  38. package/dist/commands/package-deploys.d.ts +86 -0
  39. package/dist/commands/package-deploys.js +113 -0
  40. package/dist/commands/sign.d.ts +19 -1
  41. package/dist/commands/sign.js +47 -12
  42. package/dist/commands/verify.d.ts +3 -0
  43. package/dist/commands/verify.js +43 -24
  44. package/dist/commands/zip.d.ts +12 -8
  45. package/dist/commands/zip.js +135 -78
  46. package/dist/ed25519.d.ts +25 -0
  47. package/dist/ed25519.js +44 -0
  48. package/dist/file-selection.d.ts +21 -0
  49. package/dist/file-selection.js +35 -0
  50. package/dist/git-deploy.d.ts +32 -0
  51. package/dist/git-deploy.js +57 -1
  52. package/dist/git.d.ts +17 -0
  53. package/dist/git.js +23 -0
  54. package/dist/index.d.ts +13 -5
  55. package/dist/index.js +7 -3
  56. package/dist/minisign.d.ts +41 -0
  57. package/dist/minisign.js +128 -0
  58. package/dist/package-deploy.d.ts +133 -0
  59. package/dist/package-deploy.js +323 -0
  60. package/dist/package-zip.d.ts +52 -0
  61. package/dist/package-zip.js +78 -0
  62. package/dist/release-repository.d.ts +18 -5
  63. package/dist/release-repository.js +51 -11
  64. package/dist/release-tags.d.ts +3 -1
  65. package/dist/release-tags.js +5 -2
  66. package/dist/signing.d.ts +64 -9
  67. package/dist/signing.js +113 -40
  68. package/dist/ssh-signature.d.ts +43 -0
  69. package/dist/ssh-signature.js +156 -0
  70. package/dist/summary.d.ts +3 -3
  71. package/dist/summary.js +2 -2
  72. package/dist/tag-format.d.ts +23 -14
  73. package/dist/tag-format.js +26 -40
  74. package/dist/version-files.d.ts +8 -8
  75. package/dist/version-files.js +22 -18
  76. package/dist/versions.d.ts +28 -3
  77. package/dist/versions.js +84 -9
  78. package/package.json +2 -2
package/README.md CHANGED
@@ -28,12 +28,14 @@ npx wp-release --help
28
28
  |---|---|
29
29
  | `wp-release zip [<source-dir>] [options]` | Create a ZIP archive with a `{slug}/` root folder, respecting `.distignore`. |
30
30
  | `wp-release checksums [<source-dir>] [options]` | Write the MD5 and SHA-256 hashes of every distributed file to a checksums manifest. |
31
- | `wp-release sign <file>... [options]` | Sign files with Ed25519, writing a detached `<file>.sig` next to each. |
32
- | `wp-release keygen` | Generate a base64 Ed25519 private seed and public key for signing and verification. |
31
+ | `wp-release sign <file>... [options]` | Sign files with Ed25519, writing a detached `<file>.sig` next to each, or minisign and SSH signatures with `--format`. |
32
+ | `wp-release keygen` | Generate a base64 Ed25519 private seed and its public key, also as minisign and SSH public keys. |
33
33
  | `wp-release verify [<source-dir>] --checksums <file> [options]` | Check a plugin or theme directory, or a ZIP, against a checksums manifest. |
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. |
@@ -65,19 +67,20 @@ Each command also adds a section to the [job summary](https://docs.github.com/en
65
67
  | `deploy-git` | Repository, release commit and the pushed branch and tag. |
66
68
  | `zip` | ZIP, number of files and checksums manifest. |
67
69
  | `checksums` | Manifest and number of files. |
68
- | `sign` | Signature files and the public key. |
70
+ | `sign` | Signature files and the public key of each written format. |
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
 
74
77
  A failed command adds its error to the summary, or a table of its problems with their files and lines, listing up to 100 and counting the rest. The Markdown of the summaries isn't part of the library API. File paths are relative to `GITHUB_WORKSPACE`, or the current directory without it. Files outside it, like those of an extracted ZIP, get an annotation without a file. Multi-line messages stay one annotation.
75
78
 
76
- Use global `--config <path>` to select a config; relative paths resolve from the current directory. Otherwise, Changesets commands discover `wp-release.config.js`, `.mjs` or `.cjs` there.
79
+ Use global `--config <path>` to select a config; relative paths resolve from the current directory. Otherwise, Changesets commands discover `wp-release.config.js`, `.mjs` or `.cjs` there. Global `--tag-format <format>` replaces the `tagFormat` of the config for one command, see [Packages and tags](#packages-and-tags).
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
 
@@ -122,13 +125,36 @@ export default defineConfig( {
122
125
  } );
123
126
  ```
124
127
 
125
- `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.
128
+ `version.from` selects the current version; `version.to` lists release targets. 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
+ #### Packages and tags
128
131
 
129
- 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.
132
+ The config describes the repository, so wp-release assumes no directory structure or naming convention of its own:
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
+ - **Packages** are the directories that the `glob` of a `packages` entry matches under `root`, with `*`, `?` and `**`, like `plugins/*`, or `.` for a repository with one package at its root. The settings of an entry apply to each of its packages. Discovery never looks inside `.git`, `node_modules`, `vendor` or `dist` directories. A package is identified by its path, or with `idFrom` by the name in its `package.json` or `composer.json`, and commands that take a package accept either.
135
+ - **Templates** name the tags and the deploy targets of a package with these placeholders:
136
+
137
+ | Placeholder | Value | `plugins/alpha` | Root package |
138
+ |---|---|---|---|
139
+ | `{path}` | Package directory relative to `root`, empty for a package at the root | `plugins/alpha` | (empty) |
140
+ | `{name}` | Package identifier | `plugins/alpha`, or its manifest name with `idFrom` | the manifest name, or the directory name |
141
+ | `{version}` | Released version | `1.2.0` | `1.2.0` |
142
+
143
+ Deploy templates also have `{branch}`, `{ref}` and `{revision}` (see [Deploy configured packages](#deploy-configured-packages)), and any template can be a callback that receives the package, for names that the placeholders don't cover, like the last directory of the path.
144
+ - **`tagFormat`** names the release tags, `{path}/{version}` by default, like `plugins/alpha/1.2.0`. Empty placeholders leave no leading or repeated slashes in a tag, so a package at the root gets plain version tags, like `1.2.0`, from the same default. Deploy templates, like repository URLs, are filled in as they are. A `gitTagVersion()` version source reads the tags with its own `pattern`, also `{path}/{version}` by default, so set both when you change the format.
145
+ - **`--tag-format <format>`** replaces the `tagFormat` of the config for one command, for a workflow or job that names its tags differently, like `--tag-format '{name}@{version}'`. Every command that creates or reads the same tags needs the same format: `changeset-tag`, which creates them and finds the previous tag for the release notes, `changeset-version --prerelease`, which numbers pre-releases from them, and `deploy-git` and `deploy-svn --release-ref`, which read the package and version of a tag.
146
+ - **Slugs** are a WordPress concept: the folder of a plugin or theme. Only the commands that produce something for WordPress use one: the `{slug}/` root folder and name of a `zip`, the `checksums` manifest, the WordPress.org slug of `deploy-svn`, and `version-check`. Each takes `--slug`, which defaults to the name of the current directory, or of the package directory with `zip --package`, and an SVN deploy target takes `slug`, which defaults to the name of the package directory. Tags and Git deploy targets don't have a slug.
147
+ - **`remote`** is the git remote that `changeset-tag --push` pushes tags to and that the base branch is compared on, `origin` by default.
148
+
149
+ 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.
150
+
151
+ Without `--since`, `changeset-status` compares against `<remote>/<baseBranch>`, where `remote` in the config defaults to `origin` and is also the remote that `changeset-tag --push` pushes tags to; 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.
152
+
153
+ 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 `<remote>/<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.
154
+
155
+ 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.
156
+
157
+ 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
158
 
133
159
  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
160
 
@@ -152,6 +178,8 @@ dependents: {
152
178
 
153
179
  Excluded packages still release with their own changesets and stay out of the automatic re-releases.
154
180
 
181
+ 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.
182
+
155
183
  The bundle changelog entry defaults to `Bundle <source> <version>.` and is formatted with `dependents.changelog( { package, source } )`:
156
184
 
157
185
  ```js
@@ -203,10 +231,37 @@ To release a single package, for example from a manual workflow, pass `--package
203
231
  prerelease: ${{ fromJSON( steps.tag.outputs.result ).tags[0].prerelease }}
204
232
  ```
205
233
 
206
- 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.
234
+ 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.
207
235
 
208
236
  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.
209
237
 
238
+ #### Pre-releases
239
+
240
+ `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`.
241
+
242
+ 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.
243
+
244
+ 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:
245
+
246
+ ```sh
247
+ git switch --force-create prerelease/plugins/alpha
248
+ npx wp-release changeset-version --package plugins/alpha --prerelease rc
249
+ git add --all
250
+ git commit --message "Prerelease plugins/alpha"
251
+ npx wp-release changeset-tag --package plugins/alpha --version 1.3.0-rc.1 --push
252
+ ```
253
+
254
+ 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.
255
+
256
+ #### Snapshots
257
+
258
+ `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:
259
+
260
+ ```sh
261
+ npx wp-release changeset-version --package @vendor/library --snapshot pr-251
262
+ npm publish --workspace packages/library --tag pr-251
263
+ ```
264
+
210
265
  ### Naming convention
211
266
 
212
267
  `@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:
@@ -274,6 +329,7 @@ wp-release zip dist --slug my-plugin --checksums
274
329
  - Entries are sorted and use a fixed timestamp and file mode, so the same files always give the same ZIP.
275
330
  - `--checksums` also writes the [checksums manifest](#checksums) of the archived files next to the ZIP, with `.checksums.json` instead of `.zip`.
276
331
  - An existing ZIP or manifest isn't overwritten without `--force`. A ZIP or manifest written inside the source directory is never archived.
332
+ - `--package <name>` zips a package of the [configured packages](#deploy-configured-packages), by identifier or path under the configured root like `plugins/alpha`, instead of a source directory: its directory with the `files` config of its package group, the same files that its deploys select. The slug defaults to the directory name and the version to the `Version` header, and `--slug` and `--version` still set them. Any package of the config works, with or without a version or deploy targets. To zip what a deploy just released, pass the `package` and `version` of the `deploy-git --release-ref` result, in the same job and from the same build, so the ZIP has exactly the files of the release tag in the release repository. It can't be combined with `<source-dir>` or the file selection options. The `--json` result has the `package`, `slug` and `version`. `zipPackage()` builds the same ZIP from a script, with `getPackageFiles()` for the directory and the files of a package.
277
333
 
278
334
  ### Checksums
279
335
 
@@ -316,9 +372,9 @@ wp-release verify wp-content/plugins/my-plugin --checksums my-plugin.1.2.0.check
316
372
  - Manifest paths outside the package folder (absolute or with `..`) are rejected.
317
373
  - `--zip <file|url>` checks the files of a ZIP instead of a directory. The ZIP can be a URL, such as the one that WordPress.org builds for a release, and its single root folder is found automatically (or set `--root-folder`). The ZIP is extracted to a temporary directory after the same safety checks as `deploy-svn --from-zip`.
318
374
  - The version in the manifest must match the `Version` header of the checked files, so that an older release with its own valid manifest can't pass as a newer one.
319
- - A manifest can't list itself. A copy of the manifest and of its `.sig` inside the checked files, at the root with the same names as the `--checksums` file or at the path of `--embedded`, is skipped when comparing files, but it must be identical to the `--checksums` file and its `.sig`. A path that the manifest lists is never skipped, and a file that isn't a matching copy is reported as not in the checksums. See [Manifest inside the plugin](#manifest-inside-the-plugin).
375
+ - A manifest can't list itself. A copy of the manifest and of its signature inside the checked files, at the root with the same names as the `--checksums` file or at the path of `--embedded`, is skipped when comparing files, but it must be identical to the `--checksums` file and its signature. The signature is the `.sig` file, or the file of `--signature-format`. A path that the manifest lists is never skipped, and a file that isn't a matching copy is reported as not in the checksums. See [Manifest inside the plugin](#manifest-inside-the-plugin).
320
376
 
321
- A checksums manifest proves only that the files match the manifest. With `--public-key <key>`, `verify` first checks `<checksums>.sig` against the base64 Ed25519 public key and fails with exit code `2` before comparing any file when the signature is missing or invalid, so that the manifest itself can be trusted. See [Sign](#sign).
377
+ A checksums manifest proves only that the files match the manifest. With `--public-key <key>`, `verify` first checks `<checksums>.sig` against the Ed25519 public key and fails with exit code `2` before comparing any file when the signature is missing or invalid, so that the manifest itself can be trusted. `--signature-format minisign` or `ssh` checks `<checksums>.minisig` or `<checksums>.sshsig` instead. The key can be the base64 key, the minisign public key or the `ssh-ed25519` public key, since they're forms of the same key. See [Sign](#sign).
322
378
 
323
379
  ### Manifest inside the plugin
324
380
 
@@ -331,7 +387,7 @@ wp-release zip build --slug my-plugin
331
387
  wp-release deploy-svn build --trunk --svn-tag 1.2.0
332
388
  ```
333
389
 
334
- - Write the manifest into the directory that is zipped and deployed. It lists every file except itself, and `checksums`, `zip` and `verify` also skip its `<name>.sig`, so running `checksums` again on a build that already has the pair gives the same manifest. Sign it last, since the signature covers the exact bytes of the manifest.
390
+ - Write the manifest into the directory that is zipped and deployed. It lists every file except itself, and `checksums`, `zip` and `verify` also skip its signatures (`<name>.sig`, `<name>.minisig` and `<name>.sshsig`), so running `checksums` again on a build that already has the pair gives the same manifest. Sign it last, since the signature covers the exact bytes of the manifest.
335
391
  - `zip` and `deploy-svn` carry the pair like any other file. Don't use `zip --checksums` here, since it would write a second manifest that lists the first one. Attach `build/checksums.json` and `build/checksums.json.sig` to the GitHub release as they are.
336
392
  - The signature covers file hashes and not a ZIP, so it stays valid for the ZIP that WordPress.org builds from SVN. Check what WordPress.org serves after a deploy, with the manifest and public key from your build:
337
393
 
@@ -340,7 +396,7 @@ wp-release deploy-svn build --trunk --svn-tag 1.2.0
340
396
  --checksums build/checksums.json --public-key "$PUBLIC_KEY"
341
397
  ```
342
398
 
343
- - The manifest can have any name and location, for example `--output build/includes/signatures/checksums.json`, and missing folders are created. Tell `verify` where the shipped copy is with `--embedded <path>`, like `--embedded includes/signatures/checksums.json`. Both the `--checksums` file and its `.sig` must then exist, both copies must be shipped, and they're skipped when comparing files and must be identical to the originals. The manifest can't list the embedded path. Without `--embedded`, `verify` looks for the manifest name at the root, and skips a file only when it exists, the matching original exists and the manifest doesn't list it. Otherwise it's checked like any other file, so a listed file that has the name of the manifest can't be deleted unnoticed, and a shipped `.sig` without an original is reported as not in the checksums.
399
+ - The manifest can have any name and location, for example `--output build/includes/signatures/checksums.json`, and missing folders are created. Tell `verify` where the shipped copy is with `--embedded <path>`, like `--embedded includes/signatures/checksums.json`. Both the `--checksums` file and its signature must then exist, both copies must be shipped, and they're skipped when comparing files and must be identical to the originals. The manifest can't list the embedded path. Without `--embedded`, `verify` looks for the manifest name at the root, and skips a file only when it exists, the matching original exists and the manifest doesn't list it. Otherwise it's checked like any other file, so a listed file that has the name of the manifest can't be deleted unnoticed, and a shipped `.sig` without an original is reported as not in the checksums.
344
400
 
345
401
  The public key has to come from somewhere other than the plugin that it checks, because someone who can overwrite the plugin files can also replace a key and a signature inside them. A copy of the manifest also doesn't prevent a downgrade to an older release that was signed too, so the installing side has to enforce a minimum version.
346
402
 
@@ -356,10 +412,28 @@ WP_RELEASE_SIGNING_KEY=… wp-release sign my-plugin.1.2.0.zip my-plugin.1.2.0.c
356
412
 
357
413
  - The signature is the one WordPress core's `verify_file_signature()` checks: a base64 Ed25519 signature of the raw SHA-384 digest of the file. The tests check it against `sodium_crypto_sign_verify_detached()` in PHP. A `.sig` file can hold several signatures, one per line, and a file is valid when any of them match, which allows a key rotation window.
358
414
  - The key is read from the `WP_RELEASE_SIGNING_KEY` environment variable only, as a base64 32-byte Ed25519 seed or a 64-byte libsodium secret key, the formats that `sodium_crypto_sign_seed_keypair()` and the update-pilot-server package signing key take. There's no flag for it, since it would show in the process list, and it's never logged.
359
- - `--verbose` prints the public key of the signing key, which `--json` also returns as `publicKey`. Pass it to `verify --public-key` and to the plugin that checks the signatures.
360
- - An existing `.sig` file isn't overwritten without `--force`.
415
+ - `--verbose` prints the public key of the signing key in the form of each written format. `--json` returns it as `publicKey`, and in every form as `publicKeys`. Pass it to `verify --public-key` and to the plugin that checks the signatures.
416
+ - An existing signature file isn't overwritten without `--force`.
417
+
418
+ #### Signature formats
361
419
 
362
- Generate a key pair with `wp-release keygen`. It prints the private seed once as `WP_RELEASE_SIGNING_KEY=…` and the matching public key. Store the private seed securely; never commit it or put it in a workflow log. Don't run `keygen` in CI or with `--github-output`, since those outputs can expose the private seed. The public key can be distributed to verifiers.
420
+ The same key can also sign in formats that tools outside WordPress check, for people who download a release by hand or verify it in other pipelines. Pick them with `--format`, comma-separated or repeated; the default is `wordpress`:
421
+
422
+ ```bash
423
+ wp-release sign my-plugin.1.2.0.zip --format wordpress,minisign,ssh
424
+ ```
425
+
426
+ | Format | File | Checked by |
427
+ |---|---|---|
428
+ | `wordpress` | `<file>.sig` | WordPress core's `verify_file_signature()`, update-client and `wp-release verify` |
429
+ | `minisign` | `<file>.minisig` | [minisign](https://jedisct1.github.io/minisign/) and compatible tools: `minisign -Vm <file> -P <minisign public key>` |
430
+ | `ssh` | `<file>.sshsig` | OpenSSH 8.1 or later: `ssh-keygen -Y verify -f allowed_signers -I <identity> -n file -s <file>.sshsig < <file>` |
431
+
432
+ - `minisign` signs the BLAKE2b-512 digest of the file, like `minisign -S` does by default, and adds a trusted comment with the signing time and file name that a second signature covers. minisign generates a random key ID for its keys, but the key here is a plain Ed25519 seed, so wp-release derives the key ID from the public key. `wp-release verify` doesn't compare key IDs, so signatures of keys that minisign generated verify too.
433
+ - `ssh` is the armored `SSHSIG` format that `ssh-keygen -Y sign` writes, with a SHA-512 digest and the `file` namespace, so that the signature can't pass as one for commits or another namespace. It's written to `.sshsig` instead of the `.sig` of `ssh-keygen`, which the `wordpress` format uses. Put the SSH public key in an `allowed_signers` file as `<identity> ssh-ed25519 AAAA…`.
434
+ - All formats are Ed25519 signatures by the same key, so one key and one secret cover all of them. The tests check each format with `minisign`, `ssh-keygen` and libsodium, and check the signatures that `minisign` and `ssh-keygen` write with `wp-release verify`.
435
+
436
+ Generate a key pair with `wp-release keygen`. It prints the private seed once as `WP_RELEASE_SIGNING_KEY=…` and the matching public key, also as a minisign public key and an `ssh-ed25519` public key. Store the private seed securely; never commit it or put it in a workflow log. Don't run `keygen` in CI or with `--github-output`, since those outputs can expose the private seed. The public key can be distributed to verifiers.
363
437
 
364
438
  ### Version check
365
439
 
@@ -456,7 +530,55 @@ How a deploy works:
456
530
 
457
531
  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.
458
532
 
459
- `--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`).
533
+ `--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`).
534
+
535
+ ### Deploy configured packages
536
+
537
+ `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.
538
+
539
+ ```js
540
+ packages: [
541
+ {
542
+ glob: 'plugins/*',
543
+ version: { from: wordpressPluginVersion(), to: [ wordpressPluginVersion(), wordpressStableTag() ] },
544
+ // Files that the deploys ship, instead of each package's .distignore.
545
+ files: { distignore: false, exclude: [ '.*', 'node_modules/', '/tests/' ] },
546
+ deploy: {
547
+ git: [
548
+ // The release repository of each plugin, with its history.
549
+ { repository: ( { package: pkg } ) => `https://github.com/me/${ pkg.path.split( '/' ).pop() }.git`, message: 'Release {path} {ref} from {revision}' },
550
+ // A mirror that only gets release tags, named v1.2.0.
551
+ { repository: ( { package: pkg } ) => `https://gitlab.com/me/${ pkg.path }.git`, branches: false, tag: 'v{version}' },
552
+ ],
553
+ svn: { slug: ( { package: pkg } ) => `me-${ pkg.path.split( '/' ).pop() }`, assetsDir: '.wordpress-org' },
554
+ },
555
+ },
556
+ // A package that isn't versioned, like a Docker image, only deploys branches.
557
+ { glob: 'docker/php-fpm', deploy: { git: { repository: 'https://github.com/me/php-fpm.git' } } },
558
+ ],
559
+ ```
560
+
561
+ | Mode | Command | Use |
562
+ |---|---|---|
563
+ | 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. |
564
+ | 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. |
565
+ | 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 }}`. |
566
+ | Single directory | `deploy-git <dir> --git-url <url> [options]` | Deploy one directory without a config, as described above. |
567
+
568
+ `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.
569
+
570
+ - **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.
571
+ - **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`, the WordPress.org plugin slug, defaults to the name of the package directory, `url` to the WordPress.org plugin repository, and `message` sets the commit message. Credentials come from `SVN_USERNAME` and `SVN_PASSWORD`, or `--svn-username`.
572
+ - **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.
573
+ - **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.
574
+ - **Templates and callbacks**: `repository`, `slug` and `message` are templates with `{name}` and `{path}`, 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 name a target after the package directory, 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.
575
+ - **Results**: with `--json` or `--github-output`, the result lists each deploy in `deploys` with its `package`, `label`, `target` (the repository or WordPress.org plugin, with credentials redacted), `targetRef` (the tag or branch written there), `version` or `branch`, and its `result` or `error`. A remote target repository also has its `targetHost`, like `github.com`, and `targetPath`, like `me/my-plugin`, without credentials, for a workflow step that calls the API of the forge that hosts it, such as to create the release of the deployed tag. The targets of `--list` have the same `host` and `path`, and `parseRepositoryUrl()` reads them from an HTTP(S), SSH or scp-like URL. `wp-release` doesn't call forge APIs itself.
576
+ - **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.
577
+ - **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. `zip --package <name>` zips a package with the same files, `getPackageFiles()` returns the directory and the file selection of any configured package, and `getDeployPackages()` lists the packages with deploy targets and their file selection.
578
+
579
+ 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.
580
+
581
+ 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.
460
582
 
461
583
  ### Exit codes
462
584
 
@@ -627,6 +749,13 @@ The `wpelevator-source` export condition points at `src/index.ts`. The tests and
627
749
 
628
750
  ### Architecture
629
751
 
752
+ wp-release has two layers, so that it fits any repository layout:
753
+
754
+ - **Primitives** work on the directory, file or values they're given and know nothing about the repository: `zip <dir>`, `checksums`, `sign`, `verify`, `version-check`, `deploy-git <dir>` and `deploy-svn <dir>`.
755
+ - **The config layer** reads `wp-release.config.js` for everything about the repository: where packages are (`root` and the package globs), how their versions and tags are named (`version`, `tagFormat`), which files they ship (`files`), where they deploy (`deploy`) and the git `remote`. The changeset commands, `deploy-git` and `deploy-svn --release-ref`, and `zip --package` belong to it, and the global `--config` and `--tag-format` options select or adjust the config. Only `--release-ref` reads which package and version a ref releases. A primitive with a config mode, like `deploy-git --release-ref` beside `deploy-git <dir>` or `zip --package` beside `zip <dir>`, only takes the package directory, its files and its targets from the config there, and otherwise works the same.
756
+
757
+ Nothing assumes a directory structure or naming convention that the config doesn't set.
758
+
630
759
  The package uses ES modules as its unit of encapsulation, like most Node.js CLI tools: a module exports a few functions and keeps its helpers private, so there's no class to wrap them in. A class is used only when it holds state across steps or binds dependencies, so that callers and tests can replace them:
631
760
 
632
761
  - **Workflows with steps** are classes, so that their steps share state and dependencies on the instance: `SvnDeploy` shares the temporary working copy across `prepare()`, `checkout()`, `sync()` and `commit()`, `GitDeploy` binds the options and the git runner to its steps, and `ChangesetRelease` binds the repository and the runners to `add()`, `version()` and `tag()`.
@@ -641,7 +770,7 @@ The changeset release modules, from the entry point down:
641
770
  |---|---|
642
771
  | `changeset-release.ts` | `ChangesetRelease`, which plans releases and runs `add()`, `getStatus()`, `version()` and `tag()`, and the function form of each operation. |
643
772
  | `changeset-config.ts` | Loading and validating `wp-release.config.js`, and the version source and target helpers. |
644
- | `changeset-packages.ts` | Discovering the packages of the configured globs and finding the changed ones. |
773
+ | `changeset-packages.ts` | Discovering the packages of the configured globs, with or without a version, and finding the changed ones. |
645
774
  | `changeset-files.ts` | Reading, writing and consuming changeset files. |
646
775
  | `changeset-dependents.ts` | The dependents policy: which packages re-release with their sources, and rewriting their dependency specs. |
647
776
  | `changesets.ts` | Parsing and formatting changesets, and the release types. |
@@ -649,6 +778,8 @@ The changeset release modules, from the entry point down:
649
778
  | `changelog.ts` | Changelog entries, and the `CHANGELOG.md` and readme changelog sections. |
650
779
  | `release-tags.ts` | Choosing the version and previous tag of a release, and building its release notes. |
651
780
  | `tag-format.ts` | Formatting tag names from the tag format. |
781
+ | `package-deploy.ts` | Resolving the configured deploys of a ref: the released packages, their targets and file selections, for `deploy-git` and `deploy-svn`, and the file selection of a package for `zip --package`. |
782
+ | `package-zip.ts` | `zipPackage()`, the ZIP and checksums manifest of the selected files of a package, for `zip` and the snapshot builds of a monorepo. |
652
783
  | `release-repository.ts` | `ReleaseRepository`, the git commands of a release: tags, commits and changed files. |
653
784
  | `file-journal.ts` | `FileJournal`, which restores every file a failed `changeset-version` run changed. |
654
785
  | `versions.ts` | Version checks and SemVer precedence. |
@@ -62,12 +62,85 @@ export interface ChangesetPackageGroup {
62
62
  glob: string;
63
63
  /** Package paths excluded from this group, relative to root. */
64
64
  exclude?: string[];
65
- /** Optional type used by the {type} tag template. */
66
- tagType?: string;
67
65
  /** Manifest field used as the changeset name; defaults to `path`. */
68
66
  idFrom?: PackageIdSource;
69
- /** Version source and files to update for every discovered package. */
70
- version: PackageVersionConfig;
67
+ /**
68
+ * Version source and files to update for every discovered package. Without
69
+ * it, the packages aren't released with changesets and only deploy branches.
70
+ */
71
+ version?: PackageVersionConfig;
72
+ /** Files that the deploys of the packages ship. */
73
+ files?: PackageFilesConfig;
74
+ /** Where `deploy-git` and `deploy-svn --release-ref` deploy the packages. */
75
+ deploy?: PackageDeployConfig;
76
+ }
77
+ /** Files that the deploys of a package ship. */
78
+ export interface PackageFilesConfig {
79
+ /** Ignore file relative to the package directory, or false to not read one; defaults to .distignore when present. */
80
+ distignore?: string | false;
81
+ /** Extra gitignore-style patterns relative to the package directory. */
82
+ exclude?: string[];
83
+ }
84
+ /** A package and the release that a deploy target is resolved for. */
85
+ export interface DeployTargetInput {
86
+ /** Deployed package. */
87
+ package: DeployPackage;
88
+ /** Released version, for a release tag. */
89
+ version?: string;
90
+ /** Deployed branch, for a branch. */
91
+ branch?: string;
92
+ /** Tag or branch that the deploy writes in the target: the tag of a release, or the branch. */
93
+ ref: string;
94
+ /** Source commit of the deploy. */
95
+ revision: string;
96
+ }
97
+ /** A configured package that a deploy target receives. */
98
+ export interface DeployPackage {
99
+ /** Package identifier. */
100
+ name: string;
101
+ /** Absolute package directory. */
102
+ directory: string;
103
+ /** Package path relative to the configured root. */
104
+ path: string;
105
+ type: ChangesetPackage['type'];
106
+ }
107
+ /**
108
+ * A deploy setting: a template with {name}, {path},
109
+ * {version}, {branch}, {ref} and {revision}, or a callback that receives the
110
+ * package and its release. An empty repository or slug skips the target, and
111
+ * an empty message uses the default one.
112
+ */
113
+ export type DeployTemplate = string | ((input: DeployTargetInput) => string | undefined | Promise<string | undefined>);
114
+ /** A Git repository that `deploy-git` releases a package to. */
115
+ export interface GitDeployTarget {
116
+ /** Repository URL, like `https://github.com/me/{path}.git`. */
117
+ repository: DeployTemplate;
118
+ /** Branches released to the same branch of the repository: all with true (the default), none with false, or the listed ones. */
119
+ branches?: boolean | string[];
120
+ /** Tag of a release in the repository, with {version} and the package placeholders, defaults to `{version}`; false to not release tags. */
121
+ tag?: string | false;
122
+ /** Whether the package history from git subtree split is the source history of the releases; defaults to true. */
123
+ history?: boolean;
124
+ /** Release commit message, like `Release {path} {ref} from {revision}`; defaults to `Release {ref} from {revision}`. */
125
+ message?: DeployTemplate;
126
+ }
127
+ /** A WordPress.org plugin that `deploy-svn` releases a package to. */
128
+ export interface SvnDeployTarget {
129
+ /** WordPress.org plugin slug, defaults to the name of the package directory. */
130
+ slug?: DeployTemplate;
131
+ /** SVN repository URL with {slug}, defaults to WordPress.org. */
132
+ url?: string;
133
+ /** Directory with banners, icons and screenshots relative to the package, synced to assets/ with stable releases. */
134
+ assetsDir?: string;
135
+ /** Readme file relative to the package, defaults to readme.txt. */
136
+ readme?: string;
137
+ /** SVN commit message, like `Release {version}`. */
138
+ message?: DeployTemplate;
139
+ }
140
+ /** Deploy targets of a package group, each one target or a list of them. */
141
+ export interface PackageDeployConfig {
142
+ git?: GitDeployTarget | GitDeployTarget[];
143
+ svn?: SvnDeployTarget | SvnDeployTarget[];
71
144
  }
72
145
  /** Inputs supplied to a dependents changelog formatter. */
73
146
  export interface DependentsChangelogInput {
@@ -146,11 +219,13 @@ export interface DependentsConfig {
146
219
  export interface ChangesetConfig {
147
220
  /** Branch receiving ordinary changes. */
148
221
  baseBranch?: string;
222
+ /** Git remote that tags are pushed to and the base branch is compared on, origin by default. */
223
+ remote?: string;
149
224
  /** Directory containing Markdown changesets, relative to the repository root. */
150
225
  changesets?: string;
151
226
  /** Package groups to discover and release. */
152
227
  packages: ChangesetPackageGroup[];
153
- /** Git tag template supporting {name}, {slug}, {path}, {type} and {version}. */
228
+ /** Git tag template supporting {name}, {path} and {version}. */
154
229
  tagFormat?: string;
155
230
  /** Directory against which package paths in tag formats are calculated. */
156
231
  root?: string;
@@ -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 {
@@ -281,6 +358,8 @@ export declare function defineConfig(config: ChangesetConfig): ChangesetConfig;
281
358
  export interface LoadChangesetConfigInput {
282
359
  cwd?: string;
283
360
  configFile?: string;
361
+ /** Tag format that replaces the tagFormat of the config, like from --tag-format. */
362
+ tagFormat?: string;
284
363
  }
285
364
  /**
286
365
  * Load and validate the repository's JavaScript wp-release configuration.
@@ -293,3 +372,10 @@ export declare function loadChangesetConfig(input?: LoadChangesetConfigInput): P
293
372
  config: ChangesetConfig;
294
373
  file: string;
295
374
  } | undefined>;
375
+ /**
376
+ * Normalize a deploy target setting to a list of targets.
377
+ *
378
+ * @param value One target, a list of them or undefined.
379
+ * @return Targets.
380
+ */
381
+ export declare function toTargetList<Target>(value: Target | Target[] | undefined): Target[];
@@ -138,7 +138,12 @@ export async function loadChangesetConfig(input = {}) {
138
138
  ? loaded.default
139
139
  : undefined;
140
140
  validateChangesetConfig(config, configFile);
141
- return { config, file: configFile };
141
+ return {
142
+ config: input.tagFormat
143
+ ? { ...config, tagFormat: input.tagFormat }
144
+ : config,
145
+ file: configFile,
146
+ };
142
147
  }
143
148
  async function findConfigFile(cwd) {
144
149
  for (const fileName of CONFIG_FILES) {
@@ -171,6 +176,7 @@ function validateChangesetConfig(config, configFile) {
171
176
  }
172
177
  for (const [key, value] of Object.entries({
173
178
  baseBranch: candidate.baseBranch,
179
+ remote: candidate.remote,
174
180
  changesets: candidate.changesets,
175
181
  changelogInsertAfter: candidate.changelogInsertAfter,
176
182
  root: candidate.root,
@@ -250,13 +256,15 @@ function validateChangesetConfig(config, configFile) {
250
256
  'function' !== typeof candidate.releaseNotes) {
251
257
  throw new ValidationError(`wp-release config ${configFile}.releaseNotes must be a function.`);
252
258
  }
259
+ validateFilesConfig(candidate.files, `${configFile}.files`);
253
260
  candidate.packages.forEach((group, index) => {
254
261
  if ('object' !== typeof group ||
255
262
  null === group ||
256
- 'object' !== typeof group.version ||
257
- null === group.version ||
258
- 'object' !== typeof group.version.from ||
259
- !Array.isArray(group.version.to)) {
263
+ (undefined !== group.version &&
264
+ ('object' !== typeof group.version ||
265
+ null === group.version ||
266
+ 'object' !== typeof group.version.from ||
267
+ !Array.isArray(group.version.to)))) {
260
268
  throw new ValidationError(`wp-release config ${configFile} has an invalid packages[${index}] entry.`);
261
269
  }
262
270
  if (undefined !== group.idFrom &&
@@ -266,19 +274,108 @@ function validateChangesetConfig(config, configFile) {
266
274
  if ('string' !== typeof group.glob || !group.glob.trim()) {
267
275
  throw new ValidationError(`wp-release config ${configFile} packages[${index}].glob must be a non-empty string.`);
268
276
  }
269
- if ('string' !== typeof group.tagType &&
270
- undefined !== group.tagType) {
271
- throw new ValidationError(`wp-release config ${configFile} packages[${index}].tagType must be a string.`);
272
- }
273
277
  if (undefined !== group.exclude &&
274
278
  (!Array.isArray(group.exclude) ||
275
279
  group.exclude.some((item) => 'string' !== typeof item))) {
276
280
  throw new ValidationError(`wp-release config ${configFile} packages[${index}].exclude must be an array of strings.`);
277
281
  }
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));
282
+ if (group.version) {
283
+ validateVersionDescriptor(group.version.from, `${configFile}.packages[${index}].version.from`, false);
284
+ group.version.to.forEach((target, targetIndex) => validateVersionDescriptor(target, `${configFile}.packages[${index}].version.to[${targetIndex}]`, true));
285
+ }
286
+ validateFilesConfig(group.files, `${configFile}.packages[${index}].files`);
287
+ validateDeployConfig(group.deploy, `${configFile}.packages[${index}].deploy`);
280
288
  });
281
289
  }
290
+ function isObject(value) {
291
+ return ('object' === typeof value && null !== value && !Array.isArray(value));
292
+ }
293
+ function validateFilesConfig(files, location) {
294
+ if (undefined !== files && !isObject(files)) {
295
+ throw new ValidationError(`wp-release config ${location} must be an object.`);
296
+ }
297
+ const { distignore, exclude } = (files ?? {});
298
+ if (undefined !== distignore &&
299
+ false !== distignore &&
300
+ ('string' !== typeof distignore || !distignore)) {
301
+ throw new ValidationError(`wp-release config ${location}.distignore must be a file path or false.`);
302
+ }
303
+ if (undefined !== exclude &&
304
+ (!Array.isArray(exclude) ||
305
+ exclude.some((item) => 'string' !== typeof item))) {
306
+ throw new ValidationError(`wp-release config ${location}.exclude must be an array of strings.`);
307
+ }
308
+ }
309
+ function validateDeployConfig(deploy, location) {
310
+ if (undefined !== deploy && !isObject(deploy)) {
311
+ throw new ValidationError(`wp-release config ${location} must be an object.`);
312
+ }
313
+ const targets = (deploy ?? {});
314
+ Object.keys(targets).forEach((key) => {
315
+ if (!['git', 'svn'].includes(key)) {
316
+ throw new ValidationError(`wp-release config ${location}.${key} is not a deploy target; use git or svn.`);
317
+ }
318
+ });
319
+ toTargetList(targets.git).forEach((target, index) => validateGitTarget(target, `${location}.git[${index}]`));
320
+ toTargetList(targets.svn).forEach((target, index) => validateSvnTarget(target, `${location}.svn[${index}]`));
321
+ }
322
+ /**
323
+ * Normalize a deploy target setting to a list of targets.
324
+ *
325
+ * @param value One target, a list of them or undefined.
326
+ * @return Targets.
327
+ */
328
+ export function toTargetList(value) {
329
+ if (undefined === value) {
330
+ return [];
331
+ }
332
+ return Array.isArray(value) ? value : [value];
333
+ }
334
+ function isTemplate(value) {
335
+ return (('string' === typeof value && '' !== value) ||
336
+ 'function' === typeof value);
337
+ }
338
+ function validateGitTarget(target, location) {
339
+ if (!isObject(target) || !isTemplate(target.repository)) {
340
+ throw new ValidationError(`wp-release config ${location} must be an object with a repository URL template or callback.`);
341
+ }
342
+ if (undefined !== target.branches &&
343
+ 'boolean' !== typeof target.branches &&
344
+ (!Array.isArray(target.branches) ||
345
+ target.branches.some((item) => 'string' !== typeof item))) {
346
+ throw new ValidationError(`wp-release config ${location}.branches must be a boolean or an array of branch names.`);
347
+ }
348
+ if (undefined !== target.tag &&
349
+ false !== target.tag &&
350
+ ('string' !== typeof target.tag ||
351
+ !target.tag.includes('{version}'))) {
352
+ throw new ValidationError(`wp-release config ${location}.tag must be a template with {version}, or false.`);
353
+ }
354
+ validateMessage(target.message, location);
355
+ if (undefined !== target.history && 'boolean' !== typeof target.history) {
356
+ throw new ValidationError(`wp-release config ${location}.history must be a boolean.`);
357
+ }
358
+ }
359
+ function validateMessage(message, location) {
360
+ if (undefined !== message && !isTemplate(message)) {
361
+ throw new ValidationError(`wp-release config ${location}.message must be a template or a callback.`);
362
+ }
363
+ }
364
+ function validateSvnTarget(target, location) {
365
+ if (!isObject(target)) {
366
+ throw new ValidationError(`wp-release config ${location} must be an object.`);
367
+ }
368
+ validateMessage(target.message, location);
369
+ if (undefined !== target.slug && !isTemplate(target.slug)) {
370
+ throw new ValidationError(`wp-release config ${location}.slug must be a template or a callback.`);
371
+ }
372
+ for (const key of ['url', 'assetsDir', 'readme']) {
373
+ if (undefined !== target[key] &&
374
+ ('string' !== typeof target[key] || !target[key])) {
375
+ throw new ValidationError(`wp-release config ${location}.${key} must be a non-empty string.`);
376
+ }
377
+ }
378
+ }
282
379
  function validateVersionDescriptor(descriptor, location, isTarget) {
283
380
  if ('object' !== typeof descriptor || null === descriptor) {
284
381
  throw new ValidationError(`wp-release config ${location} must be an object.`);