From 022f1e9de32e4ffc96035c18497636df544dee46 Mon Sep 17 00:00:00 2001 From: ben Date: Mon, 28 Sep 2026 14:39:31 +0200 Subject: [PATCH] ci(wordpress): record each plugin release as a wordpress@ GitHub release --- .github/workflows/wordpress-release.yaml | 5 +- scripts/announce-wordpress-release.mjs | 73 +++++++++++++++++++-- scripts/announce-wordpress-release.test.mjs | 12 +++- wordpress/README.md | 2 +- 4 files changed, 84 insertions(+), 8 deletions(-) diff --git a/.github/workflows/wordpress-release.yaml b/.github/workflows/wordpress-release.yaml index ee12608..5ddf776 100644 --- a/.github/workflows/wordpress-release.yaml +++ b/.github/workflows/wordpress-release.yaml @@ -6,8 +6,10 @@ on: jobs: announce: - name: Announce the plugin release on Discord + name: Record the plugin release on GitHub and announce it on Discord runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Checkout code @@ -22,3 +24,4 @@ jobs: run: node scripts/announce-wordpress-release.mjs env: DISCORD_RELEASES_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASES_WEBHOOK_URL }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/scripts/announce-wordpress-release.mjs b/scripts/announce-wordpress-release.mjs index fd2152b..2633fd6 100644 --- a/scripts/announce-wordpress-release.mjs +++ b/scripts/announce-wordpress-release.mjs @@ -1,7 +1,9 @@ -// Posts a WordPress plugin release to the Discord releases channel, from the changelog in README.txt. -// The plugin ships through WordPress.org SVN, which GitHub never sees: the "WordPress release" workflow is run by -// hand after the SVN commit, and this script refuses to post until WordPress.org serves the Stable tag. -// Usage: DISCORD_RELEASES_WEBHOOK_URL=… node scripts/announce-wordpress-release.mjs (DRY_RUN=1 prints the message) +// Records a WordPress plugin release on GitHub and posts it to the Discord releases channel, from the changelog in +// README.txt. The plugin ships through WordPress.org SVN, which GitHub never sees: the "WordPress release" workflow is +// 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 +// 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' import { fileURLToPath } from 'node:url' @@ -14,11 +16,14 @@ export const EXIT_CODES = { discord_rejected_message: 4, wordpress_org_unreachable: 5, not_live_on_wordpress_org: 6, + github_unreachable: 7, + github_rejected_release: 8, } const PLUGIN_URL = 'https://wordpress.org/plugins/simplepdf-embed/' const PLUGIN_INFO_URL = 'https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&slug=simplepdf-embed' const SOURCE_URL = 'https://github.com/SimplePDF/simplepdf-embed/tree/main/wordpress' +const GITHUB_API_URL = 'https://api.github.com' // The Stable tag and its `= =` changelog bullets, or null without a Stable tag. export const parseReadme = (readme) => { @@ -66,6 +71,45 @@ export const buildWordPressReleaseMessage = ({ version, entries }) => ({ ], }) +// make_latest is off: the "Latest" badge stays on the SDK packages' releases. +export const buildGitHubRelease = ({ version, entries, commitSha }) => ({ + tag_name: `wordpress@${version}`, + target_commitish: commitSha, + name: `SimplePDF Embed for WordPress ${version}`, + body: ["## What's new", '', ...entries.map((entry) => `- ${entry}`), '', `[WordPress.org plugin page](${PLUGIN_URL})`].join('\n'), + make_latest: 'false', +}) + +const githubRequest = ({ token, repository, method, pathname, body }) => + fetch(`${GITHUB_API_URL}/repos/${repository}${pathname}`, { + method, + headers: { + accept: 'application/vnd.github+json', + authorization: `Bearer ${token}`, + 'x-github-api-version': '2022-11-28', + ...(body === undefined ? {} : { 'content-type': 'application/json' }), + }, + ...(body === undefined ? {} : { body: JSON.stringify(body) }), + }) + +// 'exists' | 'missing', or null when GitHub cannot say. +const findGitHubRelease = async ({ token, repository, tagName }) => { + try { + const response = await githubRequest({ token, repository, method: 'GET', pathname: `/releases/tags/${encodeURIComponent(tagName)}` }) + if (response.status === 404) { + return 'missing' + } + return response.ok ? 'exists' : null + } catch { + return null + } +} + +const createGitHubRelease = async ({ token, repository, release }) => { + const response = await githubRequest({ token, repository, method: 'POST', pathname: '/releases', body: release }) + return response.ok ? null : `${response.status} ${await response.text()}` +} + const fetchLiveVersion = async () => { try { const response = await fetch(PLUGIN_INFO_URL) @@ -90,9 +134,10 @@ const main = async () => { } const message = buildWordPressReleaseMessage(release) + const githubRelease = buildGitHubRelease({ ...release, commitSha: process.env.GITHUB_SHA ?? 'main' }) const webhookUrl = process.env.DISCORD_RELEASES_WEBHOOK_URL if (webhookUrl === undefined || webhookUrl === '' || process.env.DRY_RUN === '1') { - console.log(JSON.stringify(message, null, 2)) + console.log(JSON.stringify({ discord: message, github: githubRelease }, null, 2)) return EXIT_CODES.success } @@ -106,12 +151,30 @@ const main = async () => { return EXIT_CODES.not_live_on_wordpress_org } + const github = { token: process.env.GITHUB_TOKEN ?? '', repository: process.env.GITHUB_REPOSITORY ?? 'SimplePDF/simplepdf-embed' } + const existingRelease = await findGitHubRelease({ ...github, tagName: githubRelease.tag_name }) + if (existingRelease === null) { + console.error(`Could not check GitHub for the ${githubRelease.tag_name} release`) + return EXIT_CODES.github_unreachable + } + if (existingRelease === 'exists') { + console.log(`${githubRelease.tag_name} is already released and announced: nothing to do`) + return EXIT_CODES.success + } + const failure = await postToDiscord({ webhookUrl, message }) if (failure !== null) { console.error(`Discord rejected the announcement for ${release.version}: ${failure}`) return EXIT_CODES.discord_rejected_message } console.log(`Announced SimplePDF Embed for WordPress ${release.version}`) + + const releaseFailure = await createGitHubRelease({ ...github, release: githubRelease }) + if (releaseFailure !== null) { + console.error(`Announced on Discord, but GitHub rejected the ${githubRelease.tag_name} release (create it by hand so a rerun does not post again): ${releaseFailure}`) + return EXIT_CODES.github_rejected_release + } + console.log(`Released ${githubRelease.tag_name} on GitHub`) return EXIT_CODES.success } diff --git a/scripts/announce-wordpress-release.test.mjs b/scripts/announce-wordpress-release.test.mjs index 0203e36..16071cc 100644 --- a/scripts/announce-wordpress-release.test.mjs +++ b/scripts/announce-wordpress-release.test.mjs @@ -1,7 +1,7 @@ import assert from 'node:assert/strict' import { readFile } from 'node:fs/promises' import { test } from 'node:test' -import { buildWordPressReleaseMessage, parseReadme } from './announce-wordpress-release.mjs' +import { buildGitHubRelease, buildWordPressReleaseMessage, parseReadme } from './announce-wordpress-release.mjs' const README = `=== SimplePDF Embed === Stable tag: 1.2.0 @@ -40,3 +40,13 @@ test('the message links the plugin page, never pings, and lists every entry', () assert.equal(embed.description, "**What's new**\n- New settings page\n- Update web-embed-pdf to 1.9.0") assert.match(embed.fields[1].value, /\[Plugin page\]\(https:\/\/wordpress\.org\/plugins\/simplepdf-embed\/\)/) }) + +test('the GitHub release is tagged wordpress@ on the workflow commit and never takes the Latest badge', () => { + const githubRelease = buildGitHubRelease({ ...parseReadme(README), commitSha: 'abc1234' }) + + assert.equal(githubRelease.tag_name, 'wordpress@1.2.0') + assert.equal(githubRelease.target_commitish, 'abc1234') + assert.equal(githubRelease.name, 'SimplePDF Embed for WordPress 1.2.0') + assert.equal(githubRelease.make_latest, 'false') + assert.match(githubRelease.body, /^## What's new\n\n- New settings page\n- Update web-embed-pdf to 1\.9\.0\n/) +}) diff --git a/wordpress/README.md b/wordpress/README.md index 8c410a5..3abf4d6 100644 --- a/wordpress/README.md +++ b/wordpress/README.md @@ -40,5 +40,5 @@ svn commit -m 'Release ' --username bendersej # the SVN password from Wor ``` 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, announce it on Discord: run the **WordPress release** workflow (`gh workflow run wordpress-release.yaml --repo SimplePDF/simplepdf-embed`). It posts the `README.txt` changelog for the Stable tag, and refuses to post until WordPress.org serves that version. +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.