Skip to main content
Tutorials

How to Deploy a WordPress Plugin to WordPress.org

How WordPress.org's SVN plugin release works, where releases go wrong, and how svnPush — a free, open-source app — makes each deploy checked and previewed.

Arif Hossain
Arif Hossain
11 min read
ShareXLinkedInReddit
Abstract cover of a cone resting on a cylinder like an upward arrow beside a glass sphere, representing a plugin release pushed to WordPress.org

You finished the feature. The tests pass, the changelog is written in your head, and the Git tag is pushed. All that's left is putting the new version on WordPress.org, where every site that has your plugin installed will see the update. The job should take two minutes, and somehow it takes forty.

Your code lives in Git, but the WordPress.org plugin directory lives in Subversion. For most WordPress developers, SVN is a tool used a few times a year, only on release day, and only from memory. This article walks through how WordPress.org plugin deployment actually works, where it tends to go wrong, and how svnPush, a free, open-source desktop app, turns it into a checked, repeatable routine.

How WordPress.org plugin releases actually work

When your plugin is approved for the directory, you get a Subversion repository at https://plugins.svn.wordpress.org/your-plugin. It has three top-level folders that matter:

  • trunk/ holds the current development copy of the plugin.
  • tags/ holds one folder per released version, such as tags/1.4.2/. This is what sites actually download.
  • assets/ holds your plugin page's banners, icons and screenshots. They are never part of the plugin zip.

The link between them is the Stable tag line in trunk/readme.txt. WordPress.org reads it and serves the matching folder from tags/ as the current version. So a release is really a sequence:

  1. Bump the version everywhere it appears: the Version: header in your main plugin file, the Stable tag: in readme.txt, the changelog entry, the upgrade notice, and often a PHP constant or package.json.
  2. Build the plugin if it has a build step, and work out which files belong in the release and which are development-only.
  3. Check out (or update) your SVN working copy.
  4. Copy the release files into trunk/, and add or remove files so SVN knows what changed.
  5. Commit trunk.
  6. Copy trunk to tags/<version> and commit that.
  7. Update assets/ if your banner or screenshots changed.
  8. Open the plugin page and hope the right version is showing.

None of these steps is hard. The trouble is doing all eight correctly, in order, from memory, at the end of a long piece of work.

Where releases go wrong

If you have shipped a few plugin updates, you have probably met at least one of these.

The version disagrees with itself. The header says 1.4.2, the Stable tag still says 1.4.1, and the constant your code uses for cache-busting says 1.4.0. Depending on which one is wrong, sites either never see the update or see it with the wrong version number.

The readme gets rejected or mangled. The directory has its own readme.txt parser with rules about required headers, short-description length, the number of tags and more. A readme that looks fine in your editor can render badly or trigger warnings on the plugin page.

Something ships that shouldn't. Your Git repository contains a .env, a test fixture with real credentials, a node_modules folder, or a stray wp-config.php from local development. Copy the wrong folder into trunk and it is now public, downloadable, and pushed to every site that auto-updates.

The tag didn't land. Trunk got committed, then the connection dropped or you forgot the second commit. Now trunk says 1.4.2 and there is no tags/1.4.2 for WordPress.org to serve.

The assets are silently ignored. A banner at 772×249 instead of 772×250, or a screenshot whose number doesn't match the readme, and the plugin page shows the old image with no error.

Every one of these is a checkable condition. That's the idea behind svnPush.

What svnPush does

svnPush is a desktop app for Windows, macOS and Linux that takes a WordPress plugin from a local folder to a published, verified WordPress.org release. Its rule is simple: AI drafts, deterministic checks decide, and nothing is committed until you click Publish.

A release is a seven-step checklist, and each step shows exactly what it did:

  1. Detect. Read the plugin header and readme, and find what changed since your last Git tag.
  2. Changes and draft. Propose the new version, changelog entry, upgrade notice and summary, by hand or with AI.
  3. Write. Update every version source together and show the result as a diff.
  4. Verify. Run the release gate: 17 blocking checks and 12 warnings.
  5. Build. Run your pre-build command and assemble exactly the package that will ship.
  6. Preview SVN. List every file that will be added, modified or deleted in trunk and assets.
  7. Publish. Commit trunk, create the tag on the server, and verify the tag exists.

Every version source in step

The Write step updates the plugin header, the readme's Stable tag, the changelog, the upgrade notice, and any extra locations you configure in one pass. An extra location is a file plus a regular expression with one capture group, which covers a PHP constant, package.json, or anything else that holds the version. You see the whole change as a diff before it is written. The first blocking check, V01, then confirms that every version source holds the same value.

A release gate that actually stops you

Seventeen blocking checks decide whether a release may go ahead. A failed one stops the release and shows its fix. A few examples from the list:

  • The version is valid, newer than the previous release, and not already tagged.
  • The readme's changelog names this version first, and the Stable tag is this version.
  • The readme has every required header, and the short description is 150 characters or fewer.
  • The Text Domain equals the plugin slug, and the main plugin file blocks direct access.
  • The package contains every required path, and no archive, executable or version-control folder.
  • Subversion 1.10 or newer is available, the working copy is conflict-free and up to date, and your SVN credentials are in the vault.
  • readme.txt passes the WordPress.org readme validator with no errors.

Twelve warnings cover softer problems. They flag "Tested up to" lagging behind the current WordPress version, more than five readme tags, readme screenshots that don't match the assets folder, an oversized vendor/, and plugin images with the wrong names or sizes.

The readme validator, built in

svnPush checks readme.txt with the same rules as the official WordPress.org readme validator, instantly and offline. It runs on every release and whenever you click Check readme.txt on the project page. Validator errors stop the release; warnings are shown. For the few checks that need WordPress.org's own data, svnPush links you to the official validator.

Secrets never ship

Which files reach WordPress.org is decided by a .distignore file. It uses the same format as .gitignore and is the WordPress standard also used by WP-CLI and the 10up deploy action. If your plugin doesn't have one yet, svnPush pauses at Build and proposes one. It leaves out hidden files and folders like .git, .github and .env, as well as docs/, tests/, node_modules/, and developer files like composer.json. You see exactly what will be released and what is left out, edit the rules if you need to, and save. The same check comes back on your first release and whenever new top-level files or folders appear.

And regardless of any rule, a .env file, a private key, an SSH key or wp-config.php in the package stops the release (check V11).

Safe SVN, with a way back

svnPush uses a sparse working copy, so it doesn't download every tag you have ever released. It shows a full preview of trunk and assets before anything is committed. After you confirm Publish, it commits trunk, creates the tag server-side, and verifies the tag is there before opening your plugin page.

Before the trunk commit, you can cancel at any step and every change is rolled back. If a release is interrupted after trunk was committed, Resume creates only the tag, so you never commit the same trunk twice.

A practical workflow

Here's what a real release looks like once svnPush is set up.

One-time setup. On first launch, the Help tab opens a setup checklist. If Subversion isn't installed, Install Subversion does it in one click: through winget on Windows or Homebrew on macOS, while the Linux packages install it automatically. In the Vault, add your WordPress.org account with host plugins.svn.wordpress.org, your username, and your SVN password. That is not your account password; you'll find it on your WordPress.org profile under Account & Security → Subversion password. It is stored in your operating system's keychain and passed to svn on stdin, never through a file or a log.

Add the project. Choose the plugin folder and enter its SVN URL. svnPush reads the header and readme. In Project settings you can set a package root such as dist, the main plugin file, extra version locations, required paths, a pre-build command such as npm run build, the assets folder (.wordpress-org by default), and post-publish options like creating a matching Git tag.

Working in a team? Commit the same settings as a .svnpush.json file in the plugin folder, so everyone who releases the plugin uses the same package root, build command and version locations. The file itself is never packaged.

Rehearse it. Turn on Dry run and click Dry run. Approve the draft, read the checks and the SVN preview, and svnPush restores your files afterwards. A rehearsal costs nothing, and it is the fastest way to find out that a check is going to fail before release day.

Release. Click Release, approve the draft, check the preview, and confirm Publish. The tag is verified on the server and your plugin page opens.

Want to test the zip first? Build package produces exactly the folder and zip a release would publish, without touching SVN, so you can install it on a test site.

Updating plugin page assets without a release

New banner, new icon, new screenshots, but no new version? Update assets syncs and commits only the files in your .wordpress-org folder, with the same preview and confirmation as a release and no version change.

The Plugin images card on the project page lists each file with its pixel size and tells you what WordPress.org needs: icon-128x128.png and icon-256x256.png (or icon.svg plus the PNGs), banner-772x250.png and banner-1544x500.png, and one screenshot-N.png per caption in your readme's Screenshots section. Wrong names or sizes are reported as warning W12 before you upload.

Where AI fits, and where it doesn't

svnPush can use AI, and it's deliberately careful about how.

When you connect a provider, it suggests the version, changelog entry, upgrade notice and summary from what changed: the diffs of changed files, commit subjects since your last Git tag, and readme excerpts. When a blocking check fails, it can explain why and suggest readme fixes as a diff you apply with one click. Revoye is the recommended provider, and Gemini, Claude, OpenAI, OpenRouter, DeepSeek, Qwen, Perplexity, any OpenAI-compatible endpoint, or a local model like Ollama that needs no key all work too.

What AI never does is decide. The output is plain text that you edit and approve. It never runs commands or writes files. The release gate is deterministic, and a release never depends on AI. You can switch it off per project, and before a provider receives anything for the first time, svnPush shows you exactly what will be sent. Files that match your exclude patterns, and anything that looks like a secret (.env, *.pem, *.key, wp-config.php), are never sent.

Who svnPush is for

  • Independent plugin developers who release a few times a year and would rather not relearn SVN each time.
  • Agencies with a portfolio of plugins and several people shipping them.
  • Teams where the person releasing isn't the person who wrote the release procedure.
  • Plugins with a build step, where what ships is a dist folder, not the repository.
  • Monorepos and Git-first workflows, where plugins in a subfolder of a larger repository still list their changes correctly.

We publish plugins to the directory ourselves, including GuestDock, AuthDock and SyncDock. svnPush is the tool we wanted for that job.

Why open source

A tool you trust with your WordPress.org SVN password, and with the code that ends up on thousands of sites, is one you should be able to read. svnPush is MIT-licensed and built in the open. The repository documents the exact release protocol, every check, rollback and resume, the architecture, and the decisions made while building it. If your release process needs a check svnPush doesn't have, you can open an issue, send a pull request, or fork it.

Get started

svnPush is free and lives on GitHub at github.com/FlyToRakib/svnpush, with the source, build instructions and release protocol. Installers for Windows, macOS and Linux are distributed through the project's GitHub releases page. You will need Subversion 1.10 or newer, which svnPush can install for you, and a WordPress.org plugin you can commit to. Git is optional.

Read more on the svnPush product page. If you publish to WordPress sites as well as for them, SyncDock Client 1.2 covers the content side: running and automating many WordPress sites from one console.

Arif Hossain

Written by

Arif Hossain

WordPress & Plugin Engineer

WordPress & plugin engineer at Degird, building the plugins that harden and extend WordPress — GuestDock, AuthDock, and SyncDock.