GCP project consolidation runbook — xlab-co as canonical umbrella
Date: 2026-05-20 · Owner: Dan (human-UI work) + Claude (toolkit/code) · Estimated effort: ~45-60 min when ready · Status: Runbook drafted; execute on a future deliberate session
Dan, 2026-05-20:
“i think we are drifting by adding a new one, we need to prune GCP for dare to one account, having multiple seems like it’s duplicating our service token surface” “it should really be xlab-co as the umbrella, unless you name it differently, or dare?” “easy path — draft the consolidation runbook”
This runbook is the deliberate-session playbook for consolidating all portfolio-internal Google Cloud work into ONE canonical project: xlab-co. The current state has 5+ projects with overlapping purposes — each carrying its own API keys, OAuth client IDs, service accounts, quota counters, and billing linkage. The destination is one project, scope-aligned with the existing GitHub umbrella org.
Current state — what we’re consolidating
Active GCP projects under the dare.co.uk GCP organisation as of 2026-05-20:
| Project ID | Type | Likely role (audit confirms) | Disposition |
|---|---|---|---|
dare-seo-audit |
Project | Newer dare-SEO container. PSI + CrUX enabled today. No API keys yet (wizard cancelled). | Migrate to xlab-co → retire |
seo-darecouk ⭐ |
Project | Older dare-SEO container, starred = previously the go-to. GSC API + OAuth from this morning likely live here. | Migrate to xlab-co → retire |
audrey-experiments |
Project | Audrey-side experiments. May hold API keys / OAuth. | Migrate to xlab-co → retire |
CloudFlare-Admin-Login |
Project | CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF-related early experiment. Probably empty/dormant. | Audit → retire if empty |
API-darecouk-YouTube |
Project | YouTube Data API + a key (likely powering dare_video_audit.py etc) |
Migrate to xlab-co → retire |
dare.co.uk |
Organization | GCP org that contains all the above. Not a project; keep. | Keep |
Target state: ONE project, xlab-co, holding every Google API and credential the portfolio needs. The other five projects retired over the 30-day soft-delete window.
The naming decision (locked 2026-05-20)
Canonical project: dare-seo-audit — for now. Long-term: rename/align with xlab-co once the broader xlab-co naming has finished cascading through everything (GitHub org, repos, identity surfaces).
Dan’s framing:
“going with dare-seo-audit as the carrier of traffic, the one we retain and build upon…” “dare-seo-audit as the MASTER (for now, until we can align it with xlab-co naming that is cascading through everything)”
This is a deliberately staged decision:
- TODAY:
dare-seo-auditis the canonical home for ALL portfolio-internal Google API work. Build on it. New APIs land in it. The legacy 8 projects retire INTO it via the runbook below. - LATER (when xlab-co naming cascade completes): rename or re-create as
xlab-co(orxlab-co-portfolio), migratingdare-seo-audit’s contents in. Same shape, different name. The principle (“one canonical home per identity”) survives the rename.
This staging is pragmatic:
- dare-seo-audit already has PSI + CrUX enabled today (less immediate rework)
- Avoids migrate-everything-twice (now → xlab-co AND legacy → xlab-co)
- The principle (consolidate to ONE canonical) is preserved today
- The xlab-co rename becomes a SINGLE Phase-2 motion when the broader xlab-co naming alignment is happening anyway
Scope of dare-seo-audit (the MASTER, for now):
- All dare.co.uk Google API work (PSI, CrUX, Search Console, YouTube Data, Indexing, etc.)
- Portfolio-internal cross-domain work without a dedicated home (e.g. weather APIs for pa.gf.cx)
- NOT audrey-specific work — that stays in audrey-experiments-496912
- NOT client engagements — those get their own per-client GCP
Per feedback_gcp_one_project_per_surface.md (banked today).
Two-phase rename plan
Phase 1 — TODAY’s decision (executing):
- dare-seo-audit = MASTER
- Retire 8 legacy projects INTO it (per runbook below)
- All new Google API work lands here
Phase 2 — when xlab-co naming cascade completes (parked):
- Rename or migrate dare-seo-audit → xlab-co (or xlab-co-portfolio)
- Triggered by: GitHub org rename if any, or when 3+ surfaces explicitly reference xlab-co as the canonical brand
- One-time migration ceremony (Phase 1 already consolidated to ONE source, so Phase 2 is only ONE project to rename, not nine)
- Update all op:// references in scripts (sed-replace pass + sibling sync)
The point of the staging: do the high-leverage consolidation now; defer the cosmetic rename until the naming alignment is happening anyway.
Pre-flight — survey the legacy state
Before any migration, enumerate what’s in each legacy project. This identifies what’ll need re-minting + what can be safely retired.
Option A — Web UI (slow but clickable):
For each legacy project: 1. Switch to the project in the picker 2. Visit APIs & Services → Enabled APIs & services → screenshot or list 3. Visit APIs & Services → Credentials → list each API key + OAuth client + service account 4. Visit IAM & Admin → Service Accounts → confirm 5. Visit Billing → note billing-account linkage
Option B — gcloud CLI (fast, ~5 min total):
# Install gcloud once if not present
brew install --cask google-cloud-sdk
gcloud auth login # opens browser, sign in as dan@dare.co.uk
# Enumerate every project's enabled APIs + credentials
for p in dare-seo-audit seo-darecouk audrey-experiments-496912 cloudflare-admin-login api-darecouk-youtube; do
echo "═══════════════════════════════════════════════════════════════"
echo " $p"
echo "═══════════════════════════════════════════════════════════════"
echo " Enabled APIs:"
gcloud services list --enabled --project=$p 2>/dev/null | tail -n +2 | awk '{print " " $1}'
echo " Service accounts:"
gcloud iam service-accounts list --project=$p 2>/dev/null | tail -n +2 | awk '{print " " $2}'
echo " API keys (need API Keys API enabled to list):"
gcloud alpha services api-keys list --project=$p 2>/dev/null | tail -n +2 | head -10
done > ~/Downloads/gcp_legacy_audit_$(date +%F).md
Output lands at ~/Downloads/gcp_legacy_audit_<date>.md — read through, identify what we keep / re-mint / retire.
Step-by-step migration
Phase 1 — Create canonical xlab-co
- Cloud Console → project picker → New Project
- Project name:
xlab-co(fallback:xlab-co-portfolio) - Organization: keep
dare.co.uk(matches existing org) - Click Create, wait ~10s, switch to it
Phase 2 — Enable the API set
Left sidebar → APIs & Services → Library, enable each:
- PageSpeed Insights API (for
dare_perf_trend.py) - Chrome UX Report API (for
dare_perf_trend.py) - Search Console API (for
gsc_pull.py) - YouTube Data API v3 (for
dare_video_audit.py+ future) - Indexing API (for future sitemap-ping work, if appetised)
Any others surfaced by the pre-flight audit get enabled too.
Phase 3 — Mint the canonical API key
APIs & Services → Credentials → + Create credentials → API key
When the success modal appears, immediately click Edit API key:
- Name:
xlab-co-portfolio-api-key - Application restrictions: None
- API restrictions: Restrict key → check ☑ all APIs from Phase 2
- Save
Show key → copy the AIza... value.
Phase 4 — Drop in 1Password
New 1Password item in Code Shared:
- Title:
xlab-co Google API - Custom concealed field:
api_key= the AIza… value - (optional second field:
project_id=xlab-co)
Reference shape: op://Code Shared/xlab-co Google API/api_key
Phase 5 — Re-create OAuth clients (for user-consent APIs)
Some APIs (Search Console, YouTube Data) need OAuth 2.0 client credentials, not just an API key. Re-create in xlab-co:
- APIs & Services → Credentials → + Create credentials → OAuth client ID
- Application type: Desktop app
- Name:
xlab-co-portfolio-oauth-client - Download the resulting client-secret JSON
- Move to a safe location (NOT a tracked repo) — recommended:
~/.config/google/xlab-co-client-secret.json - Update
~/bin/gsc_oauth_setup.pyto point at the new client-secret path - Re-run the OAuth dance:
bash gsc_oauth_setup.py --client-secret ~/.config/google/xlab-co-client-secret.json - Capture the fresh refresh token → store in 1Password:
- Title:
xlab-co Google OAuth- Custom concealed field:refresh_token= the new token - Custom concealed field:client_id= from the JSON - Custom concealed field:client_secret= from the JSON
Phase 6 — Update toolkit-script references
For each script that reads Google credentials, update the op:// reference:
| Script | Old reference | New reference |
|---|---|---|
dare_perf_trend.py |
op://Code Shared/Google PSI API/api_key |
op://Code Shared/xlab-co Google API/api_key |
gsc_pull.py |
(whatever it currently reads) | op://Code Shared/xlab-co Google OAuth/refresh_token etc. |
gsc_oauth_setup.py |
(legacy client-secret path) | ~/.config/google/xlab-co-client-secret.json |
dare_video_audit.py |
(if it uses YouTube Data) | new reference |
Single sed-replace pass once we know all the script names; sibling sync to dare-pipeline/scripts/ as usual.
Phase 7 — Verify
For each script touched in Phase 6:
- Run with
--dry-runor equivalent to confirm it can read the new credentials - Run a real pull (low-cost / read-only) to confirm the API call succeeds with the new key
- Diff the output against the most recent legacy-key output to confirm parity
If anything errors: check API restrictions on the new key (did we miss enabling one of the APIs?), check 1Password field IDs match the op:// reference syntax exactly.
Phase 8 — Retire legacy projects
For each legacy project, in this order (lowest-risk first):
CloudFlare-Admin-Login— almost certainly empty; delete via Cloud Console → IAM & Admin → Manage Resources → select project → DeleteAPI-darecouk-YouTube— confirm YouTube Data work has moved toxlab-co+ scripts updated → deleteaudrey-experiments— confirm any audrey-specific OAuth/API work has moved → delete (note: project ID isaudrey-experiments-496912per the picker)dare-seo-audit— confirm PSI/CrUX work has moved → deleteseo-darecouk— LAST, since this is the starred/older one. Confirm GSC API + OAuth have moved + scripts work → delete
Google holds deleted projects for 30 days before final purge — recoverable if we missed something. Cloud Console will show them in “Recently deleted” under IAM & Admin → Manage Resources.
Phase 9 — Documentation
- Update
~/Code/dare-co-uk/CLAUDE.mdwith a note: “All Google Cloud APIs for portfolio-internal work live inxlab-co. Project ID:xlab-co. Key reference:op://Code Shared/xlab-co Google API/api_key. OAuth reference:op://Code Shared/xlab-co Google OAuth/refresh_token.” - Commit + push the CLAUDE.md update
- Add reference to the
feedback_gcp_one_project_per_surface.mdmemo so future-Claude doesn’t re-derive
Risk profile
| Risk | Severity | Mitigation |
|---|---|---|
| Legacy project deleted before all consumers migrated | High if missed | Pre-flight audit (Phase 0) lists everything; verify each script in Phase 7 BEFORE Phase 8 deletion; 30-day soft-delete is the safety net |
| OAuth refresh token lost during re-creation | Medium | Capture immediately on Phase 5 step 8; keep legacy refresh tokens in 1Password until verification complete |
| API restrictions miss an API the scripts need | Low | Phase 7 verification catches this; just edit the key + add the missing API |
Project ID xlab-co already taken globally |
Low | Try xlab-co-portfolio or xlab-co-toolkit as fallback (still mirrors org) |
| Billing-account linkage accidentally severs working service | Low | We’re free-tier — none of these projects should be on billing. Pre-flight confirms. |
Rollback plan
Within 30 days of deletion:
- Cloud Console → IAM & Admin → Manage Resources → tab “Pending deletion”
- Find the deleted project → click → Restore project
- All APIs / keys / OAuth clients / service accounts restored
- Update scripts back to the restored project’s references
After 30 days: legacy projects are permanently purged. Anything not migrated by then is lost.
Estimated time + sequence
| Phase | Effort | Can run in parallel? |
|---|---|---|
| 0. Pre-flight audit (gcloud CLI) | 10 min | — |
1. Create xlab-co |
2 min | No |
| 2. Enable APIs | 5 min (5 APIs × ~1 min each) | No |
| 3. Mint API key + restrict | 5 min | No |
| 4. Drop in 1Password | 2 min | No |
| 5. Re-create OAuth + re-run dance | 15 min | No |
| 6. Update toolkit scripts | 10 min | No (but sibling sync at end) |
| 7. Verify each script | 10 min | Can parallelise script-by-script |
| 8. Retire legacy projects | 5 min (5 × 1 min, mostly clicking “Delete”) | Can parallelise once Phase 7 is done |
| 9. Documentation | 5 min | Can be done while verifications run |
Total active work: ~60 min in a single deliberate session, or ~30-45 if some phases parallelise. Phase 0 audit can happen any time; it’s the only step that doesn’t make changes.
When to do this
Resume conditions:
- A new Google API gets enabled anywhere — that’s the moment to ask “does this go in xlab-co?” rather than reflexively creating a new project
- A 30-60 min slot opens with focus to do it deliberately
- A script needs a Google API touch (key rotation, new feature) — fold the consolidation in
Don’t rush this. Legacy projects keep working; the consolidation is hygiene, not crisis-response. Pick a calm session.
Sibling memories + cross-references
feedback_gcp_one_project_per_surface.md— the principle this runbook executesfeedback_1password_custom_concealed_fields.md— credential storage disciplinefeedback_1password_edit_in_place.md— for rotations during the migrationfeedback_save_vs_find_alignment.md— the parent principle: save-time choice must match find-time intentproject_zshrc_secrets_migration.md— sibling migration in zshrc → op:// space; same shape, different surfaceproject_ebay_seller_api_integration_parked.md+project_harvest_api_integration_parked.md— future API integrations; all new GCP keys land inxlab-cogoing forward~/bin/gsc_pull.py,~/bin/dare_perf_trend.py, future~/bin/dare_video_audit.py— the consumers that need theirop://references updated
The aphorism
One umbrella per identity, one project per umbrella, one canonical key per project. Sprawl is a tax that compounds; consolidation is a session that buys back every hour of “which one was that in?” — forever.