1020a496ab65e695ad4ac15c9afb7a3a31296720
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1020a496ab |
Fix seven medium-severity bugs from the full-session code review
- SupplierOffer::insert(): now checks $wpdb->insert()'s return value and
throws, matching Work/Edition/Isbn (the one Catalog class that hadn't
been hardened this way). Work::set_product_id() gets the same treatment
(was flagged low-severity but same fix, bundled here) — a real update
failure now counts as a per-work sync failure instead of silently
leaving a stale wc_product_id pointer.
- SupplierOffer: fetched_at was written via current_time('mysql') (site-
local) while expires_at (SyntheticOfferGenerator) is written in UTC, and
best_offer_for_isbns() compared against site-local time too — a mismatch
masked today only because dev's gmt_offset is 0. Switched both writer and
reader to current_time('mysql', true) (UTC). Verified the read path
still finds all active offers correctly under a simulated -5 (US
Eastern) offset, not just at offset 0.
- HardcoverAdapter: rate-limit throttling moved from "once per work" (in
Commands.php) to "once per actual HTTP request" (inside query() itself).
find_book()'s ISBN-then-title/author fallback can fire two real requests
per work — under the old scheme both shared one throttle sleep, roughly
doubling the real request rate against a beta API. query() also now
retries network errors/5xx/429 up to 3x (mirroring
OpenLibraryAdapter::get_with_retry()), while a GraphQL-level `errors` field
or other 4xx throws immediately (retrying a rejected query can't fix it).
Commands.php adds a 5-consecutive-failure circuit breaker so a bad token
or a wrong field in the still-unverified schema can't silently burn
through the whole catalog with zero progress. Verified all of this
directly against Hardcover's real API with a deliberately invalid token:
5 fast (non-retried) 401s, correct abort message, and confirmed the
failed works were NOT marked synced (so a real token can retry them).
- restore.sh: now drops and recreates the target database before restoring
the dump, and clears the uploads directory before extracting the
archive — previously both restored on top of existing state, so a stray
table or file NOT in the backup would silently survive a restore drill.
Verified end-to-end against a real local staging stack: planted a stray
table and a stray upload file after taking a backup, ran restore.sh, and
confirmed both were gone afterward while the actual backed-up data (20
works, a known upload file) came back correctly. Also fixed a real
permission gap hit during that same test: a fresh volume's uploads dir
is root-owned until something chowns it, which broke the new www-data
clear step — now clears as root and chowns to www-data afterward, which
also means restore self-heals the exact root-owned-uploads class of bug
fixed earlier this session for the cron sidecar.
- poll-deploy.sh: added a non-blocking flock so a deploy that runs longer
than the cron interval can't have a second poll fire mid-deploy and race
its git checkout/reset against the same live working tree. Verified: a
concurrent run correctly skips instantly while the lock is held, and
proceeds normally once it's released. (Full atomicity of the live PHP
file swap under real traffic is a bigger architectural question —
blue-green or symlinked releases — flagged to the user rather than
attempted here.)
- backup.sh: now also archives .env.<environment> itself (chmod 600) and
includes it in the off-host rclone sync alongside the DB dump and
uploads archive. Every API key and both DB passwords previously lived
only on the host in this one gitignored file — losing the host lost all
of it even with DB/uploads backups intact.
|
||
|
|
df34fe8e54 |
Fix five high-severity bugs from the full-session code review
- ProductSync: bsc_work.wc_product_id could go stale if a product was ever
deleted out-of-band (wp-admin, a cleanup script). wc_get_product() then
correctly detects "no product" and creates a new one, but the repoint was
gated behind `if (!$existing_id)` — which was already true, so the stale
ID never got corrected. Every future sync repeated this, one duplicate
product per run. Fixed by making the repoint unconditional (cheap,
idempotent UPDATE either way). Reproduced the exact scenario against dev
(deleted a product out-of-band, ran sync-products twice) and confirmed:
one repoint, zero duplicates, product count and per-work product count
both correct across repeated runs.
- Commands.php (sync-hardcover-tags): wp_set_object_terms() with an empty
array clears the taxonomy rather than leaving it alone — verified
directly. Hardcover legitimately returns no moods/content-warnings for
plenty of books, so a --force re-sync could silently wipe existing tags,
including anything hand-tagged. Fixed by skipping the call per-category
when that category's array is empty. Verified via a direct eval test:
pre-existing genre tag survives a sync where genre comes back empty,
mood still gets set normally.
- docker-compose.yml: two stray root-owned secrets/db_password and
secrets/db_root_password directories were already sitting on disk —
Docker auto-creating a bind-mount source as a directory from an earlier
manual `docker compose` call that ran without ENVIRONMENT exported
(reproducing the exact bug already fixed once this session). Removed
the stray dirs and changed every `${ENVIRONMENT}` in a volume mount to
`${ENVIRONMENT:?ENVIRONMENT must be set}` so Compose now hard-fails
instead of silently defaulting to empty. Verified: unset ENVIRONMENT now
fails config validation with a clear error; set, it still works.
- HARDCOVER_API_TOKEN moved to the same _FILE secrets pattern already used
for DB_PASSWORD/DB_ROOT_PASSWORD (docker-compose.yml, deploy.sh,
HardcoverAdapter.php) — it was a live, consumed secret still going
through Compose's ${VAR} interpolation, exposed to the same
mangling bug already fixed for the DB passwords, plus visible via
`docker inspect`. deploy.sh now writes secrets/<env>/hardcover_api_token
(optionally empty). Verified via deploy.sh dev + wp eval: empty file ->
is_configured() false, a real token value -> true.
- backup.sh: mariadb-dump had no --single-transaction, so a dump against a
live site would either table-lock for its duration or produce a
non-atomic/inconsistent dump. Added; verified a real dump still runs
clean and produces a valid, restorable-looking .sql.gz.
|
||
|
|
6299391f23 |
Run wp-cli as www-data instead of root; drop --allow-root
--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> |
||
|
|
f65581bc50 |
Permanently eliminate the "$X variable not set" warning, and fix Caddy auto-HTTPS on private IPs
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>
|
||
|
|
3876a6ba30 |
Fix DB password corruption via Docker secrets file mechanism
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>
|
||
|
|
b43852e733 |
Switch deploy pipeline to Gitea polling; fix env-parsing and volume-isolation bugs
- 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>
|
||
|
|
c9d637c907 |
Week 1 infrastructure: Docker environments, deploy pipeline, bookstore-core scaffold
Docker Compose environments for dev/staging/production (MariaDB, Redis, Caddy, Action Scheduler cron sidecar), an idempotent deploy script, git-hook-based deploy pipeline, backup/restore scripts, and the bookstore-core plugin stub with WooCommerce HPOS compatibility declared. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |