Skip to main content
AI/MLWaterplanAI

ac-tools-gcp-setup

Interactive GCP Cloud Build + Cloud Run setup. Provisions APIs, AR, SAs, IAM, secrets, and triggers.

Stars
31
Source
WaterplanAI/agentic-config
Updated
2026-05-25
Slug
WaterplanAI--agentic-config--ac-tools-gcp-setup
View on GitHubRaw SKILL.md

// install — copy + paste into any project

mkdir -p .claude/skills && curl -fsSL https://raw.githubusercontent.com/WaterplanAI/agentic-config/HEAD/packages/pi-ac-tools/skills/ac-tools-gcp-setup/SKILL.md -o .claude/skills/ac-tools-gcp-setup.md

Drops the SKILL.md into .claude/skills/ac-tools-gcp-setup.md. Works with Claude Code, Cursor, and any agent that loads SKILL.md files from .claude/skills/.

GCP Setup — Cloud Build + Cloud Run

Interactive skill for bootstrapping GCP infrastructure for Cloud Build + Cloud Run projects. Handles everything from gcloud auth login to verified production deploy.

Security First

Every phase validates security posture. The skill enforces:

  • Dedicated runtime service account (never default compute SA)
  • Per-secret IAM bindings (never project-level secretAccessor)
  • Approval-gated Cloud Build triggers
  • Non-root container execution
  • Optional app-level OAuth with configurable domain restriction
  • Same-origin CORS in production
  • Secrets in Secret Manager only (never env vars, never in images)

Tool Paths

All tools are relative to this skill directory:

SKILL_DIR="."

Workflow

Phase 0: Config Detection

Detect and load configuration before any infrastructure operations.

0a: User-Level Customization (BLOCKING)

Check ~/.agents/customization/gcp-setup/index.md for saved preferences.

If missing, use AskUserQuestion to collect:

  • Company domain (e.g., example.com)
  • Company name (e.g., Example Corp)
  • GitHub organization (e.g., ExampleOrg)
  • Preferred GCP region (e.g., us-central1)

Save to ~/.agents/customization/gcp-setup/index.md:

# GCP Setup Preferences

- domain: example.com
- company: Example Corp
- github_org: ExampleOrg
- region: us-central1

If values already exist, present them as defaults. If user provides different values, update index.md.

0b: Project-Level Config (BLOCKING)

Detect .gcp-setup.yml in the project root.

If exists: Load and present current config to user. AskUserQuestion: "Config found. Proceed with these settings, or modify?"

If missing: Auto-detect everything possible, then confirm. Typeform-style: one question at a time, smart defaults, minimal friction.

Step 1: Silent auto-detection (no user interaction — run all probes first)

# GitHub repo from git remote
GITHUB_REMOTE=$(git remote get-url origin || true)
GITHUB_SLUG=$(printf "%s\n" "$GITHUB_REMOTE" | sed -E 's#.*[:/]([^/]+/[^/.]+)(\.git)?$#\1#')
DIR_NAME=$(basename "$(pwd)")

# Runtime + framework
RUNTIME=""
[[ -f package.json ]] && RUNTIME="nodejs"
[[ -f requirements.txt || -f pyproject.toml ]] && RUNTIME="python"
[[ -f go.mod ]] && RUNTIME="go"
FRAMEWORK=""
[[ -f package.json ]] && grep -q '"express"' package.json && FRAMEWORK="express"
for file in requirements.txt pyproject.toml; do
  [[ -f "$file" ]] && grep -q 'fastapi' "$file" && FRAMEWORK="fastapi"
done

# Feature detection from code/dependencies
HAS_OAUTH=$(grep -rl -m 1 "passport\|GoogleStrategy\|google-auth-library\|authlib" \
  --include="*.ts" --include="*.js" --include="*.py" \
  --exclude-dir=node_modules --exclude-dir=.git --exclude-dir=vendor . | head -1)
HAS_FIRESTORE=$(grep -rl -m 1 "@google-cloud/firestore\|google.cloud.firestore" \
  --include="*.ts" --include="*.js" --include="*.py" --include="*.go" \
  --exclude-dir=node_modules --exclude-dir=.git --exclude-dir=vendor . | head -1)

# Build configs, Dockerfile, health endpoint
CLOUDBUILD_FILES=$(compgen -G "cloudbuild*.yaml"; compgen -G "cloudbuild*.yml")
DOCKERFILE=$(find . -maxdepth 2 -name Dockerfile -print | head -1)
HEALTH_EP=$(grep -roh '"/health[z]*"\|"/api/health"' \
  --include="*.ts" --include="*.js" --include="*.py" . | head -1 | tr -d '"')

# Secret manifest from .env.example
ENV_EXAMPLE=""
for candidate in .env.example .env.sample env.example; do
  [[ -f "$candidate" ]] && ENV_EXAMPLE="$candidate" && break
done

# GCP projects (for matching)
if GCLOUD_BIN=$(command -v gcloud); then
  GCP_PROJECTS=$("$GCLOUD_BIN" projects list --format="value(projectId)")
else
  GCP_PROJECTS=""
fi

Load user preferences from Phase 0a (~/.agents/customization/gcp-setup/index.md): domain, company, org, region.

Step 2: Typeform questionnaire — one AskUserQuestion per step

Rules:

  • First option = recommended default (auto-detected or convention-based)
  • Skip question entirely if auto-detection is unambiguous (e.g., GitHub repo from remote)
  • NEVER ask about convention-derived values — fill them automatically
  • Options must be concrete and actionable — no "Chat about this" or "Need to setup later"
  • Target: 2-4 questions max for a typical project

Q1: GCP Projects (required — cannot auto-detect)

Filter $GCP_PROJECTS for pairs matching *-stage/*-dev + *-prod patterns, or containing $DIR_NAME. Present best-match pair first:

  1. detected-app-dev + detected-app-prod (if pair found)
  2. Pick from my GCP projects (show filtered short-list)
  3. Enter project IDs manually

Multi-app pattern: Multiple apps can share the same GCP projects. Each app gets its own .gcp-setup.yml with unique sa_prefix, service_name, and secret names. Shared resources (AR repo, KMS keyring, GitHub connection) are created idempotently — the second app's provisioning simply finds them already in place. See cookbook/environments.md for naming conventions and examples.

Q2: Service Names (skip if $DIR_NAME is clean)

  1. ${DIR_NAME}-stage, ${DIR_NAME} — convention: stage suffixed, prod is base name
  2. Enter custom names

If $DIR_NAME produces clean service names (lowercase, no special chars, <30 chars), skip this question and use option 1 automatically — show in review.

Q3: Detected Features (confirmation only)

Present auto-detected features as a summary:

  • Runtime: $RUNTIME / $FRAMEWORK (or "not detected")
  • OAuth: Yes/No (based on $HAS_OAUTH)
  • Firestore: Yes/No (based on $HAS_FIRESTORE)
  • Health endpoint: $HEALTH_EP or /api/health (default)
  1. Confirm
  2. Change (let user toggle specific features)

If nothing ambiguous was detected, skip this question too.

Q3b: OAuth Mode (conditional — only when OAuth = Yes from Q3)

How should this app handle authentication?

  1. Standalone — This app authenticates users directly via Google OAuth. Each app needs its own redirect URIs registered in the GCP Console. Standard setup for single apps or small teams.

  2. Auth-proxy — This app IS a centralized authentication service. Register its OAuth redirect URIs once. Future apps delegate auth to this proxy — no additional Console configuration needed per app. See cookbook/auth-proxy.md for full architecture.

  3. IAP (recommended for internal tools) — Google Cloud Identity-Aware Proxy. Google manages the entire auth flow. Enable per service with one CLI flag (--iap). No redirect URIs, no OAuth clients to manage per app, no extra infrastructure. Access controlled via IAM policies. Trade-off: no custom login UI (Google's IAP login page). See cookbook/iap-setup.md for setup guide.

Cannot be auto-detected — always ask when OAuth is enabled.

Set oauth.mode to standalone (option 1), auth-proxy (option 2), or iap (option 3).

Q4: Review & Generate

Build the COMPLETE config YAML from auto-detected values + user answers + conventions. Present it formatted.

Convention-derived values (NEVER ask the user for these):

  • artifact_registry.repo_name: docker-images
  • sa_prefix.runtime: app-runtime
  • sa_prefix.cloudbuild: app-cloudbuild
  • kms.keyring: cloudbuild-keys
  • kms.key: connection-key
  • github.connection_name: repo name from slug
  • github.linked_repo: slug with / replaced by -
  • oauth.callback_path: /auth/google/cb (Express + Passport) or /api/auth/callback/google (Auth.js / NextAuth) — register BOTH in OAuth client redirect URIs
  • oauth.domain_restriction: company domain from user preferences
  • oauth.consent_type: Internal
  • oauth.mode: from Q3b answer (default: standalone). When iap is selected, oauth.callback_path and oauth.consent_type are not used (IAP manages these).
  • firestore.collection: express-sessions
  • firestore.ttl_field: expiresAt
  • Build configs: detected files or cloudbuild-stage.yaml / cloudbuild-prod.yaml
  • Secrets: OAuth secrets if OAuth detected + session-secret with auto_generate: true
  1. Save as .gcp-setup.yml
  2. Modify specific values (re-present after changes)

Generate .gcp-setup.yml using assets/gcp-setup-example.yml as template reference. Write to project root.

.gcp-setup.yml contains no secrets and should be committed to version control.

After saving config, ensure .env files are gitignored:

# Ensure .env files are gitignored (secret values must never be committed)
if [[ -f .gitignore ]]; then
  grep -qxF '.env' .gitignore || echo '.env' >> .gitignore
  grep -qxF '.env.*' .gitignore || echo '.env.*' >> .gitignore
else
  printf '.env\n.env.*\n' > .gitignore
fi

0c: Build Files Generation

After config is saved, check for missing build files and generate from templates. See cookbook/build-setup.md for the full non-tech guide.

Detection (uses variables from Phase 0b Step 1):

HAS_DOCKERFILE=$([[ -n "$DOCKERFILE" ]] && echo true || echo false)
HAS_DOCKERIGNORE=$([[ -f .dockerignore ]] && echo true || echo false)
HAS_CLOUDBUILD=$([[ -n "$CLOUDBUILD_FILES" ]] && echo true || echo false)

1. .dockerignore (always generate if missing):

cp "$SKILL_DIR/assets/dockerignore-template" .dockerignore

2. Dockerfile (generate if missing, based on detected $RUNTIME):

Runtime Template Default CMD
nodejs $SKILL_DIR/assets/dockerfile-node.template CMD ["node", "server.js"]
python $SKILL_DIR/assets/dockerfile-python.template CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
go $SKILL_DIR/assets/dockerfile-go.template ENTRYPOINT ["/app/server"]

After copying the template, adjust the CMD/ENTRYPOINT based on detected framework and entry point:

  • Express: detect main field in package.json or server.js/index.js/app.js
  • Next.js: CMD ["npm", "start"]
  • FastAPI: detect main:app or app.main:app from imports
  • Flask/Django: detect app:app or project.wsgi:application

If runtime is not detected, use AskUserQuestion to ask which runtime to use.

3. CloudBuild YAMLs (generate if missing):

Read $SKILL_DIR/assets/cloudbuild-template.yaml and substitute tokens from config. Generate TWO files:

cloudbuild-stage.yaml:

CONFIG=".gcp-setup.yml"
PROJECT_ID=$(yq -r '.projects.stage.id' "$CONFIG")
SERVICE_NAME=$(yq -r '.projects.stage.service_name' "$CONFIG")
REGION=$(yq -r '.region' "$CONFIG")
REPO_NAME=$(yq -r '.artifact_registry.repo_name' "$CONFIG")
IMAGE_NAME="$SERVICE_NAME"
MAX_INSTANCES=$(yq -r '.app.max_instances_stage // 5' "$CONFIG")
SA_PREFIX=$(yq -r '.sa_prefix.runtime' "$CONFIG")
RUNTIME_SA="${SA_PREFIX}@${PROJECT_ID}.iam.gserviceaccount.com"
APP_DIR=$(dirname "$(yq -r '.app.dockerfile_path // "Dockerfile"' "$CONFIG")")
[[ "$APP_DIR" == "." || -z "$APP_DIR" ]] && APP_DIR="."

# Resolve auth flag and IAP settings based on oauth.mode
OAUTH_MODE=$(yq -r '.oauth.mode // "standalone"' "$CONFIG")
if [[ "$OAUTH_MODE" == "iap" ]]; then
  _AUTH_FLAG="--no-allow-unauthenticated"
  _IAP_FLAG="--iap"
  _ENTRYPOINT="gcloud beta"
else
  _AUTH_FLAG="--allow-unauthenticated"
  _IAP_FLAG=""
  _ENTRYPOINT="gcloud"
fi

# Generate secrets mapping from manifest
SECRETS_MAPPING=""
SECRET_COUNT=$(yq -r '.secrets | length' "$CONFIG")
for ((i=0; i<SECRET_COUNT; i++)); do
  NAME=$(yq -r ".secrets[$i].name" "$CONFIG")
  ENV_VAR=$(yq -r ".secrets[$i].env_var" "$CONFIG")
  [[ -n "$SECRETS_MAPPING" ]] && SECRETS_MAPPING="${SECRETS_MAPPING},"
  SECRETS_MAPPING="${SECRETS_MAPPING}${ENV_VAR}=${NAME}:latest"
done

Substitute all __TOKEN__ placeholders in the template and write to cloudbuild-stage.yaml:

  • Replace __ENTRYPOINT__ with $_ENTRYPOINT (i.e., gcloud beta for IAP, gcloud otherwise)
  • Replace __AUTH_FLAG__ with $_AUTH_FLAG
  • If $_IAP_FLAG is non-empty, replace __IAP_FLAG__ with --iap
  • If $_IAP_FLAG is empty, remove the entire - '__IAP_FLAG__' line from the generated YAML (do not leave an empty string arg)
  • If $SECRETS_MAPPING is empty, remove the entire --set-secrets=__SECRETS_MAPPING__ line (an empty --set-secrets= causes deploy failure)
  • If $ENV_VARS is empty, remove the entire --set-env-vars=__ENV_VARS__ line (an empty --set-env-vars= causes deploy failure)

cloudbuild-prod.yaml: Same process with prod values and max_instances_prod (default: 100).

AskUserQuestion gate: Present a summary of generated files:

Generated build files:
  - .dockerignore (universal exclusions)
  - Dockerfile (Node.js multi-stage, non-root)
  - cloudbuild-stage.yaml (stage project, max 5 instances)
  - cloudbuild-prod.yaml (prod project, max 100 instances)

Review and confirm, or modify specific files.

All generated files should be committed to version control.

0d: Project Documentation

After build files are in place, generate project documentation if missing. These docs help onboard new team members and guide AI assistants.

Detection:

HAS_SECRETS_GUIDE=$([[ -f docs/managing-secrets-and-envs.md ]] && echo true || echo false)
HAS_README=$([[ -f README.md ]] && echo true || echo false)
HAS_AGENTS=$([[ -f AGENTS.md || -f CLAUDE.md ]] && echo true || echo false)

Compute deterministic URLs (needed for templates):

CONFIG=".gcp-setup.yml"
STAGE_PROJECT=$(yq -r '.projects.stage.id' "$CONFIG")
PROD_PROJECT=$(yq -r '.projects.prod.id' "$CONFIG")
STAGE_SERVICE=$(yq -r '.projects.stage.service_name' "$CONFIG")
PROD_SERVICE=$(yq -r '.projects.prod.service_name' "$CONFIG")
REGION=$(yq -r '.region' "$CONFIG")
STAGE_NUM=$(gcloud projects describe "$STAGE_PROJECT" --format="value(projectNumber)")
PROD_NUM=$(gcloud projects describe "$PROD_PROJECT" --format="value(projectNumber)")
STAGE_URL="https://${STAGE_SERVICE}-${STAGE_NUM}.${REGION}.run.app"
PROD_URL="https://${PROD_SERVICE}-${PROD_NUM}.${REGION}.run.app"
CB_CONSOLE_STAGE="https://console.cloud.google.com/cloud-build/builds;region=${REGION}?project=${STAGE_PROJECT}"
CB_CONSOLE_PROD="https://console.cloud.google.com/cloud-build/builds;region=${REGION}?project=${PROD_PROJECT}"

For each missing doc, generate from template:

  1. docs/managing-secrets-and-envs.md — Read $SKILL_DIR/assets/managing-secrets-and-envs.template.md, substitute tokens from config. Generate ${SECRETS_TABLE} from secrets manifest:

    | Secret Name | Env Var | Environments |
    |-------------|---------|--------------|
    | oauth-client-id | GOOGLE_CLIENT_ID | stage, prod |
    ...
    

    Generate ${ENV_VARS_TABLE} for non-secret env vars (AUTH_URL, AUTH_TRUST_HOST, etc.). Create docs/ directory if needed.

  2. README.md — Read $SKILL_DIR/assets/readme.template.md, substitute tokens. Set ${PROJECT_NAME} from directory name (title-cased). Set ${PROJECT_DESCRIPTION} from a brief AskUserQuestion or auto-detect from package.json description. Compute ${HEALTH_ENDPOINT} and ${HEALTH_RESPONSE} from config.

  3. AGENTS.md — Read $SKILL_DIR/assets/agents.template.md, substitute tokens. Framework-specific values:

    Token Node.js/Express Node.js/Next.js Python/FastAPI Go
    ${RUNTIME_DISPLAY} Node.js 22 (Alpine in Docker) Node.js 22 (Alpine in Docker) Python 3.12 (slim in Docker) Go 1.23 (distroless in Docker)
    ${LANGUAGE} JavaScript/TypeScript TypeScript (strict mode) Python Go
    ${PACKAGE_MANAGER} npm npm pip go modules
    ${BUILD_CMD} npm run build npm run build N/A go build
    ${DEV_CMD} npm run dev npm run dev uvicorn main:app --reload go run .
    ${LINT_CMD} npm run lint npm run lint ruff check golangci-lint run
    ${TYPECHECK_CMD} npx tsc --noEmit npx tsc --noEmit pyright go vet ./...

    ${STYLE_CONVENTIONS} — framework-specific conventions (App Router for Next.js, route structure for Express, etc.). ${AUTH_DESCRIPTION} — derived from oauth config (e.g., "Google OAuth with @example.com domain restriction (Auth.js v5)").

AskUserQuestion gate: Present generated docs summary:

Generated project documentation:
  - docs/managing-secrets-and-envs.md (secrets guide with current inventory)
  - README.md (environments, local dev, deploy workflow)
  - AGENTS.md (AI assistant project guidelines)

Review and confirm, or modify specific files.

All generated docs should be committed to version control.

0e: GCP State Probe

Probe existing infrastructure state (unchanged logic, reads from config):

CONFIG=".gcp-setup.yml"
STAGE=$(yq -r '.projects.stage.id' "$CONFIG")
PROD=$(yq -r '.projects.prod.id' "$CONFIG")

# Quick check — do APIs exist?
gcloud services list --enabled --filter="name:run.googleapis.com" --project="$STAGE" --format="value(name)"

Present state table to user and use AskUserQuestion:

  • "Here's what already exists. Which phases do you want to run? (0=all from scratch, 1=preflight only, 2=provision, 3=secrets, 4=oauth, 5=verify)"
  • If everything exists: suggest Phase 5 (verify) only

Phase 1: Preflight Validation

bash "$SKILL_DIR/tools/preflight.sh" --config .gcp-setup.yml

AskUserQuestion gate: If any required check fails, present install instructions and wait for confirmation before proceeding.

Phase 2: GCP Resource Provisioning

MANDATORY — CMEK Encryption Key Setup:

Before creating the GitHub connection, provision a Cloud KMS encryption key. Connection data (including GitHub OAuth tokens stored in Secret Manager) MUST be encrypted with a customer-managed key.

bash "$SKILL_DIR/tools/provision.sh" --config .gcp-setup.yml --cmek-only --env all

This enables the Cloud KMS API, creates the keyring + key, provisions the Secret Manager service agent, and grants it cryptoKeyEncrypterDecrypter on the key.

MANUAL GATE — 2nd-gen GitHub Connection:

After CMEK setup, the user must create GitHub connections in both GCP projects. This CANNOT be automated (requires browser OAuth).

Use AskUserQuestion: "CMEK keys are provisioned. Have you created the 2nd-gen GitHub connection in Cloud Build for both projects? You MUST select the encryption key during connection creation."

If NO, read and present the guide:

cat "$SKILL_DIR/cookbook/oauth-setup.md"

Present the Console URLs (read project IDs from config):

STAGE=$(yq -r '.projects.stage.id' .gcp-setup.yml)
PROD=$(yq -r '.projects.prod.id' .gcp-setup.yml)
echo "Stage: https://console.cloud.google.com/cloud-build/repositories/2nd-gen?project=$STAGE"
echo "Prod: https://console.cloud.google.com/cloud-build/repositories/2nd-gen?project=$PROD"

Steps:

  1. Open Console URL
  2. Click "Create Host Connection" -> GitHub
  3. Set Region from config
  4. Set Name from github.connection_name
  5. Encryption: Select the KMS key — do NOT skip this
  6. Click Connect -> Authorize Cloud Build GitHub App
  7. Link the repository

SECURITY WARNING: Do NOT skip the encryption key selection. Google-managed encryption is NOT acceptable. The connection stores GitHub OAuth tokens in Secret Manager — these must be encrypted with the customer-managed key for key rotation control and compliance.

Wait for user confirmation.

Run provision:

bash "$SKILL_DIR/tools/provision.sh" --config .gcp-setup.yml --env all

Note: The temporary secretmanager.admin grant auto-expires in 20 minutes. No manual revocation needed.

Phase 3: Secrets Setup

Create secret shells:

bash "$SKILL_DIR/tools/secrets.sh" --config .gcp-setup.yml --action create --env all

AskUserQuestion: "How do you want to populate secrets?"

  • Option A: From an existing .env file -> --action populate --env-file /path/to/.env
  • Option B: Interactively (guided prompts)

Populate secrets:

bash "$SKILL_DIR/tools/secrets.sh" --config .gcp-setup.yml \
  --action populate --env "$TARGET_ENV" --env-file "$ENV_FILE"

SECURITY WARNING: If populating both envs from the same .env, warn:

"OAuth credentials SHOULD differ per environment. Consider running --env stage first, then --env prod --env-file /path/to/prod.env."

Phase 4: Authentication Configuration (Conditional)

Skip entirely when oauth.enabled is false in config.

Read oauth.mode from config (default: standalone).


Mode: IAP (recommended for internal tools)

When oauth.mode is iap, authentication is handled entirely by Google Cloud Identity-Aware Proxy. No OAuth clients, no redirect URIs, no custom auth code needed.

One-time project setup (first time enabling IAP in this GCP project):

  1. Enable required APIs:

    for PROJECT in "$STAGE_PROJECT" "$PROD_PROJECT"; do
      gcloud services enable iap.googleapis.com cloudresourcemanager.googleapis.com --project="$PROJECT"
    done
    
  2. Configure Google Auth Platform (Console — one time per project):

    • Go to Google Auth Platform > Branding in GCP Console
    • Set app name, support email, authorized domain
    • Go to Audience and set to configured consent type (default: Internal)
    • Go to Data Access and add scopes: openid, email, profile
  3. AskUserQuestion gate:

    • "Have you configured Google Auth Platform (Branding + Audience) in the GCP Console? This is a one-time step per project."

Per-service setup (automated — repeat for each app):

  1. Enable IAP on Cloud Run services:

    for PROJECT in "$STAGE_PROJECT" "$PROD_PROJECT"; do
      ENV_KEY=$( [ "$PROJECT" = "$STAGE_PROJECT" ] && echo stage || echo prod )
      SERVICE_NAME=$(yq -r ".projects.${ENV_KEY}.service_name" "$CONFIG")
      # For existing services:
      gcloud beta run services update "$SERVICE_NAME" \
        --region="$REGION" \
        --iap \
        --project="$PROJECT"
      # For new deploys, use: gcloud beta run deploy ... --iap --no-allow-unauthenticated
    done
    

    Note: services update --iap works on already-deployed services. For first deploys, use gcloud beta run deploy ... --iap --no-allow-unauthenticated or add these flags to the CloudBuild deploy step.

  2. Grant IAP service agent invoker permission:

    for PROJECT in "$STAGE_PROJECT" "$PROD_PROJECT"; do
      ENV_KEY=$( [ "$PROJECT" = "$STAGE_PROJECT" ] && echo stage || echo prod )
      SERVICE_NAME=$(yq -r ".projects.${ENV_KEY}.service_name" "$CONFIG")
      PROJECT_NUMBER=$(gcloud projects describe "$PROJECT" --format='value(projectNumber)')
      gcloud run services add-iam-policy-binding "$SERVICE_NAME" \
        --region="$REGION" \
        --member="serviceAccount:service-${PROJECT_NUMBER}@gcp-sa-iap.iam.gserviceaccount.com" \
        --role="roles/run.invoker" \
        --project="$PROJECT"
    done
    
  3. Remove public access (if the service was previously public):

    for PROJECT in "$STAGE_PROJECT" "$PROD_PROJECT"; do
      ENV_KEY=$( [ "$PROJECT" = "$STAGE_PROJECT" ] && echo stage || echo prod )
      SERVICE_NAME=$(yq -r ".projects.${ENV_KEY}.service_name" "$CONFIG")
      gcloud run services remove-iam-policy-binding "$SERVICE_NAME" \
        --region="$REGION" \
        --member="allUsers" \
        --role="roles/run.invoker" \
        --project="$PROJECT" || true
    done
    

    Warning: If allUsers has roles/run.invoker, anyone can bypass IAP by calling the Cloud Run URL directly. Always remove public access when enabling IAP.

  4. Grant domain access:

    DOMAIN=$(yq -r '.oauth.domain_restriction' "$CONFIG")
    for PROJECT in "$STAGE_PROJECT" "$PROD_PROJECT"; do
      ENV_KEY=$( [ "$PROJECT" = "$STAGE_PROJECT" ] && echo stage || echo prod )
      SERVICE_NAME=$(yq -r ".projects.${ENV_KEY}.service_name" "$CONFIG")
      gcloud beta iap web add-iam-policy-binding \
        --resource-type=cloud-run \
        --service="$SERVICE_NAME" \
        --region="$REGION" \
        --member="domain:${DOMAIN}" \
        --role="roles/iap.httpsResourceAccessor" \
        --project="$PROJECT"
    done
    
  5. Read the IAP cookbook:

    cat "$SKILL_DIR/cookbook/iap-setup.md"
    
  6. Present IAP guidance to the user:

    • IAP is now active. Users visiting the app will see Google's IAP login page.
    • The app receives user identity via headers: X-Goog-Authenticated-User-Email and X-Goog-IAP-JWT-Assertion.
    • Future apps: just add --iap --no-allow-unauthenticated to the deploy command and run the IAM bindings above. No Console changes needed.
    • See cookbook/iap-setup.md for header verification, service-to-service auth, and local development.

Note: When oauth.mode is iap, OAuth secrets (oauth-client-id, oauth-client-secret) are NOT needed in the secrets manifest. Google manages the OAuth client internally. Only session-secret is needed if the app manages its own sessions.


Mode: Standalone (default)

When oauth.mode is standalone (or not set):

SECURITY WARNING — Display this to the user before proceeding:

The OAuth consent screen MUST be set to the configured consent type (default: "Internal"). If set to "Internal", this restricts authentication to the configured domain only. Do NOT select "External" unless required — doing so would allow ANY Google account to reach the OAuth flow. If you are unsure, stop and consult the team.

This sub-phase is entirely manual. The skill provides exact step-by-step guidance.

  1. Compute deterministic Cloud Run URLs:

    CONFIG=".gcp-setup.yml"
    STAGE=$(yq -r '.projects.stage.id' "$CONFIG")
    PROD=$(yq -r '.projects.prod.id' "$CONFIG")
    STAGE_SVC=$(yq -r '.projects.stage.service_name' "$CONFIG")
    PROD_SVC=$(yq -r '.projects.prod.service_name' "$CONFIG")
    REGION=$(yq -r '.region' "$CONFIG")
    STAGE_NUM=$(gcloud projects describe "$STAGE" --format="value(projectNumber)")
    PROD_NUM=$(gcloud projects describe "$PROD" --format="value(projectNumber)")
    echo "Stage URL: https://${STAGE_SVC}-${STAGE_NUM}.${REGION}.run.app"
    echo "Prod URL:  https://${PROD_SVC}-${PROD_NUM}.${REGION}.run.app"
    

    Warning: Do NOT use status.url to get the service URL for OAuth configuration. It returns a legacy hash-based URL that differs from the deterministic format. Always compute deterministic URLs.

  2. Read the full OAuth guide:

    cat "$SKILL_DIR/cookbook/oauth-setup.md"
    
  3. Present step-by-step with pre-computed URLs:

    • Configure Google Auth Platform: Branding, Audience, Data Access
    • Create per-environment OAuth 2.0 Client IDs
    • Register callback URLs: https://<cloud-run-url><callback_path from config>
    • Add authorized JavaScript origins
  4. AskUserQuestion gates:

    • "Have you configured Google Auth Platform (Audience set to configured consent type) in the GCP project?"
    • "Have you created OAuth 2.0 Client IDs for each environment?"
    • "Have you populated the oauth secrets via Phase 3?"

Mode: Auth-Proxy (Additional Steps)

When oauth.mode is auth-proxy, first complete ALL standalone steps above (the proxy itself needs OAuth registration), then:

  1. Read the auth-proxy cookbook:

    cat "$SKILL_DIR/cookbook/auth-proxy.md"
    
  2. Present auth-proxy guidance to the user:

    • This app is the centralized auth service. The OAuth registration just completed is the ONLY registration needed.
    • Future apps that need authentication will delegate to this proxy instead of registering with Google directly.
    • The proxy must implement: Google OAuth flow, JWT signing with a shared secret, return_url allowlist validation, domain restriction.
    • Client apps must implement: redirect to proxy, JWT verification, session creation.
    • Share the proxy URL with the team: https://<proxy-service>-<project-num>.<region>.run.app
    • See cookbook/auth-proxy.md for the complete architecture, implementation checklist, and client integration guide.
  3. AskUserQuestion gate:

    • "Have you reviewed the auth-proxy architecture in cookbook/auth-proxy.md? This app will serve as the centralized auth service for future apps."

Phase 5: Verification

Run full verification:

bash "$SKILL_DIR/tools/verify.sh" --config .gcp-setup.yml --env all

Interpret results:

  • All PASS -> Setup complete. Present next steps (first deploy, PR workflow).
  • Any FAIL -> Present remediation commands from verify.sh output.
  • Re-run verify after remediation.

Post-Setup: First Deploy

After all phases pass, guide the user through the first deploy:

  1. Push a branch and open a PR
  2. Comment /gcbrun on the PR to trigger stage build
  3. Approve the build in Cloud Build console
  4. Wait for build + deploy
  5. Verify health check using configured endpoint
  6. If OAuth enabled: test authentication with configured domain account
  7. Merge PR to main for production deploy

Troubleshooting

If issues arise post-deploy, run diagnostics:

bash "$SKILL_DIR/tools/diagnose.sh" --config .gcp-setup.yml --env "$TARGET_ENV"

For detailed troubleshooting reference:

cat "$SKILL_DIR/cookbook/troubleshooting.md"

CI Setup: PR Template

To add the deploy checklist to PRs:

cat "$SKILL_DIR/assets/pr-template.md"

Copy this to .github/PULL_REQUEST_TEMPLATE.md, replacing ${STAGE_URL} and ${CB_CONSOLE_URL} with actual values.

Reference

Resource Location
Build setup guide cookbook/build-setup.md
Prerequisites cookbook/prerequisites.md
Security model cookbook/security-model.md
OAuth setup guide cookbook/oauth-setup.md
Auth.js on Cloud Run cookbook/authjs-setup.md
Auth-proxy guide cookbook/auth-proxy.md
IAP setup guide cookbook/iap-setup.md
Troubleshooting cookbook/troubleshooting.md
Environment configs cookbook/environments.md
Cloud Build template assets/cloudbuild-template.yaml
PR template assets/pr-template.md
.env template assets/env-example
Secrets guide template assets/managing-secrets-and-envs.template.md
README template assets/readme.template.md
AGENTS.md template assets/agents.template.md
Config example assets/gcp-setup-example.yml