Skip to content
System nominalUTC --:--:--Articles 080
MODULE 06 Lesson 17 / 18 30 MIN

Shipping

The wordpress.org Submission and SVN

What the SVN working copy in this project contains, how a release is cut, and which directory guidelines the shipped plugin draws a reviewer's attention to.

The source tree for this plugin contains a directory called SVN/ with twelve PNGs, a trunk and a tags folder. It is not a mock. Its .svn metadata points at https://plugins.svn.wordpress.org, the tag tags/1.2.0 is at revision 3556106, and the working copy root is at 3556093. That is a wordpress.org plugin-directory checkout, and reading it tells you more about how releases work than any tutorial, because you can see what the last release published.

What it does not contain is a preview of the submission. The 1.2.0 tag already exists, so this plugin has been through review at least once. The licence rule, the queue, the things reviewers send back and the reason a paid "Upgrade to Pro" page is either fine or fatal are all part of shipping that the code does not document.

The working copy has three directories, and only two hold code

A repository is created for you at https://plugins.svn.wordpress.org/your-plugin-slug and arrives with trunk/, tags/ and assets/. The handbook notes that branches/ is no longer created by default because it went unused. Here is what this working copy holds:

SVN/
├── .svn/                       metadata: repository URL, revisions, pristine copies
├── assets/
│   ├── banner-772x250.png      plugin page banner
│   ├── banner-1544x500.png     high-DPI banner
│   ├── icon-128x128.png        icon shown in search results
│   ├── icon-256x256.png        high-DPI icon
│   ├── icon.svg                vector icon
│   ├── icon-1024.png           1,207,510 bytes, source asset
│   └── screenshot-1..6.png     six screenshots with no readme section to show them
├── tags/
│   └── 1.2.0/                  a full copy of the shipping code, revision 3556106
└── trunk/                      the shipping code, revision 3556106
    ├── assets/css, assets/js, assets/img
    ├── includes/*.php
    ├── languages/*.pot
    ├── templates/*.php
    ├── readme.txt
    └── sifero-courier-timeline-for-woocommerce.php

The main plugin file must be at the root of trunk/. Putting it one folder deep, trunk/my-plugin/my-plugin.php, breaks downloads; subfolders for includes are fine, and this plugin uses them the way the handbook expects. assets/ is a sibling of trunk/ for a reason. Screenshots and banners never ship inside the plugin ZIP, so putting them in trunk would make every install larger for images the user never sees.

You can read the two revisions without an SVN client. The working copy's SQLite database holds the node table:

sqlite3 SVN/.svn/wc.db \
  "select local_relpath, revision from NODES where op_depth=0 and kind='file'
   and (local_relpath like 'trunk/readme%' or local_relpath like 'tags/%/readme%');"
tags/1.2.0/readme.txt|3556106
trunk/readme.txt|3556106

Same revision for both files, and that matters. The directory reads trunk/readme.txt only for the Stable tag line, then serves everything else from the tag folder that line names. If the two readmes differ, the directory can point users at a tag whose readme does not describe it.

A release is one copy command and two version bumps

Start from a checkout of the repository root, not from trunk. The handbook's example is a checkout into a local directory, then a copy from trunk to the tag:

svn co https://plugins.svn.wordpress.org/sifero-courier-timeline-for-woocommerce sfct-svn
cd sfct-svn
svn cp trunk tags/1.2.0
svn ci -m "Tagging version 1.2.0"

That is the whole mechanism, and the details are where people lose a release. The version bump happens in two files before the tag exists: the Version header in the main PHP file and the Stable tag line in trunk/readme.txt. Both must say 1.2.0. svn cp preserves history, so the tag is a copy inside the repository and the folder is created as the files appear in it.

What you do not do is edit the tag afterwards. Editing tags/1.2.0/readme.txt to fix a typo means the directory rebuilds the 1.2.0 ZIP with different content than the people who already downloaded it, and those users have no version number to compare. If the bug is in the readme only, the temptation is strong and the answer is still no: cut 1.2.1.

The other half of the discipline is commit frequency. SVN here is a release repository, and every commit triggers a rebuild of every ZIP. The handbook calls out rapid-fire commits that only tweak minor things, including the readme, as strain on the system and gaming of the recently-updated list, with one documented exception: readme commits that only declare support for the latest WordPress release. The 1.2.1 commit for this plugin should therefore carry the version bump, the changelog entry and the corrected Tested up to together.

The licence is the first thing a reviewer checks

Guideline 1 cannot be negotiated: plugins must be compatible with the GNU General Public License. The handbook's summary says that if a licence is not specified the code is treated as "GPLv2 or later", and that is also the recommended choice because it matches WordPress. This plugin declares it in both required places:

# plugin header
 * License:     GPLv2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html

# readme.txt
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Both places matter. The common-issues list has an entry for a missing GPL-compatible licence and a separate one for a mismatched declaration, and it requires a single declared licence for your own code. Code from elsewhere can be under a different GPL-compatible licence, but it has to be documented.

This plugin is the clean case. It bundles no third-party library: twenty-seven files of GPL PHP, CSS and JavaScript that call WordPress and WooCommerce APIs. No vendor/ directory, no minified bundle from a CDN, no font. It owes no licence statement beyond its own.

Bundling changes that. Guideline 1 says included libraries, code and images must be compatible, and guideline 2 puts the burden on you: confirm the licensing of every included file before uploading, and if you cannot validate a library's licence you cannot use it. The common-issues list adds two more entry points, a library no longer maintained and one that is out of date, both treated as security problems. The practical format is a licence block in the file header or a LICENSE.txt beside it, plus a readme line naming the library, its version and its licence.

Minified code is not a shortcut

Guideline 4 requires code to be mostly human readable, and the common-issues list is blunt about build output: if you ship compressed files, make the source publicly available and document it in the readme.

What reviewers reject, and how long the queue takes

The numbers in the developer FAQ are specific. A submission should get an initial review within four weeks, and only one submission per developer is allowed at a time. If the code is small and correct, the FAQ's phrasing is approval "within fourteen days of initial review". When the team sends fixes to make, they aim to reply within ten business days, and a review left untouched for three months is rejected. Reply to the email rather than resubmitting; the FAQ says not to resubmit a plugin rejected for any other reason.

The rejections cluster around a short list, and this plugin is on the right side of almost all of it: headers missing or split across files, an incomplete readme, no GPL-compatible licence, a stable tag that does not match the plugin version, SQL built without $wpdb->prepare(), generic function and option names that collide with other plugins, and an update checker that phones home.

That last one deserves care because it usually arrives through a dependency. Plugins may not contact other servers for updates, because wordpress.org provides that service, and they must not interfere with the built-in updater. Guideline 8 adds that serving updates or installing premium versions of the same plugin from other servers is not allowed. An Update URI header pointing at your own update server is the documented way to update a plugin hosted elsewhere, not a loophole for a directory plugin.

This plugin's free build makes no outbound HTTP requests. No wp_remote_get, no curl_init, no remote CSS or JavaScript, no QR image service: the changelog records that external QR loading was removed from the wordpress.org build. The courier companies table still carries api_url, api_key, token, auth_type, timeout and custom_headers columns, but nothing in this build reads or writes them. They came across from the Premium codebase. A reviewer who greps for api_url finds a credential-shaped column with no call site, which produces a question you then answer in email.

An upgrade prompt is allowed, with conditions

The plugin ships an admin page called "Upgrade to Pro" at sfct-optional-addon, with a price strip, a feature comparison and an external link. That is permissible, and the rule that permits it is guideline 5. Trialware is not permitted: no functionality locked behind payment, no feature that switches off after a trial, no sandbox-only API access. But the same guideline says upselling is acceptable "provided it falls within bounds of guideline 11", and it recommends hosting premium code in a separate add-on outside wordpress.org, which is what the premium build here is.

Guideline 11 holds the conditions. Upgrade prompts must be limited in scope, used sparingly, and appear contextually or only on the plugin's own settings page. Advertising in the dashboard should be avoided, and any site-wide notice must be dismissible. Links to your own sites are encouraged; tracking referrals through them is not, and guideline 7 classifies that as contact with an external server without consent.

Measured against that, this page is on the right side. It is a submenu item under the plugin's own top-level menu, not a dashboard widget and not an admin notice, so it cannot follow a user to unrelated screens. It links out with rel="noopener noreferrer" and no referral parameters, and it states that the free build keeps working without the add-on, which guideline 9 separately requires because implying users must pay to unlock included features is prohibited. The one thing to fix is the readme's "Upgrade to Pro" section, which sits inside the public description page that guideline 12 governs. That guideline permits links to required products in moderation and forbids keyword stuffing. One section is moderation; a second in the FAQ is a pattern.

Guideline 10 covers the front end. Credit links on public pages must be optional and off by default. The public tracking template renders courier data and no "powered by" footer, so there is nothing to remove, but a branded footer added later needs a setting that defaults to off.

Lab 45 minutes

Prepare the next release of this plugin without touching the existing tag. Work on a copy of the SVN/ working copy.

First, write the commit plan as an ordered list: which files change, which command creates the tag, and which line in which file makes the directory serve the new version. For each step, say what breaks if it happens in the wrong order.

Second, do the review yourself. Produce two lists from the source: findings that come from the Plugin Handbook's common-issues page, and findings that come from the detailed guidelines. Cite the guideline number or section name for each, and say whether the fix is code, a readme edit, or a licence statement.

Third, audit the licence position. Name every file in the free build that is not PHP you wrote or a WordPress API call, and for each say what licence statement it needs and where it goes. If the list is empty, show the command you ran to establish that.

Show the solution and the reasoning

The commit plan has four steps and the order is the lesson. Bump the Version header. Bump Stable tag in trunk/readme.txt to the same number and add the changelog entry in the same pass. Run svn cp trunk tags/1.2.1 from the repository root. Commit with a message that names the version. Tag first and the directory reads a stable tag pointing at a folder whose readme claims a different version, which the handbook warns can break updates.

The common-issues pass is short here. No update checker, no remote assets, no bundled library, SQL that uses $wpdb and dbDelta(), names prefixed sfct_ throughout. The readme is incomplete rather than missing, and the gap is the changelog entry for the shipped version, because a plugin with dependencies or external services is expected to document them.

The guideline pass is where the judgements are. Guideline 5 permits the upgrade page, 11 constrains it, 7 forbids the referral tracking that would make it a violation, and 10 constrains any credit link on the public template. Guideline 13 is worth checking once, because it forbids bundling libraries WordPress ships: the free build enqueues its own script with no jQuery dependency, so it passes.

The licence audit should produce an empty list, and the honest way to show that is a command rather than an assurance:

find sifero-courier-timeline-for-woocommerce -type f \
  | grep -vE '\.(php|css|js|pot|png|txt)$|\.DS_Store'
find sifero-courier-timeline-for-woocommerce -type d -name vendor

Both return nothing: twenty-seven files, no third-party code, so the declared GPLv2-or-later licence covers everything and no additional statement is owed. Run the check anyway, because a release or two later somebody adds a QR library and this is the only thing that would have noticed.

One limitation. Two plugins with the same code can come back with different requests, so check the guidelines before submitting rather than arguing about them afterwards.

Official reference

The WordPress developer reference for what this lesson uses.

Progress

Mark this lesson done and it stays marked in this browser.