Pasture: Pixelfed and WordPress join

Pixelfed 0.14.4 (FrankenPHP with Horizon and the scheduler, on the shared
Postgres and Redis) and WordPress 6 with ActivityPub 9.3.1 (on a new shared
MySQL, WP-Cron run by a sidecar) are peers now, each with its scenario:
Pixelfed 26 checks (photos both ways with alt text, its place arriving as
Rome with its coordinates, comments, likes, a boost, edits and deletes),
WordPress 17 (an Article with its title, our reply, like and boost kept as
comments of their kinds, its edit and removal). Pixelfed has a town driver
and a pair spec: 145 checks pass, 2 expected (G-0007).

What they showed: Pixelfed names our posts by their page (fixed in
26dac40); on PostgreSQL its migration making caption nullable never runs,
so every remote boost failed (the pasture applies it); it files a DM it
fetches as followers-only (G-0007, upstream); Passport refuses a token
whose user id equals its client's id. WordPress signs with RFC 9421
first, which PrivaPub now verifies (c5a69d2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-05 07:01:38 +02:00
1 parent c5a69d240a
commit 01e5aa4ddd
14 files changed
+601 -4

No files matched your search

+78
View File
@@ -0,0 +1,78 @@
# WordPress 6 with the ActivityPub plugin 9.3.1: posts as Articles (or Notes when short), comments as replies, each
# author an actor and the blog one too, likes and boosts kept as comments of their kind. On a shared MySQL 8.4 (database
# wordpress). The plugin federates from WP-Cron, so a sidecar runs the due events every few seconds. WordPress sends its
# requests through its own CA bundle (wp-includes/certificates), to which Caddy's root is appended; its URL guard refuses
# private addresses with no switch, which the pasture's public-looking subnet passes. The admin is wpuser; the town's
# accounts are authors with application passwords for the REST API.
WORDPRESS_IMAGE=${WORDPRESS_IMAGE:-docker.io/library/wordpress:6-apache}
WORDPRESS_CLI_IMAGE=${WORDPRESS_CLI_IMAGE:-docker.io/library/wordpress:cli}
WORDPRESS_ACTIVITYPUB=9.3.1
WORDPRESS_PASSWORD=Wordpress-Pasture-1
. "$here/peers/shared.sh"
shared_mysql_up() {
podman container exists pasture-mysql && return 0
podman run -d --replace --name pasture-mysql --network $net --network-alias mysql \
-e MYSQL_ROOT_PASSWORD=pasture -e MYSQL_USER=pasture -e MYSQL_PASSWORD=pasture docker.io/library/mysql:8.4 >/dev/null
# its first start runs a temporary server without networking to set itself up: only TCP answers when it is ready
for _ in $(seq 1 90); do podman exec pasture-mysql mysqladmin ping -h127.0.0.1 --protocol=tcp -uroot -ppasture --silent >/dev/null 2>&1 && return 0; sleep 2; done
echo "mysql did not start" >&2; return 1
}
# mysql_db <name>: a database of the shared MySQL for one peer, the pasture user owning it
mysql_db() {
podman exec pasture-mysql mysql -uroot -ppasture -e \
"create database if not exists \`$1\` character set utf8mb4 collate utf8mb4_unicode_ci; grant all on \`$1\`.* to 'pasture'@'%';" 2>/dev/null
}
# wp <args>: wp-cli against the site, sharing its files
wp() {
podman run --rm --network $net --volumes-from pasture-wordpress --user 33:33 -e HOME=/tmp \
-e WORDPRESS_DB_HOST=mysql -e WORDPRESS_DB_USER=pasture -e WORDPRESS_DB_PASSWORD=pasture -e WORDPRESS_DB_NAME=wordpress \
"$WORDPRESS_CLI_IMAGE" wp "$@"
}
wordpress_up() {
shared_mysql_up
mysql_db wordpress
podman volume exists pasture-wordpress-html || podman volume create --label pasture=1 pasture-wordpress-html >/dev/null
# behind Caddy: https as the proxy says, and the site's own address
local extra="if (isset(\$_SERVER['HTTP_X_FORWARDED_PROTO']) && \$_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { \$_SERVER['HTTPS'] = 'on'; }
define('WP_HOME', 'https://wordpress.test'); define('WP_SITEURL', 'https://wordpress.test'); define('DISABLE_WP_CRON', true);
define('FS_METHOD', 'direct');"
podman run -d --replace --name pasture-wordpress --network $net \
-e WORDPRESS_DB_HOST=mysql -e WORDPRESS_DB_USER=pasture -e WORDPRESS_DB_PASSWORD=pasture -e WORDPRESS_DB_NAME=wordpress \
-e WORDPRESS_CONFIG_EXTRA="$extra" -v pasture-wordpress-html:/var/www/html -v "$ca:/pasture/ca:z,ro" \
"$WORDPRESS_IMAGE" >/dev/null
for _ in $(seq 1 60); do podman exec pasture-wordpress test -f /var/www/html/wp-config.php 2>/dev/null && break; sleep 2; done
if ! wp core is-installed >/dev/null 2>&1; then
wp core install --url=https://wordpress.test --title="Pasture WordPress" --admin_user=wpuser \
--admin_password="$WORDPRESS_PASSWORD" --admin_email=wpuser@wordpress.test --skip-email >/dev/null
fi
wp rewrite structure '/%postname%/' --hard >/dev/null
wp plugin is-installed activitypub 2>/dev/null || wp plugin install activitypub --version=$WORDPRESS_ACTIVITYPUB >/dev/null
wp plugin activate activitypub >/dev/null
# both the blog and its authors are actors
wp option update activitypub_actor_mode actor_blog >/dev/null
podman exec pasture-wordpress sh -c 'grep -q "Caddy Local Authority" wp-includes/certificates/ca-bundle.crt || { echo; cat /pasture/ca/root.crt; } >> wp-includes/certificates/ca-bundle.crt'
podman run -d --replace --name pasture-wordpress-cron --network $net --volumes-from pasture-wordpress --user 33:33 -e HOME=/tmp \
-e WORDPRESS_DB_HOST=mysql -e WORDPRESS_DB_USER=pasture -e WORDPRESS_DB_PASSWORD=pasture -e WORDPRESS_DB_NAME=wordpress \
--entrypoint sh "$WORDPRESS_CLI_IMAGE" -c 'while :; do wp cron event run --due-now >/dev/null 2>&1; sleep 5; done' >/dev/null
wait_http https://wordpress.test:6443/ 1 >/dev/null 2>&1 || true
for _ in $(seq 1 60); do
site wordpress.test -s -o /dev/null -w '%{http_code}' "https://wordpress.test:6443/.well-known/webfinger?resource=acct:wpuser@wordpress.test" 2>/dev/null | grep -q 200 && break
sleep 2
done
wordpress_app_password wpuser > "$here/.state/wordpress.token"
echo "wordpress: https://wordpress.test:6443"
}
# wordpress_user <name>: an author (who publishes, and so is an actor)
wordpress_user() {
wp user get "$1" >/dev/null 2>&1 || wp user create "$1" "$1@wordpress.test" --role=author --user_pass="$WORDPRESS_PASSWORD" >/dev/null
}
# wordpress_app_password <name>: "name:password" for the REST API's basic authentication
wordpress_app_password() {
echo "$1:$(wp user application-password create "$1" pasture --porcelain)"
}