- PricingEngine::round_to_99(): ceil($price) - 0.01 undershoots whenever
$price's cents are already .99 or higher — an exact integer (ceil()
equals floor(), landing a full cent below $price) or, more subtly, any
fractional price above X.99 itself (a division result, not something
pre-rounded to 2 decimals, e.g. 20.995 -> old formula gave 20.99, below
the input). Rewritten as floor()+0.99, bumped by 1 if still under $price.
Verified against 8 cases including both boundary classes: every result
now >= its input.
- CoverSync::attach_cover(): wp_generate_attachment_metadata()'s return
value was never checked. Assumed it'd return empty on failure — verified
directly it does NOT: fed it 2000 bytes of garbage and got back
['filesize' => 2000], no width/height, since GD/Imagick couldn't decode
it. Old code would report "attached" for a degraded image with no
dimensions/srcset. Now checks for width+height specifically, and cleans
up the orphaned attachment on failure so a re-run retries the product.
Verified both the corrupt-image rejection (no orphan left, no thumbnail
set) and that a real image still attaches normally.
- Makefile: the per-invocation .env.$(ENV).compose file (holds every API
key and both DB passwords, stripped of DB_PASSWORD/DB_ROOT_PASSWORD only)
was never cleaned up, left at default 644 in the repo root after every
`make` command. Now chmod 600 on creation and removed at the end of every
target, preserving the underlying command's exit code through the
cleanup. Verified both the happy path (file gone after, exit 0) and the
failure path (bad ENV: file still cleaned up, real exit code still
propagates through make).
- docker-compose.yml: added a healthcheck to the wordpress service (bash's
/dev/tcp against php-fpm's port 9000 — no HTTP endpoint to hit directly,
and no `nc` in this image; verified it correctly succeeds once php-fpm is
listening and fails against a closed port) and switched cron's and
caddy's depends_on (across all three env overlays) from bare
container-started to condition: service_healthy. Previously both could
start against a wordpress container that had started but wasn't actually
ready yet. Verified via a full down/up cycle: db+redis healthy, then
wordpress starts and becomes healthy, only then do cron and caddy start.
- .env.dev/.env.staging/.env.production chmod'd 600 (were 644) — same
plaintext-credential content as secrets/<env>/, which is already 700/644
at the directory/file level respectively for a different reason (container
UID readability); these have no such constraint, only the host CLI reads
them. Also removed a stray .env.staging.compose left over from before the
Makefile fix above existed. Noted the convention in .env.example so newly
created env files follow it too.
--allow-root was routing around wp-cli's own safety check rather than
addressing why root was there in the first place: docker exec defaults to
the container's root user because the image never sets a non-root user
for exec sessions — it was never about the actual web-facing attack
surface, which already runs as www-data (verified: php-fpm's worker
processes, the ones executing plugin/theme code for real requests, run as
uid 33, not root; only our own deliberate admin commands were root).
wp-config.php and the rest of wp-core are already owned by www-data (the
official entrypoint sets this up), so there's no actual reason for our
own commands to run as anything else. Verified: a full clean deploy and
a bookstore-core wp-cli command both work identically running as
www-data, no functional change, just removed an unnecessary privilege.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two separate leaks were causing the same cosmetic-but-annoying warning to
survive every previous fix attempt:
1. Compose's own `secrets:` block reads the referenced file's *content*
as part of its config model, and applies the same interpolation
warning to it — even with DB_PASSWORD fully removed from every ${VAR}
and --env-file path. Switched from Compose's native `secrets:` to plain
bind mounts at the same /run/secrets/* paths: a bind mount only ever
touches the file's path, never its content, so it's immune. (Verified
this precisely with an isolated repro before rolling it out — the two
mechanisms behave differently even though they look equivalent.)
2. caddy's `env_file: .env.staging` (a leftover from the since-removed
basic-auth setup) loaded the *raw*, unfiltered env file directly,
bypassing deploy.sh/backup.sh/restore.sh's filtered-copy mechanism
entirely. Caddy only ever needed SITE_DOMAIN; switched to passing that
one value directly instead of the whole file.
Also fixed Caddyfile.staging: the site address had no explicit scheme, so
Caddy's automatic-HTTPS logic still applied to a private IP (registering
its own internal CA and redirecting HTTP->HTTPS) — not the "plain HTTP
only" behavior I'd assumed and told the user earlier. Prefixed with
`http://` to genuinely disable automatic HTTPS for this LAN-only box.
Verified end-to-end: fresh deploy with dollar-sign DB passwords produces
zero warnings, serves HTTP 200 on plain http:// with no redirect, and the
DB connection genuinely authenticates.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The reported bug (a generated password containing "$BRjTx3tlmSpz" got
silently blanked, breaking the DB connection) is the same Compose
interpolation issue as the earlier bcrypt hash, but this time in fields
that are genuinely user-chosen and can't just be avoided by convention.
Switched DB_PASSWORD/DB_ROOT_PASSWORD to Docker's official `_FILE`
secrets convention (MARIADB_PASSWORD_FILE / WORDPRESS_DB_PASSWORD_FILE),
backed by Compose's native `secrets:` mechanism: deploy.sh writes the raw
value to secrets/<env>/db_password, and the container reads that file
directly — the value never passes through Compose's ${VAR} interpolation
at all. Verified end-to-end with an actual `$`-containing password,
including a full deploy → backup → restore → still-serving round trip.
Also fixed along the way (found while actually testing, not assumed):
- Makefile never exported ENVIRONMENT, so `make up` alone (bypassing
deploy.sh) would have left the new secrets path unresolved.
- deploy.sh chmod'd the secret files 600, unreadable by the container's
own UID (www-data) — fixed to 644, relying on the containing directory
(700) to keep other host users out instead.
- backup.sh/restore.sh still called `mysqldump`/`mysql`, which don't
exist in the mariadb:11 image under those names — renamed to
mariadb-dump/mariadb. (This means neither script had actually
succeeded before now; both are verified working end-to-end here.)
The supplier/payment API keys remain passed the old way — nothing reads
them yet (bookstore-core is still a stub), so there's no live bug to fix
there; noted in .env.example that the same _FILE pattern should be used
once that code exists.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Replace the bare-repo post-receive hook with deploy/poll-deploy.sh: Gitea
and the Docker hosts are separate machines, so each box polls its branch
via host crontab instead of needing an exposed webhook receiver.
- Add Blocksy theme + Blocksy Companion auto-install to deploy.sh (free
tier; the paid Book Store starter site still needs a manual license step).
- Fix deploy.sh/backup.sh/restore.sh sourcing .env files as bash: a bcrypt
hash's `$2a$14$...` shape breaks under `set -u`. Replaced with
deploy/lib/env.sh, a literal (non-executing) KEY=VALUE reader.
- Fix docker compose itself mangling the same kind of value: both
`environment: ${VAR}` and `env_file:` run values through Compose's
interpolation, which silently blanks `$identifier`-shaped substrings.
The staging basic-auth hash is now rendered directly into the Caddyfile
by deploy.sh, bypassing Compose's variable system entirely.
- Fix dev/staging/production silently sharing one Compose project (and
therefore one db_data volume) by pinning an explicit -p per environment.
- cron and wordpress now share one environment anchor so they can't drift
apart again (cron was silently missing WORDPRESS_CONFIG_EXTRA before).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>