build(sirius-press): build from the vendored subtree instead of a download

The subtree is in place, so everything that used to fetch and patch core at
build time now just copies it.

  tools/build.sh        copies wordpress/ — no download, no checksum step,
                        because there is nothing to fetch and nothing to
                        trust that is not already in the repository
  docker/Dockerfile     COPY wordpress/ instead of curl + sha256 + patch;
                        the build args and the `patch` package are gone
  docker-compose.yml    no WP_VERSION / WP_URL / WP_SHA256 to keep in step

tools/update-wordpress.sh is rewritten around what the subtree makes
possible. It imports the pristine release onto sirius-press/wordpress-upstream
and then `git subtree merge`s that branch, which three-way merges upstream
against the fork's own commit. A patch either applies with fuzz and hopes or
fails and leaves you re-deriving the change by hand; a merge conflict is
resolved once, in the file, and the next release merges against the
resolution.

patches/ survives as documentation rather than mechanism, and is now
generated: tools/refresh-patches.sh diffs the subtree against the pristine
import and rewrites the directory, with --check for CI. It answers the
question anyone auditing a fork asks first — what exactly did you change
inside WordPress? — in a minute, which `git log wordpress/` cannot, because
that log is mostly upstream imports. Generated documentation stays true; a
hand-maintained record of a core diff drifts, and a stale one is worse than
none because people trust it.

One test change worth noting: the syntax sweep no longer walks all of
wordpress/. It lints the fork's own PHP plus every core file patches/ says
the fork touches, which keeps the suite at seven seconds instead of a minute
while still covering the only core file that can break.
This commit is contained in:
Silent Mode 2026-09-21 03:19:28 +02:00
parent 34fd3f062c
commit 961cb108ca
12 changed files with 396 additions and 376 deletions

View file

@ -121,7 +121,8 @@ plugins/
sirius-press-sia-export/ the static export queue and uploader
sirius-press-compat/ shims for plugins that insist on an admin email
mu-plugins/ the bits that must load before plugins do
patches/ the core diff — currently one file
wordpress/ WordPress itself, vendored as a subtree, patched
patches/ a generated record of the core diff — one file
docker/ compose stack: MariaDB, PHP-FPM, nginx
tools/ build, upstream update, release
tests/ 138 unit checks plus a live end-to-end suite
@ -130,19 +131,20 @@ docs/
### Where WordPress itself is
Not in this repository. `tools/wordpress.lock` pins an exact upstream version
and its SHA-256; `tools/build.sh` and the Docker image each download it, verify
the hash, and apply `patches/`.
In `wordpress/`, as a git subtree, already patched. That is what gets built
and what gets shipped — there is no download step and no checksum to trust at
build time.
This deviates from the original plan of vendoring core as a git subtree, and
the reason is arithmetic: WordPress 7.1.1 is 149 MB and 5,008 files, and the
fork's entire core diff is 75 lines in one file. Carrying the former to express
the latter would make every clone, every `git status` and every subtree split
in the parent monorepo pay for a patch you can read in a minute. Upstream
security releases still merge cleanly — `tools/update-wordpress.sh` bumps the
pin, reapplies the series with fuzz, and tells you exactly which hunk needs a
human if one does. [docs/upstream-merges.md](docs/upstream-merges.md) covers
the procedure and how to switch to a vendored subtree if you would rather.
Upstream releases arrive through `git subtree merge` against a branch of
pristine imports, which three-way merges them against the fork's own commit
on top. `tools/update-wordpress.sh <version>` does the whole thing;
`patches/` keeps a generated, readable record of the 75 lines that differ
from stock WordPress, so nobody has to read a 3,800-file log to find out what
this fork changes in core.
The trade is repository size — WordPress is about 149 MB. Worth it for a fork
that can absorb a security release in a minute.
[docs/upstream-merges.md](docs/upstream-merges.md) has the procedure.
---

View file

@ -32,8 +32,6 @@ WP_DEBUG=false
# It is not the phrase itself and is useless without the database.
SIRIUS_PRESS_KEY=
# --- core pin ---------------------------------------------------------------
# Mirrors tools/wordpress.lock. Change both together, never just one.
WP_VERSION=7.1.1
WP_URL=https://wordpress.org/wordpress-7.1.1.tar.gz
WP_SHA256=3996fee13448ef12e07e9f0c77db2f655ffa1b7cde71c80a4965d3bf1fb956b3
# WordPress itself is vendored in the repository (wordpress/) and baked into
# the image, so there is no version or checksum to configure here. See
# docs/upstream-merges.md for how a new upstream release gets in.

View file

@ -1,13 +1,13 @@
# Sirius Press — PHP-FPM image with a verified, patched WordPress baked in.
# Sirius Press — PHP-FPM image with the patched WordPress baked in.
#
# The image builds core rather than inheriting the official `wordpress` image
# on purpose. That image ships whatever WordPress version it was tagged with,
# and the patch series here is pinned to an exact one; silently applying a
# fork's patches to a different core is how a setup wizard ends up half
# rewritten. Downloading the pinned tarball and checking its hash during the
# build makes the version an explicit, verifiable property of the image.
# Core comes from this repository's `wordpress/` subtree, not from a download
# and not from the official `wordpress` image. That image ships whatever
# version it was tagged with, and the fork's core patch is pinned to an exact
# one; applying a fork's patch to a different core is how a setup wizard ends
# up half rewritten. Copying the vendored tree makes the image contain exactly
# what `git log wordpress/` describes, with nothing fetched at build time.
#
# Core is installed to /opt/sirius-press/core, not to the document root. The
# Core is staged at /opt/sirius-press/core, not at the document root. The
# entrypoint copies it into place on every start, which is what makes
# `docker compose build --pull && up -d` a real upgrade: the usual layout,
# where the document root is itself a volume, pins core to whatever version
@ -20,14 +20,10 @@
FROM php:8.3-fpm-bookworm
ARG WP_VERSION
ARG WP_URL
ARG WP_SHA256
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
ca-certificates curl patch \
ca-certificates curl \
libfreetype6-dev libjpeg62-turbo-dev libpng-dev libwebp-dev \
libzip-dev libgmp-dev libicu-dev \
; \
@ -49,29 +45,24 @@ RUN { \
echo 'expose_php = Off'; \
} > /usr/local/etc/php/conf.d/sirius-press.ini
# --- core, verified -----------------------------------------------------------
# --- core, from the subtree --------------------------------------------------
COPY wordpress/ /opt/sirius-press/core/
RUN set -eux; \
curl -fsSL -o /tmp/wp.tar.gz "$WP_URL"; \
echo "$WP_SHA256 /tmp/wp.tar.gz" | sha256sum -c -; \
mkdir -p /opt/sirius-press; \
tar -xzf /tmp/wp.tar.gz -C /tmp; \
mv /tmp/wordpress /opt/sirius-press/core; \
rm -rf /tmp/wp.tar.gz
test -f /opt/sirius-press/core/wp-includes/version.php; \
sed -n "s/.*wp_version = '\\(.*\\)'.*/\\1/p" \
/opt/sirius-press/core/wp-includes/version.php \
> /opt/sirius-press/core/.sirius-core-version; \
echo "vendored WordPress $(cat /opt/sirius-press/core/.sirius-core-version)"
# --- the fork -----------------------------------------------------------------
#
# The patch is already applied in the subtree, so there is nothing to patch
# here. patches/ is carried for auditing, not for building.
WORKDIR /opt/sirius-press/core
COPY patches/ /tmp/patches/
RUN set -eux; \
for p in /tmp/patches/*.patch; do \
patch -p1 -F3 --forward --silent < "$p" \
|| { echo "patch $p did not apply to WordPress $WP_VERSION"; exit 1; }; \
done; \
rm -rf /tmp/patches; \
echo "$WP_VERSION" > /opt/sirius-press/core/.sirius-core-version
COPY plugins/ /opt/sirius-press/core/wp-content/plugins/
COPY mu-plugins/ /opt/sirius-press/core/wp-content/mu-plugins/

View file

@ -3,9 +3,10 @@
# Three containers: MariaDB, PHP-FPM with the patched WordPress baked in, and
# nginx in front. Only nginx is published.
#
# The volume layout is the part worth reading. WordPress core lives in the
# image, not in a volume, so `docker compose build --pull && up -d` genuinely
# upgrades it — the usual arrangement, where the whole document root is a
# The volume layout is the part worth reading. WordPress core comes from the
# repository's `wordpress/` subtree, is baked into the image, and is refilled
# into the document root on every start — so `docker compose build && up -d`
# genuinely upgrades it — the usual arrangement, where the whole document root is a
# volume, freezes core at whatever version first created the volume and turns
# every security release into a manual migration. What people actually need to
# keep — uploads, plugins and themes they installed, and wp-config.php — is
@ -43,13 +44,6 @@ services:
build:
context: ..
dockerfile: docker/Dockerfile
args:
# install.sh copies these out of tools/wordpress.lock into .env.
# Defaults are pinned here too, so a plain `docker compose build`
# cannot quietly drift onto a different core.
WP_VERSION: ${WP_VERSION:-7.1.1}
WP_URL: ${WP_URL:-https://wordpress.org/wordpress-7.1.1.tar.gz}
WP_SHA256: ${WP_SHA256:-3996fee13448ef12e07e9f0c77db2f655ffa1b7cde71c80a4965d3bf1fb956b3}
restart: unless-stopped
depends_on:
db:

View file

@ -70,9 +70,9 @@ faster and signature verification sits on the login path.
tools/build.sh --zip
```
Downloads the pinned WordPress, verifies its SHA-256, applies `patches/`, adds
the plugins, and writes `dist/sirius-press-<version>.zip` alongside its
checksum.
Copies the vendored WordPress out of `wordpress/`, adds the plugins, and
writes `dist/sirius-press-<version>.zip` alongside its checksum. Nothing is
downloaded — core is in the repository.
---
@ -129,9 +129,9 @@ git pull
cd docker && docker compose build && docker compose up -d
```
Core, the fork's plugins and the patch series all come from the image, so this
upgrades all three. Your uploads, your database, your other plugins and themes,
and `wp-config.php` live in volumes and are untouched.
Core and the fork's plugins both come from the image, so this upgrades both.
Your uploads, your database, your other plugins and themes, and
`wp-config.php` live in volumes and are untouched.
This is why the document root is refilled from the image on every start rather
than being a volume of its own — the usual WordPress-in-Docker layout pins core

View file

@ -41,7 +41,7 @@ built-in server plus the official SQLite drop-in is enough, and it starts in
seconds.
```bash
# 1. a patched tree
# 1. a complete tree (copied out of the vendored wordpress/, plugins added)
tools/build.sh
cp -R dist/sirius-press /tmp/wpsite

View file

@ -7,35 +7,33 @@ a liability rather than a project. This page is how Sirius Press takes them.
## The arrangement
WordPress is not in this repository. Instead:
WordPress lives in this repository, at `wordpress/`, as a **git subtree**. The
fork's own change sits on top of it as an ordinary commit.
- `tools/wordpress.lock` pins an exact version, its download URL and its
SHA-256.
- `patches/` holds the fork's core diff as a patch series.
- `tools/build.sh` and the Docker image each download the pinned tarball,
verify the hash, and apply the series.
Three pieces make that work:
The entire core diff is currently **one file, 75 lines**: the setup wizard, at
`wp-admin/install.php`. Everything else the fork does is plugins and hooks.
| | |
|---|---|
| `wordpress/` | The vendored tree, patched. This is what gets built and shipped. |
| `sirius-press/wordpress-upstream` | A branch holding **pristine** upstream releases, one commit each, never edited. The other side of the merge. |
| `patches/` | A readable record of what the fork changes in core. Generated, not maintained. |
### Why not a vendored subtree
The entire core diff is **one file, 75 lines**: `wp-admin/install.php`, the
setup wizard. Everything else Sirius Press does is plugins and hooks.
The original plan was to vendor core as a `git subtree`, which is the standard
way to run a fork with a substantial diff. Two numbers argued against it.
### Why a subtree and not a patch series
WordPress 7.1.1 is **149 MB and 5,008 files**. The fork's core diff is **75
lines**. Carrying the first to express the second means every clone, every
`git status`, every `git subtree split` in the parent monorepo pays for a patch
you can read in a minute — and it buries that patch in a haystack where nobody
will ever review it again.
Because a three-way merge understands something a patch does not. When
upstream edits lines near the fork's change, `git subtree merge` merges them
and moves on; `patch` either applies with fuzz and hopes, or fails and hands
you the job of re-deriving the change by hand. A conflict from the merge is
resolved once, in the file, and stays resolved — the next release merges
against the resolution.
A patch series makes the opposite trade. The core change is a file you can read
in one sitting, review in a pull request, and re-read whenever it stops
applying. The cost is that upstream merges are `patch` rather than a three-way
git merge — which, for 75 lines in one file, is not much of a cost.
This holds as long as the diff stays small. If the fork ever needs to change
core substantially, vendor it properly: see "Switching to a subtree" below.
The cost is repository size: WordPress is about 149 MB and 3,800 files. That
is the price of a fork that can take a security release in a minute, and it is
the right trade for a project whose whole argument is that it should outlive
its maintainers' attention.
---
@ -45,46 +43,33 @@ core substantially, vendor it properly: see "Switching to a subtree" below.
tools/update-wordpress.sh 7.1.2
```
It fetches the release, verifies its SHA-1 against what wordpress.org
publishes, records a SHA-256, applies the patch series to the new tree, and
reports what happened.
It downloads the release, checks it against the SHA-1 wordpress.org publishes,
imports the pristine tree onto `sirius-press/wordpress-upstream`, and merges
that branch into `wordpress/`. Then it updates `tools/wordpress.lock` and
regenerates `patches/`.
Three possible outcomes:
Two outcomes.
**Clean.** The series applied. `tools/wordpress.lock` is updated, the tests
run, and you commit the lock change. Usually thirty seconds of work.
**Clean.** The merge went through. Run the tests, build, commit. Usually a
minute of work.
**Applied with fuzz.** Upstream moved code near a hunk but not the hunk itself.
The script says which hunk and by how many lines. Look at the result, then
refresh the series so the next release starts from a clean base:
**Conflicted.** Upstream changed the same lines the fork changes — in
practice, `wp-admin/install.php`. Git leaves the conflict in the file:
```bash
tools/update-wordpress.sh 7.1.2 --refresh
git status
$EDITOR wordpress/wp-admin/install.php
git add wordpress/wp-admin/install.php
git commit
tools/refresh-patches.sh
```
**Failed.** Upstream rewrote the code the patch touches. This is the case that
needs a person: open the new `wp-admin/install.php`, redo the change by hand,
and regenerate the patch. The script leaves the unpacked tree in
`dist/.upstream/` so there is something to work in.
Then set `WP_VERSION`, `WP_URL`, `WP_SHA256` and `WP_SHA1` in
`tools/wordpress.lock` by hand, since the script stopped before it got there.
In every case the fork is **not broken while you work** — the lock file still
points at the last known-good version, and builds keep producing that until you
change it.
---
## Reviewing a release before taking it
Security releases are usually small, and it is worth knowing what changed near
the code the fork patches:
```bash
tools/update-wordpress.sh 7.1.2 --diff wp-admin/install.php
```
prints upstream's own diff for that file between the pinned version and the new
one. If it is empty — which it usually is — the patch will apply and there is
nothing to think about.
Either way the fork is **not broken while you work**: the merge is in your
working tree, and until you commit it, `wordpress/` still holds the last
version that worked.
---
@ -92,42 +77,58 @@ nothing to think about.
```bash
tests/run.sh
```
The suite does not test core, but it does test every assumption the fork makes
about it. Then build and try an actual install:
```bash
tools/build.sh
cd docker && docker compose build && docker compose up -d
```
and walk through `wp-admin/install.php` once. The setup wizard is the only
patched file, so it is the only thing an upstream change can break in a way
the tests would miss.
The suite does not test core, but it tests every assumption the fork makes
about it. Then walk through `wp-admin/install.php` once in a browser — the
setup wizard is the only patched file, so it is the only thing an upstream
change can break in a way the tests would miss.
[testing.md](testing.md) has a recipe for a throwaway instance that needs no
database server.
---
## Switching to a subtree
## Keeping `patches/` honest
If the diff ever grows past what a patch series is comfortable with, the
mechanics are:
`patches/` is documentation. It answers the question anyone auditing this fork
asks first — *what exactly did you change inside WordPress?* — in a minute,
which `git log wordpress/` cannot, because that log is mostly upstream
imports.
It is generated from the tree, never edited:
```bash
git remote add wordpress https://github.com/WordPress/WordPress.git
git fetch --depth=1 wordpress 7.1.1
git subtree add --prefix=wordpress FETCH_HEAD --squash
tools/refresh-patches.sh # rewrite it
tools/refresh-patches.sh --check # fail if it has drifted (for CI)
```
and then, per release:
Generated documentation stays true. A hand-maintained record of a fork's core
diff drifts, and a stale one is worse than none, because people trust it.
---
## Changing core yourself
Edit the file under `wordpress/` and commit it like any other change. The next
upstream merge will three-way it.
Before you do: check whether a plugin hook can carry the change instead. Every
line added to `wordpress/` is a line that can conflict with upstream forever,
and the reason this fork's merges are cheap is that there are only 75 of them.
The registration form, the password-reset page and the mail pipeline were all
replaced from plugins precisely so they would never appear in this directory.
---
## If the upstream branch is missing
A clone made with `--single-branch` will not have
`sirius-press/wordpress-upstream`, and `tools/update-wordpress.sh` will say so
rather than guessing. Fetch it:
```bash
git fetch --depth=1 wordpress 7.1.2
git subtree merge --prefix=wordpress FETCH_HEAD --squash
git fetch origin sirius-press/wordpress-upstream:sirius-press/wordpress-upstream
```
with `patches/` becoming the record of what was changed rather than the
mechanism that changes it. The build script would then copy `wordpress/`
instead of downloading and patching.
Worth doing when the core diff reaches, say, a dozen files. Not before.
Building and running do not need it — only taking a new upstream release does.

View file

@ -1,31 +1,13 @@
Subject: [PATCH] setup wizard: ask for a wallet, not a mailbox
Subject: [PATCH] wp-admin/install.php
The installer is the one place in WordPress where the fork cannot do its
work from a plugin. It runs before plugins load, and it refuses to finish
without an email address, so a site with no mail identity cannot be
installed at all.
The change is the smallest one that removes the requirement:
- "Your Email" becomes "Your wallet address", and it is optional. The
installer has nothing to verify a signature against yet — the site
does not exist — so an address typed here is taken on trust and can be
replaced from the profile screen afterwards, where it CAN be proved.
- The two is_email() gates become one address check.
- wp_install() still receives an email string, because its signature and
everything downstream of it expects one. It gets a permanently
unroutable .invalid placeholder.
The two helper functions are defined by the Sirius Press bootstrap
must-use plugin, which loads before this file on every request including
the installer. If that plugin is missing, they fall back to accepting
nothing and the installer behaves as it does on stock WordPress minus the
email field.
The fork's change to this file, regenerated by tools/refresh-patches.sh.
It is a record, not the mechanism: core is vendored under wordpress/ and
this diff is what distinguishes it from pristine upstream.
Applies to: WordPress 7.1.1
--- a/wp-admin/install.php
+++ b/wp-admin/install.php
@@ -101,6 +101,8 @@
@@ -101,6 +101,8 @@ function display_setup_form( $error = null ) {
$weblog_title = isset( $_POST['weblog_title'] ) ? trim( wp_unslash( $_POST['weblog_title'] ) ) : '';
$user_name = isset( $_POST['user_name'] ) ? trim( wp_unslash( $_POST['user_name'] ) ) : '';
$admin_email = isset( $_POST['admin_email'] ) ? trim( wp_unslash( $_POST['admin_email'] ) ) : '';
@ -34,7 +16,7 @@ Applies to: WordPress 7.1.1
if ( ! is_null( $error ) ) {
?>
@@ -175,9 +177,9 @@
@@ -175,9 +177,9 @@ function display_setup_form( $error = null ) {
</tr>
<?php endif; ?>
<tr>
@ -47,7 +29,7 @@ Applies to: WordPress 7.1.1
</tr>
<?php $blog_privacy_selector_title = has_action( 'blog_privacy_selector' ) ? __( 'Site visibility' ) : __( 'Search engine visibility' ); ?>
<tr>
@@ -411,10 +413,20 @@
@@ -411,10 +413,20 @@ switch ( $step ) {
$user_name = isset( $_POST['user_name'] ) ? trim( wp_unslash( $_POST['user_name'] ) ) : '';
$admin_password = isset( $_POST['admin_password'] ) ? wp_unslash( $_POST['admin_password'] ) : '';
$admin_password_check = isset( $_POST['admin_password2'] ) ? wp_unslash( $_POST['admin_password2'] ) : '';
@ -70,7 +52,7 @@ Applies to: WordPress 7.1.1
$error = false;
if ( empty( $user_name ) ) {
// TODO: Poka-yoke.
@@ -427,19 +439,20 @@
@@ -427,19 +439,20 @@ switch ( $step ) {
// TODO: Poka-yoke.
display_setup_form( __( 'Your passwords do not match. Please try again.' ) );
$error = true;

View file

@ -39,14 +39,28 @@ fi
bar "syntax"
syntax_ok=1
if command -v "$PHP_BIN" >/dev/null 2>&1; then
# The fork's own PHP, plus every core file the fork patches. Sweeping all
# of wordpress/ would lint 3,800 files of upstream code that upstream
# already tests, and turn a one-second suite into a minute of waiting.
lint_targets() {
find "$here/../plugins" "$here/../mu-plugins" "$here" \
-name '*.php' -not -path '*/node_modules/*' 2>/dev/null
for patch in "$here"/../patches/*.patch; do
[ -e "$patch" ] || continue
sed -n 's|^--- a/||p' "$patch" | while IFS= read -r rel; do
case "$rel" in *.php) echo "$here/../wordpress/$rel" ;; esac
done
done
}
while IFS= read -r file; do
[ -f "$file" ] || continue
if ! "$PHP_BIN" -l "$file" >/dev/null 2>&1; then
echo " FAIL $file"
"$PHP_BIN" -l "$file" 2>&1 | head -3
syntax_ok=0
status=1
fi
done < <(find "$here/.." -name '*.php' -not -path '*/dist/*' -not -path '*/node_modules/*')
done < <(lint_targets)
fi
if command -v "$NODE_BIN" >/dev/null 2>&1; then
while IFS= read -r file; do
@ -55,9 +69,9 @@ if command -v "$NODE_BIN" >/dev/null 2>&1; then
syntax_ok=0
status=1
fi
done < <(find "$here/.." -name '*.js' -not -path '*/dist/*' -not -path '*/node_modules/*')
done < <(find "$here/../plugins" "$here" -name '*.js' -not -path '*/node_modules/*' 2>/dev/null)
fi
[ "$syntax_ok" -eq 1 ] && echo " ok every PHP and JS file parses"
[ "$syntax_ok" -eq 1 ] && echo " ok the fork's PHP and JS parses, including every patched core file"
echo
if [ "$status" -eq 0 ]; then

View file

@ -1,18 +1,17 @@
#!/usr/bin/env bash
# build.sh — assemble a complete, patched Sirius Press tree.
# build.sh — assemble a complete Sirius Press tree.
#
# Downloads the pinned WordPress, verifies it against the checksum in
# tools/wordpress.lock, applies the patch series in patches/, drops this
# repository's plugins and must-use plugins into place, and leaves the result
# in dist/sirius-press/.
# Copies the vendored WordPress from wordpress/, drops this repository's
# plugins and must-use plugins into it, and leaves the result in
# dist/sirius-press/.
#
# With --zip it also produces dist/sirius-press-<version>.zip, which is the
# artifact shared-hosting users upload.
#
# The verification is not a formality. This script fetches executable code
# over the network and then runs it as a web server; the checksum is the only
# thing standing between a compromised mirror and every site built from it.
# If it fails, the build stops — there is no --skip-verify, on purpose.
# There is no download and no checksum step, because there is nothing to
# fetch: core is in the repository, already patched, and what you build is
# exactly what you can read in `git log wordpress/`. Moving to a new upstream
# release is tools/update-wordpress.sh, not a flag here.
set -euo pipefail
@ -25,7 +24,7 @@ source tools/wordpress.lock
VERSION="$(grep -m1 "^ \* Version:" plugins/sirius-press-core/sirius-press-core.php | awk '{print $3}')"
DIST="$here/dist"
TARGET="$DIST/sirius-press"
CACHE="${SIRIUS_BUILD_CACHE:-$DIST/.cache}"
SOURCE="$here/wordpress"
MAKE_ZIP=0
for arg in "$@"; do
@ -38,67 +37,22 @@ done
say() { printf '\033[1m→\033[0m %s\n' "$*"; }
die() { printf '\033[31merror:\033[0m %s\n' "$*" >&2; exit 1; }
command -v curl >/dev/null || die "curl is required"
command -v tar >/dev/null || die "tar is required"
command -v patch >/dev/null || die "patch is required (used to apply the core patch series)"
[ -f "$SOURCE/wp-includes/version.php" ] \
|| die "wordpress/ is missing or empty — check the whole repository was cloned."
# ---------------------------------------------------------------- fetch core
mkdir -p "$CACHE"
tarball="$CACHE/wordpress-$WP_VERSION.tar.gz"
if [ ! -f "$tarball" ]; then
say "downloading WordPress $WP_VERSION"
curl -fsSL -o "$tarball.part" "$WP_URL" || die "download failed"
mv "$tarball.part" "$tarball"
vendored="$(grep -m1 'wp_version = ' "$SOURCE/wp-includes/version.php" | sed "s/.*'\(.*\)'.*/\1/")"
if [ "$vendored" != "$WP_VERSION" ]; then
die "wordpress/ holds $vendored but tools/wordpress.lock says $WP_VERSION.
One of them is stale; tools/update-wordpress.sh keeps them in step."
fi
say "verifying checksum"
actual="$(sha256sum "$tarball" | cut -d' ' -f1)"
if [ "$actual" != "$WP_SHA256" ]; then
rm -f "$tarball"
die "checksum mismatch for WordPress $WP_VERSION
expected $WP_SHA256
got $actual
The cached download has been deleted. If this repeats, the mirror is serving
something other than what this fork was built against — do not work around it."
fi
# ------------------------------------------------------------------ assemble
# --------------------------------------------------------------- assemble
say "unpacking into dist/sirius-press"
say "copying WordPress $vendored out of wordpress/"
rm -rf "$TARGET"
mkdir -p "$TARGET"
tar -xzf "$tarball" -C "$DIST"
mv "$DIST/wordpress"/* "$TARGET"/
mv "$DIST/wordpress"/.[!.]* "$TARGET"/ 2>/dev/null || true
rmdir "$DIST/wordpress"
# ------------------------------------------------------------ patch series
shopt -s nullglob
patches=(patches/*.patch)
shopt -u nullglob
if [ ${#patches[@]} -eq 0 ]; then
say "no core patches to apply"
else
say "applying ${#patches[@]} core patch(es)"
for patch in "${patches[@]}"; do
# -F3 tolerates a few lines of drift around the hunk, so a WordPress
# point release that shifts the surrounding code still takes the patch.
# If it genuinely no longer fits, stop: a half-patched installer is
# worse than a failed build.
if (cd "$TARGET" && patch -p1 -F3 --forward --silent < "$here/$patch"); then
echo " ok $(basename "$patch")"
else
die "$(basename "$patch") did not apply to WordPress $WP_VERSION.
Run tools/update-wordpress.sh to refresh the series against this version."
fi
done
fi
# ---------------------------------------------------------------- our code
# The trailing /. copies the contents rather than nesting another directory.
cp -a "$SOURCE/." "$TARGET/"
say "installing Sirius Press plugins"
mkdir -p "$TARGET/wp-content/plugins" "$TARGET/wp-content/mu-plugins"
@ -107,13 +61,6 @@ for plugin in plugins/*/; do
done
cp mu-plugins/*.php "$TARGET/wp-content/mu-plugins/"
# The stock wp-config-sample.php has no SIRIUS_PRESS_KEY, and a site that
# never sets one falls back to its auth salts — which works, but means
# rotating salts orphans the stored publishing key.
if [ -f docker/wp-config-sirius.php ]; then
cp docker/wp-config-sirius.php "$TARGET/wp-config-sirius-sample.php"
fi
say "built dist/sirius-press ($(du -sh "$TARGET" | cut -f1))"
# --------------------------------------------------------------------- zip
@ -151,9 +98,9 @@ fi
cat <<EOF
Sirius Press $VERSION, built on WordPress $WP_VERSION.
Sirius Press $VERSION, on WordPress $vendored.
dist/sirius-press/ a complete, patched WordPress tree
dist/sirius-press/ a complete tree, ready to serve
$( [ "$MAKE_ZIP" -eq 1 ] && echo " dist/sirius-press-$VERSION.zip upload this to a shared host" )
To run it locally: cd docker && docker compose up -d

107
tools/refresh-patches.sh Normal file
View file

@ -0,0 +1,107 @@
#!/usr/bin/env bash
# refresh-patches.sh — regenerate patches/ from what the fork actually changed.
#
# Since core is vendored, `patches/` is no longer how the build works — it is
# documentation. It answers the question anyone auditing this fork asks first:
# *what exactly did you change inside WordPress?* A directory of readable
# diffs answers that in a minute; `git log wordpress/` does not, because it is
# mostly upstream imports.
#
# Documentation that is generated stays true. Documentation that is maintained
# by hand drifts, and a stale record of a fork's core diff is worse than none,
# because people trust it.
#
# tools/refresh-patches.sh # rewrite patches/ from the tree
# tools/refresh-patches.sh --check # fail if it would change anything (CI)
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
repo="$(git -C "$here" rev-parse --show-toplevel)"
# Ask git where we are rather than subtracting paths: on Windows
# --show-toplevel answers "D:/..." while $PWD is "/d/...", and the
# subtraction silently leaves an absolute path behind.
prefix="$(cd "$here" && git rev-parse --show-prefix)wordpress"
UPSTREAM_BRANCH="sirius-press/wordpress-upstream"
CHECK=0
for arg in "$@"; do
case "$arg" in
--check) CHECK=1 ;;
*) echo "unknown option: $arg" >&2; exit 2 ;;
esac
done
say() { printf '\033[1m→\033[0m %s\n' "$*"; }
die() { printf '\033[31merror:\033[0m %s\n' "$*" >&2; exit 1; }
git -C "$repo" rev-parse --verify --quiet "$UPSTREAM_BRANCH" >/dev/null \
|| die "the branch $UPSTREAM_BRANCH is missing; there is nothing to diff against."
# shellcheck source=wordpress.lock
source "$here/tools/wordpress.lock"
staging="$here/dist/.patch-refresh"
rm -rf "$staging"
mkdir -p "$staging"
say "comparing ${prefix}/ against pristine WordPress ${WP_VERSION}"
# Every path that differs between the vendored tree and the pristine import.
#
# Compared tree to tree, not commit to commit: the upstream branch keeps
# WordPress at its root while the subtree keeps it under a prefix, so a
# plain `git diff BRANCH HEAD -- <prefix>` compares two different path
# spaces and reports the whole distribution as added.
UPSTREAM_TREE="${UPSTREAM_BRANCH}^{tree}"
VENDORED_TREE="HEAD:${prefix}"
mapfile -t changed < <(
git -C "$repo" diff --name-only "$UPSTREAM_TREE" "$VENDORED_TREE"
)
if [ ${#changed[@]} -eq 0 ]; then
say "the fork changes nothing in core — writing an empty series"
fi
index=0
for file in "${changed[@]}"; do
[ -n "$file" ] || continue
index=$((index + 1))
slug="$(echo "$file" | tr '/.' '--' | tr -cd 'A-Za-z0-9-')"
out="$staging/$(printf '%04d' "$index")-${slug}.patch"
{
echo "Subject: [PATCH] ${file}"
echo
echo "The fork's change to this file, regenerated by tools/refresh-patches.sh."
echo "It is a record, not the mechanism: core is vendored under wordpress/ and"
echo "this diff is what distinguishes it from pristine upstream."
echo
echo "Applies to: WordPress ${WP_VERSION}"
git -C "$repo" diff "$UPSTREAM_TREE" "$VENDORED_TREE" -- "$file" | tail -n +3
} > "$out"
done
if [ "$CHECK" -eq 1 ]; then
if diff -rq "$here/patches" "$staging" >/dev/null 2>&1; then
say "patches/ matches the tree"
rm -rf "$staging"
exit 0
fi
diff -ru "$here/patches" "$staging" || true
rm -rf "$staging"
die "patches/ is out of date. Run tools/refresh-patches.sh and commit the result."
fi
rm -f "$here/patches"/*.patch
if [ "$index" -gt 0 ]; then
cp "$staging"/*.patch "$here/patches/"
fi
rm -rf "$staging"
say "wrote ${index} patch file(s) describing the fork's core diff"
for f in "$here/patches"/*.patch; do
[ -e "$f" ] || continue
printf ' %s (%s lines)\n' "$(basename "$f")" "$(grep -c '' "$f")"
done

View file

@ -1,51 +1,57 @@
#!/usr/bin/env bash
# update-wordpress.sh — move the fork onto a new WordPress release.
# update-wordpress.sh — bring a new WordPress release into the fork.
#
# tools/update-wordpress.sh 7.1.2
# tools/update-wordpress.sh 7.1.2 --refresh # also rewrite the patch series
# tools/update-wordpress.sh 7.1.2 --diff wp-admin/install.php
#
# Fetches the release, verifies it against the SHA-1 wordpress.org publishes,
# applies the patch series, and reports whether the fork still fits.
# Two steps, and the second is the one that matters:
#
# Nothing is committed and tools/wordpress.lock is only rewritten once the
# series has actually applied — so a failed run leaves the fork building the
# last known-good version rather than a broken one.
# 1. Import the pristine release onto the `sirius-press/wordpress-upstream`
# branch — one commit per version, nothing but upstream, never edited.
# 2. `git subtree merge` that branch into wordpress/, which three-way merges
# it against the fork's own commits on top of the last import.
#
# That second step is why core is vendored rather than patched at build time.
# A three-way merge understands that upstream changed lines A and B while the
# fork changed line C, and only stops when they overlap. It also leaves a
# conflict you resolve once, in the file, instead of a patch you re-derive
# every release.
#
# Nothing is pushed. Run it, look at the merge, run the tests, then commit.
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$here"
repo="$(git -C "$here" rev-parse --show-toplevel)"
prefix="${here#"$repo"/}/wordpress"
VERSION="${1:-}"
REFRESH=0
DIFF_FILE=""
shift || true
while [ $# -gt 0 ]; do
case "$1" in
--refresh) REFRESH=1 ;;
--diff) shift; DIFF_FILE="${1:-}" ;;
*) echo "unknown option: $1" >&2; exit 2 ;;
esac
shift
done
say() { printf '\033[1m→\033[0m %s\n' "$*"; }
warn() { printf '\033[33mwarning:\033[0m %s\n' "$*" >&2; }
die() { printf '\033[31merror:\033[0m %s\n' "$*" >&2; exit 1; }
[ -n "$VERSION" ] || die "usage: tools/update-wordpress.sh <version> [--refresh] [--diff <file>]"
[ -n "$VERSION" ] || die "usage: tools/update-wordpress.sh <version>"
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]] || die "'$VERSION' does not look like a WordPress version"
# shellcheck source=wordpress.lock
source tools/wordpress.lock
source "$here/tools/wordpress.lock"
CURRENT="$WP_VERSION"
[ "$VERSION" != "$CURRENT" ] || die "the fork is already on WordPress $VERSION"
UPSTREAM_BRANCH="sirius-press/wordpress-upstream"
git -C "$repo" rev-parse --verify --quiet "$UPSTREAM_BRANCH" >/dev/null \
|| die "the branch $UPSTREAM_BRANCH is missing. It holds the pristine upstream
imports that the subtree merges against; without it this fork cannot take an
upstream release. See docs/upstream-merges.md."
# A subtree merge writes to the working tree, so it has to be clean — at least
# for the paths involved. Refuse early rather than half way through.
if ! git -C "$repo" diff --quiet -- "$prefix" 2>/dev/null; then
die "wordpress/ has uncommitted changes. Commit or stash them first."
fi
WORK="$here/dist/.upstream"
CACHE="$here/dist/.cache"
mkdir -p "$WORK" "$CACHE"
mkdir -p "$CACHE"
URL="https://wordpress.org/wordpress-${VERSION}.tar.gz"
TARBALL="$CACHE/wordpress-${VERSION}.tar.gz"
@ -57,155 +63,133 @@ if [ ! -f "$TARBALL" ]; then
mv "$TARBALL.part" "$TARBALL"
fi
say "verifying against wordpress.org's published SHA-1"
say "checking it against the SHA-1 wordpress.org publishes"
published="$(curl -fsSL "${URL}.sha1" 2>/dev/null || true)"
actual_sha1="$(sha1sum "$TARBALL" | cut -d' ' -f1)"
if [ -z "$published" ]; then
warn "wordpress.org did not serve a .sha1 for this release; continuing on the SHA-256 recorded below"
warn "wordpress.org served no .sha1 for this release; recording the sha256 below unverified"
elif [ "$published" != "$actual_sha1" ]; then
rm -f "$TARBALL"
die "SHA-1 mismatch — the download does not match what wordpress.org publishes.
die "SHA-1 mismatch — the download is not what wordpress.org publishes.
published $published
got $actual_sha1"
fi
NEW_SHA256="$(sha256sum "$TARBALL" | cut -d' ' -f1)"
# ------------------------------------------------------------------ unpack
# ------------------------------------------- import onto the upstream branch
say "unpacking"
rm -rf "${WORK:?}/new" "${WORK:?}/pristine" "${WORK:?}/wordpress"
tar -xzf "$TARBALL" -C "$WORK"
[ -d "$WORK/wordpress" ] || die "the tarball did not contain a wordpress/ directory"
mv "$WORK/wordpress" "$WORK/new"
cp -R "$WORK/new" "$WORK/pristine"
WORKTREE="$here/dist/.vendor-import"
say "importing the pristine tree onto $UPSTREAM_BRANCH"
rm -rf "$WORKTREE"
git -C "$repo" worktree add --quiet "$WORKTREE" "$UPSTREAM_BRANCH"
# --------------------------------------------------------- what changed?
cleanup() {
git -C "$repo" worktree remove --force "$WORKTREE" >/dev/null 2>&1 || true
}
trap cleanup EXIT
if [ -n "$DIFF_FILE" ]; then
old_tarball="$CACHE/wordpress-${CURRENT}.tar.gz"
if [ ! -f "$old_tarball" ]; then
say "fetching $CURRENT for comparison"
curl -fsSL -o "$old_tarball" "$WP_URL"
fi
rm -rf "$WORK/old"
mkdir -p "$WORK/old"
tar -xzf "$old_tarball" -C "$WORK/old"
say "upstream's own changes to $DIFF_FILE between $CURRENT and $VERSION:"
diff -u "$WORK/old/wordpress/$DIFF_FILE" "$WORK/new/$DIFF_FILE" || true
echo
# Replace the tree wholesale: files upstream deleted have to disappear, or the
# subtree merge would keep resurrecting them.
find "$WORKTREE" -mindepth 1 -maxdepth 1 ! -name '.git' -exec rm -rf {} +
tar -xzf "$TARBALL" -C "$WORKTREE" --strip-components=1
git -C "$WORKTREE" add -A
if git -C "$WORKTREE" diff --cached --quiet; then
die "WordPress $VERSION is byte-identical to what is already imported."
fi
# ------------------------------------------------------------ the series
git -C "$WORKTREE" \
-c user.name="Silent Mode" -c user.email="hephaestus@silentmode.st" \
commit -q -m "WordPress ${VERSION}
shopt -s nullglob
patches=(patches/*.patch)
shopt -u nullglob
Pristine upstream, unpacked from the official wordpress.org tarball.
failed=0
fuzzed=0
${URL}
sha1 ${actual_sha1} (published by wordpress.org)
sha256 ${NEW_SHA256}
if [ ${#patches[@]} -eq 0 ]; then
say "no patches to apply"
This branch carries nothing but upstream releases, one commit each, and is
never edited."
say "imported as $(git -C "$WORKTREE" rev-parse --short HEAD)"
cleanup
trap - EXIT
# ------------------------------------------------------------ subtree merge
say "merging into ${prefix}/"
if git -C "$repo" \
-c user.name="Silent Mode" -c user.email="hephaestus@silentmode.st" \
subtree merge --prefix="$prefix" "$UPSTREAM_BRANCH" \
-m "merge: WordPress ${VERSION} into the vendored subtree"
then
merged=1
else
say "applying ${#patches[@]} patch(es) to WordPress $VERSION"
for patch in "${patches[@]}"; do
name="$(basename "$patch")"
output="$(cd "$WORK/new" && patch -p1 -F3 --forward < "$here/$patch" 2>&1)" && rc=0 || rc=$?
if [ "$rc" -ne 0 ]; then
echo " FAILED $name"
echo "$output" | sed 's/^/ /'
failed=1
elif echo "$output" | grep -q 'with fuzz'; then
echo " fuzz $name"
echo "$output" | grep 'with fuzz' | sed 's/^/ /'
fuzzed=1
else
echo " ok $name"
fi
done
merged=0
fi
echo
if [ "$failed" -eq 1 ]; then
if [ "$merged" -eq 0 ]; then
cat >&2 <<EOF
$(printf '\033[31mThe patch series no longer fits WordPress %s.\033[0m' "$VERSION")
Upstream has rewritten code the fork patches. Nothing has changed here — the
lock file still points at $CURRENT and builds still produce a working fork.
$(printf '\033[31mThe merge stopped on a conflict.\033[0m')
Upstream changed code the fork also changes — almost certainly
wp-admin/install.php, the only file this fork patches.
git status # what conflicted
git diff # the overlap
\$EDITOR ${prefix}/wp-admin/install.php
git add ${prefix}/wp-admin/install.php
git commit # finish the merge
Then regenerate the readable copy of the diff and update the lock:
tools/refresh-patches.sh
# set WP_VERSION / WP_URL / WP_SHA256 / WP_SHA1 in tools/wordpress.lock
To fix it:
1. The unpacked tree is at dist/.upstream/new — edit it by hand.
2. Diff it against the pristine copy:
diff -u dist/.upstream/pristine/<file> dist/.upstream/new/<file>
3. Replace the hunks in the failing patch with the result, keeping its
Subject: header and explanation.
4. Run this script again.
EOF
exit 1
fi
# ------------------------------------------------------------ refresh
if [ "$REFRESH" -eq 1 ]; then
say "regenerating the patch series against $VERSION"
for patch in "${patches[@]}"; do
file="$(grep -m1 '^--- a/' "$patch" | sed 's|^--- a/||')"
[ -n "$file" ] || { warn "$(basename "$patch") has no file header; left alone"; continue; }
header="$(sed -n '1,/^--- a\//p' "$patch" | sed '$d')"
{
echo "$header" | sed "s/^Applies to: WordPress .*/Applies to: WordPress $VERSION/"
diff -u "$WORK/pristine/$file" "$WORK/new/$file" \
| sed -e "1s|.*|--- a/$file|" -e "2s|.*|+++ b/$file|"
} > "$patch.new"
mv "$patch.new" "$patch"
echo " refreshed $(basename "$patch")"
done
elif [ "$fuzzed" -eq 1 ]; then
warn "the series applied with fuzz. Run again with --refresh so the next release starts clean."
fi
# --------------------------------------------------------------- the lock
say "updating tools/wordpress.lock"
python3 - "$VERSION" "$URL" "$NEW_SHA256" "$actual_sha1" <<'PY'
python3 - "$VERSION" "$URL" "$NEW_SHA256" "$actual_sha1" "$here/tools/wordpress.lock" <<'PY'
import io, sys
version, url, sha256, sha1 = sys.argv[1:5]
path = 'tools/wordpress.lock'
version, url, sha256, sha1, path = sys.argv[1:6]
fields = {
'WP_VERSION=': version,
'WP_URL=': url,
'WP_SHA256=': sha256,
'WP_SHA1=': sha1,
}
out = []
for line in io.open(path, encoding='utf-8'):
if line.startswith('WP_VERSION='):
out.append(f'WP_VERSION={version}\n')
elif line.startswith('WP_URL='):
out.append(f'WP_URL={url}\n')
elif line.startswith('WP_SHA256='):
out.append(f'WP_SHA256={sha256}\n')
elif line.startswith('WP_SHA1='):
out.append(f'WP_SHA1={sha1}\n')
else:
out.append(line)
for key, value in fields.items():
if line.startswith(key):
line = f'{key}{value}\n'
break
out.append(line)
io.open(path, 'w', encoding='utf-8', newline='').write(''.join(out))
PY
# The compose defaults mirror the lock so a plain `docker compose build`
# cannot drift onto a different core.
sed -i \
-e "s|WP_VERSION: \${WP_VERSION:-.*}|WP_VERSION: \${WP_VERSION:-$VERSION}|" \
-e "s|WP_URL: \${WP_URL:-.*}|WP_URL: \${WP_URL:-$URL}|" \
-e "s|WP_SHA256: \${WP_SHA256:-.*}|WP_SHA256: \${WP_SHA256:-$NEW_SHA256}|" \
docker/docker-compose.yml
if [ -x "$here/tools/refresh-patches.sh" ]; then
say "refreshing patches/ so it still describes the fork's core diff"
"$here/tools/refresh-patches.sh" || warn "could not refresh patches/ — do it by hand"
fi
cat <<EOF
$(printf '\033[32mSirius Press now builds on WordPress %s.\033[0m' "$VERSION")
tools/wordpress.lock $CURRENT -> $VERSION
docker/docker-compose.yml build args updated
${CURRENT} -> ${VERSION}
The merge and the lock update are staged as commits already; patches/ may
have changed too. Before pushing:
Next:
tests/run.sh
tools/build.sh
then walk through wp-admin/install.php once — the setup wizard is the only
patched file, so it is the only thing this can have broken.
file this fork patches, so it is the only thing this can have broken.
EOF