WP Elevator Docs

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/alpha

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.

--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-checksums

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.

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-folder puts the files at the root of the ZIP.
  • --filename-format changes the name, like --filename-format {slug} for my-plugin.zip.
  • Entries are sorted and use a fixed file mode and a fixed timestamp (1980-01-01), or the SOURCE_DATE_EPOCH Unix 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.
  • --checksums also writes the checksums manifest of the archived files next to the ZIP, with .checksums.json instead of .zip.
  • --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.
  • --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.
  • 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 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.

To embed a signed manifest with a stable filename, use:

wp-release zip dist --checksums-file checksums.json --embed-checksums --sign-checksums

See Checksums for filenames, placement, formats and configuration.

Signing targets for builds and ZIPs

Flagzipbuild
--checksumsGenerate an unsigned manifest beside the ZIP.Generate an unsigned manifest beside the directory.
--signSign only the ZIP.Unsupported.
--sign-checksumsGenerate and sign the manifest.Generate and sign the manifest.
--sign-formatChoose 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 minisign

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.

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.0

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.

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 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.

  • 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.

  • 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. Tell verify where the shipped copy is with --embedded <path>, like --embedded includes/signatures/checksums.json. Both the --checksums file and its signature must then exist, both copies must be shipped, and they're skipped when comparing files and must be identical to the originals. The manifest can't list the embedded path. Without --embedded, verify looks for the manifest name at the root, and skips a file only when it exists, the matching original exists and the manifest doesn't list it. Otherwise it's checked like any other file, so a listed file that has the name of the manifest can't be deleted unnoticed, and a shipped .sig without an original is reported as not in the checksums.

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.
  • 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.
  • 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 as deploy-svn --from-zip.
  • 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.
  • 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.

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 against sodium_crypto_sign_verify_detached() in PHP. A .sig file can hold several signatures, one per line, and a file is valid when any of them match, which allows a key rotation window.
  • 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.
  • --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.
  • An existing signature file isn't overwritten without --force.
  • zip --sign signs only the ZIP; add --sign-checksums to 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
FormatFileChecked by
wordpress<file>.sigWordPress core's verify_file_signature(), update-client and wp-release verify
minisign<file>.minisigminisign and compatible tools: minisign -Vm <file> -P <minisign public key>
ssh<file>.sshsigOpenSSH 8.1 or later: ssh-keygen -Y verify -f allowed_signers -I <identity> -n file -s <file>.sshsig < <file>
  • minisign signs the BLAKE2b-512 digest of the file, like minisign -S does by default, and adds a trusted comment with the signing time and file name that a second signature covers. minisign generates a random key ID for its keys, but the key here is a plain Ed25519 seed, so wp-release derives the key ID from the public key. wp-release verify doesn't compare key IDs, so signatures of keys that minisign generated verify too.
  • 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….
  • 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.

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 the Version header.
  • The readme Stable tag must match for plugins; --no-stable-tag skips it, and themes have no Stable tag.
  • --constant NAME (repeatable) checks define( 'NAME', '1.2.0' ) or const NAME = '1.2.0' in every PHP file outside vendor/, node_modules/ and dot directories. A constant that isn't defined is a mismatch.
  • --git-tag defaults to GITHUB_REF when it's a tag. --git-tag-format (default {version}) can have a prefix or the slug, like v{version} or {slug}-{version}; a leading v in the version is ignored, like for SVN tags.

On this page