Test-Environment Mirror
Test-Environment Provisioning & prod→test Data Mirror (#451)
Section titled “Test-Environment Provisioning & prod→test Data Mirror (#451)”R6 of the promotion pipeline (#445).
Provisions a test environment that mirrors prod — CF-first — and syncs prod data
into it with mandatory redaction. Engine: ~/.claude/scripts/deploy/test-env.js.
Config lives in the project’s dev.json under environments.<env>.provision and
environments.<env>.mirror (schema: schemas/dev.schema.json).
Safety model
Section titled “Safety model”- Dry-run by default. Every wrangler mutation prints intent and does nothing
unless
--applyis passed. CI and a bare invocation never touch live Cloudflare. - Data-sync is opt-in, off by default.
mirror.enabledmust be explicitlytrue; otherwisemirroris a clean skip — no prod data moves. - Redaction is mandatory. When the mirror is enabled,
mirror.redactionmust declare a policy (drop_tables/columns/patterns); the mirror refuses (exit 2) otherwise. Copying prod data into a less-guarded test env without sanitization is the capability’s top risk. - The two human decisions never auto-enable. The redaction policy and the
test-env secrets are handed back — not automated — by the human-gate emitter
(
~/.claude/scripts/setup-promotion/human-gate.js, #929). It files a single self-containedtodoand the mirror stays off until you clear it and add thecompletedlabel; the scaffolder resumes the mirror step only on that done-signal, and a todo closed withoutcompletedis cancelled (never resumes). Query the gate withhuman-gate.js gate. See Promotion-pipeline setup → Step 5.
Commands
Section titled “Commands”# Ensure the declared test resources exist (idempotent; dry-run unless --apply)test-env.js provision --env test [--project DIR] [--apply]
# Mirror prod → test: export → redact → import, then record freshnesstest-env.js mirror --env test [--project DIR] [--apply]
# Report when the test data was last mirroredtest-env.js status --env test [--project DIR]Config example (CF / D1)
Section titled “Config example (CF / D1)”{ "environments": { "prod": { "db": { "database": "app-prod-db", "adapter": "d1" } }, "test": { "deploy": { "adapter": "cloudflare" }, "db": { "database": "app-test-db", "adapter": "d1" }, "provision": { "pages_project": "app-test", "d1_database": "app-test-db" }, "mirror": { "enabled": true, "source": "prod", "redaction": { "drop_tables": ["secrets", "sessions"], "columns": ["users.email", "users.phone"], "patterns": [{ "match": "[\\w.]+@[\\w.]+\\.\\w+", "replace": "redacted@example.test" }] } } } }}Redaction layers
Section titled “Redaction layers”| Layer | Effect |
|---|---|
drop_tables |
The table’s CREATE/INSERT statements are removed from the dump entirely. |
columns |
Declared table.column values are set to NULL in column-listed INSERTs (quote-aware — commas inside quoted strings are handled). |
patterns |
Regex find/replace across the whole dump — a second net for free-text PII (emails in note/log columns). |
End-to-end test deploy + migrate + health (AC-06)
Section titled “End-to-end test deploy + migrate + health (AC-06)”The pipeline composes the existing deploy framework: the Cloudflare adapter maps
test → --preview, and deploy.sh --env test runs check→build→migrate→deploy→verify.
A full live run on a real CF project is a deliberate, credentialed operation —
it touches live Cloudflare and real prod data, so it is not run autonomously. The
verification procedure:
# 1. Provision the test resourcestest-env.js provision --env test --project <repo> --apply# 2. Mirror prod data (redacted) into the test D1test-env.js mirror --env test --project <repo> --apply# 3. Deploy + migrate + health-check the test environment~/.claude/scripts/deploy/deploy.sh --env test --project <repo># 4. Confirm freshness was recordedtest-env.js status --env test --project <repo>A green run of steps 1–4 against one CF project satisfies AC-06.
Generalization path for non-CF stacks (AC-07)
Section titled “Generalization path for non-CF stacks (AC-07)”The engine is CF-first but the model generalizes — the schema’s provision/mirror
blocks and the dry-run/opt-in/redaction discipline are stack-agnostic. Per stack,
only the export/import/provision primitives change:
| Stack | Provision | Export (source) | Import (target) |
|---|---|---|---|
| Cloudflare / D1 (implemented) | wrangler pages project create, wrangler d1 create |
wrangler d1 export |
wrangler d1 execute --file |
| node / Postgres | create test DB/role (psql/createdb) |
pg_dump |
psql -f |
| node / SQLite | create the db file | sqlite3 .dump |
sqlite3 < dump |
| Supabase | supabase projects create (or branch) |
supabase db dump |
supabase db push / psql |
| Drupal (external — nanawalld8) | n/a (Acquia owns the pipeline) | drush sql-dump |
drush sql-cli |
The redaction stage (redactSql) operates on dumped SQL and is already
stack-agnostic — drop_tables/columns/patterns apply to any SQL dump, so
only the transport primitives (the wrangler calls in probeExisting/run) are
swapped per stack. Implementing a new stack means adding its provision/export/import
commands behind the same opt-in + mandatory-redaction gates; the contract and the
safety model do not change.