GitHub Action that generates a per-file checksum manifest for every WordPress
plugin or theme zip in a directory. Each <name>.zip gets a <name>.json next
to it, listing the MD5 and SHA-256 of every file inside the archive — the same
format WordPress.org publishes for plugins at
downloads.wordpress.org/plugin-checksums/<slug>/<version>.json.
Publish the manifest alongside your release and anything that can download it (a support tool, an integrity checker, WP-CLI-style verification) can tell whether the files on a site still match what you shipped.
The action only generates files. Uploading the zip and its manifest is up to your workflow, with whatever tool you already use.
- name: Build release zip
run: ./build.sh # produces artifact/my-plugin.zip
- name: Generate checksum manifests
uses: Codeinwp/action-release-checksums@main
with:
artifact_dir: artifact # default
- name: Upload release
run: aws s3 sync artifact/ s3://my-bucket/releases/my-plugin/${{ github.ref_name }}/
# artifact/ now contains my-plugin.zip and my-plugin.jsonWorks with any number of zips in the directory — one manifest is written per zip.
| Input | Default | Description |
|---|---|---|
artifact_dir |
artifact |
Directory containing the release *.zip files |
out_dir |
same as artifact_dir |
Where to write the <name>.json manifests |
strict |
true |
Fail the job if any zip cannot be processed (corrupt archive, empty archive, or no plugin/theme header found). Set to false to skip such zips with a warning instead. |
| Output | Description |
|---|---|
manifests |
JSON array of the manifest file paths that were written |
{
"plugin": "my-plugin",
"version": "1.4.2",
"type": "plugin",
"source": "https://github.com/org/repo/actions/runs/123456789",
"zip": "my-plugin.zip",
"generated_at": "2026-01-01T12:00:00.000Z",
"files": {
"my-plugin.php": { "md5": "…", "sha256": "…" },
"includes/class-a.php": { "md5": "…", "sha256": "…" }
}
}- The top-level key is
pluginortheme, holding the slug. - Slug = the zip's root folder (the conventional
my-plugin/...layout, which is also the directory the product is installed into). Flat zips fall back to the zip's base name. - Type and version come from the file headers inside the archive:
Theme Name:/Version:instyle.cssfor themes,Plugin Name:/Version:in the main PHP file for plugins. fileskeys are paths relative to the root folder,/-separated, sorted. Directories and symbolic links are never listed.sourceis the URL of the workflow run that produced the manifest (empty when run outside GitHub Actions).
Because files uses the same shape as WordPress.org's plugin checksums, a
consumer can treat both sources identically.
Any language works; the check is just "hash the local file, compare". For example, in PHP:
$manifest = json_decode( file_get_contents( $manifest_url ), true );
foreach ( $manifest['files'] as $path => $hashes ) {
$local = $install_dir . '/' . $path;
if ( ! is_file( $local ) ) {
$missing[] = $path;
} elseif ( hash_file( 'sha256', $local ) !== $hashes['sha256'] ) {
$modified[] = $path;
}
}- A runner with
unzipand Node.js 20+ (both present on the hostedubuntu-latestimage). No npm dependencies are installed at run time.
node src/generate.js --artifact-dir path/to/zips [--out-dir path] [--no-strict]npm test # node --test, no dependenciesGPL-2.0-or-later