Skip to content

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 --apply is passed. CI and a bare invocation never touch live Cloudflare.
  • Data-sync is opt-in, off by default. mirror.enabled must be explicitly true; otherwise mirror is a clean skip — no prod data moves.
  • Redaction is mandatory. When the mirror is enabled, mirror.redaction must 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-contained todo and the mirror stays off until you clear it and add the completed label; the scaffolder resumes the mirror step only on that done-signal, and a todo closed without completed is cancelled (never resumes). Query the gate with human-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

LayerEffect
drop_tablesThe table's CREATE/INSERT statements are removed from the dump entirely.
columnsDeclared table.column values are set to NULL in column-listed INSERTs (quote-aware — commas inside quoted strings are handled).
patternsRegex 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:

StackProvisionExport (source)Import (target)
Cloudflare / D1 (implemented)wrangler pages project create, wrangler d1 createwrangler d1 exportwrangler d1 execute --file
node / Postgrescreate test DB/role (psql/createdb)pg_dumppsql -f
node / SQLitecreate the db filesqlite3 .dumpsqlite3 < dump
Supabasesupabase projects create (or branch)supabase db dumpsupabase db push / psql
Drupal (external — nanawalld8)n/a (Acquia owns the pipeline)drush sql-dumpdrush sql-cli

The redaction stage (redactSql) operates on dumped SQL and is already stack-agnosticdrop_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.