Packaging and integrity
Build a directory
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.
wp-release build --output dist/code-prettify
wp-release build --package plugins/alpha --output dist/alphaThe 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.
--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.
wp-release build --output dist/code-prettify --sign-checksumsIn 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.
ZIP
The wp-release-zip GitHub Action runs this command.
zip writes {slug}.{version}.zip to the current directory, or to --output <file>, with the selected files in a {slug}/ root folder:
wp-release zip dist --slug my-plugin --checksums--root-folder <name>changes the root folder and--no-root-folderputs the files at the root of the ZIP.--filename-formatchanges the name, like--filename-format {slug}formy-plugin.zip.- Entries are sorted and use a fixed file mode and a fixed timestamp (1980-01-01), or the
SOURCE_DATE_EPOCHUnix time when that environment variable is set, so the same files give the same ZIP. Built-in metadata is deterministic except for embedded minisign signatures, which include their signing timestamp; custom codec callbacks control their own encoded bytes. --checksumsalso writes the checksums manifest of the archived files next to the ZIP, with.checksums.jsoninstead of.zip.--signsigns only the ZIP withWP_RELEASE_SIGNING_KEY.--sign-checksumsgenerates 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.--sign-formatselects formats for requested signatures, such as--sign-format wordpress,minisign,ssh. It requires--signor--sign-checksumsand never enables signing by itself. Signing keys, formats and existing signature destinations are validated before writing the ZIP or manifest.--forcealso overwrites requested signatures. Job summaries include signatures and public keys; JSON results includesignaturesandpublicKey.- An existing ZIP or manifest isn't overwritten without
--force. A ZIP or manifest written inside the source directory is never archived. --package <name>zips a package of the configured packages, by identifier or path under the configured root likeplugins/alpha, instead of a source directory: its directory with thefilesconfig of its package group, the same files that its deploys select. The slug defaults to the directory name and the version to theVersionheader, and--slugand--versionstill set them. Any package of the config works, with or without a version or deploy targets. To zip what a deploy just released, pass thepackageandversionof thedeploy-git --release-refresult, 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--jsonresult has thepackage,slugandversion.zipPackage()builds the same ZIP from a script, withgetPackageFiles()for the directory and the files of a package.
To embed a signed manifest with a stable filename, use:
wp-release zip dist --checksums-file checksums.json --embed-checksums --sign-checksumsSee Checksums for filenames, placement, formats and configuration.
Signing targets for builds and ZIPs
| Flag | zip | build |
|---|---|---|
--checksums | Generate an unsigned manifest beside the ZIP. | Generate an unsigned manifest beside the directory. |
--sign | Sign only the ZIP. | Unsupported. |
--sign-checksums | Generate and sign the manifest. | Generate and sign the manifest. |
--sign-format | Choose formats for explicitly requested signatures. | Same; requires --sign-checksums. |
wp-release zip --slug code-prettify --sign --sign-checksums
wp-release build --output dist/code-prettify --sign-checksums --sign-format minisignThese 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.
Checksums
The wp-release-checksums GitHub Action runs this command.
checksums writes a manifest with the MD5 and SHA-256 hash of every selected file, in the format of the WordPress.org plugin checksums API (https://downloads.wordpress.org/plugin-checksums/{slug}/{version}.json). Paths are relative to the plugin or theme folder, sorted, and use forward slashes:
{
"plugin": "my-plugin",
"version": "1.2.0",
"files": {
"my-plugin.php": {
"md5": "…",
"sha256": "…"
}
}
}Themes use a theme key instead of plugin, and the version is left out when it's unknown. The manifest is named {slug}.{version}.checksums.json after --filename-format, the same name as the ZIP with .checksums.json instead of .zip, so the two are published side by side; --output <file> writes it elsewhere. A manifest written inside <source-dir> is never listed in itself.
# Hash the build that is zipped and deployed
wp-release checksums dist --version 1.2.0Because 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.
Filenames, embedding and signing
--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.
# External manifest beside releases/my-plugin.1.5.1.zip.
wp-release zip dist --output releases/my-plugin.1.5.1.zip --checksums-file checksums.json --sign-checksums
# Signed manifest inside the package, with a stable filename.
wp-release zip dist --output releases/my-plugin.1.5.1.zip --checksums-file checksums.json --embed-checksums --sign-checksums
# Sign the completed downloadable archive separately.
wp-release sign releases/my-plugin.1.5.1.zip
# Verify installed files or an archive using its embedded manifest.
wp-release verify installed-plugin --public-key "$PUBLIC_KEY"
wp-release verify --zip releases/my-plugin.1.5.1.zip --public-key "$PUBLIC_KEY"--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.
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.
Formats
--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.
wp-release zip dist --embed-checksums --checksums-format sha256sum --sign-checksums --sign-format minisign
wp-release verify --zip my-plugin.1.5.1.zip --checksums-format sha256sum --public-key "$MINISIGN_PUBLIC_KEY" --signature-format minisign
# From the extracted package directory:
minisign -Vm SHA256SUMS -P "$MINISIGN_PUBLIC_KEY"
sha256sum --check SHA256SUMS--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.
Configuration and custom codecs
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.
export default {
packages: [],
integrity: {
codec: 'wordpress-json', // --checksums-format
algorithms: ['md5', 'sha256'], // --checksums-algorithms
file: 'checksums.json', // --checksums-file
embed: true, // --embed-checksums
sign: { formats: ['wordpress'] }, // --sign-checksums and --sign-format
},
};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.
export default {
packages: [],
integrity: {
file: 'metadata/integrity.json',
embed: true,
sign: { formats: ['wordpress'] },
codec: {
id: 'custom-v1',
filename: 'integrity.json',
encode(manifest) {
return Buffer.from(JSON.stringify({ schema: 'custom-v1', payload: manifest }) + '\n');
},
decode(bytes) {
const document = JSON.parse(Buffer.from(bytes).toString('utf8'));
if (document.schema !== 'custom-v1') {
throw new Error('Unknown integrity schema.');
}
return document.payload;
},
},
},
};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.
Manifest inside the plugin
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 when only the archive should contain the generated metadata. The source-directory workflow uses the same manifest for installed copies and release assets:
wp-release checksums build --slug my-plugin --output build/checksums.json
wp-release sign build/checksums.json
wp-release zip build --slug my-plugin
wp-release deploy-svn build --trunk --svn-tag 1.2.0-
Write the manifest into the directory that is zipped and deployed. The
checksumscommand leaves its output manifest and its signatures (<name>.sig,<name>.minisigand<name>.sshsig) out of the hashes, so running it again with the same output path and--forcegives the same manifest.verifychecks matching copies as described below. Sign it last, since the signature covers the exact bytes of the manifest. -
zipanddeploy-svncarry the pair like any other file. Don't usezip --checksumshere, since it would write a second manifest that lists the first one. Attachbuild/checksums.jsonandbuild/checksums.json.sigto the GitHub release as they are. -
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:
wp-release verify --zip https://downloads.wordpress.org/plugin/my-plugin.1.2.0.zip \ --checksums build/checksums.json --public-key "$PUBLIC_KEY" -
The manifest can have any name and location, for example
--output build/includes/signatures/checksums.json, and missing folders are created. Tellverifywhere the shipped copy is with--embedded <path>, like--embedded includes/signatures/checksums.json. Both the--checksumsfile 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,verifylooks 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.sigwithout an original is reported as not in the checksums.
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.
Verify
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:
wp-release verify wp-content/plugins/my-plugin --checksums my-plugin.1.2.0.checksums.json- 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.
verifydoesn'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.- Manifest paths outside the package folder (absolute or with
..) are rejected. --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 asdeploy-svn --from-zip.- The version in the manifest must match the
Versionheader of the checked files, so that an older release with its own valid manifest can't pass as a newer one. - 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
--checksumsfile or at the path of--embedded, is skipped when comparing files, but it must be identical to the--checksumsfile and its signature. The signature is the.sigfile, 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.
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.
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.
Sign
The wp-release-sign GitHub Action runs this command.
sign writes a detached signature next to each file, <file>.sig, so that downloads and installed copies can be trusted. Sign both the ZIP and the checksums manifest: the ZIP signature covers installs and updates, and the manifest signature covers the contents after unpacking and copies that weren't built by you.
WP_RELEASE_SIGNING_KEY=… wp-release sign my-plugin.1.2.0.zip my-plugin.1.2.0.checksums.json- 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 againstsodium_crypto_sign_verify_detached()in PHP. A.sigfile can hold several signatures, one per line, and a file is valid when any of them match, which allows a key rotation window. - The key is read from the
WP_RELEASE_SIGNING_KEYenvironment variable only, as a base64 32-byte Ed25519 seed or a 64-byte libsodium secret key, the formats thatsodium_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. --verboseprints the public key of the signing key in the form of each written format.--jsonreturns it aspublicKey, and in every form aspublicKeys. Pass it toverify --public-keyand to the plugin that checks the signatures.- An existing signature file isn't overwritten without
--force. zip --signsigns only the ZIP; add--sign-checksumsto sign its manifest too.signFiles()signs files from a script.
Signature formats
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:
wp-release sign my-plugin.1.2.0.zip --format wordpress,minisign,ssh| Format | File | Checked by |
|---|---|---|
wordpress | <file>.sig | WordPress core's verify_file_signature(), update-client and wp-release verify |
minisign | <file>.minisig | minisign and compatible tools: minisign -Vm <file> -P <minisign public key> |
ssh | <file>.sshsig | OpenSSH 8.1 or later: ssh-keygen -Y verify -f allowed_signers -I <identity> -n file -s <file>.sshsig < <file> |
minisignsigns the BLAKE2b-512 digest of the file, likeminisign -Sdoes 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 verifydoesn't compare key IDs, so signatures of keys that minisign generated verify too.sshis the armoredSSHSIGformat thatssh-keygen -Y signwrites, with a SHA-512 digest and thefilenamespace, so that the signature can't pass as one for commits or another namespace. It's written to.sshsiginstead of the.sigofssh-keygen, which thewordpressformat uses. Put the SSH public key in anallowed_signersfile as<identity> ssh-ed25519 AAAA….- 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-keygenand libsodium, and check the signatures thatminisignandssh-keygenwrite withwp-release verify.
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.
Version check
version-check compares the versions that have to agree before a release, and reports every mismatch with exit code 2:
wp-release version-check --constant MY_PLUGIN_VERSION --git-tag v1.2.0- The expected version is
--version, or theVersionheader. - The readme
Stable tagmust match for plugins;--no-stable-tagskips it, and themes have no Stable tag. --constant NAME(repeatable) checksdefine( 'NAME', '1.2.0' )orconst NAME = '1.2.0'in every PHP file outsidevendor/,node_modules/and dot directories. A constant that isn't defined is a mismatch.--git-tagdefaults toGITHUB_REFwhen it's a tag.--git-tag-format(default{version}) can have a prefix or the slug, likev{version}or{slug}-{version}; a leadingvin the version is ignored, like for SVN tags.