diff --git a/.github/workflows/wordpress-release.yaml b/.github/workflows/wordpress-release.yaml index 5ddf776..ac9ebc7 100644 --- a/.github/workflows/wordpress-release.yaml +++ b/.github/workflows/wordpress-release.yaml @@ -1,12 +1,49 @@ name: WordPress release -# Run by hand once the SVN tag is committed: WordPress.org releases happen outside GitHub. +# A merge to main that changes the plugin publishes it to WordPress.org SVN, then records the GitHub release and +# announces it on Discord once WordPress.org serves it. Safe to re-run: an unchanged plugin publishes nothing, and a +# version already released on GitHub is not announced twice. Release steps: wordpress/README.md on: + push: + branches: + - main + paths: + - wordpress/svn/** workflow_dispatch: +concurrency: + group: wordpress-release + cancel-in-progress: false + jobs: + publish: + name: Publish to WordPress.org SVN + runs-on: ubuntu-latest + # SVN_USERNAME and SVN_PASSWORD live in this environment, deployable from main only: whoever holds them ships + # code to every site that auto-updates the plugin. + environment: wordpress-org + permissions: + contents: read + outputs: + version: ${{ steps.publish.outputs.version }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Subversion + run: sudo apt-get update -qq && sudo apt-get install -y -qq subversion + + - name: Publish + id: publish + run: scripts/publish-wordpress-svn.sh + env: + SVN_USERNAME: ${{ secrets.SVN_USERNAME }} + SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} + announce: - name: Record the plugin release on GitHub and announce it on Discord + name: Record the release on GitHub and announce it on Discord + needs: publish runs-on: ubuntu-latest permissions: contents: write @@ -23,5 +60,6 @@ jobs: - name: Announce the release run: node scripts/announce-wordpress-release.mjs env: + WAIT_FOR_LIVE_MINUTES: "45" DISCORD_RELEASES_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASES_WEBHOOK_URL }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/wordpress.yaml b/.github/workflows/wordpress.yaml index 983678f..bcb3931 100644 --- a/.github/workflows/wordpress.yaml +++ b/.github/workflows/wordpress.yaml @@ -6,6 +6,7 @@ on: - main paths: - wordpress/** + - scripts/publish-wordpress-svn.sh - .github/workflows/wordpress.yaml jobs: @@ -53,3 +54,17 @@ jobs: run: | pinned_version=$(node -p "require('./package.json').dependencies['@simplepdf/web-embed-pdf']") grep -q "define('SIMPLEPDF_WEB_EMBED_VERSION', '${pinned_version}');" svn/trunk/simplepdf-embed.php + + publish-preview: + name: WordPress.org publish preview (dry run) + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Subversion + run: sudo apt-get update -qq && sudo apt-get install -y -qq subversion + + - name: What merging this sends to WordPress.org + run: DRY_RUN=1 scripts/publish-wordpress-svn.sh diff --git a/scripts/announce-wordpress-release.mjs b/scripts/announce-wordpress-release.mjs index 2633fd6..5f1246d 100644 --- a/scripts/announce-wordpress-release.mjs +++ b/scripts/announce-wordpress-release.mjs @@ -3,6 +3,7 @@ // run by hand after the SVN commit. It refuses to run until WordPress.org serves the Stable tag, and the GitHub release // (`wordpress@`) is the record that the version was announced, so a second run posts nothing. // Usage: DISCORD_RELEASES_WEBHOOK_URL=… GITHUB_TOKEN=… GITHUB_REPOSITORY=… GITHUB_SHA=… node scripts/announce-wordpress-release.mjs +// WAIT_FOR_LIVE_MINUTES=… polls WordPress.org once a minute until it serves the Stable tag (its import lags the SVN commit). // Without DISCORD_RELEASES_WEBHOOK_URL, or with DRY_RUN=1, it prints the Discord message and the GitHub release instead. import { readFile } from 'node:fs/promises' import path from 'node:path' @@ -110,6 +111,19 @@ const createGitHubRelease = async ({ token, repository, release }) => { return response.ok ? null : `${response.status} ${await response.text()}` } +const LIVE_VERSION_POLL_MS = 60_000 + +const waitForLiveVersion = async ({ version, minutes }) => { + const deadline = Date.now() + minutes * 60_000 + const liveVersion = await fetchLiveVersion() + if (liveVersion === version || Date.now() >= deadline) { + return liveVersion + } + console.log(`WordPress.org serves ${liveVersion ?? 'nothing yet'}, waiting for ${version}…`) + await new Promise((resolve) => setTimeout(resolve, LIVE_VERSION_POLL_MS)) + return waitForLiveVersion({ version, minutes: (deadline - Date.now()) / 60_000 }) +} + const fetchLiveVersion = async () => { try { const response = await fetch(PLUGIN_INFO_URL) @@ -141,7 +155,7 @@ const main = async () => { return EXIT_CODES.success } - const liveVersion = await fetchLiveVersion() + const liveVersion = await waitForLiveVersion({ version: release.version, minutes: Number(process.env.WAIT_FOR_LIVE_MINUTES ?? 0) }) if (liveVersion === null) { console.error(`Could not read the live version from ${PLUGIN_INFO_URL}`) return EXIT_CODES.wordpress_org_unreachable diff --git a/scripts/publish-wordpress-svn.sh b/scripts/publish-wordpress-svn.sh new file mode 100755 index 0000000..6485673 --- /dev/null +++ b/scripts/publish-wordpress-svn.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# Publishes wordpress/svn (git main is the source) to the WordPress.org SVN repository. +# - A new Stable tag: trunk/ and assets/ are synced, trunk is copied to tags/, one commit. +# - A Stable tag already published: only assets/ and the readme (trunk/ and tags//) are synced, since +# WordPress.org reads the description from the tag; the code of a published tag never changes. +# Usage (the WordPress release workflow): SVN_USERNAME=… SVN_PASSWORD=… scripts/publish-wordpress-svn.sh +# DRY_RUN=1 stops before the commit and prints what would be sent; it needs no credentials. +# Writes `version=` to $GITHUB_OUTPUT when set. +set -euo pipefail + +readonly EXIT_SUCCESS=0 +readonly EXIT_VERSION_MISMATCH=2 +readonly EXIT_MISSING_CREDENTIALS=3 + +readonly SVN_URL="https://plugins.svn.wordpress.org/simplepdf-embed" +readonly SOURCE_DIR="$(cd "$(dirname "$0")/../wordpress/svn" && pwd)" +readonly WORKING_COPY="$(mktemp -d)" +trap 'rm -rf "$WORKING_COPY"' EXIT + +stable_tag="$(sed -n 's/^Stable tag:[[:space:]]*//p' "$SOURCE_DIR/trunk/README.txt" | tr -d '[:space:]')" +header_version="$(sed -n 's/^Version:[[:space:]]*//p' "$SOURCE_DIR/trunk/simplepdf-embed.php" | tr -d '[:space:]')" +constant_version="$(sed -n "s/^define('SIMPLEPDF_PLUGIN_VERSION', '\(.*\)');/\1/p" "$SOURCE_DIR/trunk/simplepdf-embed.php")" +if [ -z "$stable_tag" ] || [ "$stable_tag" != "$header_version" ] || [ "$stable_tag" != "$constant_version" ]; then + echo "Version mismatch: Stable tag '$stable_tag', plugin header '$header_version', SIMPLEPDF_PLUGIN_VERSION '$constant_version'" >&2 + exit "$EXIT_VERSION_MISMATCH" +fi +if [ -n "${GITHUB_OUTPUT:-}" ]; then + echo "version=$stable_tag" >> "$GITHUB_OUTPUT" +fi + +svn checkout --quiet --non-interactive --depth immediates "$SVN_URL" "$WORKING_COPY" +svn update --quiet --non-interactive --set-depth infinity "$WORKING_COPY/trunk" "$WORKING_COPY/assets" + +if svn ls --non-interactive "$SVN_URL/tags/$stable_tag" > /dev/null 2>&1; then + echo "$stable_tag is already on WordPress.org: syncing the readme and the listing assets only" + svn update --quiet --non-interactive --set-depth infinity "$WORKING_COPY/tags/$stable_tag" + rsync --archive --delete --exclude .svn "$SOURCE_DIR/assets/" "$WORKING_COPY/assets/" + cp "$SOURCE_DIR/trunk/README.txt" "$WORKING_COPY/trunk/README.txt" + cp "$SOURCE_DIR/trunk/README.txt" "$WORKING_COPY/tags/$stable_tag/README.txt" + if ! diff -rq --exclude .svn --exclude README.txt "$SOURCE_DIR/trunk" "$WORKING_COPY/tags/$stable_tag" > /dev/null; then + echo "::warning::The plugin code on main differs from the published $stable_tag: bump the version to publish it" + fi + commit_message="Listing: readme and assets for $stable_tag" +else + echo "Publishing $stable_tag" + rsync --archive --delete --exclude .svn "$SOURCE_DIR/trunk/" "$WORKING_COPY/trunk/" + rsync --archive --delete --exclude .svn "$SOURCE_DIR/assets/" "$WORKING_COPY/assets/" + commit_message="Release $stable_tag" +fi + +cd "$WORKING_COPY" +svn status | awk '$1 == "?" { print $2 }' | while read -r added_path; do svn add --quiet --parents "$added_path"; done +svn status | awk '$1 == "!" { print $2 }' | while read -r removed_path; do svn rm --quiet "$removed_path"; done +if ! svn ls --non-interactive "$SVN_URL/tags/$stable_tag" > /dev/null 2>&1; then + svn cp --quiet trunk "tags/$stable_tag" +fi + +pending_changes="$(svn status)" +if [ -z "$pending_changes" ]; then + echo "Nothing to publish: WordPress.org already matches main" + exit "$EXIT_SUCCESS" +fi +echo "$pending_changes" | grep -v "^A *+\{0,1\} *tags/$stable_tag/" || true + +if [ "${DRY_RUN:-}" = "1" ]; then + echo "DRY_RUN: stopping before the commit ('$commit_message')" + exit "$EXIT_SUCCESS" +fi +if [ -z "${SVN_USERNAME:-}" ] || [ -z "${SVN_PASSWORD:-}" ]; then + echo "SVN_USERNAME and SVN_PASSWORD are required to publish" >&2 + exit "$EXIT_MISSING_CREDENTIALS" +fi + +svn commit --quiet --non-interactive --no-auth-cache --username "$SVN_USERNAME" --password "$SVN_PASSWORD" \ + -m "$commit_message (${GITHUB_SHA:-local})" +echo "Committed '$commit_message' to WordPress.org" diff --git a/wordpress/README.md b/wordpress/README.md index 3abf4d6..bc93891 100644 --- a/wordpress/README.md +++ b/wordpress/README.md @@ -7,9 +7,30 @@ The plugin is published to the Wordpress SVN registry https://plugins.svn.wordpr ## How to publish -`svn/` is both the git folder and the WordPress.org SVN working copy (its `.svn` is gitignored). Git `main` is the source of truth: publish from `main`, and SVN only carries it to WordPress.org. +Merging to `main` publishes. The **WordPress release** workflow (`.github/workflows/wordpress-release.yaml`) runs on every merge that changes `wordpress/svn/`: + +1. `scripts/publish-wordpress-svn.sh` checks that the plugin header `Version`, `SIMPLEPDF_PLUGIN_VERSION` and the readme `Stable tag` agree, then: + - a new Stable tag: syncs `trunk/` and `assets/` to WordPress.org SVN and copies `trunk` to `tags/` + - a Stable tag already published: syncs `assets/` and the readme only (in `trunk/` and `tags//`); the code of a published tag never changes +2. Once WordPress.org serves the version (its import lags the commit by minutes), it creates the `wordpress@` GitHub release and posts the `README.txt` changelog to Discord. A version already released on GitHub is never announced twice. + +Every pull request touching `wordpress/` runs the same script as a dry run (the "publish preview" job) and lists what merging would send. + +To release a new version: + +1. Set `SIMPLEPDF_WEB_EMBED_VERSION` in [simplepdf-embed.php](./svn/trunk/simplepdf-embed.php) to the `@simplepdf/web-embed-pdf` version pinned in [package.json](./package.json) (CI fails on a mismatch), and run `npm run package-plugin` +2. Set the new version in the [simplepdf-embed.php](./svn/trunk/simplepdf-embed.php) header and `SIMPLEPDF_PLUGIN_VERSION`, the `Stable tag` of [README.txt](./svn/trunk/README.txt) and [blueprint.json](./svn/assets/blueprints/blueprint.json) +3. Add the changelog entry in [README.txt](./svn/trunk/README.txt): it is posted to Discord as written +4. Merge. Then commit the new `svn/tags//` folder to git (it mirrors WordPress.org) + +WordPress.org strips images from `README.txt`: show the plugin through `svn/assets/screenshot-N.png`, captioned under `== Screenshots ==`. + +The SVN credentials (`SVN_USERNAME`, and the SVN password from WordPress.org > Profile > Account & Security as `SVN_PASSWORD`) live in the repository's `wordpress-org` environment, deployable from `main` only. + +### Manual fallback + +`svn/` is also the SVN working copy (its `.svn` is gitignored): git `main` stays the source of truth. -_Pre-requisites (once per clone)_ ```bash brew install svn cd wordpress @@ -17,28 +38,9 @@ cd wordpress svn checkout --force --depth immediates https://plugins.svn.wordpress.org/simplepdf-embed svn cd svn && svn update --force --set-depth infinity trunk assets git checkout -- . # SVN may have written its own copies over the git files: git wins -``` -Never run `svn revert`: it replaces the git files with the last published version. To discard a change, restore from git. - -1. Set `SIMPLEPDF_WEB_EMBED_VERSION` in [simplepdf-embed.php](./svn/trunk/simplepdf-embed.php) to the `@simplepdf/web-embed-pdf` version pinned in [package.json](./package.json) (CI fails on a mismatch) -2. Update the TAG / version in [simplepdf-embed.php](./svn/trunk/simplepdf-embed.php) -3. Update the TAG / version in [README.txt](./svn/trunk/README.txt) -4. Update the TAG / version in [blueprint.json](./svn/assets/blueprints/blueprint.json) -5. Update changelog in [README.txt](./svn/trunk/README.txt) - WordPress.org strips images from `README.txt`: show the plugin through `svn/assets/screenshot-N.png`, captioned under `== Screenshots ==`. It reads the description from the Stable tag's folder, so a readme fix after release goes in both `trunk/` and `tags//`. -6. Merge to `main`, check it out, then run the following - -```bash -npm run package-plugin -cd svn -svn update -svn status # "?" = new file, "!" = removed file -svn add -svn rm +svn status # "?" = new file (svn add), "!" = removed file (svn rm) svn cp trunk tags/ -svn commit -m 'Release ' --username bendersej # the SVN password from WordPress.org > Profile > Account & Security +svn commit -m 'Release ' --username bendersej ``` -Leave the older `tags/*` folders git tracks out of `svn add`: they already exist on WordPress.org. -7. Once https://wordpress.org/plugins/simplepdf-embed/ shows the new version, run the **WordPress release** workflow (`gh workflow run wordpress-release.yaml --repo SimplePDF/simplepdf-embed`). It creates the `wordpress@` GitHub release and posts the `README.txt` changelog for the Stable tag to Discord. It refuses to run until WordPress.org serves that version, and does nothing when the GitHub release already exists. -8. Commit the new `svn/tags//` to git. +Never run `svn revert`: it replaces the git files with the last published version. Then run the **WordPress release** workflow by hand (`gh workflow run wordpress-release.yaml --repo SimplePDF/simplepdf-embed`) for the GitHub release and the Discord post.