Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,12 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '24'
cache: 'pnpm'

- name: Install dependencies
run: |
composer config --global --unset github-oauth.github.com || true
composer install --no-interaction --no-progress
pnpm install --frozen-lockfile

Expand Down Expand Up @@ -208,7 +209,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '24'
cache: 'pnpm'

- name: Capture build information
Expand Down Expand Up @@ -302,6 +303,7 @@ jobs:

- name: Install production dependencies
run: |
composer config --global --unset github-oauth.github.com || true
composer install --no-dev --optimize-autoloader --no-interaction --no-progress
pnpm install --frozen-lockfile

Expand Down
18 changes: 12 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,9 @@ jobs:

- name: Install composer dependencies
if: steps.changed-files.outputs.any_changed == 'true'
run: composer install --no-interaction --optimize-autoloader --ignore-platform-reqs
run: |
composer config --global --unset github-oauth.github.com || true
composer install --no-interaction --optimize-autoloader --ignore-platform-reqs

- name: Run PHPCS on changed files
if: steps.changed-files.outputs.any_changed == 'true'
Expand Down Expand Up @@ -233,7 +235,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '24'
cache: 'pnpm'

- name: Install dependencies
Expand Down Expand Up @@ -301,7 +303,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '24'
cache: 'pnpm'

- name: Install dependencies
Expand Down Expand Up @@ -460,7 +462,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '24'
cache: 'pnpm'

- name: Install pnpm dependencies
Expand Down Expand Up @@ -490,7 +492,9 @@ jobs:
tools: composer

- name: Install composer dependencies
run: composer install --no-interaction --ignore-platform-reqs
run: |
composer config --global --unset github-oauth.github.com || true
composer install --no-interaction --ignore-platform-reqs

- name: Run composer audit
continue-on-error: true
Expand Down Expand Up @@ -528,7 +532,9 @@ jobs:
coverage: xdebug

- name: Install composer dependencies
run: composer install --prefer-dist --no-progress --ignore-platform-reqs
run: |
composer config --global --unset github-oauth.github.com || true
composer install --prefer-dist --no-progress --ignore-platform-reqs

- name: Install WordPress test environment
run: bash bin/install-wp-tests.sh wordpress_test root 'password' mysql
Expand Down
164 changes: 150 additions & 14 deletions .github/workflows/deploy-readme-assets.yml
Original file line number Diff line number Diff line change
@@ -1,28 +1,164 @@
name: Sync readme/assets to WordPress.org
name: Sync approved readme/assets PR to WordPress.org

on:
push:
branches:
- master
paths:
- readme.txt
pull_request_target:
branches: [master]
types: [closed]
workflow_dispatch:
inputs:
pull_request_number:
description: Merged readme/assets PR number to retry.
required: true
type: number

permissions:
contents: read
pull-requests: read

concurrency:
group: popup-maker-publication
cancel-in-progress: false

jobs:
authorize:
name: Authorize approved readme/assets PR
runs-on: ubuntu-latest
outputs:
should_sync: ${{ steps.authorize.outputs.should_sync }}
merge_sha: ${{ steps.authorize.outputs.merge_sha }}
pull_request_number: ${{ steps.authorize.outputs.pull_request_number }}

steps:
- name: Verify merged PR, approval, and exact file scope
id: authorize
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const number = context.eventName === 'workflow_dispatch'
? Number('${{ inputs.pull_request_number }}')
: context.payload.pull_request.number;
const { data: pull } = await github.rest.pulls.get({ owner, repo, pull_number: number });

core.setOutput('should_sync', 'false');
core.setOutput('pull_request_number', String(number));

if (!pull.merged || pull.base.ref !== 'master') {
core.info('PR is not merged into master; nothing will be synced.');
return;
}
if (pull.head.repo?.full_name !== `${owner}/${repo}`) {
core.setFailed('Publication PRs must come from this repository.');
return;
}

const reviews = await github.paginate(github.rest.pulls.listReviews, {
owner,
repo,
pull_number: number,
per_page: 100,
});
const latestByReviewer = new Map();
for (const review of reviews) {
if (
review.user?.login &&
['APPROVED', 'CHANGES_REQUESTED', 'DISMISSED'].includes(review.state)
) {
latestByReviewer.set(review.user.login, review.state);
}
}
const approvedReviewers = [...latestByReviewer.entries()]
.filter(([, state]) => state === 'APPROVED')
.map(([login]) => login);
let authorizedApproval = false;
for (const username of approvedReviewers) {
const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({
owner,
repo,
username,
});
if (['admin', 'maintain', 'write'].includes(permission.permission)) {
authorizedApproval = true;
break;
}
}
if (!authorizedApproval && pull.merged_by?.login) {
const { data: mergerPermission } = await github.rest.repos.getCollaboratorPermissionLevel({
owner,
repo,
username: pull.merged_by.login,
});
authorizedApproval = ['admin', 'maintain', 'write'].includes(
mergerPermission.permission
);
}
if (!authorizedApproval) {
core.setFailed('The PR was not approved or merged by an authorized maintainer.');
return;
}

const files = await github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: number,
per_page: 100,
});
const isAllowedPath = (filename) =>
filename === 'readme.txt' || filename.startsWith('.wordpress-org/');
const removesReadme = files.some((file) =>
(file.filename === 'readme.txt' && file.status === 'removed') ||
(file.status === 'renamed' && file.previous_filename === 'readme.txt')
);
const allowed = files.length > 0 && !removesReadme && files.every((file) =>
isAllowedPath(file.filename) &&
(file.status !== 'renamed' || isAllowedPath(file.previous_filename || ''))
);
Comment on lines +107 to +116

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep stable-tag changes out of the narrow sync path

A same-repository PR that changes only readme.txt is authorized solely by file scope, so it can change Stable tag to a future, nonexistent, or older SVN version and this workflow will publish that value without checking it against popup-maker.php or package.json. In that scenario WordPress.org can advertise the wrong release or stop serving the intended current version; require the stable tag to remain unchanged for readme/assets-only PRs, or verify it still matches the current released version before enabling the sync.

Useful? React with 👍 / 👎.

if (!allowed) {
core.info('PR is not readme/assets-only; the narrow SVN sync will not run.');
return;
}

core.setOutput('merge_sha', pull.merge_commit_sha);
core.setOutput('should_sync', 'true');

update:
name: Update WordPress.org readme & assets
name: Update WordPress.org readme and assets
needs: authorize
if: needs.authorize.outputs.should_sync == 'true'
runs-on: ubuntu-latest
# Only run against the protected master branch. A manual dispatch on any
# other ref must not be able to sync unreviewed content — or a malicious
# branch copy of this workflow — using the WordPress.org SVN credentials.
if: github.ref == 'refs/heads/master'

steps:
- uses: actions/checkout@v6
- name: Checkout approved merge
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
ref: master
ref: ${{ needs.authorize.outputs.merge_sha }}
Comment on lines +132 to +135

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refuse stale readme and asset retries

A manual retry always checks out the selected PR's historical merge SHA without verifying that its readme.txt and .wordpress-org/ contents still match master. Retrying an older approved PR after a newer readme, asset, or release update therefore sends stale files to WordPress.org and can revert the public stable tag or assets. Compare the publication paths against current master before syncing, or deploy those paths from the current branch tip.

Useful? React with 👍 / 👎.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
fetch-depth: 0
persist-credentials: false

- name: Refuse stale readme or assets
Comment thread
coderabbitai[bot] marked this conversation as resolved.
run: |
git fetch --no-tags origin master
if ! git diff --quiet HEAD FETCH_HEAD -- readme.txt .wordpress-org; then
echo 'A newer readme or asset change exists on master. Refusing to publish stale files.'
exit 1
fi

- name: Reject symlinked publication files
run: |
if [ ! -f readme.txt ]; then
echo 'readme.txt is required for a WordPress.org update.'
exit 1
fi
if [ -L readme.txt ] ||
[ -L .wordpress-org ] ||
{ [ -d .wordpress-org ] && find .wordpress-org -type l -print -quit | grep -q .; }; then
echo 'Symlinks are not allowed in publication files.'
exit 1
fi

- uses: 10up/action-wordpress-plugin-asset-update@stable
- name: Sync readme and assets
uses: 10up/action-wordpress-plugin-asset-update@2480306f6f693672726d08b5917ea114cb2825f7 # stable
env:
SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }}
SVN_USERNAME: ${{ secrets.SVN_USERNAME }}
Expand Down
Loading
Loading