Summary
The GCP branch of cleanup-staging.yml selects the Cloud SQL instance to delete with gcloud sql instances list --filter="name:cudly-staging" | head -1. gcloud's : operator is a contains match, not equality, so any instance whose name contains cudly-staging is a candidate and head -1 picks whichever sorts first. The delete runs with || true, and the following terraform state rm google_sql_database_instance.main runs unconditionally outside the if [ -n "$INSTANCE" ] block, so a failed or wrong delete still leaves the real instance running, billing, and untracked. This is the exact over-match that scripts/select-owned-name.sh was written to remove for ECR (#1592/#1820) and RDS (#1821); the GCP path never got the guard, and the two regression sweeps (test-ecr-delete-selection.sh, test-rds-deletion-protection-scope.sh) key on aws ecr delete-repository and aws rds modify-db-instance, so they never look at gcloud sql instances delete. The step validates PROJECT carefully at line 396 precisely because a silent skip is unacceptable, but applies no equivalent check to INSTANCE.
Location
.github/workflows/cleanup-staging.yml:401 at 3c0f8ac (selection at 401-402, delete with || true at 405, unconditional terraform state rm at 419-421)
Failure scenario
The GCP project holds cudly-staging-prod-mirror and cudly-staging-5e4d3c2b. The cleanup run lists both, head -1 returns the mirror, and gcloud sql instances delete --quiet destroys it. The staging instance survives, terraform state rm drops it from state anyway, and the next destroy neither sees nor bills for it.
Evidence
INSTANCE=$(gcloud sql instances list --project="$PROJECT" \
--filter="name:cudly-staging" --format="value(name)" 2>/dev/null | head -1)
if [ -n "$INSTANCE" ]; then
echo "Deleting Cloud SQL instance $INSTANCE..."
gcloud sql instances delete "$INSTANCE" --project="$PROJECT" --quiet || true
terraform state rm google_sql_database_instance.main 2>/dev/null || true
Suggested fix
Read the owned instance name from terraform output -json and pipe the gcloud sql instances list output through scripts/select-owned-name.sh, matching the ECR/RDS pattern; drop the || true on the delete and move the terraform state rm lines inside the branch that confirmed the deletion. Add a gcloud sql instances delete case to the existing selection sweep.
Found by the 2026-09-02 codebase audit, finding A14-004, reported by one reviewer and independently confirmed by a second. Full report: docs/audits/codebase-audit-2026-09-02.md.
Summary
The GCP branch of
cleanup-staging.ymlselects the Cloud SQL instance to delete withgcloud sql instances list --filter="name:cudly-staging" | head -1. gcloud's:operator is a contains match, not equality, so any instance whose name containscudly-stagingis a candidate andhead -1picks whichever sorts first. The delete runs with|| true, and the followingterraform state rm google_sql_database_instance.mainruns unconditionally outside theif [ -n "$INSTANCE" ]block, so a failed or wrong delete still leaves the real instance running, billing, and untracked. This is the exact over-match thatscripts/select-owned-name.shwas written to remove for ECR (#1592/#1820) and RDS (#1821); the GCP path never got the guard, and the two regression sweeps (test-ecr-delete-selection.sh,test-rds-deletion-protection-scope.sh) key onaws ecr delete-repositoryandaws rds modify-db-instance, so they never look atgcloud sql instances delete. The step validatesPROJECTcarefully at line 396 precisely because a silent skip is unacceptable, but applies no equivalent check toINSTANCE.Location
.github/workflows/cleanup-staging.yml:401at 3c0f8ac (selection at 401-402, delete with|| trueat 405, unconditionalterraform state rmat 419-421)Failure scenario
The GCP project holds
cudly-staging-prod-mirrorandcudly-staging-5e4d3c2b. The cleanup run lists both,head -1returns the mirror, andgcloud sql instances delete --quietdestroys it. The staging instance survives,terraform state rmdrops it from state anyway, and the next destroy neither sees nor bills for it.Evidence
Suggested fix
Read the owned instance name from
terraform output -jsonand pipe thegcloud sql instances listoutput throughscripts/select-owned-name.sh, matching the ECR/RDS pattern; drop the|| trueon the delete and move theterraform state rmlines inside the branch that confirmed the deletion. Add agcloud sql instances deletecase to the existing selection sweep.Found by the 2026-09-02 codebase audit, finding
A14-004, reported by one reviewer and independently confirmed by a second. Full report:docs/audits/codebase-audit-2026-09-02.md.