GCP acceptance (local, destroy-on-exit)¶
Status: certified for the maintainer create-once path (GCP-06). Live pass
2026-08-02 on a disposable maintainer project (IDs redacted in public docs;
placeholders below) recorded harness PASS rows (committed sample:
gcp-matrix-results-2026-08-02.md;
local re-runs under .magelift/gcp-matrix/). Sibling certified path:
aws-acceptance.md.
Offline harness shape¶
GCP shares the AWS acceptance control-flow shape (checkpoint/resume + evidence
append_row + destroy/force_clean_orphans/assert_clean on EXIT) without
spending credits in dry-run:
export MAGELIFT_ACCEPTANCE_DRY_RUN=1
bash tests/acceptance/gcp_harness_shape_test.sh
# or: ./scripts/gcp-acceptance-local.sh
| Artifact | Path |
|---|---|
| Cell catalog | scripts/acceptance/cells-gcp-preview.txt |
| Checkpoint | .magelift/gcp-matrix/acceptance-checkpoint.json |
| Evidence | .magelift/gcp-matrix/matrix-results.md (six columns; provider=gcp); committed sample gcp-matrix-results-2026-08-02.md |
Evidence contract: certification requires harness append_row
PASS rows for SC1-SC5 cells. Hand-typed matrix rows are not certification
evidence. Resume kills mid-matrix at the first incomplete cell via
shared lib-checkpoint.sh (cell_done / record_cell).
Dry-run never sets created=1 and never calls Pulumi/gcloud up. Live create,
PSA soak timing (MAGELIFT_GCP_PSA_SOAK_SECS), and leftover proof are the paid
pass documented in gcp-certified-pass-2026-08-02.md.
Ordered cells (create-once, then updates)¶
Catalog order (teardown is EXIT, not a cell):
bootstrap:wif: Ensure WIF plus Act or gcloud STS/WIF token-exchange proof (MAGELIFT_GCP_WIF_ACT_LOG/MAGELIFT_GCP_WIF_ACT_PROOF=1or impersonation exchange). Ensure-only is refused.composer:sm-write/composer:sm-read: Secret Manager Composer creds (gcp-secret-manager://); no nil-success.day2:secrets/day2:state/day2:logs/day2:exec/day2:healthdeploy:candidate: migrate → cutover → health → record (kube.Steps)migrate:dump: after deploy (D-03); kube dumpimport runner for private SQLcost:estimatecutover:dns: afterapplicationURLexists (D-04);scripts/cutover-dns-cloudflare.sh
Live EXIT contract: when created=1 and KEEP is false, cleanup runs destroy →
force_clean_orphans (PSA soak + peering teardown) → assert_clean. Dry-run
short-circuits soaks and never enters that path.
Safety rules¶
- Set
MAGELIFT_GCP_ACCEPTANCE=1or the script refuses to run (unless dry-run). - Default mode is
preview(Pulumi plan only). upcreates real GKE Autopilot, Cloud SQL, and Memorystore Valkey. Costly and slow. Destroy always runs on EXIT unlessMAGELIFT_GCP_ACCEPTANCE_KEEP=true.MAGELIFT_GCP_ACCEPTANCE_DIGESTmust be pullable for Magento cells (day2 logs/exec/health,deploy:candidate). The default placeholder digest fails ImagePull; create-once falls back to--infra-onlyonly for that case.- After destroy, the script asserts zero leftovers matching the acceptance name
prefix (
mlaccby default; override withMAGELIFT_GCP_ACCEPTANCE_NAME) for VPC, GKE, Cloud SQL, Memorystore, and secrets. - Never interrupt mid-create/destroy.
- Serial builds for the harness binary (
GOMAXPROCS=1); seeagents/skills/magelift-serial-builds - When another agent works on AWS in the main checkout, run this from a separate
checkout and keep a unique acceptance prefix so resource names and
/tmpworkdirs do not collide.
Invoke¶
# From this repository (use a separate checkout only if you need parallel AWS work)
cd /path/to/magelift
# Preview graph (default)
MAGELIFT_GCP_ACCEPTANCE=1 ./scripts/gcp-acceptance-local.sh preview
# Short-lived real stack + live_cell_loop (destroy on EXIT)
MAGELIFT_GCP_ACCEPTANCE=1 \
MAGELIFT_GCP_ACCEPTANCE_DIGEST='ghcr.io/magelift/magento@sha256:…' \
./scripts/gcp-acceptance-local.sh up
Resume after a mid-matrix kill (stack kept with KEEP):
MAGELIFT_GCP_ACCEPTANCE=1 \
MAGELIFT_GCP_ACCEPTANCE_KEEP=true \
MAGELIFT_GCP_ACCEPTANCE_RESUME=1 \
MAGELIFT_GCP_ACCEPTANCE_DIGEST='ghcr.io/…@sha256:…' \
./scripts/gcp-acceptance-local.sh up
Optional env:
| Variable | Default | Purpose |
|---|---|---|
MAGELIFT_GCP_PROJECT |
required (live); dry-run uses example-gcp-project |
Your disposable GCP project; never commit real IDs |
MAGELIFT_GCP_REGION |
europe-west1 |
Region |
MAGELIFT_GCP_ACCEPTANCE_NAME |
mlacc |
Magento project name / resource prefix |
MAGELIFT_GCP_ACCEPTANCE_DIGEST |
placeholder digest | Must be pullable for Magento cells |
MAGELIFT_GCP_ACCEPTANCE_DIR |
/tmp/magelift-gcp-wt |
Work dir + logs |
MAGELIFT_GCP_ACCEPTANCE_RESUME |
unset | Skip create-once when stack already up |
MAGELIFT_GCP_WIF_ACT_LOG |
unset | Act smoke log proving token exchange |
MAGELIFT_GCP_WIF_ACT_PROOF |
unset | Operator-attested Act proof (=1) |
MAGELIFT_DUMPIMPORT_RUNNER |
unset | Set kube for private Cloud SQL dump cell |
GOOGLE_APPLICATION_CREDENTIALS |
auto from gcloud login | Preferred; refreshable ADC |
GOOGLE_OAUTH_ACCESS_TOKEN |
avoided when ADC exists | Static token; expires mid-Up |
Logs: /tmp/magelift-gcp-wt/logs/*.log. Grep those files; do not rely on
shell scrollback.
Auth note: long Ups must use Application Default Credentials (refreshable).
A static GOOGLE_OAUTH_ACCESS_TOKEN expires (~40-60m) and fails GKE/Memorystore
operation polls with ACCESS_TOKEN_TYPE_UNSUPPORTED. The script materializes ADC
from ~/.config/gcloud/credentials.db when ADC is missing.
Force cleanup after a failed destroy¶
Cloud SQL can keep a Service Networking allocation for several minutes after the
instance is gone (FLOW_SN_DC_RESOURCE_PREVENTING_DELETE_CONNECTION). The EXIT
trap waits for GKE/SQL/Memorystore/SCP producers to clear, soaks
(MAGELIFT_GCP_PSA_SOAK_SECS, default 180), then removes the peering via Compute
networks.removePeering (and async vpc-peerings delete), the PSA address, and
the VPC. If assert_clean still fails, wait and retry:
# Prefer Compute removePeering when services vpc-peerings delete races soft-delete
# Substitute YOUR_PROJECT and PREFIX from the failed run.
TOKEN=$(gcloud auth print-access-token)
curl -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
"https://compute.googleapis.com/compute/v1/projects/YOUR_PROJECT/global/networks/PREFIX-preview-net/removePeering" \
-d '{"name":"servicenetworking-googleapis-com"}'
gcloud compute addresses delete PREFIX-preview-sql-psa --global --project=YOUR_PROJECT
gcloud compute networks delete PREFIX-preview-net --project=YOUR_PROJECT
Credentials¶
gcloudmust be authenticated to the target project.- Pulumi needs a backend (
pulumi loginorPULUMI_BACKEND_URL). Ephemeral agent accounts from Automation API must be claimed by a human if used. - Prefer Application Default Credentials; the script falls back to
GOOGLE_OAUTH_ACCESS_TOKENfromgcloud auth print-access-token. - Cloudflare DNS cell needs
CLOUDFLARE_API_TOKEN/CF_API_TOKENwith Zone.DNS Edit (Wrangler OAuth is insufficient).
Known gaps (post-certify)¶
- Memorystore Valkey needs a regional Service Connection Policy
(
serviceClass=gcp-memorystore). Created by the GCP cache component. - Non-preview presets remain spend-gated (not free-tier certified).
- GitHub WIF CI proof is Act-only until hosted Actions minutes return. See
gcp-experimental.md
and
.github/workflows/gcp-wif-act-smoke.yml. Do not commit SA keys. - Live dump cell requires
MAGELIFT_DUMPIMPORT_RUNNER=kube(+ mysql client pod when the Magento image lacksmysql); evidenced in the certified pass.
Certified pass citation (2026-08-02)¶
| Item | Value |
|---|---|
| Project / region / prefix | redacted disposable project / europe-west1 / operator prefix |
| Evidence | gcp-matrix-results-2026-08-02.md |
| Narrative | gcp-certified-pass-2026-08-02.md |
| Teardown | assert_clean ok, DNS cleanup, state bucket deleted |