| name | groups-gatherpress-compat-test |
| description | Run the Groups/GatherPress front-end integration compatibility checklist — per-role browser pass, direct REST pass, GatherPress-off pass, and security/perf sanity checks. Use after any GatherPress version bump, or any change to mu-plugins/wporg-groups-frontend, mu-plugins/groups, or the groups-site theme. |
Groups / GatherPress compatibility checklist
This checklist exists because GatherPress updates fairly often and its
changes can silently break backwards compatibility with our Groups
integration. It is self-contained — follow it with no other context. It
covers three plugins: mu-plugins/wporg-groups-frontend (front-end
event/member management), mu-plugins/groups/gatherpress-groups-tweaks.php
(settings/capability/query overrides), and the groups-site block theme.
Keep this skill current. This file is a living record, not a one-time
checklist — whenever a session working on this integration (a version bump,
a bug fix, a test-flakiness investigation, anything touching the plugins or
theme above) turns up an essential finding — a real root cause, a bug in
GatherPress or this integration and its fix, a test-environment gotcha, a
methodology that generalizes — add it here before the session ends, in the
section it belongs to (or a new one, if it doesn't fit an existing one).
"Essential" means: would the next person hit the same wall without it?
Prefer folding a finding into existing prose over appending a new bullet
list per session — this file should read as current best understanding, not
a changelog.
Compact it periodically. Growth without pruning eventually makes this
file slower to read than it's worth. Every so often (when a section is
getting long, or findings start to overlap/repeat), reread the whole file
and tighten it: merge redundant points, cut anything superseded by a later
fix or no longer true of the current code, and re-verify specifics (file
paths, function names, version numbers) against the actual repo rather than
trusting older prose. Compacting is itself worth a small dedicated pass, not
just an incidental side effect of adding one more finding.
Two automated layers exist alongside this manual checklist — run them
first, then use this checklist for what they can't cover (real browser
interaction, cross-plugin capability leaks, exploratory checks):
- PHPUnit —
docker compose -f docker-compose.phpunit.yml up (or your
usual local phpunit invocation). Covers the WordCamp Groups suite:
capability logic, gatherpress-groups-tweaks.php option/query overrides,
Members_Controller role rules, REST permission/IDOR checks, draft flow,
block registration.
- E2E (Playwright) —
npx playwright test (or trigger the
e2e-tests.yml GitHub Action manually — it's workflow_dispatch-only).
Covers anonymous front-page rendering, an author creating an event
end-to-end through the real browser UI, per-event messaging, the
auto-publish-notification email, the member directory, and Reply →
Cancel on a group news post's comment form. Needs
organiser1/organiser2/organiser3/organiser4/eventorganiser1/
eventorganiser2/eventorganiser3/eventorganiser4/eventorganiser5/
eventorganiser6/member1 (all password) — every
individual test that logs in as an author has its own dedicated
eventorganiser* account, not just every spec file. organiser2/
3/4 (editor tier) are event-form.spec.js's own, one per test, for
the same reason.
event-manage-messaging.spec.js alone needs three (2/4/5) because
its own three tests also run concurrently against each other under
fullyParallel, not just against other files. Don't consolidate any of
these back onto a shared account.
If a spec times out waiting on a UI interaction, don't assume it's
fullyParallel contention and reach for --workers=1 — read the actual
failure first. Every Playwright failure writes an error-context.md
(path is in the terminal output) with a full accessibility-tree snapshot
of the page at the moment of timeout: read it before touching the test.
Several real, deterministic bugs were all initially mistaken for "just
flaky":
- A "Choose a pattern" starter-pattern dialog appears on every fresh
post-new.php visit for a post type with starter patterns
( has them) and — unlike the separate "Welcome to
the editor" guide the specs already handled — its own Close button
and Escape do dismiss it. It silently sat on top of the
sidebar, and every subsequent interaction hung until the test
timeout. Fixed by ,
which every spec creating an event must call.
If either automated suite fails, stop and fix that before doing the manual
pass below — don't duplicate debugging effort across layers.
0. Before bumping the pinned version
- Confirm the target version is actually stable, not a beta/RC:
curl -s https://api.wordpress.org/plugins/info/1.0/gatherpress.json | python3 -c "import json,sys; print(json.load(sys.stdin)['version'])".
- Diff GatherPress core itself for breaking API changes before touching
any code here. Clone/pull
github.com/GatherPress/gatherpress locally and
run git diff <old-tag> <new-tag> (and git log <old-tag>..<new-tag> --oneline for the human-readable summary). Specifically check every class
this integration touches directly —
GatherPress\Core\{Rsvp\Rsvp,Event\Event,Venue\Venue,Venue\Setup,User,Setup,Blocks\Setup,Blocks\Event_Query}
(grep GatherPress\\\\ across mu-plugins/groups,
mu-plugins/wporg-groups-frontend, and themes/groups-site to get the
current list) — for renamed/removed constants, changed return types
(Rsvp::get() went from always-array to array|null in 0.35.0 and broke
a test that indexed it directly), and classes marked final (harmless
unless something here extends one — check with
grep -rn "extends.*GatherPress").
Checking that a class still exists is not enough — verify every
individual method/constant/property call site, not just the class. A
wrong-but-existing class import (Blocks\Event_Query vs. the real
Event\Query) passed every "does this class exist" check across two
GatherPress versions and only surfaced as a live production fatal,
because nothing had verified the specific methods called actually
existed on that specific class. Section 8 below has the exhaustive,
mechanical version of this check — run it now too, not just after
landing, since catching this before the bump is cheaper than catching it
in production.
- Grep theme templates for GatherPress block usage, not just PHP:
grep -rn "wp:gatherpress" public_html/wp-content/themes/groups-site.
GatherPress Alpha's migration (see below) only rewrites block content
stored in the database — it does not touch theme .html template
files. If a version bump removes/renames a block GatherPress itself used
to ship (e.g. gatherpress/icon → core core/icon in 0.35.0), any
in-repo template using it needs a manual, matching edit or it silently
renders blank.
- Find every place the version string is pinned — there is no single
source of truth, so grep for the old version across the whole repo before
declaring the bump done:
.
As of the 0.35.0 bump these were: ,
,
(which was stale at a different, older version than the two
CI workflows — don't assume they're in sync), and this skill's own
install command below.
1. Environment setup
# Checkout & build
npm ci
npm run build --workspace=public_html/wp-content/mu-plugins/wporg-groups-frontend
# Confirm the dev stack is up
docker compose ps # expect wordcamp.test + wordcamp.db running
# Confirm GatherPress is installed & active on the groups site (gitignored,
# not committed — install manually if missing):
docker compose exec wordcamp.test wp plugin list --url=events.wordpress.test/group/sunshine-coast-qld/ | grep gatherpress
# If missing/inactive:
docker compose exec wordcamp.test wp plugin install \
https://github.com/GatherPress/gatherpress/releases/download/0.35.0/gatherpress.0.35.0.zip \
--activate --url=events.wordpress.test/group/sunshine-coast-qld/
Environment gotchas:
- Adding/removing a
groups-site pattern requires a theme Version: bump.
Locally, clear the otherwise-stale scan with
wp eval 'wp_get_theme()->delete_pattern_cache();' --url=<site>;
wp cache flush does not clear it.
- If PHPUnit cannot read a mounted file that exists on the host, run
docker compose -f docker-compose.phpunit.yml up -d --force-recreate phpunit_wp.
- Hooks loaded in
wpSetUpBeforeClass() disappear after the first test in
a full suite because WP_UnitTestCase restores a process-wide hook
snapshot. Re-add named callbacks in setUp().
go_to( get_permalink( $id ) ) does not establish a singular query in
this fixture. Use go_to( home_url( "?p={$id}&post_type={$type}" ) ).
- If a new test account's first E2E run fails in
dismissEditorOnboarding, rerun once only when error-context.md shows
the Welcome dialog reinitializing.
After bumping the pinned GatherPress version, also install/update
GatherPress Alpha
(version-locked to core) and run its one-time migration. GatherPress ships
breaking content migrations (old gatherpress/icon block usage, old
gatherpress/venue-map width/height attributes, renamed RSVP settings
values) through this companion plugin rather than an automatic upgrade
routine. Skipping this step is exactly what it looks like when a venue's
address/phone/website/map silently vanish from the front end after a version
bump — it reads like a GatherPress regression but is actually just unmigrated
data:
docker compose exec wordcamp.test wp plugin install \
https://github.com/GatherPress/gatherpress-alpha/releases/download/0.35.0/gatherpress-alpha.0.35.0.zip \
--activate --url=events.wordpress.test/group/sunshine-coast-qld/
docker compose exec wordcamp.test wp gatherpress alpha fix --url=events.wordpress.test/group/sunshine-coast-qld/
Create one test user per role tier, with both a browser login password
and a REST application password (the browser pass needs the former, the
REST pass needs the latter):
for pair in "organiser1:editor" eventorganiser{1..6}:author "member1:subscriber"; do
u="${pair%%:*}"; role="${pair##*:}"
docker compose exec wordcamp.test wp user create "$u" "$u@example.test" \
--role="$role" --user_pass=password \
--url=events.wordpress.test/group/sunshine-coast-qld/
docker compose exec wordcamp.test wp user application-password create "$u" "compat-test" --porcelain \
--url=events.wordpress.test/group/sunshine-coast-qld/
done
Save the printed application passwords, then:
ORG="organiser1:<app-password>" # editor tier — "Organiser"
AUTHOR="eventorganiser1:<app-password>" # author tier — "Event Organiser"
MEM="member1:<app-password>" # subscriber tier — "Member"
BASE="https://events.wordpress.test/group/sunshine-coast-qld"
2. Direct REST pass (bypass the UI)
Namespace wporg-groups/v1 should expose these routes — confirm the full
list first:
curl -sk "$BASE/wp-json/wporg-groups/v1" | python3 -c "
import json, sys
d = json.load(sys.stdin)
for r in sorted(d['routes'].keys()): print(r)
"
Expect: event-form-data, event, event/{id}, drafts, draft,
draft/{id}, draft/{id}/publish, members, members/{id},
members/{id}/role, members/join, members/leave (plus the namespace
root).
Checks (run as anonymous + each role where relevant):
3. Per-role browser pass
Log in as each role in turn ($BASE/wp-login.php) and walk the actual UI —
don't just check curl responses render correctly, click through:
- Event create/edit — via the front-end modal (not wp-admin). Confirm
the venue picker, date/time fields, and featured image work.
- Also open the block editor for a
gatherpress_event post in wp-admin
(not just the front-end modal) and check the browser console / PHP error
log for fatals. WordPress enumerates every registered block pattern —
including Inserter: no ones never placed in any template — via
/wp/v2/block-patterns/patterns on every block-editor page load,
executing each pattern's PHP to build the response. A pattern that's
broken but unused on the front end (wrong query, undefined method, etc.)
only surfaces here. This is exactly how a pre-existing bug (three
groups-site event-cards patterns importing the wrong GatherPress class —
fixed in #1874) went undetected through multiple GatherPress versions
until a real production rollout finally hit it.
- RSVP — as Member and as Event Organiser.
- Group settings tabs (Events / Venues / Members / Design / About) —
confirm each tab loads without a console error or 403, and that
Organiser-only tabs are entirely absent (not just disabled) for lower
roles.
- Member directory (
/members/) — role labels correct, join/leave
buttons behave. If this 404s, check wp post list --post_type=page for a
page with slug members before assuming it's broken — the theme resolves
it via the page-members.html template, so it needs an actual WP page
with that slug to exist; a fresh/reset dev site may simply be missing it
(wp post create --post_type=page --post_title=Members --post_name=members --post_status=publish).
- wp-admin, not just the front end — capability leaks (like the fixed
promote_users escalation) only show up here. Log in as each non-admin
role and check: can they reach Users → a role-promotion UI beyond what
the front-end allows? Can they reach Site Editor (should be yes for
editors, via the edit_theme_options grant — confirm that grant doesn't
also unlock anything broader)?
- Anonymous (logged out) — public pages render; every interactive
action (RSVP, join, edit) redirects to login rather than silently
failing or 403ing with no feedback.
Explicitly note what's absent for each lower role, not just what's
disabled — a Member should see no event-management UI rendered at all,
not a greyed-out button.
4. GatherPress-deactivated pass
wp plugin deactivate gatherpress does NOT work in this environment —
mu-plugins/wcorg-network-plugin-control.php force-activates
gatherpress/gatherpress.php on every request for the groups network
(GROUPS_NETWORK_ID), silently re-activating it immediately after
deactivation. Verified: wp plugin deactivate gatherpress --network reports
success, but the plugin's REST routes/classes are still live on the very
next request. To genuinely test with GatherPress absent, remove the plugin
files instead:
docker compose exec wordcamp.test mv wp-content/plugins/gatherpress wp-content/plugins/gatherpress.disabled
# ... run the checks below ...
docker compose exec wordcamp.test mv wp-content/plugins/gatherpress.disabled wp-content/plugins/gatherpress
5. Security-focused pass
6. Performance sanity check
7. Production rollout notes
Once the passes above are green and the version-bump PR is merged, the
actual production deploy has its own gotchas — found the hard way during
the 0.35.0 rollout on events.wordpress.org.
-
wp plugin update alone is not sufficient/durable on production.
wp-content/plugins there is managed via svn:externals pinned to a
specific tag per plugin. Check what's actually tracked before updating
anything: svn propget svn:externals -R (run from wp-content/plugins).
wp plugin update only replaces files on disk — it does not update
the tracked external pin, so the change is a silent, undurable override
that the next svn update (run by anyone, any time, for any reason)
would silently revert. To make it durable: svn propedit svn:externals .
and hand-edit only the one plugin's line (this property holds ~30
plugins' pins in a single multi-line blob — don't reconstruct it with a
scripted find/replace, one wrong character corrupts every other plugin's
pin too). Review with svn diff --depth=empty . and confirm only
that one line changed before committing. Then svn commit and
svn update — this should reconcile with zero file churn, since it's
the same code wp plugin update already fetched from the same wp.org
release.
-
Run GatherPress Alpha's fix per-site with visible progress, not the
bare wp gatherpress alpha fix command — not only because of the
100-site cap (see the [gatherpress-alpha#66] note above), but because
the bare command is completely silent until it finishes, which reads as
"hung" on a slow/first-ever run and invites a premature Ctrl-C. Call the
same underlying method directly instead, scoped to one site:
wp eval '
$setup = \GatherPress_Alpha\Setup::get_instance();
$ref = new ReflectionMethod( $setup, "run_fixes" );
$ref->setAccessible( true );
echo "Starting...\n";
$start = microtime( true );
$ref->invoke( $setup );
echo "Done in " . round( microtime( true ) - $start, 2 ) . "s\n";
echo "gatherpress_alpha_last_version: " . get_option( "gatherpress_alpha_last_version" ) . "\n";
' --url=<site>
This runs the exact same code fix() would call — fix() itself only
adds a capability check (irrelevant under CLI) and the buggy/capped
multisite loop, both irrelevant/skipped here since --url already scopes
wp-cli to one site. If a run does get interrupted early, it's safe:
GatherPress Alpha's fixes are idempotent (confirmed by an upstream
reviewer on PR #1873, and again live here — an interrupted run left no
gatherpress_alpha_last_version recorded at all, so re-running just
started clean with nothing partially applied).
8. Post-upgrade code audit (mandatory, run after every version bump lands)
Run this once the plugin update is live (locally after bumping the pin, and
again after the production rollout) — it's what would have caught the
Blocks\Event_Query vs. Event\Query wrong-class bug (#1874) before it hit
production instead of after, since that bug passed every class-existence
check across two GatherPress versions and only surfaced as a live fatal.
The point isn't spot-checking "the classes we remember using" — it's
mechanically verifying every call site, including the ones nobody's
thought about in months.
-
Extract every GatherPress class this integration actually imports,
across all three plugins:
grep -rn "^use GatherPress\|GatherPress\\\\Core" \
public_html/wp-content/mu-plugins/groups \
public_html/wp-content/mu-plugins/wporg-groups-frontend \
public_html/wp-content/themes/groups-site \
--include="*.php" | grep -v "/tests/\|/build/"
(Exclude /build/ — it's compiled from /src/; diff the two afterward
with plain diff to confirm they're actually in sync rather than
assuming it.)
-
For each imported class, extract every method/constant/property call
site — not just the ones near the use statement. A call several
functions away from the import, or reached through a differently-named
local variable, is exactly the kind of thing a quick skim misses. Grep
broadly per class/alias: ClassName::, new ClassName(, and
$lowercase_var->method( for every plausible variable name that
instance might be assigned to ($event, $rsvp, $venue, $query,
$setup — check the surrounding code when a grep for $obj-> comes up
empty, since the call is often on the next line rather than the same
one, e.g. $venue = new Venue(...); then $term = $venue->get_term();
two lines later).
-
Cross-check each one against the actual GatherPress source for the
now-installed version (not the old one):
# from a local clone of github.com/GatherPress/gatherpress
git show <new-tag>:path/to/class-file.php | grep -n "function method_name\|const CONST_NAME"
A hit confirms it; nothing confirms a break. Also diff the whole file
against the previous tag (git diff <old-tag> <new-tag> -- path) — an
empty diff is the strongest possible signal that nothing there changed.
-
Report clean only when every single call site has been individually
confirmed — "the class exists" or "the file's diff was small" are not
substitutes for "this exact method still exists with this signature."
-
Note the audit's scope and result in the version-bump PR description and
the tracking issue, the same way the manual/automated test passes are
recorded.
9. Extend automated test coverage (mandatory, run after every version bump)
Every version bump so far has surfaced at least one thing the automated
suites didn't catch — that's exactly the gap tracked by
issue #1863
("Identify gaps in unit and e2e test coverage"). Manually re-discovering
the same class of bug on the next upgrade is a wasted rediscovery — turn
what this pass found into a permanent regression test before moving on,
not a one-off fix.
The remaining reference test lives in
mu-plugins/groups/tests/ (not themes/groups-site/tests/, which doesn't
exist and isn't wired into phpunit.xml.dist — theme code is tested from
there by including the real theme files directly):
test-gatherpress-api-contract.php — a data-provider-driven test
asserting every GatherPress class/method/constant/property this
integration calls still exists, sourced from the same list Section 8's
audit builds. This is what makes Section 8's audit durable instead of
a one-time manual pass that has to be redone by hand on every future
bump — extend CONTRACT in that file whenever the audit finds a call
site it doesn't cover yet, rather than leaving the gap for next time.
test-groups-site-event-cards-patterns.php was removed with the old PHP
event-card patterns. Directly include any future orphaned PHP pattern that
normal template rendering cannot exercise.
Its successor, test-groups-site-event-cards.php, preserves thumbnail
cache priming that core cannot infer through a core/pattern reference.
The PHP API contract does not detect missing blocks, which render blank.
Also audit the theme's dependencies: gatherpress/event-date,
gatherpress/rsvp-count,
gatherpress/venue, gatherpress/venue-detail, gatherpress/venue-map,
gatherpress/online-event, gatherpress/online-event-link,
gatherpress/add-to-calendar, and the gatherpress-event-query query
variation; wporg-groups-frontend/tests/test-blocks.php asserts only
wporg/* blocks. Prune CONTRACT entries when call sites disappear.
When a version bump (or this checklist) surfaces something that wasn't
caught automatically, before moving on:
- Identify why it wasn't caught — what code path was it in, and what
(if anything) currently exercises that path in CI.
- Write the smallest test that would have failed before the fix and
passes after it. Verify this concretely, not just in principle — run
the test against the broken state (e.g.
git stash the fix, run the
test, confirm it fails with a clear message, git stash pop) before
trusting it as a regression guard.
- Prefer extending an existing data-provider/contract-style test (like
test-gatherpress-api-contract.php's CONTRACT list) over writing a
narrow one-off test, when the gap is really "we don't check X across
the board" rather than "this one specific call is wrong."
- Note what was added (and why) in the version-bump PR description, the
tracking issue, and — if it changes the shape of how this integration
should be tested going forward, not just what — issue #1863 too.
Known-issues appendix
Use this to distinguish "this checklist found something new" from
"this is a known, already-filed issue." Check your repo's issue tracker for
current status before treating any of these as new bugs:
| Symptom | Exercised by | Notes |
|---|
| Join/leave silently 403s via the UI (nonce lookup finds nothing on the front end) | Section 3, join/leave click-through | Front-end-only bug — the REST layer itself works fine when hit directly with a valid nonce (section 2's join/leave check). |
| Group Settings → About tab 403s for Organisers | Section 3, About tab | Calls a core admin-only REST endpoint directly instead of a plugin-scoped one. |
wporg/event-manage block registered but not placed in any template | Section 3/4 grep, or wp eval block-registry dump | Dead code, not a functional gap — Event Organisers manage events fine via wp-admin. |
/members fully public with no membership/auth requirement | Section 5 | Open product/privacy question, not a bug in itself — confirm current decision, don't assume it's wrong. |
my-events block empty for a user who created events but never RSVP'd | Section 3, per-role browser pass as an event creator | RSVP-attendance based by design — confirm this still matches the current product decision. |
groups-site theme activatable on non-groups-network sites | Not covered by this checklist (network-admin action, not a groups-site page) | If auditing this, attempt wp theme activate groups-site --url=<non-groups-network-site> and confirm it's blocked. |