Appearance
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
- 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
bash
# 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 freshness
test-env.js mirror --env test [--project DIR] [--apply]
# Report when the test data was last mirrored
test-env.js status --env test [--project DIR]Config example (CF / D1)
json
{
"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
| 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)
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:
bash
# 1. Provision the test resources
test-env.js provision --env test --project <repo> --apply
# 2. Mirror prod data (redacted) into the test D1
test-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 recorded
test-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)
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.