@wpelevator/wp-release 0.15.0 → 0.17.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 (98) hide show
  1. package/README.md +366 -39
  2. package/dist/changeset-config.d.ts +8 -0
  3. package/dist/changeset-config.js +3 -0
  4. package/dist/changeset-release.d.ts +9 -3
  5. package/dist/changeset-release.js +23 -14
  6. package/dist/cli.d.ts +6 -0
  7. package/dist/cli.js +12 -0
  8. package/dist/commands/artifact-signing.d.ts +20 -0
  9. package/dist/commands/artifact-signing.js +42 -0
  10. package/dist/commands/build.d.ts +32 -0
  11. package/dist/commands/build.js +91 -0
  12. package/dist/commands/changeset-add.d.ts +55 -6
  13. package/dist/commands/changeset-add.js +12 -3
  14. package/dist/commands/changeset-args.js +2 -2
  15. package/dist/commands/changeset-project-args.d.ts +63 -0
  16. package/dist/commands/changeset-project-args.js +114 -0
  17. package/dist/commands/changeset-status.d.ts +50 -1
  18. package/dist/commands/changeset-status.js +4 -2
  19. package/dist/commands/changeset-tag.d.ts +55 -6
  20. package/dist/commands/changeset-tag.js +7 -4
  21. package/dist/commands/changeset-version.d.ts +55 -6
  22. package/dist/commands/changeset-version.js +4 -2
  23. package/dist/commands/checksums.d.ts +4 -1
  24. package/dist/commands/checksums.js +27 -6
  25. package/dist/commands/i18n-audit.d.ts +2 -0
  26. package/dist/commands/i18n-audit.js +2 -0
  27. package/dist/commands/i18n-compile.d.ts +6 -0
  28. package/dist/commands/i18n-compile.js +76 -0
  29. package/dist/commands/i18n-json.d.ts +2 -0
  30. package/dist/commands/i18n-json.js +2 -0
  31. package/dist/commands/i18n-mo.d.ts +2 -0
  32. package/dist/commands/i18n-mo.js +2 -0
  33. package/dist/commands/i18n-php.d.ts +2 -0
  34. package/dist/commands/i18n-php.js +2 -0
  35. package/dist/commands/i18n-pot.d.ts +2 -0
  36. package/dist/commands/i18n-pot.js +2 -0
  37. package/dist/commands/i18n.d.ts +12 -0
  38. package/dist/commands/i18n.js +174 -0
  39. package/dist/commands/package-source.d.ts +43 -0
  40. package/dist/commands/package-source.js +101 -0
  41. package/dist/commands/verify.d.ts +7 -1
  42. package/dist/commands/verify.js +53 -17
  43. package/dist/commands/zip.d.ts +20 -8
  44. package/dist/commands/zip.js +109 -124
  45. package/dist/i18n/atomic-output.d.ts +23 -0
  46. package/dist/i18n/atomic-output.js +90 -0
  47. package/dist/i18n/audit.d.ts +7 -0
  48. package/dist/i18n/audit.js +69 -0
  49. package/dist/i18n/catalog.d.ts +54 -0
  50. package/dist/i18n/catalog.js +86 -0
  51. package/dist/i18n/compile.d.ts +27 -0
  52. package/dist/i18n/compile.js +71 -0
  53. package/dist/i18n/extraction.d.ts +45 -0
  54. package/dist/i18n/extraction.js +105 -0
  55. package/dist/i18n/files.d.ts +7 -0
  56. package/dist/i18n/files.js +45 -0
  57. package/dist/i18n/generate.d.ts +49 -0
  58. package/dist/i18n/generate.js +240 -0
  59. package/dist/i18n/javascript.d.ts +7 -0
  60. package/dist/i18n/javascript.js +143 -0
  61. package/dist/i18n/metadata.d.ts +16 -0
  62. package/dist/i18n/metadata.js +117 -0
  63. package/dist/i18n/options.d.ts +52 -0
  64. package/dist/i18n/options.js +156 -0
  65. package/dist/i18n/packages.d.ts +24 -0
  66. package/dist/i18n/packages.js +86 -0
  67. package/dist/i18n/php.d.ts +9 -0
  68. package/dist/i18n/php.js +238 -0
  69. package/dist/i18n/placeholders.d.ts +9 -0
  70. package/dist/i18n/placeholders.js +13 -0
  71. package/dist/i18n/po.d.ts +24 -0
  72. package/dist/i18n/po.js +180 -0
  73. package/dist/i18n/pot.d.ts +16 -0
  74. package/dist/i18n/pot.js +119 -0
  75. package/dist/i18n/translation-json.d.ts +14 -0
  76. package/dist/i18n/translation-json.js +96 -0
  77. package/dist/i18n/translation-php.d.ts +6 -0
  78. package/dist/i18n/translation-php.js +35 -0
  79. package/dist/index.d.ts +28 -0
  80. package/dist/index.js +16 -0
  81. package/dist/integrity.d.ts +33 -0
  82. package/dist/integrity.js +115 -0
  83. package/dist/output-files.d.ts +7 -0
  84. package/dist/output-files.js +25 -0
  85. package/dist/package-build.d.ts +33 -0
  86. package/dist/package-build.js +112 -0
  87. package/dist/package-files.d.ts +25 -0
  88. package/dist/package-files.js +24 -0
  89. package/dist/package-zip.d.ts +14 -2
  90. package/dist/package-zip.js +129 -38
  91. package/dist/release-tags.js +6 -0
  92. package/dist/single-project.d.ts +32 -0
  93. package/dist/single-project.js +144 -0
  94. package/dist/summary.d.ts +9 -0
  95. package/dist/summary.js +25 -0
  96. package/dist/zip.d.ts +3 -0
  97. package/dist/zip.js +16 -6
  98. package/package.json +20 -3
package/README.md CHANGED
@@ -1,12 +1,10 @@
1
1
  # WP Release
2
2
 
3
- Package, checksum, verify and version-check WordPress plugins and themes, deploy plugins to the WordPress.org plugin SVN repository, and deploy plugins, themes and packages to Git repositories. Usable as a CLI, as a library, and through the [`wp-release-zip`](https://github.com/wpelevator/wp-release-zip), [`wp-release-checksums`](https://github.com/wpelevator/wp-release-checksums), [`wp-release-sign`](https://github.com/wpelevator/wp-release-sign) and [`wp-release-deploy-svn`](https://github.com/wpelevator/wp-release-deploy-svn) GitHub Actions.
3
+ Package, checksum, sign, verify and version-check WordPress plugins and themes, extract and compile their translations, deploy plugins to the WordPress.org plugin SVN repository, and deploy plugins, themes and packages to Git repositories. Usable as a CLI, as a library, and through the [`wp-release-zip`](https://github.com/wpelevator/wp-release-zip), [`wp-release-checksums`](https://github.com/wpelevator/wp-release-checksums), [`wp-release-sign`](https://github.com/wpelevator/wp-release-sign) and [`wp-release-deploy-svn`](https://github.com/wpelevator/wp-release-deploy-svn) GitHub Actions.
4
4
 
5
5
  Themes are released by uploading a ZIP on WordPress.org, so `deploy-svn` is for plugins only; use `zip` to build the theme upload.
6
6
 
7
- **Status:** first release (0.x). Signatures are tested against libsodium, but haven't been used for a real release yet.
8
-
9
- Option names follow the existing community tools (10up WordPress.org actions, `wp dist-archive`, `wp-scripts plugin-zip`, `grunt-wp-deploy`) so that migrating needs few or no renames. See [Comparison with existing tools](#comparison-with-existing-tools) for the differences.
7
+ Packaging and deploy options follow the existing community tools (10up WordPress.org actions, `wp dist-archive`, `wp-scripts plugin-zip`, `grunt-wp-deploy`) so that migrating needs few or no renames. The internationalization commands favor simpler, CI-friendly options over matching `wp i18n`. See [Comparison with existing tools](#comparison-with-existing-tools) for the differences.
10
8
 
11
9
  ## Install
12
10
 
@@ -14,7 +12,7 @@ Option names follow the existing community tools (10up WordPress.org actions, `w
14
12
  npm install --save-dev @wpelevator/wp-release
15
13
  ```
16
14
 
17
- Requires Node.js 22.12 or later. `deploy-svn` also needs the `svn` command line client; the optional `--exclude-gitattributes` flag needs Git.
15
+ Requires Node.js 22.12 or later. `deploy-svn` also needs the `svn` command line client. Git deployments, changeset commands and the optional `--exclude-gitattributes` flag need Git.
18
16
 
19
17
  The package is an ES module with TypeScript type definitions. CommonJS projects can load it with `require( '@wpelevator/wp-release' )`, which Node.js supports for ES modules without top-level `await` since 22.12.
20
18
 
@@ -26,6 +24,7 @@ npx wp-release --help
26
24
 
27
25
  | Command | Description |
28
26
  |---|---|
27
+ | `wp-release build [<source-dir>] --output <dir> [options]` | Copy selected release files into a directory. |
29
28
  | `wp-release zip [<source-dir>] [options]` | Create a ZIP archive with a `{slug}/` root folder, respecting `.distignore`. |
30
29
  | `wp-release checksums [<source-dir>] [options]` | Write the MD5 and SHA-256 hashes of every distributed file to a checksums manifest. |
31
30
  | `wp-release sign <file>... [options]` | Sign files with Ed25519, writing a detached `<file>.sig` next to each, or minisign and SSH signatures with `--format`. |
@@ -36,12 +35,21 @@ npx wp-release --help
36
35
  | `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
36
  | `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
37
  | `wp-release deploy-svn --release-ref <ref> [--package <name>] [--list]` | Deploy the configured plugin of a release tag to WordPress.org. |
38
+ | `wp-release i18n-pot [<source-dir>] [options]` | Extract translatable strings into a reproducible POT file. |
39
+ | `wp-release i18n-audit [<source-dir>] [options]` | Report translation string problems without writing files. |
40
+ | `wp-release i18n-mo <file.po> [options]` | Compile a translated PO file into MO. |
41
+ | `wp-release i18n-json <file.po> --domain <domain> [options]` | Compile the JavaScript strings of a translated PO file into WordPress script JSON files. |
42
+ | `wp-release i18n-php <file.po> [options]` | Compile a translated PO file into a WordPress `.l10n.php` file. |
39
43
  | `wp-release changeset-add [options]` | Record the release impact of a pull request. |
40
44
  | `wp-release changeset-status [options]` | List pending releases and check changed packages for changesets. |
41
45
  | `wp-release changeset-version [options]` | Apply version bumps and changelog entries. |
42
46
  | `wp-release changeset-tag [options]` | Create missing package release tags. |
43
47
 
44
- Run `npx wp-release <command> --help` for the options of each command. Every command reads the plugin or theme directory from the first argument or `--source-dir`, defaulting to the current directory, and prints its result as JSON with `--json`. In GitHub Actions, `--github-output` sets the result as JSON to the `result` output of the step instead, for later steps and jobs to read with `fromJSON( steps.<id>.outputs.result )`. Each top-level string, number or boolean of the result is also its own output, like `steps.<id>.outputs.zipPath` for `zip`, so a step reads it without `fromJSON()`; lists, like the `tags` of `changeset-tag` or the `deploys` of `deploy-git`, are only in `result`. Since GitHub evaluates the `env` of a step also when its `if` is false, and `fromJSON()` fails for the empty output of a skipped step, read the plain outputs in steps that run conditionally.
48
+ Run `npx wp-release <command> --help` for the options of each command. Directory-based commands accept a first `<source-dir>` argument or `--source-dir`, defaulting to the current directory. `sign` and the `i18n-mo`, `i18n-json` and `i18n-php` compilers take file arguments, `keygen` generates keys, and changeset commands operate on the configured repository. Every command prints its result as JSON with `--json`.
49
+
50
+ In GitHub Actions, `--github-output` writes the complete result as JSON to `steps.<id>.outputs.result` and exposes each top-level string, number or boolean as a plain output. Use `steps.zip.outputs.zipPath` for a ZIP path, for example. Arrays and objects remain in `result`: tag metadata is read with `fromJSON( steps.tag.outputs.result ).tags[0].tag`, even when only one package is selected. There is no plain `steps.tag.outputs.tag` output.
51
+
52
+ Prefer plain outputs in steps that run conditionally, since a skipped step has empty outputs. When reading JSON from a step that may be skipped, use a fallback such as `fromJSON( steps.tag.outputs.result || '{"tags":[]}' ).tags[0].tag`. For several tags or deploys, read the arrays in `result` to build a job matrix or process each item.
45
53
 
46
54
  ### GitHub Actions annotations and job summaries
47
55
 
@@ -65,6 +73,7 @@ Each command also adds a section to the [job summary](https://docs.github.com/en
65
73
  |---|---|
66
74
  | `deploy-svn` | Plugin, linked to its WordPress.org page, version, written SVN paths, the commit linked to its WordPress.org changeset (or the dry run or no-op result), and the SVN changes in a collapsed table. |
67
75
  | `deploy-git` | Repository, release commit and the pushed branch and tag. |
76
+ | `build` | Output directory, number of copied files and optional manifest; requested signatures and public keys. |
68
77
  | `zip` | ZIP, number of files and checksums manifest; with `--sign`, signature files and the public key of each written format. |
69
78
  | `checksums` | Manifest and number of files. |
70
79
  | `sign` | Signature files and the public key of each written format. |
@@ -76,11 +85,63 @@ Each command also adds a section to the [job summary](https://docs.github.com/en
76
85
 
77
86
  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.
78
87
 
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).
88
+ Use global `--config <path>` to select a config; relative paths resolve from the current directory. Otherwise, commands using the config discover `wp-release.config.js`, `.mjs` or `.cjs` at the repository root. Global `--tag-format <format>` replaces the `tagFormat` of the config for one command, see [Packages and tags](#packages-and-tags).
89
+
90
+ ### Changesets support
91
+
92
+ The changeset commands manage versioning, changelogs and release tags for single-package repositories and monorepos, including this repository. They read Changesets-style Markdown and load JavaScript configs with descriptors for npm and Composer manifests, plugin and theme headers, readme Stable tags, PHP constants, and Git tags. Use JavaScript configs with ESM (`export default`) or CommonJS (`module.exports`). Frontmatter uses `package-name: patch|minor|major` entries. For pre-releases, use the built-in [pre-release workflow](#pre-releases); the parser supports this simple frontmatter format rather than general YAML or the Changesets pre mode.
80
93
 
81
- ### Changesets support (experimental)
94
+ #### Single-project repositories without a config file
95
+
96
+ The `changeset-*` commands can release one project at the repository root without a `wp-release.config.*` file. Run them from the root directory. With no version descriptor flags, they detect a root `package.json` or `composer.json` with a version, a plugin entry PHP file with a `Plugin Name` header, or a theme `style.css` with a `Theme Name` header. A detected plugin also updates the Stable tag in `readme.txt`, or `readme.md` when no Stable tag is found in `readme.txt`. Multiple version sources, multiple plugin entry files and npm workspaces require explicit descriptors or a config; files belonging to different projects aren't silently synchronized. A directory without a recognized version source still requires a config or explicit descriptors.
97
+
98
+ For a root npm package, the complete workflow needs no project flags:
99
+
100
+ ```bash
101
+ npx wp-release changeset-add --bump minor --summary "Add a new feature."
102
+ npx wp-release changeset-status --since origin/main
103
+ npx wp-release changeset-version --dry-run
104
+ npx wp-release changeset-version
105
+ # Review and commit the updated version and changelog files.
106
+ npx wp-release changeset-tag --push
107
+ ```
82
108
 
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).
109
+ A single root package is selected automatically for `changeset-add --bump` when `--since` isn't supplied, and for `changeset-tag --version`; `--since` still selects changed packages. npm and Composer projects use their root manifest's name as the changeset identifier when that manifest supplies the version; other projects use the root directory's basename. Keep that directory name consistent between local checkouts and CI, or use a config with a manifest-based `idFrom`. The fallback uses `.changeset/`, `CHANGELOG.md`, tags named `v{version}`, and the `origin` remote. There is no default comparison branch: pass `--since` or `--base-branch` when status checks should compare changes. Readme changelog updates are opt-in.
110
+
111
+ Explicit descriptors replace automatic detection and define the entire version target list. For a plugin whose version appears in its header, readme and a PHP constant:
112
+
113
+ ```bash
114
+ npx wp-release changeset-version \
115
+ --version-from wordpress-plugin \
116
+ --version-wordpress-plugin my-plugin.php \
117
+ --version-wordpress-stable-tag readme.txt \
118
+ --version-php-constant 'my-plugin.php#MY_PLUGIN_VERSION'
119
+ ```
120
+
121
+ The same project flags belong on `changeset-add`, `changeset-status`, `changeset-version` and `changeset-tag`. Keep them consistent across all four commands, for example in command-specific npm scripts. An accompanying `package.json` used for build tools isn't changed unless it is included explicitly.
122
+
123
+ | Flag | Version descriptor |
124
+ |---|---|
125
+ | `--version-package-json <file>` | npm manifest version source and target |
126
+ | `--version-composer-json <file>` | Composer manifest version source and target; a target without a `version` field is skipped, while a source must have one |
127
+ | `--version-wordpress-plugin <file>` | WordPress plugin Version header source and target |
128
+ | `--version-wordpress-theme <file>` | WordPress theme Version header source and target |
129
+ | `--version-wordpress-stable-tag <file>` | WordPress readme Stable tag source and target; keeps its stable version during pre-releases and snapshots |
130
+ | `--version-php-constant <file>#<constant>` | PHP `define()` or `const` version source and target |
131
+ | `--version-git-tag <pattern>` | Current version from existing stable Git tags, with a pattern such as `v{version}`; source-only, never a version target |
132
+
133
+ File descriptor flags are repeatable. The source is inferred from a sole descriptor, or a unique descriptor other than a Stable tag. Otherwise use `--version-from <kind>`, where kind is `package-json`, `composer-json`, `wordpress-plugin`, `wordpress-theme`, `wordpress-stable-tag`, `php-constant` or `git-tag`. For repeated kinds, qualify the selector with the exact descriptor value: `--version-from package-json:package.json` or `--version-from 'php-constant:my-plugin.php#MY_PLUGIN_VERSION'`. Argument order doesn't select the source. A Git source requires a matching existing stable tag; its pattern selects the tags to read, while `--tag-format` controls tags to create. With multiple descriptors, explicitly select `git-tag` if it is the authoritative source.
134
+
135
+ | Shared option | Config equivalent |
136
+ |---|---|
137
+ | `--base-branch <branch>` | `baseBranch`, used with the remote for the default comparison ref |
138
+ | `--remote <name>` | `remote`, used for comparisons and pushing release tags |
139
+ | `--changesets-dir <dir>` | `changesets`, relative to the repository root |
140
+ | `--readme-changelog` / `--no-readme-changelog` | `readmeChangelog`, independent of updating the readme Stable tag |
141
+ | `--changelog-insert-after <marker>` | `changelogInsertAfter`; must exist in `CHANGELOG.md` and, with readme changelog updates, the plugin readme |
142
+ | `--tag-format <format>` | `tagFormat`, replacing the default `v{version}` |
143
+
144
+ An explicit `--config` must load successfully. Otherwise a discovered config takes precedence over the single-project fallback. Single-project descriptor and settings flags cannot be mixed with a config file; put those settings in the config instead. The existing global `--tag-format` override works in both modes. Config files remain the place for package layouts, dependents, custom version and changelog callbacks, deployment destinations and distribution settings. Packaging and deployment commands retain their existing config requirements.
84
145
 
85
146
  Typical release flow, from a feature pull request to the publish workflows triggered by release tags:
86
147
 
@@ -127,6 +188,23 @@ export default defineConfig( {
127
188
 
128
189
  `version.from` selects the current version; `version.to` lists release targets. Use `phpConstantVersion( { file, constant } )` to sync a PHP constant.
129
190
 
191
+ #### Single-project checks in GitHub Actions
192
+
193
+ There are no dedicated build or changeset wrapper actions. Install the CLI in your project and run it directly. A root npm package can check a pull request with:
194
+
195
+ ```yaml
196
+ - uses: actions/checkout@v7
197
+ with:
198
+ fetch-depth: 0
199
+ - uses: actions/setup-node@v6
200
+ with:
201
+ node-version: 24
202
+ - run: npm ci
203
+ - run: npx --no-install wp-release changeset-status --since origin/main
204
+ ```
205
+
206
+ For a plugin with several version targets, put the same descriptor flags in npm scripts for all four changeset commands. Full Git history and tags are needed for comparisons, Git version sources and release notes. Keep the checkout directory name consistent when it supplies the changeset identifier. Directory artifacts use `npx --no-install wp-release build --output dist/my-plugin --github-output`; upload that directory with your artifact action. `build` writes manifest sidecars outside the directory and does not accept the ZIP-only embedding, codec or manifest-filename flags.
207
+
130
208
  #### Packages and tags
131
209
 
132
210
  The config describes the repository, so wp-release assumes no directory structure or naming convention of its own:
@@ -143,7 +221,7 @@ The config describes the repository, so wp-release assumes no directory structur
143
221
  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
222
  - **`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
223
  - **`--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.
224
+ - **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 working 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
225
  - **`remote`** is the git remote that `changeset-tag --push` pushes tags to and that the base branch is compared on, `origin` by default.
148
226
 
149
227
  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.
@@ -215,7 +293,7 @@ The `changeset-version` result lists each release with its `package`, `bump`, `c
215
293
 
216
294
  After merging the release PR, `changeset-tag --since <merge-commit>^1 --push` pushes tags only for packages changed by that release. It pushes one tag at a time because GitHub doesn't trigger workflows for a push of more than three tags. wp-release doesn't create GitHub Releases: with `--json` or `--github-output`, each entry of `tags` has the `tag`, a `title` like `plugins/alpha@1.2.0`, the release `notes` and a `prerelease` flag, for a workflow step like [`softprops/action-gh-release`](https://github.com/softprops/action-gh-release) to create the release (see the example below). For initial adoption, preview all missing tags with `changeset-tag --dry-run` (without `--since`), then push if needed. To release each package with a separate workflow instead, `changeset-tag --since <merge-commit>^1 --untagged --github-output` lists the releases that don't have a tag yet, without creating any. Tag pushes need Git write credentials.
217
295
 
218
- To release a single package, for example from a manual workflow, pass `--package <name>`, repeatable, and with one package `--version <version>` to fail unless the package is at that version. Packages versioned by git tags have no version file, so for them `--version` is the version to tag. Before a tag is created, every `version.to` target must match the version, so a plugin `Version` header that disagrees with its readme `Stable tag` blocks the tag. Versions with a pre-release part, like `1.0.0-rc.1`, have `prerelease` set, so their GitHub Releases can be marked as pre-releases:
296
+ To release a single package, for example from a manual workflow, pass `--package <name>`, repeatable, and with one package `--version <version>` to fail unless the package is at that version. Packages versioned by git tags have no version file, so for them `--version` is the version to tag. Before a tag is created, every `version.to` target must match the version, so a plugin `Version` header that disagrees with its readme `Stable tag` blocks the tag. Versions with a pre-release part, like `1.0.0-rc.1`, have `prerelease` set, so their GitHub Releases can be marked as pre-releases. This example selects one package, so `tags[0]` is its release; these fields are nested in the JSON result:
219
297
 
220
298
  ```yaml
221
299
  - name: Tag a release
@@ -268,6 +346,7 @@ npm publish --workspace packages/library --tag pr-251
268
346
 
269
347
  | Capability | CLI command | GitHub Action |
270
348
  |---|---|---|
349
+ | Build a directory | `wp-release build` | |
271
350
  | Build a ZIP | `wp-release zip` | [`wpelevator/wp-release-zip`](https://github.com/wpelevator/wp-release-zip) |
272
351
  | Create a checksums manifest | `wp-release checksums` | [`wpelevator/wp-release-checksums`](https://github.com/wpelevator/wp-release-checksums) |
273
352
  | Sign files | `wp-release sign` | [`wpelevator/wp-release-sign`](https://github.com/wpelevator/wp-release-sign) |
@@ -276,12 +355,15 @@ npm publish --workspace packages/library --tag pr-251
276
355
  | Deploy to WordPress.org SVN | `wp-release deploy-svn` | [`wpelevator/wp-release-deploy-svn`](https://github.com/wpelevator/wp-release-deploy-svn) |
277
356
  | Deploy to a Git repository | `wp-release deploy-git` | |
278
357
 
279
- Each concept has one name, written in the syntax of each interface:
358
+ The names below are scoped to their commands. `--output` selects the primary artifact of each command; ZIP packaging uses `--checksums-file` for its accompanying manifest:
280
359
 
281
360
  | Concept | CLI | GitHub Action | Library | Meaning |
282
361
  |---|---|---|---|---|
283
362
  | Plugin or theme directory | `<source-dir>` argument or `--source-dir` | `source-dir` | `sourceDir` | Directory that a command reads, whether it's the repository or a build directory. |
284
- | Generated file | `--output <file>` | `output` | `outputPath` | File written by `zip` or `checksums`. |
363
+ | Build directory | `build --output <dir>` | — | `outputDir` | Directory written by `build`; files go directly inside it. |
364
+ | ZIP file | `zip --output <file>` | `output` in ZIP action | `zipPath` | Destination passed to `zipPackage()`; also returned as `zipPath`. |
365
+ | Standalone checksums file | `checksums --output <file>` | `output` in checksums action | first argument of `writeChecksums()` | Manifest destination relative to the working directory; the CLI returns its absolute path as `checksumsPath`. |
366
+ | Checksums filename when zipping | `zip --checksums-file <file>` | `checksums-file` in ZIP action | `checksumsFile` | Relative manifest filename beside the ZIP or inside its package root when embedded. |
285
367
  | ZIP to deploy | `--from-zip <file>` | `from-zip` | `fromZip` | Existing ZIP read as the deploy source. |
286
368
  | Git tag to compare | `--git-tag <tag>` | | `gitTag` | Git tag checked by `version-check`. |
287
369
  | SVN tag to create | `--svn-tag <version>` | `svn-tag` | `svnTag` | Tag written by `deploy-svn`, distinct from a git tag. |
@@ -291,18 +373,20 @@ Each concept has one name, written in the syntax of each interface:
291
373
  | Source history | `--split-prefix <dir>` | | `splitPrefix` | Directory of a monorepo package whose history, from `git subtree split`, becomes the source history of `deploy-git` releases. |
292
374
  | SVN credentials | `--svn-username <name>`, `SVN_USERNAME` and `SVN_PASSWORD` env vars | `svn-username`, `svn-password` | `svnUsername`, `svnPassword` | Needed only to commit. There's no password flag, since it would show in the process list; the action inputs and library options fall back to the env vars. |
293
375
  | Log format | `--log-format <text\|github>`, `--ci` for `github` | always `github` | `writer: new TextWriter()` or `new GitHubActionsWriter()` | How the log is printed: plain text, or GitHub Actions annotations and job summaries. Each name matches the start of its writer class. |
294
- | Release config | `--config <path>` | `config` in ZIP and SVN actions | `configFile` | Explicit JavaScript config path for configured packaging or deployment. |
376
+ | Release config | `--config <path>` | `config` in ZIP, checksums and SVN actions | `configFile` | Explicit JavaScript config path for package selection, integrity settings or deployment. |
295
377
  | Release tag format | `--tag-format <format>` | `tag-format` in SVN action | `tagFormat` | How `release-ref` identifies a package and its version. |
296
378
  | Release ref to deploy | `--release-ref <ref>` | `release-ref` in SVN action | `ref` | Read configured packages and deployment targets from a release ref. |
297
379
  | Assets sync | `--no-assets` to skip | `assets: false` to skip | `assets` | Booleans are positive in the library and the action; `--no-assets` is only the CLI negation, never `noAssets` or a `no-assets` input. |
298
380
 
299
- CLI JSON results use camelCase keys like `zipPath`, and GitHub Action outputs use kebab-case like `zip-path`. The ZIP action supports `package` and `config`, and the SVN action supports `release-ref`, repeated `package` values as newline-separated input, `list`, `config` and `tag-format`.
381
+ CLI JSON results use camelCase keys like `zipPath`, and GitHub Action outputs use kebab-case like `zip-path`. The ZIP action supports `package` and `config`, the checksums action supports `config` for codec selection, and the SVN action supports `release-ref`, repeated `package` values as newline-separated input, `list`, `config` and `tag-format`.
382
+
383
+ The ZIP CLI result uses `checksumsEmbedded` for the package-relative embedded manifest path. Its `signatures` array includes external absolute paths and embedded package-relative paths, while `checksumsSignatures` contains only manifest signatures. The ZIP action exposes those locations as `checksums-embedded`, `signatures` for external signatures, and `signatures-embedded` for embedded signatures, so asset uploads receive local files. With CLI `--github-output`, signature arrays remain in the JSON `result` output.
300
384
 
301
385
  Library classes are named after the capability (`SvnDeploy`, `Svn`, `GitDeploy`) without repeating "release".
302
386
 
303
387
  ### File selection
304
388
 
305
- `zip`, `checksums`, `deploy-svn` and `deploy-git` select files the same way:
389
+ `build`, `zip`, `checksums`, `deploy-svn` and `deploy-git` select files the same way:
306
390
 
307
391
  | Option | Description |
308
392
  |---|---|
@@ -319,6 +403,25 @@ The commands read the same headers as WordPress. The main plugin file is the top
319
403
 
320
404
  `--version` defaults to the `Version` header in `zip`, `checksums` and `version-check`, and `checksums --type` defaults to the detected package type.
321
405
 
406
+ ### Build a directory
407
+
408
+ `build` and `zip` use the same internal helper to load file rules, exclude their outputs, select release files and reject an empty selection. `build` copies the selected files directly into `--output <dir>`, without a slug wrapper or ZIP. Checksums and manifest signatures are optional. It supports the shared file selection flags above and `--package <name>` with `--config <path>` for configured package rules. A configured package cannot be combined with a source directory or file selection flags.
409
+
410
+ ```sh
411
+ wp-release build --output dist/code-prettify
412
+ wp-release build --package plugins/alpha --output dist/alpha
413
+ ```
414
+
415
+ The destination is required and automatically excluded when it is inside the source. It cannot be the source directory or an ancestor, including through symlinked parents. Existing destinations require `--force`, which replaces the directory and removes stale files; existing files and symbolic links are rejected. Files are copied to a temporary directory before the existing build is replaced. Selected symbolic links are rejected as with ZIP packaging. `--json` returns `outputDir`, `files` and, for configured builds, `package`; it also returns `checksumsPath` when a manifest is generated, and `signatures` and `publicKey` when signing is requested. `--github-output` exposes these results too.
416
+
417
+ `--checksums` writes `<output-dir>.checksums.json` beside the directory and hashes the copied build files. `--sign-checksums` generates and signs that manifest, implying `--checksums`. Use `--slug` and `--version` to override its metadata. The manifest and its signatures are excluded from the source selection, so rebuilding with `--force` does not package previous sidecars. Existing manifests and requested signature files also require `--force`; signing keys, formats and signature destinations are validated before creating or replacing the build. `--sign` is unsupported because a directory has no single file to sign.
418
+
419
+ ```sh
420
+ wp-release build --output dist/code-prettify --sign-checksums
421
+ ```
422
+
423
+ In the Deploy workflow, a directory build can be deployed with `wp-release deploy-svn dist/code-prettify --slug code-prettify --trunk --svn-tag "$GITHUB_REF_NAME" --no-distignore`. Keep `--from-zip` when the SVN deployment must match the attached ZIP exactly. Run dependency installation and compilation before `build`; it only selects and copies files.
424
+
322
425
  ### ZIP
323
426
 
324
427
  The [`wp-release-zip`](https://github.com/wpelevator/wp-release-zip) GitHub Action runs this command.
@@ -331,12 +434,37 @@ wp-release zip dist --slug my-plugin --checksums
331
434
 
332
435
  - `--root-folder <name>` changes the root folder and `--no-root-folder` puts the files at the root of the ZIP.
333
436
  - `--filename-format` changes the name, like `--filename-format {slug}` for `my-plugin.zip`.
334
- - Entries are sorted and use a fixed timestamp and file mode, so the same files always give the same ZIP.
437
+ - Entries are sorted and use a fixed timestamp and file mode. Built-in metadata is deterministic except for embedded minisign signatures, which include their signing timestamp; custom codec callbacks control their own encoded bytes.
335
438
  - `--checksums` also writes the [checksums manifest](#checksums) of the archived files next to the ZIP, with `.checksums.json` instead of `.zip`.
336
- - `--sign` also [signs](#sign) the ZIP and the manifest with the `WP_RELEASE_SIGNING_KEY` key in the same command that writes them, like `wp-release sign` with the same files, writing a WordPress `.sig` next to each. `--sign-format <names>` picks the [signature formats](#signature-formats), like the `--format` of `sign`, such as `--sign-format wordpress,minisign`, and implies `--sign`. The signature formats, signing key and existing signature paths are checked before writing the ZIP or manifest, so correcting invalid signing options permits a retry without `--force`. `--force` also overwrites existing signatures. With the GitHub log format, the job summary includes the signatures and the public key of each written format. The result lists the signature files in `signatures` and the public key of the signing key in `publicKey`.
439
+ - `--sign` signs only the ZIP with `WP_RELEASE_SIGNING_KEY`. `--sign-checksums` generates and signs only the checksums manifest, implying `--checksums`. Use both flags to sign both files. External signatures are written alongside their files; embedded manifest signatures are written inside the package. The default format is WordPress `.sig`, unless integrity configuration selects another format.
440
+ - `--sign-format` selects formats for requested signatures, such as `--sign-format wordpress,minisign,ssh`. It requires `--sign` or `--sign-checksums` and never enables signing by itself. Signing keys, formats and existing signature destinations are validated before writing the ZIP or manifest. `--force` also overwrites requested signatures. Job summaries include signatures and public keys; JSON results include `signatures` and `publicKey`.
337
441
  - An existing ZIP or manifest isn't overwritten without `--force`. A ZIP or manifest written inside the source directory is never archived.
338
442
  - `--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.
339
443
 
444
+ To embed a signed manifest with a stable filename, use:
445
+
446
+ ```bash
447
+ wp-release zip dist --checksums-file checksums.json --embed-checksums --sign-checksums
448
+ ```
449
+
450
+ See [Checksums](#checksums) for filenames, placement, formats and configuration.
451
+
452
+ ### Signing targets for builds and ZIPs
453
+
454
+ | Flag | `zip` | `build` |
455
+ |---|---|---|
456
+ | `--checksums` | Generate an unsigned manifest beside the ZIP. | Generate an unsigned manifest beside the directory. |
457
+ | `--sign` | Sign only the ZIP. | Unsupported. |
458
+ | `--sign-checksums` | Generate and sign the manifest. | Generate and sign the manifest. |
459
+ | `--sign-format` | Choose formats for explicitly requested signatures. | Same; requires `--sign-checksums`. |
460
+
461
+ ```sh
462
+ wp-release zip --slug code-prettify --sign --sign-checksums
463
+ wp-release build --output dist/code-prettify --sign-checksums --sign-format minisign
464
+ ```
465
+
466
+ These flags deliberately replace the former implicit signing behavior: `zip --checksums --sign` now leaves the manifest unsigned, and `--sign-format` alone is rejected. Add `--sign-checksums` to sign a manifest and `--sign` to sign a ZIP. The standalone `checksums` and `sign` commands remain available for workflows that write and sign a manifest inside the source before packaging it.
467
+
340
468
  ### Checksums
341
469
 
342
470
  The [`wp-release-checksums`](https://github.com/wpelevator/wp-release-checksums) GitHub Action runs this command.
@@ -365,26 +493,92 @@ wp-release checksums dist --version 1.2.0
365
493
 
366
494
  Because the manifest lists the files and not the ZIP, it stays valid for any copy of the same files: the GitHub release ZIP, the ZIP that WordPress.org builds from SVN, or an installed plugin on a site.
367
495
 
368
- ### Verify
496
+ #### Filenames, embedding and signing
369
497
 
370
- `verify` checks a directory against a manifest from `checksums` or from the WordPress.org checksums API, and reports every changed, missing and extra file. It exits with `2` unless all files match:
498
+ `--checksums-file <file>` enables checksums and sets a relative filename independently of the ZIP name. Without `--embed-checksums`, it resolves against the ZIP output directory; with embedding, it resolves against the package root inside the ZIP (the archive root with `--no-root-folder`). Nested paths are supported, but absolute paths, backslashes, drive letters, control characters and dot segments are rejected. Embedding emits no external manifest and never modifies the source directory. Embedded minisign signatures contain a signing timestamp, so ZIP bytes can differ across signing times; unsigned metadata and WordPress/SSH signatures remain deterministic. Selected source files must not collide with generated manifest or signature entries.
371
499
 
372
500
  ```bash
373
- wp-release verify wp-content/plugins/my-plugin --checksums my-plugin.1.2.0.checksums.json
501
+ # External manifest beside releases/my-plugin.1.5.1.zip.
502
+ wp-release zip dist --output releases/my-plugin.1.5.1.zip --checksums-file checksums.json --sign-checksums
503
+
504
+ # Signed manifest inside the package, with a stable filename.
505
+ wp-release zip dist --output releases/my-plugin.1.5.1.zip --checksums-file checksums.json --embed-checksums --sign-checksums
506
+
507
+ # Sign the completed downloadable archive separately.
508
+ wp-release sign releases/my-plugin.1.5.1.zip
509
+
510
+ # Verify installed files or an archive using its embedded manifest.
511
+ wp-release verify installed-plugin --public-key "$PUBLIC_KEY"
512
+ wp-release verify --zip releases/my-plugin.1.5.1.zip --public-key "$PUBLIC_KEY"
374
513
  ```
375
514
 
376
- - A file matches when its SHA-256 equals the manifest. Manifests without SHA-256 fall back to MD5, and a hash can also be a list of accepted values.
377
- - `verify` doesn't read `.distignore`, so that an ignore file inside the checked directory can't hide added files. `--exclude <patterns>` skips paths in both the directory and the manifest, for files that aren't part of the release, such as caches.
378
- - Manifest paths outside the package folder (absolute or with `..`) are rejected.
379
- - `--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`.
380
- - 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.
381
- - 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).
515
+ `--sign-checksums` enables checksums and signs only the manifest, before embedding when requested. It reads `WP_RELEASE_SIGNING_KEY` and uses `--sign-format wordpress,minisign,ssh` to select formats; each signature travels with the manifest. `--sign-format` selects formats for explicitly requested signing targets and never enables signing. `--sign` signs only the final ZIP; `--sign-checksums` controls the manifest in either placement mode. Use both flags to sign both targets. `--force` permits replacing external output files and signatures. `--json` reports external manifests in `checksumsPath`, embedded package-relative paths in `checksumsEmbedded`, and manifest signatures in `checksumsSignatures`.
382
516
 
383
- 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).
517
+ The default WordPress filename remains the ZIP basename with `.checksums.json` instead of `.zip`, in either placement mode. Choose `--checksums-file checksums.json` for a stable name. The version remains in the JSON content.
518
+
519
+ #### Formats
520
+
521
+ `--checksums-format` chooses the manifest format. `wordpress-json` (default) uses the WordPress-compatible structure shown above. `sha256sum` uses GNU-compatible SHA-256 lines and escaping, with `SHA256SUMS` as its default filename. It carries no package identity or version, and standard `sha256sum --check` checks listed files without rejecting additional files; `wp-release verify` also checks for extra files.
522
+
523
+ ```bash
524
+ wp-release zip dist --embed-checksums --checksums-format sha256sum --sign-checksums --sign-format minisign
525
+ wp-release verify --zip my-plugin.1.5.1.zip --checksums-format sha256sum --public-key "$MINISIGN_PUBLIC_KEY" --signature-format minisign
526
+ # From the extracted package directory:
527
+ minisign -Vm SHA256SUMS -P "$MINISIGN_PUBLIC_KEY"
528
+ sha256sum --check SHA256SUMS
529
+ ```
530
+
531
+ `--checksums-algorithms md5,sha256` selects JSON digest fields; lists replace configured lists. Supported algorithms are MD5 (for compatibility) and SHA-256. The `sha256sum` codec requires SHA-256. The standalone `checksums` command also accepts `--checksums-format`, keeping `--output` relative to the working directory.
532
+
533
+ #### Configuration and custom codecs
534
+
535
+ The optional top-level `integrity` configuration maps to these flags. Explicit CLI values override configuration; `--no-embed-checksums` and `--no-sign-checksums` disable their configured behaviors. Existing release configurations retain their `packages` array; use `packages: []` for a configuration containing only integrity settings.
536
+
537
+ ```js
538
+ export default {
539
+ packages: [],
540
+ integrity: {
541
+ codec: 'wordpress-json', // --checksums-format
542
+ algorithms: ['md5', 'sha256'], // --checksums-algorithms
543
+ file: 'checksums.json', // --checksums-file
544
+ embed: true, // --embed-checksums
545
+ sign: { formats: ['wordpress'] }, // --sign-checksums and --sign-format
546
+ },
547
+ };
548
+ ```
549
+
550
+ Use `codec: 'sha256sum'`, `algorithms: ['sha256']`, `file: 'SHA256SUMS'` and `sign: { formats: ['minisign'] }` for the GNU setup. Custom codecs provide `id`, a default `filename`, and `encode`/`decode` callbacks, which may be asynchronous. Generation validates the round trip before writing artifacts: codecs must preserve all selected file paths, represented digests and any represented package identity. They receive and return the existing WordPress-compatible `ChecksumsManifest` model (`plugin` or `theme`, optional `version`, and a `files` map of relative paths to digest fields), keeping format customization separate from selection, hashing and signing.
551
+
552
+ ```js
553
+ export default {
554
+ packages: [],
555
+ integrity: {
556
+ file: 'metadata/integrity.json',
557
+ embed: true,
558
+ sign: { formats: ['wordpress'] },
559
+ codec: {
560
+ id: 'custom-v1',
561
+ filename: 'integrity.json',
562
+ encode(manifest) {
563
+ return Buffer.from(JSON.stringify({ schema: 'custom-v1', payload: manifest }) + '\n');
564
+ },
565
+ decode(bytes) {
566
+ const document = JSON.parse(Buffer.from(bytes).toString('utf8'));
567
+ if (document.schema !== 'custom-v1') {
568
+ throw new Error('Unknown integrity schema.');
569
+ }
570
+ return document.payload;
571
+ },
572
+ },
573
+ },
574
+ };
575
+ ```
576
+
577
+ `zip` and `checksums` read the caller's release configuration. For verification of custom codecs or configured filenames, pass `--config wp-release.config.mjs` explicitly. See [Verify](#verify).
384
578
 
385
- ### Manifest inside the plugin
579
+ #### Manifest inside the plugin
386
580
 
387
- A release can ship its signed manifest inside the plugin, so that an installed copy can be checked later with the public key, and also publish the same two files as release assets. Both use the same files:
581
+ For a manifest that is also published as a separate release asset or deployed directly from a source directory, write and sign it in that directory before packaging. Use `zip --embed-checksums` as described [above](#filenames-embedding-and-signing) when only the archive should contain the generated metadata. The source-directory workflow uses the same manifest for installed copies and release assets:
388
582
 
389
583
  ```bash
390
584
  wp-release checksums build --slug my-plugin --output build/checksums.json
@@ -393,7 +587,7 @@ wp-release zip build --slug my-plugin
393
587
  wp-release deploy-svn build --trunk --svn-tag 1.2.0
394
588
  ```
395
589
 
396
- - 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.
590
+ - Write the manifest into the directory that is zipped and deployed. The `checksums` command leaves its output manifest and its signatures (`<name>.sig`, `<name>.minisig` and `<name>.sshsig`) out of the hashes, so running it again with the same output path and `--force` gives the same manifest. `verify` checks matching copies as described below. Sign it last, since the signature covers the exact bytes of the manifest.
397
591
  - `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.
398
592
  - 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:
399
593
 
@@ -406,6 +600,25 @@ wp-release deploy-svn build --trunk --svn-tag 1.2.0
406
600
 
407
601
  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.
408
602
 
603
+ ### Verify
604
+
605
+ `verify` checks a directory against a manifest from `checksums` or from the WordPress.org checksums API, and reports every changed, missing and extra file. It exits with `2` unless all files match:
606
+
607
+ ```bash
608
+ wp-release verify wp-content/plugins/my-plugin --checksums my-plugin.1.2.0.checksums.json
609
+ ```
610
+
611
+ - A file matches when its SHA-256 equals the manifest. Manifests without SHA-256 fall back to MD5, and a hash can also be a list of accepted values.
612
+ - `verify` doesn't read `.distignore`, so that an ignore file inside the checked directory can't hide added files. `--exclude <patterns>` skips paths in both the directory and the manifest, for files that aren't part of the release, such as caches.
613
+ - Manifest paths outside the package folder (absolute or with `..`) are rejected.
614
+ - `--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`.
615
+ - 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.
616
+ - 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).
617
+
618
+ 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).
619
+
620
+ Verification never executes configuration from inside a downloaded package. Without `--checksums`, `verify` reads the embedded filename selected by `--checksums-file`, explicit configuration, or the codec default. For a versioned embedded filename, pass that filename explicitly. With a public key, signatures are checked before decoding; the selected signature format must exist (WordPress `.sig` by default; pass `--signature-format minisign` or `ssh` for those formats), and all other present supported signature formats are also authenticated when verifying the embedded manifest. Without a public key, checksums detect changes but do not authenticate the publisher. Trusted public keys must come from outside the package.
621
+
409
622
  ### Sign
410
623
 
411
624
  The [`wp-release-sign`](https://github.com/wpelevator/wp-release-sign) GitHub Action runs this command.
@@ -420,7 +633,7 @@ WP_RELEASE_SIGNING_KEY=… wp-release sign my-plugin.1.2.0.zip my-plugin.1.2.0.c
420
633
  - 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.
421
634
  - `--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.
422
635
  - An existing signature file isn't overwritten without `--force`.
423
- - `zip --sign` signs a ZIP and its manifest as it writes them, and `signFiles()` signs files from a script.
636
+ - `zip --sign` signs only the ZIP; add `--sign-checksums` to sign its manifest too. `signFiles()` signs files from a script.
424
637
 
425
638
  #### Signature formats
426
639
 
@@ -440,7 +653,7 @@ wp-release sign my-plugin.1.2.0.zip --format wordpress,minisign,ssh
440
653
  - `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…`.
441
654
  - 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`.
442
655
 
443
- 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.
656
+ 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. Generate keys locally and store the private seed in your secret manager or CI secrets. Distribute the public key to verifiers. `keygen --github-output` is rejected to keep the private seed out of workflow outputs; regular and `--json` output both include the seed.
444
657
 
445
658
  ### Version check
446
659
 
@@ -506,7 +719,7 @@ Validation:
506
719
  - `--readme-only` can't be combined with `--svn-tag`, since a tag is a release snapshot that a readme update never changes.
507
720
  - `--version` defaults to `--svn-tag`, then the `Version` header of the deployed files; when `--version` and `--svn-tag` are both set they must match.
508
721
  - Pre-release versions like `1.0.0-rc.1` can be tagged, but syncing one to `trunk/` warns, since trunk is for the development version.
509
- - `--trunk` without `--svn-tag` works but warns, because trunk then differs from the stable tag that WordPress.org serves downloads from.
722
+ - `--trunk` without `--svn-tag` works but warns, to remind you that updating trunk alone doesn't create a version tag for stable downloads.
510
723
  - A version mismatch throws a `VersionMismatchError`, a `ValidationError` with the `mismatches` and a problem for each, with its file and line. See [Logging](#logging).
511
724
 
512
725
  ### Deploy to Git
@@ -587,6 +800,83 @@ A deploy releases the files of the working tree, so build the packages first. On
587
800
 
588
801
  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.
589
802
 
803
+ ### WordPress internationalization
804
+
805
+ `i18n-pot` extracts plugin/theme strings and writes a reproducible UTF-8 POT. `i18n-audit` reads the same source and returns findings without writing files. Both run with Node alone: no PHP, WordPress installation, database, WP-CLI or network requests. Extraction is independent of `files`, `.distignore` and Git `export-ignore` distribution rules.
806
+
807
+ ```sh
808
+ wp-release i18n-pot .
809
+ wp-release i18n-pot --package plugins/agent-pilot
810
+ wp-release i18n-pot --all
811
+ wp-release i18n-pot --package plugins/agent-pilot --check
812
+ wp-release i18n-audit --package plugins/agent-pilot --json
813
+ wp-release i18n-audit . --strict --suppress conflicting-comments
814
+ ```
815
+
816
+ Config-free source directories must have plugin/theme headers. The actual main file is discovered using the package metadata reader, including plugins whose main filename differs from their slug. The selected domain defaults to `Text Domain`, then the directory name. Calls with omitted domains belong to WordPress's `default` domain; block metadata without a domain inherits the package domain. `--all-domains` explicitly combines domains. A block's explicit `textdomain` always controls its messages.
817
+
818
+ `--package` selects one configured package and permits extraction-specific overrides; `--all` requires configuration and selects enabled WordPress packages only. Source directories, package selectors and `--all` are mutually exclusive. Config-free directory mode reads only its explicit options and metadata; it does not import repository config implicitly. A disabled package fails when explicitly selected. Configuration uses `i18n` at the root and on package entries, including an exact `glob` for an individual package, following the existing group model. Package entries remain non-overlapping; there is no additional package manifest configuration source.
819
+
820
+ ```js
821
+ export default defineConfig( {
822
+ root: 'packages',
823
+ i18n: {
824
+ headers: { 'Report-Msgid-Bugs-To': 'https://example.org/issues' },
825
+ fileComment: `Copyright (C) Example Company
826
+ This file is distributed under the same license as the Example plugin.`,
827
+ },
828
+ packages: [ {
829
+ glob: 'plugins/*',
830
+ i18n: {
831
+ include: [ '**/*.{php,js,jsx,ts,tsx,mjs,cjs}', '**/block.json', '**/theme.json' ],
832
+ exclude: [ '**/node_modules/**', '**/vendor/**', '**/tests/**', '**/build/**' ],
833
+ destination: 'languages',
834
+ strict: false,
835
+ },
836
+ } ],
837
+ } );
838
+ ```
839
+
840
+ Precedence is defaults, root configuration, package group/individual entry, then explicit command options. Scalars override, lists replace inherited lists, and gettext headers merge by key, case-insensitively, preserving intentionally empty values. A header set to `false` is removed, like `'Project-Id-Version': false` to keep the plugin version out of the POT so that it changes only when strings change; a later layer can set it again. `Content-Type` can't be removed, since it declares the UTF-8 charset. Options include `enabled`, `domain`, `allDomains`, `include`, `exclude`, `destination`, `headers`, `fileComment`, `references`, `strict`, `legacyGlobals` and audit `suppress`. `fileComment` adds static text above the POT header; each line is emitted as a gettext comment, and no copyright year is generated. The CLI accepts the same setting with `--file-comment`. Repeat `--include` or `--exclude` for multiple globs; these use case-sensitive [minimatch](https://github.com/isaacs/minimatch) semantics with `/` paths, `**`, brace expansion and dotfile matching, without gitignore negation or comma splitting. Default excludes cover dependency directories, dot directories, tests, minified files and generated translations. Source/build exclusions are a repository choice; this repository excludes `build`, `tools` and isolated vendors in its config.
841
+
842
+ The default output is `<Domain Path>/<text-domain>.pot`, or `languages/<text-domain>.pot`. A leading `/` in `Domain Path` is package-relative, and configured destinations cannot escape the package. `--destination` changes the package-relative directory. An explicit `--output <file.pot>` resolves against the command's working directory and requires one package. All multi-package destinations are validated before writing; each completed package replaces its file atomically, independently of other packages. `--check` compares bytes without writing and fails if the POT is absent or stale. Source references are preserved by default; `--no-references` omits them. `POT-Creation-Date` and `PO-Revision-Date` default to empty values; no current date or year is inserted. Name/version headers come from metadata, and `--header NAME=VALUE` overrides any default, including with an empty value, and `--header NAME` removes the header. The POT lists the plugin or theme header strings first, then the strings of PHP, JavaScript, `block.json` and other metadata files in source order, so related strings stay together for translators. Each entry has its extracted comments, references and flags before the strings, in the usual gettext layout. The default headers come in a fixed order, and custom headers follow them. No generator header or generated copyright comment is added; set `fileComment` for a static one.
843
+
844
+ #### Supported extraction syntax
845
+
846
+ PHP extraction uses `php-parser` in PHP 8.3 mode, tested with attributes, enums, match expressions, promoted properties and readonly classes. Supported WordPress functions are `__`, `_e`, `_x`, `_ex`, `_n`, `_nx`, `_n_noop`, `_nx_noop`, `esc_attr__`, `esc_attr_e`, `esc_attr_x`, `esc_html__`, `esc_html_e` and `esc_html_x`. Global and explicitly global calls, namespace fallback and explicit function imports/aliases are supported. Known file-local declarations and unrelated namespaced imports are excluded; methods and static methods are never translation calls. Named arguments resolve by PHP parameter name. Literal strings, static concatenation and non-interpolated heredoc/nowdoc preserve decoded escapes, Unicode and newlines. Counts may be dynamic; IDs, contexts and selected domains must be static.
847
+
848
+ JavaScript extraction uses Babel's parser and lexical scope traversal, without transforms or project Babel config. JS, JSX, TS, TSX, MJS, CJS, MTS and CTS are supported. Named imports/aliases and namespace imports from `@wordpress/i18n`, unshadowed `wp.i18n` calls and unbound canonical legacy `__`, `_x`, `_n` and `_nx` calls extract strings. `--no-legacy-globals` requires imported or `wp.i18n` bindings. Locally shadowed/reassigned imports are excluded. Static literals, templates without substitutions, concatenation and TypeScript assertions are supported. Destructured CommonJS imports, transpiled `(0, i18n.__)` forms and arbitrary minified bundles are outside the supported syntax; scan source and use explicit source/build mapping for compiled JSON.
849
+
850
+ A `translators:` comment attaches to the first translation call whose containing expression or nearest statement immediately follows it. Whitespace, parsed comments and ternary branch punctuation may intervene, but unrelated statements or argument expressions may not. Comments before outer wrappers reach nested translation calls; a comment is consumed once, so unrelated sibling calls do not inherit it. Multiline comment content is preserved. WordPress plugin/theme header labels serialize as extracted comments. Bundled metadata tables follow [WordPress 6.8 block fields](https://github.com/WordPress/wordpress-develop/blob/6.8/src/wp-includes/block-i18n.json) and [theme fields](https://github.com/WordPress/wordpress-develop/blob/6.8/src/wp-includes/theme-i18n.json), including styles, variations, presets, template names and nested block settings. Their upstream data license is GPL-2.0-or-later; tables are maintained intentionally with fixtures and never fetched during generation.
851
+
852
+ Invalid syntax, unreadable selected files, selected source symlinks, empty/reserved message IDs, conflicting plural definitions and output errors fail generation. Unsupported dynamic arguments warn and are fatal with `--strict`. Audit adds `placeholder-comment`, `unordered-placeholders`, `plural-placeholders`, `empty-message` and `conflicting-comments` findings; suppress these explicitly by code where justified. Placeholders support `%%`, positional indexes, numeric width/precision, padding/flags and PHP/JS sprintf conversion types. Audit checks extraction and message quality, not every untranslated user-facing string. Audit fails on errors; strict audit also fails on warnings. Failed checks return exit code `2`; unexpected I/O/option-loading errors follow the existing CLI error handling. `--json`, GitHub annotations, summaries and step outputs use the existing CLI reporting facilities.
853
+
854
+ #### Compile translated catalogs
855
+
856
+ Runtime formats require a translated PO, not an untranslated POT. PO updating, fuzzy matching and automatic translation are not provided. Compilation validates `Language` and `Plural-Forms`, excludes fuzzy, obsolete and untranslated entries, preserves contexts and plural forms, and never changes the input PO. An incompletely translated plural entry is excluded as a unit. Gettext parsing handles source encoding; runtime outputs use UTF-8. Each output is replaced atomically; JSON outputs are independent writes, and obsolete output files are not deleted automatically.
857
+
858
+ ```sh
859
+ wp-release i18n-mo languages/my-plugin-lv.po
860
+ wp-release i18n-php languages/my-plugin-lv.po
861
+ wp-release i18n-json languages/my-plugin-lv.po --domain my-plugin --map scripts.json --output languages
862
+ wp-release i18n-mo languages/my-plugin-lv.po --check
863
+ ```
864
+
865
+ MO and `.l10n.php` default beside the PO, with `.mo` and `.l10n.php` extensions; `--output` selects an explicit file relative to cwd. WordPress PHP catalogs require WordPress 6.5 or later. JSON requires an explicit domain; `--output` is a directory, defaulting to the PO directory. `--check` compares proposed bytes without writing and exits `2` on absent/stale outputs. Compiler commands are primitives and do not select configured packages.
866
+
867
+ A script map is a JSON object from normalized package-relative source references to arrays of normalized runtime script paths. Many source files can feed one bundle, and one source can feed several scripts. An explicit map selects only listed references; an empty target array excludes that source. Without a map, JS/MJS/CJS references use identity paths, while JSX/TypeScript references require mapping. PHP-only references do not enter JSON catalogs.
868
+
869
+ ```json
870
+ {
871
+ "src/blocks/editor.tsx": [ "build/editor.js", "build/shared.js" ],
872
+ "src/components/panel.js": [ "build/editor.js" ]
873
+ }
874
+ ```
875
+
876
+ JSON filenames are `<domain>-<Language>-<md5(runtime-path)>.json`; the WordPress `default` domain uses `<Language>-<md5(runtime-path)>.json`. WordPress's `.min.js` normalization applies consistently to grouping, hashing and the JSON `source`. Jed keys preserve context through `\u0004`, plural arrays contain all translated forms, and revision dates come from the PO metadata. `.l10n.php` writes safely escaped data arrays with NUL-separated plural translations; catalog text is never treated as executable PHP.
877
+
878
+ The package can be installed and used in any WordPress plugin or theme project; repository package selection is optional, and directory mode works without a wp-release config.
879
+
590
880
  ### Exit codes
591
881
 
592
882
  | Code | Meaning |
@@ -647,7 +937,7 @@ const { releases } = await release.version( { dryRun: true } );
647
937
  const { tags } = await release.tag( { dryRun: true } );
648
938
  ```
649
939
 
650
- `ChangesetRelease` runs the `changeset-*` commands: `add()`, `getStatus()`, `version()` and `tag()`, plus `getPlan()` and `getPackages()`. Each call reads the configuration, packages and pending changesets again, so one instance can version and then tag. It takes a git runner from `createGitRunner()` as `git`, so that tests can replace it. The functions `addChangeset()`, `getChangesetStatus()`, `versionChangesets()`, `tagChangesets()`, `getChangesetReleasePlan()` and `getChangesetPackages()` do the same with the default runners, in one call.
940
+ `ChangesetRelease` runs the `changeset-*` commands: `add()`, `getStatus()`, `version()` and `tag()`, plus `getPlan()` and `getPackages()`. Each call reads the configuration, packages and pending changesets again, so one instance can version and then tag. It takes a git runner from `createGitRunner()` as `git`, so that tests can replace it. Without a config file, it detects a single root project. Its optional `singleProject` settings accept `versions` (the existing version descriptors), `versionFrom` (the same selector as `--version-from`), `baseBranch`, `remote`, `changesets`, `readmeChangelog` and `changelogInsertAfter`; `tagFormat` remains a repository option. The functions `addChangeset()`, `getChangesetStatus()`, `versionChangesets()`, `tagChangesets()`, `getChangesetReleasePlan()` and `getChangesetPackages()` do the same with the default runners, in one call.
651
941
 
652
942
  ### Logging
653
943
 
@@ -738,9 +1028,24 @@ const { valid, changed, missing, extra } = await verifyChecksums( {
738
1028
 
739
1029
  `loadFileRules()` reads the ignore file and `--exclude` patterns into a gitignore matcher, optionally adds Git `export-ignore` paths, and `selectFiles()` lists the files it doesn't ignore. `compareChecksums()` compares two hash maps without reading any files, and `parseChecksums()` validates manifest JSON. `extractZip()` extracts a ZIP after the same checks as `deploy-svn --from-zip`.
740
1030
 
1031
+ Translations:
1032
+ ```js
1033
+ import { extractCatalog, auditCatalog, serializePot, generatePackagePot } from '@wpelevator/wp-release';
1034
+
1035
+ const { catalog, diagnostics, files } = await extractCatalog( {
1036
+ sourceDir: '/project/plugin',
1037
+ options: { domain: 'my-plugin' },
1038
+ } );
1039
+ const findings = auditCatalog( catalog );
1040
+ const pot = serializePot( catalog );
1041
+ const result = await generatePackagePot( { sourceDir: '/project/plugin', check: true } );
1042
+ ```
1043
+
1044
+ Parsers return messages and structured diagnostics with stable codes, severity and source-relative locations. Catalog messages retain IDs, contexts, plurals, translations, translator comments, flags and structured references. `aggregateCatalog()` selects/merges a domain, unions references/comments and diagnoses plural conflicts. `resolveI18nPackages()` is the separate config adapter; extraction and serialization primitives know nothing about the repository. Library functions do not log, parse process arguments or set exit codes. Extraction results report unique files consulted, including the main plugin/theme file used for headers and header strings even when source globs exclude it. `parseTranslatedPo()`, `compileMo()`, `compileScriptTranslations()`, `compilePhpTranslations()` and `compileTranslations()` expose the same typed operations to library callers.
1045
+
741
1046
  ## Development
742
1047
 
743
- The source is TypeScript in `src/`, with tests next to the files they test (`*.test.ts`). The published package contains only the compiled `dist/` with JavaScript and `.d.ts` files.
1048
+ The source is TypeScript in `src/`, with tests next to the files they test (`*.test.ts`). The published package contains only the compiled `dist/` with JavaScript and `.d.ts` files; tests and translation fixtures stay in the repository.
744
1049
 
745
1050
  ```bash
746
1051
  npm run build --workspace @wpelevator/wp-release
@@ -752,6 +1057,8 @@ npm run lint --workspace @wpelevator/wp-release
752
1057
  - `test` runs the `.ts` tests directly with `node --test`, using Node.js type stripping, so it needs Node.js 22.18 or later but no build. The SVN integration tests in `svn-deploy.integration.test.ts` create repositories with `svnadmin` and deploy to their `file://` URLs; they're skipped when `svn` or `svnadmin` isn't installed.
753
1058
  - `lint` runs ESLint and a `tsc` type check.
754
1059
 
1060
+ The i18n tests validate gettext round trips, script catalogs and generated PHP data without launching PHP, WP-CLI or WordPress, covering contexts, plural forms, comments, flags, Unicode, escaping and PHP-looking input as data.
1061
+
755
1062
  The `wpelevator-source` export condition points at `src/index.ts`. The wp-release tests and monorepo CLI scripts use it to work with the source instead of a `dist/` build. Action builds use the declared dependency's default export, which resolves to `dist/` for the workspace copy and the published package. Build wp-release before building the actions.
756
1063
 
757
1064
  `execute( 'deploy-svn', args, { cwd, logger, svnRunner } )` runs a single-plugin deployment or a configured plan or deployment. The optional `svnRunner` replaces the external command runner in both modes; without it, the command uses its normal runner.
@@ -760,10 +1067,10 @@ The `wpelevator-source` export condition points at `src/index.ts`. The wp-releas
760
1067
 
761
1068
  wp-release has two layers, so that it fits any repository layout:
762
1069
 
763
- - **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>`.
764
- - **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.
1070
+ - **Primitives** work on the directory, file or values they're given and know nothing about the repository: `build <dir>`, `zip <dir>`, `checksums`, `sign`, `verify`, `version-check`, `deploy-git <dir>`, `deploy-svn <dir>`, `i18n-pot <dir>`, `i18n-audit <dir>` and the `i18n-mo`, `i18n-json` and `i18n-php` compilers.
1071
+ - **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`), how their translations are extracted (`i18n`), where they deploy (`deploy`) and the git `remote`. The changeset commands (with a single-project fallback when no config exists), `deploy-git` and `deploy-svn --release-ref`, `build --package` and `zip --package`, and `i18n-pot` and `i18n-audit` with `--package` or `--all` belong to it, and the global `--config` and `--tag-format` options select or adjust the config. `zip` and `checksums` also read caller configuration for integrity settings even without `--package`; verification reads it only when explicitly selected. 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.
765
1072
 
766
- Nothing assumes a directory structure or naming convention that the config doesn't set.
1073
+ Configured repositories use the layout and naming conventions their config sets. Config-free changeset releases use the root-only detection and defaults described above.
767
1074
 
768
1075
  The GitHub Actions are adapters to these commands: they use the same flag names and `execute()` entry point, with secrets supplied through environment variables. The SVN action injects a command runner that checks or installs SVN only when a deployment runs its first SVN command; config planning and deployment behavior remain in `wp-release`.
769
1076
 
@@ -780,6 +1087,7 @@ The changeset release modules, from the entry point down:
780
1087
  | Module | Responsibility |
781
1088
  |---|---|
782
1089
  | `changeset-release.ts` | `ChangesetRelease`, which plans releases and runs `add()`, `getStatus()`, `version()` and `tag()`, and the function form of each operation. |
1090
+ | `single-project.ts` | Resolving the config-free root project, its version source and targets, and shared release settings. |
783
1091
  | `changeset-config.ts` | Loading and validating `wp-release.config.js`, and the version source and target helpers. |
784
1092
  | `changeset-packages.ts` | Discovering the packages of the configured globs, with or without a version, and finding the changed ones. |
785
1093
  | `changeset-files.ts` | Reading, writing and consuming changeset files. |
@@ -795,6 +1103,23 @@ The changeset release modules, from the entry point down:
795
1103
  | `file-journal.ts` | `FileJournal`, which restores every file a failed `changeset-version` run changed. |
796
1104
  | `versions.ts` | Version checks and SemVer precedence. |
797
1105
 
1106
+ The internationalization modules in `src/i18n/`, from the entry points down:
1107
+
1108
+ | Module | Responsibility |
1109
+ |---|---|
1110
+ | `packages.ts` | `resolveI18nPackages()`, the config adapter that selects a directory, one package or all packages and resolves their options and POT paths. |
1111
+ | `options.ts` | Validating and merging the `i18n` options of the config layers and the CLI. |
1112
+ | `generate.ts` | `extractCatalog()` and `generatePackagePot()`, which read the selected files of a package and write or check its POT. |
1113
+ | `compile.ts` | `compileTranslations()`, which compiles a translated PO into MO, script JSON or `.l10n.php` and writes or checks the outputs. |
1114
+ | `files.ts` | Selecting source files with the include and exclude globs, independently of the distribution file rules. |
1115
+ | `php.ts`, `javascript.ts` and `metadata.ts` | Parsers for PHP, JavaScript and TypeScript, and `block.json`, `theme.json` and plugin and theme headers. |
1116
+ | `extraction.ts` | The translation functions and their arguments, shared by the parsers. |
1117
+ | `catalog.ts` | Catalog types, merging messages of a domain and the diagnostics. |
1118
+ | `audit.ts` and `placeholders.ts` | Audit findings and the sprintf placeholders that they check. |
1119
+ | `pot.ts` | Serializing a catalog as a POT. |
1120
+ | `po.ts`, `translation-json.ts` and `translation-php.ts` | Parsing translated PO files and compiling MO, script JSON and `.l10n.php`. |
1121
+ | `atomic-output.ts` | Validating output paths and replacing output files atomically. |
1122
+
798
1123
  ## Comparison with existing tools
799
1124
 
800
1125
  | Tool | What it does | How `wp-release` differs |
@@ -803,5 +1128,7 @@ The changeset release modules, from the entry point down:
803
1128
  | [`grunt-wp-deploy`](https://github.com/stephenharris/grunt-wp-deploy) | Grunt task with `deploy_trunk` and `deploy_tag` booleans, interactive password prompts and a `Stable tag` check. | No Grunt dependency and never interactive. `--readme-only` is a separate flag, where `grunt-wp-deploy` can only update trunk with the full build. Assets are independent of trunk writes. |
804
1129
  | [codeinwp theme deploy](https://github.com/codeinwp/action-wordpress-theme-deploy) | Fork of the 10up plugin action that commits themes to the themes SVN repository. | Doesn't deploy themes. Theme releases are ZIP uploads on WordPress.org; `zip` builds the upload with the theme folder as the root. |
805
1130
  | [`wp dist-archive`](https://github.com/wp-cli/dist-archive-command) | WP-CLI command that zips a project using `.distignore`, with `--plugin-dirname` and `--filename-format`. | Same `.distignore` file and `{name}.{version}` style filename format, but needs only Node.js instead of PHP and WP-CLI. `.distignore` is matched with full gitignore syntax, including `!` negation, and `--distignore` / `--exclude` let you point at another ignore file or add patterns on the command line. Builds are reproducible, and `--checksums` writes a manifest next to the ZIP. |
1131
+ | [`wp i18n make-pot`](https://developer.wordpress.org/cli/commands/i18n/make-pot/) | WP-CLI command that extracts PHP, JavaScript, `block.json` and `theme.json` strings into a POT, with `--domain`, `--include`/`--exclude`, `--headers`, `--file-comment`, `--merge`/`--subtract` and audit warnings. | `i18n-pot` and `i18n-audit` need only Node.js, write byte-for-byte reproducible POTs without dates, and `--check` fails CI on a stale POT. Headers are set one at a time with a repeatable `--header NAME=VALUE` instead of a JSON object, and `--include`/`--exclude` take one glob each, so patterns never need escaping or comma splitting. `--no-references` and `--all-domains` say what they do. Monorepos keep these settings once in `wp-release.config.js` and select packages with `--package` or `--all` instead of running the command for every directory. The audit is a separate read-only command with stable finding codes for `--suppress`, instead of warnings mixed into generation. There's no `--merge`, `--subtract` or `--skip-*`; `--include` and `--exclude` cover the same needs. |
1132
+ | [`wp i18n make-mo`, `make-json` and `make-php`](https://developer.wordpress.org/cli/commands/i18n/) | WP-CLI commands that compile PO files into MO, script JSON and `.l10n.php` files. `make-json` maps sources with `--use-map` and removes JavaScript strings from the PO unless `--no-purge` is passed. | `i18n-mo`, `i18n-json` and `i18n-php` compile one translated PO at a time, never change it, and leave out fuzzy, obsolete and untranslated entries. `i18n-json` takes a many-to-many `--map` from source files to built scripts, and JSX and TypeScript sources need one, so catalogs are never written for files WordPress doesn't load. `--check` compares the outputs without writing. There's no `update-po`, so update PO files from the POT with gettext's `msgmerge` or a translation platform. |
806
1133
  | [`wp plugin verify-checksums`](https://developer.wordpress.org/cli/commands/plugin/verify-checksums/) | WP-CLI command that checks installed plugins against the WordPress.org checksums API. | Reads the same manifest format, but from a file, so it also works for plugins and themes that aren't hosted on WordPress.org and for build directories in CI. `checksums` writes the manifest that WordPress.org generates for its own plugins. |
807
1134
  | [`wp-scripts plugin-zip`](https://github.com/WordPress/gutenberg/tree/trunk/packages/scripts#plugin-zip) | Zips files from `package.json` `files` or a fixed allowlist, named after the npm package, with `--root-folder` / `--no-root-folder`. | Reuses the root folder flags, selects files with `.distignore` by default and can optionally honor Git `export-ignore` attributes, puts the version from the plugin headers in the filename and streams the archive instead of building it in memory. |