It can, and it does. Registering the QA callback under Authorized redirect URIs was all it took. The claim was that qa-redefined-designs.bermudalamb.synology.me could never be registered, because Google requires a redirect URI's host to sit under a domain whose ownership has been proved by DNS, and Synology owns the domain above that one. It was inferred from #285, where Cloudflare's free tier genuinely cannot be applied to that hostname, and asserted with far more confidence than the inference supported. What was actually established is narrower: localhost is exempt from the authorized-domain rules, and a domain listed as an authorized domain has to be verified in Search Console. Whether either applied here was never checked. It was not a harmless error. On the strength of it, QA testing of this feature was documented as blocked behind #313, the QA compose file hardcoded its credentials to empty rather than reading the stack, #345 recorded it as a constraint, and #332 closed with it written into the summary. A QA deploy was spent on it. So the correction is left in place rather than the wrong sentences quietly deleted. A document that silently stops saying something teaches nobody why it said it, and this is the second time in this feature that a confident inference about somebody else's platform has cost a day — the first being the assumption that a passing local build said anything about another machine. What replaces it is the thing that was always true and never written down plainly: every environment sends a redirect URI derived from its own PUBLIC_URL, and each one has to exist verbatim in the console. There is now one table listing all four, including the localhost:5173 entry that local development needs and that Phase 0 originally omitted — the omission that cost an hour of redirect_uri_mismatch before any of this. The QA compose comment now says which URL to register rather than why it cannot be. The ops document gains the steps QA actually took, in order, with a note on why registering before setting the variables is the order that matters: a button that appears before its callback exists fails at Google, where nothing in the storefront logs explains it. Verified: backend tsc clean, the QA compose file still parses, and no file in the tree still claims the hostname is unusable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
270 lines
14 KiB
YAML
270 lines
14 KiB
YAML
# QA stack — a disposable copy of the app for reviewing merged-but-undeployed
|
|
# changes online. See issue #25.
|
|
#
|
|
# Deployed as its own Portainer stack, separate from production. Every value
|
|
# that could collide with production has been changed: container names, host
|
|
# port, volume paths, database name, and image tag. Do not copy a path or port
|
|
# back from the production stack — a shared Postgres data directory would mean
|
|
# QA writing into production's database files.
|
|
#
|
|
# Name the Portainer stack `redefined-designs-qa`, NOT `redefined-designs`.
|
|
# The stack name becomes the compose project name. Reusing production's name
|
|
# would make compose treat this as the same project and reconcile the two
|
|
# against each other — it would happily remove the production containers
|
|
# because they are not declared in this file.
|
|
#
|
|
# DEPLOY THIS AS A GIT REPOSITORY STACK, not from the web editor.
|
|
#
|
|
# Repository: https://gitea.bermudalamb.synology.me/bermudalamb/redefined-designs
|
|
# Reference: refs/heads/main
|
|
# Compose path: docker-compose.qa.yml
|
|
#
|
|
# The repository is public, so no credentials are needed. Portainer then does
|
|
# the whole cycle from one button: it pulls the repo, builds the image from the
|
|
# Dockerfile below, and recreates the containers. Nothing is built by hand on
|
|
# the NAS, and no image has to be pushed anywhere first.
|
|
#
|
|
# `pull_policy: build` matters. Without it the stack reuses whatever is already
|
|
# tagged redefined-designs:qa, which is how a redeploy can appear to succeed
|
|
# while still running old code. Leave any Portainer option that re-pulls images
|
|
# turned OFF — there is no registry to pull this image from.
|
|
#
|
|
# Required stack environment variables:
|
|
# QA_DB_PASSWORD — deliberately not named DB_PASSWORD, so pasting the
|
|
# production stack's variables here does nothing silently.
|
|
# PUBLIC_URL — the QA hostname, e.g.
|
|
# https://qa-redefined-designs.bermudalamb.synology.me
|
|
# QA_SMTP_USER — Brevo SMTP login. Named QA_ for the same reason as the
|
|
# QA_SMTP_PASSWORD database password: pasting production's variables in here
|
|
# QA_SMTP_FROM must not silently work.
|
|
#
|
|
# Optional, each defaulting to the Brevo value it used to be hardcoded to, so
|
|
# QA can be aimed at another relay without editing this file:
|
|
# QA_SMTP_HOST — default smtp-relay.brevo.com
|
|
# QA_SMTP_PORT — default 587
|
|
# QA_SMTP_SECURE — default false. Brevo is STARTTLS on 587, so this stays
|
|
# false unless the relay is changed to an implicit-TLS one.
|
|
# ADMIN_GATE_SECRET — the shared secret Nginx Proxy Manager injects as the
|
|
# X-Admin-Gate header on the gated location. Both sides must
|
|
# hold the same value or the admin API returns 403. See #63.
|
|
# QA_ANTHROPIC_API_KEY — optional. Drafts a listing from a submitted photo
|
|
# (#223). QA_ prefixed like the others so production's key
|
|
# cannot be pasted here by accident, and worth its own key
|
|
# rather than sharing production's: this is the only
|
|
# credential here that spends money per call, and it is
|
|
# reachable by anyone holding an upload link. Leave it unset
|
|
# and submissions still arrive, undrafted.
|
|
# QA_ANTHROPIC_WORKSPACE_ID — required alongside the key above when that key
|
|
# is identity-linked, i.e. issued against a workspace rather
|
|
# than standing alone. Without it every draft fails with a
|
|
# 400 naming the missing header (#271). Ordinary keys need
|
|
# no workspace and can leave it unset.
|
|
# QA_INTAKE_ACTION_SECRET — optional. Signs the regenerate and discard links
|
|
# in the notification email (#224). Absent, the email still
|
|
# sends and simply carries no shortcuts. Its own value, not
|
|
# production's: a link signed with it acts without a login.
|
|
# QA_GOOGLE_CLIENT_ID — optional, and all-or-nothing with the secret below:
|
|
# QA_GOOGLE_CLIENT_SECRET setting one without the other refuses to boot
|
|
# (#340). Both unset means the Google button is not offered
|
|
# at all, which is the right answer until QA's callback URL
|
|
# is registered in the Google Auth Platform. See the note
|
|
# beside the values themselves for the exact URL (#345).
|
|
# QA_REMBG_URL — optional. The background-removal sidecar, e.g.
|
|
# http://rembg-syn:7000. Unset turns the feature off rather
|
|
# than breaking anything. The sidecar must be on the same
|
|
# network as this stack.
|
|
|
|
services:
|
|
redefined-designs-qa:
|
|
# Built from this repository by Portainer rather than pulled. The context is
|
|
# the repo root, which is where the Dockerfile lives — the same Dockerfile
|
|
# production uses, so QA and production images differ only in configuration.
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile
|
|
image: redefined-designs:qa
|
|
# Always build; never reuse the existing tag.
|
|
pull_policy: build
|
|
container_name: redefined-designs-qa-syn
|
|
environment:
|
|
- TZ=America/Chicago
|
|
- PORT=3000
|
|
- PGHOST=redefined-designs-qa-db-syn
|
|
- PGPORT=5432
|
|
- PGUSER=redefined_qa
|
|
- PGPASSWORD=${QA_DB_PASSWORD}
|
|
- PGDATABASE=redefined_qa
|
|
|
|
# No PayPal credentials at all. DEMO_MODE lets the full cart and checkout
|
|
# flow run without them, so QA can exercise the whole purchase path with
|
|
# no way to reach live PayPal. Never set PAYPAL_ENV=live here. To test a
|
|
# real PayPal integration change, add sandbox credentials and set
|
|
# PAYPAL_ENV=sandbox — never the live ones.
|
|
- DEMO_MODE=true
|
|
|
|
# SMTP *is* configured here, unlike PayPal above, because the four mail
|
|
# flows — verification, password reset, favorite-sold alerts and the
|
|
# cart-reminder cron — cannot be regression tested without it. See #87.
|
|
#
|
|
# Host, port and secure are not secrets, and they are now settable from
|
|
# the stack so QA can be pointed at a different relay without editing this
|
|
# file. Each keeps the Brevo value as its default rather than being left
|
|
# to fall through: the mailer's own fallbacks are Gmail's (smtp.gmail.com,
|
|
# 465, TLS) and Brevo needs 587 with STARTTLS, so an unset variable with
|
|
# no default here would silently aim QA at Gmail and fail at send time
|
|
# rather than at boot. `:-` supplies the default only when the variable is
|
|
# unset or empty, so setting one still wins.
|
|
- SMTP_HOST=${QA_SMTP_HOST:-smtp-relay.brevo.com}
|
|
- SMTP_PORT=${QA_SMTP_PORT:-587}
|
|
- SMTP_SECURE=${QA_SMTP_SECURE:-false}
|
|
- SMTP_USER=${QA_SMTP_USER}
|
|
- SMTP_PASSWORD=${QA_SMTP_PASSWORD}
|
|
- SMTP_FROM=${QA_SMTP_FROM}
|
|
|
|
# What keeps a QA run from emailing a real customer now that it *can*
|
|
# send. Only these recipients are ever delivered to; anything else is
|
|
# skipped with a [mail-blocked] warning naming the address.
|
|
#
|
|
# Hardcoded rather than read from a stack variable, deliberately. This is
|
|
# the entire safety property, and it must not depend on somebody
|
|
# remembering to set something in Portainer — an unset variable would
|
|
# mean unrestricted sending from an environment full of test fixtures.
|
|
#
|
|
# An entry covers its plus-suffixed variants, so `+whatever` addresses
|
|
# work without editing this. Removing the line does NOT disable mail; it
|
|
# disables the restriction. Production is a separate stack that does not
|
|
# read this file, which is why it is unrestricted and correct to be.
|
|
- MAIL_ALLOWLIST=thomlamb@gmail.com
|
|
- SITE_CURRENCY=USD
|
|
- RESERVATION_MINUTES=15
|
|
- PUBLIC_URL=${PUBLIC_URL}
|
|
|
|
# Required since #64, and previously inherited from the code's fallback —
|
|
# which is exactly what that change set out to stop. Hardcoded rather than
|
|
# taken from a stack variable because it is not a secret and because it
|
|
# has to match the right-hand side of the volume mapping below; splitting
|
|
# it across two files is how they drift apart.
|
|
- UPLOADS_DIR=/app/uploads
|
|
|
|
# Interpolated from the stack environment so the secret itself never
|
|
# enters the repository. Note that setting it in Portainer alone is not
|
|
# enough: stack variables are substituted into this file, not handed to
|
|
# the container, so a variable with no line here never reaches the app.
|
|
- ADMIN_GATE_SECRET=${ADMIN_GATE_SECRET}
|
|
|
|
# Drafts a listing from a submitted photo (#223). Named QA_ in the stack
|
|
# for the same reason as the database and SMTP credentials: pasting
|
|
# production's variables in here must not silently work — and this is the
|
|
# one credential in the stack that costs money per call, on a path a
|
|
# stranger with a link can trigger.
|
|
#
|
|
# Absent is a working configuration. A submission still arrives, keeps
|
|
# its photos and waits in the queue undrafted, because losing somebody's
|
|
# consignment to an expired key would be far worse than an item arriving
|
|
# without its description written.
|
|
- ANTHROPIC_API_KEY=${QA_ANTHROPIC_API_KEY}
|
|
|
|
# Required alongside the key when that key is identity-linked — one issued
|
|
# against a workspace rather than standing alone. Such a key is refused
|
|
# with a 400 unless the request names the workspace it acts in, and
|
|
# nothing about a key's shape says which kind it is, so this only shows up
|
|
# on a real call. It did: every QA draft failed until this existed (#271).
|
|
#
|
|
# Optional. Plenty of keys need no workspace, and sending an empty one
|
|
# would turn the ordinary case into a different error.
|
|
- ANTHROPIC_WORKSPACE_ID=${QA_ANTHROPIC_WORKSPACE_ID:-}
|
|
|
|
# Optional. The background-removal sidecar (#281). Unset means the
|
|
# feature does not exist: no checkbox on the submission page, no control
|
|
# in the review queue, and the worker skips the step. Empty default so an
|
|
# unset stack variable cannot fail a deploy.
|
|
- REMBG_URL=${QA_REMBG_URL:-}
|
|
|
|
# Deliberately empty, and deliberately not a stack variable (#56).
|
|
#
|
|
# This is what keeps QA browsing out of the live Brevo account. Written as
|
|
# an empty literal rather than left out entirely so it cannot inherit a
|
|
# value from the host environment, and with no `${...}` so there is no
|
|
# stack variable anyone could set here by pasting production's in — the
|
|
# same reasoning as QA_DB_PASSWORD and the QA_SMTP_* names above.
|
|
#
|
|
# QA runs against disposable fixtures. Reporting that browsing as though
|
|
# it were customer behaviour would corrupt the segmentation the tracker
|
|
# exists to feed, and it would be indistinguishable from real traffic
|
|
# after the fact.
|
|
- BREVO_TRACKER_KEY=
|
|
|
|
# Signs the regenerate and discard links in the intake notification email
|
|
# (#224). Optional: absent, the notification still sends and simply links
|
|
# to the review queue without shortcuts. Anyone holding a link can act on
|
|
# it without signing in, so this must not be shared with production —
|
|
# rotating it revokes every outstanding link, which is the intended way to
|
|
# deal with a leak.
|
|
- INTAKE_ACTION_SECRET=${QA_INTAKE_ACTION_SECRET:-}
|
|
# Read from the stack like every other QA secret, rather than hardcoded
|
|
# empty as they were in #340. Leaving them unreadable made this file the
|
|
# odd one out and cost a QA deploy: the variables were set on the stack,
|
|
# nothing read them, and the button stayed missing with no explanation.
|
|
#
|
|
# Setting these needs one thing done first: the QA callback registered
|
|
# under Authorized redirect URIs for this client in the Google Auth
|
|
# Platform, exactly as it appears below. Google compares the two as
|
|
# strings and answers a mismatch with redirect_uri_mismatch.
|
|
#
|
|
# https://qa-redefined-designs.bermudalamb.synology.me/api/auth/google/callback
|
|
#
|
|
# An earlier version of this comment said that URI could never be
|
|
# registered, because Synology owns the domain above it. That was wrong,
|
|
# and the correction is left here rather than removed: it was inferred
|
|
# from #285, which is a related but different problem, and it put QA
|
|
# testing of this feature behind #313 for no reason.
|
|
#
|
|
# When #313 moves QA to qa.redefined-designs.com, point PUBLIC_URL at the
|
|
# new host and register that callback too. No code change either way.
|
|
- GOOGLE_CLIENT_ID=${QA_GOOGLE_CLIENT_ID:-}
|
|
- GOOGLE_CLIENT_SECRET=${QA_GOOGLE_CLIENT_SECRET:-}
|
|
volumes:
|
|
# Separate uploads directory. Sharing production's would let a QA run
|
|
# write into, and a QA teardown delete, real product images.
|
|
- /volume1/configs/redefined-designs-qa/uploads:/app/uploads
|
|
ports:
|
|
# 32751, not production's 32750.
|
|
- 32751:3000
|
|
depends_on:
|
|
redefined-designs-qa-db-syn:
|
|
condition: service_healthy
|
|
# Not `unless-stopped`: QA is meant to be up only while a review is
|
|
# happening. `unless-stopped` would silently bring it back after every NAS
|
|
# reboot and leave it running indefinitely.
|
|
restart: "no"
|
|
# Docker's default json-file driver has no size cap. POST /api/client-errors
|
|
# is unauthenticated, so an unrotated log is a disk-filling vector on its
|
|
# own — see the error-boundary design doc's backend section. This does not
|
|
# cover production, which is a separate Portainer stack outside this repo;
|
|
# the same logging options need to be added there directly.
|
|
logging:
|
|
driver: json-file
|
|
options:
|
|
max-size: 10m
|
|
max-file: "3"
|
|
|
|
redefined-designs-qa-db-syn:
|
|
image: postgres:16
|
|
container_name: redefined-designs-qa-db-syn
|
|
environment:
|
|
- POSTGRES_USER=redefined_qa
|
|
- POSTGRES_PASSWORD=${QA_DB_PASSWORD}
|
|
- POSTGRES_DB=redefined_qa
|
|
- PGDATA=/var/lib/postgresql/data/pgdata
|
|
volumes:
|
|
# Distinct data directory from production's
|
|
# /volume1/configs/redefined-designs/postgres. This is the single most
|
|
# important difference in this file.
|
|
- /volume1/configs/redefined-designs-qa/postgres:/var/lib/postgresql/data
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U redefined_qa -d redefined_qa"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 10
|
|
restart: "no"
|