A second review pass specifically targeted at the fixes just made (not the original codebase) — caught one genuine regression from this session's own earlier work, plus a couple of gaps the first pass didn't probe deeply enough to find. - REGRESSION: SupplierOffer::insert() throwing on failure (a Medium-severity fix earlier this session, matching Work/Edition/Isbn) was never given a catch anywhere in the offer-generation path. Before that fix, a bad insert silently continued; after it, nothing stopped the exception from aborting the ENTIRE generate-offers run on the first failure. Fixed with the same per-item catch pattern GutenbergImporter already established. Verified by forcing a real DB failure (renamed the table mid-run): before this fix that aborted the batch immediately; after, it correctly processed all 2000 ISBNs, reported each failure individually, and completed with an accurate failed count — table restored after, 2000 offers confirmed intact. - ProductSync::sync_one() never checked WC_Product::save()'s return value. Verified directly (forced via the wp_insert_post_empty_content filter): save() returns 0 rather than throwing on a wp_insert_post()-level rejection, which would have gone through as Work::set_product_id($id, 0) — silently "succeeding" and invisible in $failed[], inconsistent with every other failure mode in this method already routing through the per-product SAVEPOINT + $failed[] reporting added this session. Now throws instead, confirmed it does NOT corrupt the existing pointer (stays at its prior value, self-heals on a future successful run) rather than writing 0. - docker-compose.yml: the wordpress healthcheck's timeout budget (~130s) could plausibly be exceeded by a legitimately slow (not broken) first boot on the 2-core/8GB box this actually runs on — confirmed that a service_healthy dependency timing out makes `docker compose up -d` (and so deploy.sh, under set -euo pipefail) hard-fail rather than just start late, a new deploy failure mode this session's healthcheck introduced. Widened to ~4.5 minutes of headroom (retries 20->40, start_period 30s->60s), well above deploy.sh's own existing 120s precedent for just the core-file-copy portion of the same boot. - Makefile: the per-target `; rc=$?; rm -f ...; exit $rc` cleanup added earlier this session doesn't reliably run under a real Ctrl+C — a foreground SIGINT terminates that shell chain before the `;` continues. Switched to a `trap 'rm -f ...' EXIT` (matching the idiom deploy.sh/ backup.sh already use), which fires on any shell termination and doesn't need to manually thread the exit code through. Verified by actually sending SIGINT to the whole process group (matching real terminal Ctrl+C, not just a backgrounded job) during `make logs` — the compose env file was correctly removed. Also cleaned up one duplicate leftover product (work_id=1 briefly had two products, from repeated manual test scenarios reusing the same work_id across this long session's testing, not from an active bug — confirmed the current code produces 0 new duplicates on repeated sync-products runs) found while verifying the ProductSync fix above.
162 lines
7.4 KiB
YAML
162 lines
7.4 KiB
YAML
# wordpress and cron are the same image and MUST share the same
|
|
# environment: the official image's wp-config.php reads getenv() live, at
|
|
# PHP runtime, in whichever container is executing — it does not bake
|
|
# values into the file once. If cron's env drifts from wordpress's (e.g.
|
|
# WORDPRESS_CONFIG_EXTRA missing), cron silently loses WP_REDIS_HOST,
|
|
# DISABLE_WP_CRON, etc. The anchor below is what prevents that drift.
|
|
# DB_PASSWORD/DB_ROOT_PASSWORD are deliberately NOT passed via ${VAR} here.
|
|
# Docker Compose's own interpolation mangles any value containing a `$`
|
|
# followed by a letter (confirmed with a bcrypt hash earlier, and again with
|
|
# a plain generated password: "$BRjTx3tlmSpz" got silently blanked out,
|
|
# breaking the DB connection). The official mariadb/wordpress images both
|
|
# support a `_FILE` convention specifically for this — point at a file path
|
|
# (never risky) and the container reads the raw contents itself, bypassing
|
|
# Compose's interpolation entirely. deploy.sh writes these files fresh from
|
|
# .env.<environment> before every `up`.
|
|
#
|
|
# These are plain bind mounts, NOT Compose's native `secrets:` block —
|
|
# Compose's `secrets:` construct reads the referenced file's *content* as
|
|
# part of its own config model and applies the same interpolation warning
|
|
# to it (confirmed: it still warned on "$BRjTx3tlmSpz" even after that
|
|
# value was fully removed from every ${VAR} and --env-file path). A plain
|
|
# bind mount never touches the file's content, only its path, so it's
|
|
# immune to this entirely.
|
|
#
|
|
# The remaining supplier/payment keys below are still passed the old ${VAR}
|
|
# way and remain exposed to the same class of bug — nothing consumes them
|
|
# yet (bookstore-core is still a stub for those), so when that code is
|
|
# written it should read `_FILE` variants the same way HARDCOVER_API_TOKEN
|
|
# now does below.
|
|
x-bookstore-env: &bookstore-env
|
|
WORDPRESS_DB_HOST: db
|
|
WORDPRESS_DB_NAME: ${DB_NAME}
|
|
WORDPRESS_DB_USER: ${DB_USER}
|
|
WORDPRESS_DB_PASSWORD_FILE: /run/secrets/db_password
|
|
WORDPRESS_TABLE_PREFIX: ${WP_TABLE_PREFIX:-wp_}
|
|
MAIL_CATCHER_HOST: mailhog
|
|
MAIL_CATCHER_PORT: 1025
|
|
WORDPRESS_CONFIG_EXTRA: |
|
|
define('DISABLE_WP_CRON', true);
|
|
define('WP_REDIS_HOST', 'redis');
|
|
define('WP_MEMORY_LIMIT', '256M');
|
|
# Passed through for the bookstore-core plugin's supplier adapters /
|
|
# payment integration (design doc §04/§05) — not read by WordPress core.
|
|
ACTIVE_SUPPLIER_ADAPTERS: ${ACTIVE_SUPPLIER_ADAPTERS:-gutenberg_test}
|
|
BOOKSRUN_API_KEY: ${BOOKSRUN_API_KEY:-}
|
|
INGRAM_API_KEY: ${INGRAM_API_KEY:-}
|
|
INGRAM_ACCOUNT_ID: ${INGRAM_ACCOUNT_ID:-}
|
|
HELCIM_API_TOKEN: ${HELCIM_API_TOKEN:-}
|
|
HELCIM_ACCOUNT_ID: ${HELCIM_ACCOUNT_ID:-}
|
|
MAILERLITE_API_KEY: ${MAILERLITE_API_KEY:-}
|
|
HARDCOVER_API_TOKEN_FILE: /run/secrets/hardcover_api_token
|
|
|
|
services:
|
|
db:
|
|
image: mariadb:11
|
|
restart: unless-stopped
|
|
environment:
|
|
MARIADB_DATABASE: ${DB_NAME}
|
|
MARIADB_USER: ${DB_USER}
|
|
MARIADB_PASSWORD_FILE: /run/secrets/db_password
|
|
MARIADB_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
|
|
volumes:
|
|
- db_data:/var/lib/mysql
|
|
- ./secrets/${ENVIRONMENT:?ENVIRONMENT must be set}/db_password:/run/secrets/db_password:ro
|
|
- ./secrets/${ENVIRONMENT:?ENVIRONMENT must be set}/db_root_password:/run/secrets/db_root_password:ro
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "mariadb-admin ping -h 127.0.0.1 -u$$MARIADB_USER -p\"$$(cat /run/secrets/db_password)\" --silent"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 20
|
|
|
|
redis:
|
|
image: redis:7-alpine
|
|
restart: unless-stopped
|
|
volumes:
|
|
- redis_data:/data
|
|
healthcheck:
|
|
test: ["CMD", "redis-cli", "ping"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 20
|
|
|
|
wordpress:
|
|
build:
|
|
context: ./docker/php
|
|
restart: unless-stopped
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
environment: *bookstore-env
|
|
volumes:
|
|
- wp_core:/var/www/html
|
|
- wp_uploads:/var/www/html/wp-content/uploads
|
|
- wp_themes:/var/www/html/wp-content/themes
|
|
- ./wp-content/plugins/bookstore-core:/var/www/html/wp-content/plugins/bookstore-core
|
|
- ./wp-content/mu-plugins:/var/www/html/wp-content/mu-plugins
|
|
- ./secrets/${ENVIRONMENT:?ENVIRONMENT must be set}/db_password:/run/secrets/db_password:ro
|
|
- ./secrets/${ENVIRONMENT:?ENVIRONMENT must be set}/hardcover_api_token:/run/secrets/hardcover_api_token:ro
|
|
healthcheck:
|
|
# No HTTP endpoint to hit directly — php-fpm speaks FastCGI on 9000,
|
|
# not HTTP, and this image has no `nc`. bash's /dev/tcp redirection
|
|
# needs no extra binary and is enough to know php-fpm is actually
|
|
# accepting connections (verified directly: exits 0 once php-fpm is up,
|
|
# 1 against a closed port). Without this, cron/caddy's depends_on only
|
|
# waited for the container to *start*, not for php-fpm inside it to be
|
|
# ready — a slow first boot (official image copying core files in, could
|
|
# be worse on modest hardware) could let Caddy serve a transient 502
|
|
# before php-fpm was actually listening.
|
|
#
|
|
# cron and caddy now gate on this via depends_on: condition:
|
|
# service_healthy, which makes `docker compose up -d` (and so
|
|
# deploy.sh, under set -euo pipefail) hard-fail if wordpress never
|
|
# reports healthy in time — a new way for a deploy to fail that didn't
|
|
# exist before this healthcheck did. Budgeted generously (~4.5 min:
|
|
# start_period + retries*interval) for the 2-core/8GB box this
|
|
# actually runs on: deploy.sh's own wp-settings.php wait loop already
|
|
# budgets up to 120s just for the image's core-file copy step (chosen
|
|
# for this same hardware), and this check additionally waits for
|
|
# php-fpm itself to start, plus whatever `--build` is competing for on
|
|
# a 2-core box. A too-tight budget here would turn "slow but fine" into
|
|
# a spurious deploy failure, not just a slower one.
|
|
test: ["CMD-SHELL", "bash -c '(exec 3<>/dev/tcp/127.0.0.1/9000)' 2>/dev/null"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 40
|
|
start_period: 60s
|
|
|
|
cron:
|
|
build:
|
|
context: ./docker/php
|
|
restart: unless-stopped
|
|
# Safe here (unlike wordpress's own service): this is a plain shell loop,
|
|
# not php-fpm's master process, which needs to start as root so it can
|
|
# drop privileges to its www-data workers itself. Missed in the earlier
|
|
# root-hardening pass — found via a real bug: this container writing
|
|
# wp-cron-triggered uploads as root, silently blocking www-data-run
|
|
# wp-cli commands from writing new files into those same directories.
|
|
user: www-data
|
|
depends_on:
|
|
wordpress:
|
|
condition: service_healthy
|
|
entrypoint: ["/bin/sh", "/usr/local/bin/cron-entrypoint.sh"]
|
|
environment: *bookstore-env
|
|
volumes:
|
|
- wp_core:/var/www/html
|
|
- wp_uploads:/var/www/html/wp-content/uploads
|
|
- wp_themes:/var/www/html/wp-content/themes
|
|
- ./wp-content/plugins/bookstore-core:/var/www/html/wp-content/plugins/bookstore-core
|
|
- ./wp-content/mu-plugins:/var/www/html/wp-content/mu-plugins
|
|
- ./docker/cron/entrypoint.sh:/usr/local/bin/cron-entrypoint.sh:ro
|
|
- ./secrets/${ENVIRONMENT:?ENVIRONMENT must be set}/db_password:/run/secrets/db_password:ro
|
|
- ./secrets/${ENVIRONMENT:?ENVIRONMENT must be set}/hardcover_api_token:/run/secrets/hardcover_api_token:ro
|
|
|
|
volumes:
|
|
db_data:
|
|
redis_data:
|
|
wp_core:
|
|
wp_uploads:
|
|
wp_themes:
|