The short answer: push a Git tag, GitHub Actions builds the ZIP and deploys it, and every connected WordPress site pulls the update through the standard update mechanism. No dashboard logins, no email attachments, no manual file transfers.
This guide covers how to get there, whether you're distributing to WordPress.org or shipping private and commercial plugins to client sites.
Why manual plugin releases slow you down
The typical plugin release process looks something like this. You finish the code, run composer install --no-dev, delete the tests/ folder, manually zip everything up, log into your update server or each client's wp-admin, and upload. Then you repeat that across however many sites are running the plugin.
That process works fine when you're shipping once a month. It falls apart when you're shipping every week, or when you have twenty client sites running the same plugin, or when a bug fix needs to go out at 11pm.
There's also a subtler cost: slow releases make you avoid releasing. You batch small changes waiting for something bigger to justify the overhead. Bugs stay live longer. Small improvements sit in branches.
Modern software workflows solved this a decade ago. WordPress plugin distribution has been slower to catch up.
What GitHub Actions gives you
GitHub Actions is a CI/CD platform built into GitHub. You define a workflow in a YAML file, and GitHub runs it automatically when you push code, create a tag, or publish a release.
For WordPress plugin releases, the relevant trigger is a Git tag or a GitHub release. When you push v1.2.3, the workflow runs, builds your plugin ZIP and ships it wherever it needs to go.
Builds run on a clean machine every time: same steps, same result, regardless of what's on your laptop. Every release ties back to a specific commit, with logs showing exactly what ran. The build and upload happen automatically; you push a tag and walk away.
WordPress.org deployment: the 10up action
If your plugin lives on the WordPress.org plugin repository, 10up's action-wordpress-plugin-deploy is the standard tool. It's been in production use since 2019 and handles the Git-to-SVN translation that WordPress.org requires.
WordPress.org uses SVN, not Git. The action takes your Git tag, strips the files you've marked with export-ignore in .gitattributes (or listed in a .distignore file), and commits the result to the SVN repository with the same tag name.
A minimal workflow looks like this:
# .github/workflows/deploy-wporg.yml
name: Deploy to WordPress.org
on:
push:
tags:
- '*'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to WordPress.org
uses: 10up/action-wordpress-plugin-deploy@stable
env:
SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }}
SVN_USERNAME: ${{ secrets.SVN_USERNAME }}
Add your WordPress.org SVN credentials as repository secrets and the action handles the rest. You can customise the plugin slug with a SLUG env variable if your GitHub repository name differs from your WordPress.org slug.
For updating plugin assets (screenshots, banners, the readme) without cutting a new release, 10up also ships action-wordpress-plugin-asset-update, which syncs only the /assets directory.
The gap: private and commercial plugins
WordPress.org only accepts free, GPL-licensed, publicly distributed plugins. If you've built something for a specific client, or you're distributing a commercial plugin outside the repository, none of that infrastructure is available.
Your options before CI/CD became accessible were essentially: email ZIPs to clients, log into each wp-admin manually, or build your own update server. The wp-update-server library gives you a self-hosted update endpoint, but you're responsible for running the server, and the upload step is still manual; you've just changed where you upload the ZIP.
What's been missing is the combination: a managed update delivery layer that also connects to your GitHub release pipeline. That's where AutomagicWP fits. For a comparison of all the distribution options (Freemius, WordPress.org, wp-update-server, and AutomagicWP), see Best Freemius Alternative for WordPress Plugins. If you're already shipping with AI tools and hitting the distribution bottleneck, Ship WordPress Plugin Updates Without ZIP Files covers the full picture.
Private plugin deployment with AutomagicWP
AutomagicWP is a plugin update distribution platform for private and commercial plugins. Connected WordPress sites see update notifications through the standard wp-admin update UI, exactly like WordPress.org plugins, but the files come from your AutomagicWP account.
The WordPress-side integration adds an Update URI header to your plugin (introduced in WordPress 5.8) and a small PHP library that hooks into the update_plugins_automagicwp.com filter. From WordPress core's perspective it looks like any other update source.
AutomagicWP has two paths for GitHub integration, and you pick based on whether your plugin needs a build step.
Path A: Webhook (PHP-only plugins)
If your plugin is pure PHP with no build step (no npm, no Composer dependencies, nothing that transforms source into distributable files), the webhook path is the lowest-friction option.
You generate a webhook secret in your AutomagicWP plugin settings, add it to your GitHub repository's Webhooks configuration, and point it at your plugin's webhook URL. When you publish a GitHub release, AutomagicWP receives the webhook, downloads the source ZIP directly from GitHub, repackages it with the correct WordPress folder structure, validates the plugin headers, and creates a new version.
No workflow file needed. Publishing the GitHub release is the only step.
You can exclude files using a .automagicwpignore file in your repo root, using the same gitignore syntax as .gitattributes export-ignore:
tests/
*.test.php
docs/
.editorconfig
Files that are always excluded regardless: .git/, node_modules/, .github/, .automagicwpignore itself.
Path B: GitHub Action (plugins with a build step)
If your plugin runs composer install, compiles assets, or does any transformation between source and distributable, use the GitHub Action path. You control the build, then hand the finished ZIP to AutomagicWP via a presigned upload URL.
# .github/workflows/deploy.yml
name: Deploy to AutomagicWP
on:
release:
types: [published]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- name: Install dependencies
run: composer install --no-dev --optimize-autoloader
- name: Package plugin
run: |
mkdir -p build/my-plugin
rsync -av --exclude='.git' --exclude='tests' --exclude='node_modules'
--exclude='.github' --exclude='build'
. build/my-plugin/
cd build && zip -r ../my-plugin.zip my-plugin/
- name: Deploy to AutomagicWP
uses: automagicwp/action@v1
with:
api-key: ${{ secrets.AUTOMAGICWP_API_KEY }}
plugin-id: YOUR_PLUGIN_ID
zip-path: ./my-plugin.zip
The action handles the upload in two steps: it requests a presigned PUT URL from AutomagicWP, uploads the ZIP directly to Cloudflare R2, then calls the confirm endpoint. AutomagicWP validates the plugin headers from the uploaded file and creates the version record.
Your plugin settings page in the AutomagicWP dashboard shows a pre-filled version of this YAML with your plugin ID already substituted in.
A complete workflow for plugins with Node.js assets
Many modern plugins build JavaScript or CSS. Here's a more complete workflow that handles both Composer and npm:
# .github/workflows/deploy.yml
name: Deploy to AutomagicWP
on:
release:
types: [published]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install PHP dependencies
run: composer install --no-dev --optimize-autoloader
- name: Install Node dependencies and build
run: |
npm ci
npm run build
- name: Get version from tag
id: version
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
- name: Package plugin
run: |
SLUG="my-plugin"
mkdir -p "build/$SLUG"
rsync -av
--exclude='.git'
--exclude='.github'
--exclude='node_modules'
--exclude='src'
--exclude='tests'
--exclude='*.test.php'
--exclude='build'
. "build/$SLUG/"
cd build && zip -r "../${SLUG}.zip" "$SLUG/"
- name: Deploy to AutomagicWP
uses: automagicwp/action@v1
with:
api-key: ${{ secrets.AUTOMAGICWP_API_KEY }}
plugin-id: ${{ secrets.AUTOMAGICWP_PLUGIN_ID }}
zip-path: ./my-plugin.zip
Store both AUTOMAGICWP_API_KEY and AUTOMAGICWP_PLUGIN_ID as repository secrets so they don't appear in your workflow file.
Tagging strategy and versioning
A consistent tagging strategy makes the whole workflow more predictable. Two approaches are common:
Semantic versioning tags (v1.2.3): Patch for bug fixes, minor for new features, major for breaking changes. Use the v prefix; the workflows above strip it when needed to produce a plain version number.
Calendar versioning (2026.02.1): Year, month, sequential release number. Useful for plugins where "features" and "breaking changes" aren't meaningful distinctions, or where clients ask "when was this updated?" more than "what version am I on?".
For automating the version bump itself, semantic-release derives the next version number from conventional commit messages and creates the tag. That removes the manual git tag v1.2.3 step entirely.
A minimal semantic-release config for a WordPress plugin:
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/github"
]
}
Combine this with the deploy workflow above and the full pipeline runs without any manual version management: merge to main, semantic-release creates the tag and GitHub release, and the deploy workflow picks up the release.published event.
Changelog automation
WordPress displays changelog content in the plugin details modal in wp-admin. If you want clients to see what changed before updating, you need to include a changelog with each release.
When you publish a GitHub release, the release description is automatically used as the changelog in AutomagicWP. Write it in the GitHub release notes, and it appears in the WordPress update screen.
With semantic-release installed, @semantic-release/release-notes-generator creates the release description from your conventional commits automatically. The commit messages become the changelog without any manual writing.
For projects using conventional commits, the output looks like:
## What's Changed
### Bug Fixes
- fix checkout flow on mobile (#42)
### Features
- add discount code support (#38)
That content appears in the WordPress plugin update modal when clients hover over the version number.
What the release workflow looks like end to end
With all of this in place, a plugin release looks like:
- Merge a PR to
main. - semantic-release reads the commits, determines the version bump, creates
v1.3.0, generates release notes, and publishes the GitHub release. - The deploy workflow triggers on
release.published, runs Composer and npm, packages the ZIP, and uploads it to AutomagicWP. - WordPress sites running the plugin pick up the update on the next 12-hour check cycle, or immediately when a user visits Dashboard → Updates.
No manual steps. Nobody touches a dashboard.

Plugin rankings and comparisons — done right
Track how your plugin performs over time, benchmark it against competitors, and spot trends before they move your install counts. Join the waitlist and be first in when it launches.
Frequently asked questions
Does this work for both WordPress.org plugins and private plugins?
Yes, but with different tooling. For WordPress.org, use 10up's action-wordpress-plugin-deploy action, which handles the Git-to-SVN translation. For private and commercial plugins, use AutomagicWP's action or webhook integration. The GitHub Actions patterns are similar; the destination is different.
Do I need to know how to write GitHub Actions YAML to set this up?
Not really. The AutomagicWP dashboard shows a pre-filled workflow YAML for your specific plugin ID. You copy it into .github/workflows/deploy.yml and add two repository secrets. The 10up action has an equally simple setup. Neither requires you to write YAML from scratch.
What's the difference between Path A (webhook) and Path B (GitHub Action)?
The webhook path is for plugins with no build step. AutomagicWP downloads your source ZIP from GitHub and handles the packaging. The GitHub Action path is for plugins that run Composer, compile assets, or need any transformation between source code and the distributable ZIP. You control the build; AutomagicWP handles the upload and version tracking.
Can I still trigger updates for sites without waiting for the 12-hour WordPress cron?
WordPress also runs update checks when a user visits Dashboard → Updates or the Plugins page. You can also trigger wp_update_plugins() via WP-CLI (wp plugin update --all). For immediate propagation across many sites, WP-CLI in a management tool like MainWP or ManageWP is the fastest path.
What happens to old plugin versions when I release a new one?
AutomagicWP stores every release. You can download any previous version from the dashboard and redeploy it. Connected sites will see it as an available update through the standard update UI. Nothing is deleted automatically.
Can I use this setup with GitLab CI or Bitbucket Pipelines instead of GitHub Actions?
AutomagicWP's deployment works via an API, so any CI platform that can make HTTP requests works. The presigned URL upload approach means you'd replicate what the GitHub Action does: call the upload-url endpoint, PUT the ZIP to the presigned URL, then call the confirm endpoint. GitLab CI and Bitbucket Pipelines both support this with standard curl commands in your pipeline YAML.