diff --git a/.agents/skills/gforce-google-cloud/SKILL.md b/.agents/skills/gforce-google-cloud/SKILL.md new file mode 100644 index 0000000..b230a5b --- /dev/null +++ b/.agents/skills/gforce-google-cloud/SKILL.md @@ -0,0 +1,99 @@ +--- +name: gforce-google-cloud +description: > + GForce Google Cloud house rules: the folder and project map, naming, which identity each caller + uses, how changes reach the cloud, and the spending limits. + TRIGGER when: creating, deploying to, debugging, granting access in, or estimating cost for any + GForce Google Cloud project (gforce-shared-platform, gforce-shared-credentials, + gforce-playground, gforce-production), or writing Terraform or a GitHub workflow that touches + Google Cloud. + DO NOT TRIGGER when: the work is the Raspberry Pi homelab or a personal project, or it is only + Google Workspace administration (use gforce-google-workspace). +version: 1.1.0 +tags: [google-cloud, gcp, terraform, iam, finops, gforce] +--- + +# GForce Google Cloud Standard + +Source of truth: the private repo `Gforce-Innovation-Kft/gforce-google-cloud`, its design spec in +`docs/superpowers/specs/` and its Terraform. Concrete IDs (organization, billing account, project +numbers) live there, never in this public skill. Read the spec before changing structure or access. + +## The map + +| Folder | Project | Holds | Budget | +| ---------- | --------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------- | +| Shared | `gforce-shared-platform` | Shared Docker registry, Terraform state, the GitHub trust, billing export, kill switch | $5/month, email | +| Shared | `gforce-shared-credentials` | Secrets only | (Shared) | +| Playground | `gforce-playground` | Agents, experiments, anything new | $20/month, **billing disconnected at 100%** | +| Production | `gforce-production` | Only what must keep running | $10/month, email | + +One region: `europe-west1`. Names: service account `-`, secret +`-`, labels `environment`, `workload`, `repository`, `managed-by` on everything +(`managed-by` is the tool that last deploys it: `terraform` or `github-actions`). Shared registry: +`europe-west1-docker.pkg.dev/gforce-shared-platform/containers/`. + +## Which identity are you? + +| Caller | Identity | Rights | +| -------------------------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | +| Claude Code on Gabor's Mac | `claude-reader@gforce-shared-platform`, gcloud profile `claude` | Read only. No secret values, no Terraform state | +| Claude agent job | `agent-runner@gforce-playground` | Playground only | +| GitHub Actions | The repository itself (`principalSet://…/attribute.repository/Gforce-Innovation-Kft/`) | Playground deploy; Production only from `main` through the shared deploy workflow | +| Terraform apply | `terraform-deployer@gforce-shared-platform`, impersonated by Gabor on his Mac | Everything structural | +| A running service | Its own service account in its own environment's project | The single secret, topic or bucket it uses | + +## Hard stops + +- **You never change Shared or Production yourself, emergencies included.** Diagnose read-only, + then give Gabor the exact command and the matching Terraform diff. He runs the command; you open + the PR that codifies it. Your identity cannot write there, so attempting it only loses time. +- Never create, download or store a service account key. +- Never grant `roles/viewer`, `roles/editor` or `roles/owner`. Viewer reads bucket objects, and the + Terraform state holds secrets. +- Changes reach main only through a pull request; never push to main, even when asked to "just + push". GitHub Free gives these private repositories no branch protection, so this rule is the guard. +- Plan first, then apply: run `terraform plan -out=.tfplan`, show Gabor what it adds, changes + and destroys, and apply only that saved plan, only after he says so. `terraform apply` runs only + on Gabor's Mac. CI runs fmt, validate and a read-only plan. +- IAM members come from `gforceinnovation.com` only. A `gmail.com` account is refused by policy; + don't propose an exception. +- No Playground ↔ Production grants, in either direction. + +| Thought | Reality | +| ------------------------------------------------------ | ------------------------------------------------------------------------------------------- | +| "Production is losing data, faster if I fix it myself" | The fastest path is the one command Gabor can paste. Give it in your first reply. | +| "It's one narrow binding, that's safe" | Narrow or not, it's a manual Production change. Gabor runs it, then it goes into Terraform. | +| "CI applying Terraform is standard practice" | Not here. Apply is local after a reviewed plan. | + +## Adding a service: every line is required + +1. Terraform in `environments/playground` (or `production`): a service account named after the + workload; a grant on the **single** secret in `gforce-shared-credentials`; `run.developer` and + `iam.serviceAccountUser` for the repository's `principalSet`. +2. No new identity pool, provider, deploy service account or registry. Use the existing GitHub trust + and push to the shared `containers` registry. The repository's `principalSet` needs + `artifactregistry.writer` on that registry: a change in the `shared` root, so Gabor applies it. +3. Deploy flags: `--region=europe-west1`, an explicit `--service-account`, the four labels, and a + pinned secret version in Production. A service also gets `--min-instances=0` and + `--max-instances=<1-3>`; a job gets `--task-timeout`, `--tasks` and `--max-retries` instead. +4. Gabor reviews the plan and applies it. + +## Money + +- Before adding anything that runs all the time (Cloud SQL ~$10, load balancer ~$18, Cloud NAT + ~$32, VMs, GKE, static IPs), stop and ask. It would consume a whole budget. +- Exact spend: the BigQuery billing export in `gforce-shared-platform`. +- If Playground stops at once, the kill switch probably fired. Say so; never relink billing yourself. + +## Upstream skills + +Use `google/skills` for depth (foundation-builder, waf-security, waf-cost-optimization, +recipe-auth, cloud-run-basics). GForce wins on conflict: + +- foundation-builder's self-granting of admin roles → Terraform only, applied by Gabor. +- foundation-builder's four-folder layout → the map above. +- Multi-region and high-availability defaults → one region, scale to zero. + +Read `.claude/references/local-standards.md` in the consuming repo last. It wins on conflict, +except the non-overridable core in `standards/invariants.md`. diff --git a/.agents/skills/gforce-google-cloud/overrides.json b/.agents/skills/gforce-google-cloud/overrides.json new file mode 100644 index 0000000..a8bd769 --- /dev/null +++ b/.agents/skills/gforce-google-cloud/overrides.json @@ -0,0 +1,31 @@ +{ + "$comment": "Delta anchors: this skill overrides these google/skills rules. Format in standards/upstream-policy.md, schema in schemas/overrides.schema.json.", + "source": "google/skills", + "overrides": [ + { + "upstream_paths": [ + "skills/cloud/google-cloud-recipe-foundation-builder" + ], + "tripwires": [ + "Lazy Role Remediation", + "Self-Remediation", + "add-iam-policy-binding" + ], + "gforce_rule": "Administrative roles are granted only through Terraform, applied by Gabor", + "reason": "The recipe grants itself organization and billing admin roles when a step fails; GForce changes IAM only through a reviewed Terraform plan", + "delta": "SKILL.md hard stops: never change Shared or Production yourself" + }, + { + "upstream_paths": [ + "skills/cloud/google-cloud-recipe-foundation-builder" + ], + "tripwires": [ + "Non-Production", + "Development" + ], + "gforce_rule": "Folders are Shared, Playground and Production with four named projects", + "reason": "A one-person company needs two environments, not three; the GForce map is in the gforce-google-cloud spec", + "delta": "SKILL.md: The map" + } + ] +} diff --git a/.claude/skills/gforce-google-cloud b/.claude/skills/gforce-google-cloud new file mode 120000 index 0000000..724cd5a --- /dev/null +++ b/.claude/skills/gforce-google-cloud @@ -0,0 +1 @@ +../../.agents/skills/gforce-google-cloud \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index a226fa5..aefbb44 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,14 +17,24 @@ is the contract; the reference carries the rationale the YAML cannot. `sf-org-login`'s `credential-source: github-env` branch reads GitHub Environment secrets/variables (`SF_JWT_KEY_B64`, `SF_USERNAME`, `SF_CLIENT_ID`, `SF_INSTANCE_URL`) — this repo only consumes -them, it never defines them. Google Secret Manager, in -[`gforce-google-infra`](https://github.com/Gforce-Innovation-Kft/gforce-google-infra), is the -source of truth for those values; its `modules/gh-secret-sync` Terraform module mirrors them into -consumer repos' Environments (`sf-develop-demo` today). That's a stopgap for repos not yet on -`credential-source: gcp` — the not-yet-built branch that would read GCP directly via WIF instead -of GitHub Environment secrets at all (see that repo's -`docs/superpowers/specs/2026-08-19-github-secret-sync-design.md` for the full picture, including -why it's one-directional and never the other way round). +them, it never defines them. The `credential-source: gcp` branch reads Google Secret Manager +directly via WIF. The Environment secrets that `gh-secret-sync` in the retired +`gforce-google-infra` repo mirrored into consumer repos (`sf-develop-demo`, `gforce-devhub`) stay +as they are, but nothing syncs or rotates them any more. See the Google Cloud section below. + +## Google Cloud + +Load the `gforce-google-cloud` skill before any Google Cloud work. + +- This repo's actions (`sf-org-login`, `gcp-secret-get`) take the provider, project and secret as + inputs and name no project. Their callers used `gforce-salesforce-cicd` and the pool + `github-pool`; both are shutting down. +- New home: pool `github`, provider `github-actions` in `gforce-shared-platform`, set per caller as + repository variable `GCP_WORKLOAD_IDENTITY_PROVIDER`; secrets in `gforce-shared-credentials` + (`salesforce-devhub-jwt`, `salesforce-integration-jwt`, `salesforce-demo-production-jwt`, + `github-app-salesforce-key`). +- Never hardcode the provider value or a project number here. +- Details and the switch checklist: [`docs/gcloud-migration.md`](docs/gcloud-migration.md). ## Naming convention @@ -124,7 +134,7 @@ Rationale: [`docs/architecture.md`](docs/architecture.md). ## Skills & AI tooling -Eight repo-scoped skills are committed. They live in `.agents/skills/`, are symlinked +Nine repo-scoped skills are committed. They live in `.agents/skills/`, are symlinked into `.claude/skills/`, and are pinned by content hash in [`skills-lock.json`](skills-lock.json). All are vendored from upstream — do not hand-edit a vendored `SKILL.md`; the next update overwrites it and the hash stops @@ -139,6 +149,7 @@ Invoke the relevant one when the task matches: | Skill | Upstream | Use when | |-------|----------|----------| +| `gforce-google-cloud` | Gforce-Innovation-Kft/gforce-ai | Any Google Cloud work: the project map, which identity each caller uses, the hard stops. Its rules win over google/skills. | | `github-actions-docs` | xixu-me/skills | Authoring or editing a workflow / `action.yml` — keeps YAML aligned with current GitHub Actions syntax (composite, reusable, TypeScript action patterns). The default for most work in this repo. | | `github-actions-templates` | wshobson/agents | Scaffolding a *new* workflow from a known-good shape, rather than editing an existing one. | | `code-review` | mattpocock/skills | Reviewing the TypeScript under `gforce-gha-src/` for quality and correctness. | diff --git a/docs/claude-actions-reference.md b/docs/claude-actions-reference.md index 65b8f80..bd17460 100644 --- a/docs/claude-actions-reference.md +++ b/docs/claude-actions-reference.md @@ -181,9 +181,10 @@ The single login action — **two credential sources, one contract**. `auth-meth > `jwt_key` field must be the raw, multi-line PEM — this action base64-encodes it itself in > "Stage credentials from GCP" (`sf-org-login/action.yml`). A pre-base64-encoded `jwt_key` fails > here with "Decoded JWT key is not a PEM private key", because it gets double-encoded then -> single-decoded. This happened for real on `salesforce-devhub` (fixed 2026-08-20, root cause and -> the seeding convention are in `gforce-google-infra`'s `docs/SECRETS.md`) — if this error shows -> up again, the fix is in that repo's secret data, not in this action's code. +> single-decoded. This happened for real on `salesforce-devhub` (fixed 2026-08-20). The same rule +> holds for the new `salesforce-devhub-jwt` in `gforce-shared-credentials` — if this error shows +> up again, the fix is in the secret data, not in this action's code. (The old seeding notes were +> in the retired `gforce-google-infra` repo.) ### `sf-source-delta` (`.github/actions/sf-source-delta/action.yml`) diff --git a/docs/gcloud-migration.md b/docs/gcloud-migration.md new file mode 100644 index 0000000..764106f --- /dev/null +++ b/docs/gcloud-migration.md @@ -0,0 +1,51 @@ +# Google Cloud migration + +On 2026-11-10 GForce Google Cloud moved into a Terraform-managed landing zone +(`gforce-google-cloud`). The old projects `claude-integration-501116`, `gforce-salesforce-cicd`, +`gforce-sf-platform` and `gen-lang-client-0257489838` are shutting down, and `gforce-google-infra` +is being deleted. The shutdown leaves a 30-day undelete window. This repo has no Google Cloud +config of its own; its actions take the provider, project and secret as inputs. + +## What this repo touches + +| Item | File in this repo | Old location | New target | Breaks at shutdown | +| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | -------------------------------------------------------------------- | --------------------------------------------- | +| Workload Identity provider (input) | `.github/actions/sf-org-login/action.yml`, `.github/actions/gcp-secret-get/action.yml` | pool `github-pool`, provider `github-provider` | pool `github` in `gforce-shared-platform`, provider `github-actions` | Not here. Each caller's repo variable breaks. | +| Secret project (input) | the same two actions | `gforce-salesforce-cicd` | `gforce-shared-credentials` | Not here. Each caller's input breaks. | +| Secret `salesforce-devhub` (doc example) | the same two actions (input descriptions), `.github/workflows/smoke-sf-org-login.yml` (credential-free fixture) | `gforce-salesforce-cicd` `salesforce-devhub` | `gforce-shared-credentials` `salesforce-devhub-jwt` | No. The smoke test never calls Google Cloud. | +| Source of the `github-env` secrets | `CLAUDE.md`, `docs/claude-actions-reference.md` | `gforce-google-infra` `gh-secret-sync` (retired) | none; see below | Documentation only. | + +The actions themselves need no code change. They hold no project, pool or secret names. + +## Secret names + +| Old (`gforce-salesforce-cicd`) | New (`gforce-shared-credentials`) | +| ------------------------------ | --------------------------------- | +| `salesforce-devhub` | `salesforce-devhub-jwt` | +| `salesforce-integration` | `salesforce-integration-jwt` | +| `salesforce-demo-prod` | `salesforce-demo-production-jwt` | +| `salesforce-github-app` | `github-app-salesforce-key` | + +The JSON bundle shape that `gcp-secret-get` and `sf-org-login` expect is unchanged. Secret +values are copied into the new containers before any caller switches. + +## Checklist + +- [ ] Callers set the repository variable `GCP_WORKLOAD_IDENTITY_PROVIDER` to the `shared` output + `workload_identity_provider` of `gforce-google-cloud`. Never hardcode it in a caller or here. +- [ ] Callers pass `gcp-project-id: gforce-shared-credentials` and the new `gcp-secret-name`. +- [ ] Each calling repository has its `principalSet` grant in the `shared` root of + `gforce-google-cloud`. +- [ ] Check whether the new provider still requires a job `environment:`. The comment in + `gcp-secret-get/action.yml` describes the old provider's `attribute_condition`. +- [ ] After the callers switch, change the example secret names in the two action descriptions + to the `*-jwt` names, and re-run `smoke-sf-org-login.yml`. +- [ ] Regenerate `docs/usage-catalog.md` once callers are on the new inputs. + +## GitHub Environment secrets + +`credential-source: github-env` reads Environment secrets that `gh-secret-sync` in +`gforce-google-infra` used to fill. Existing values stay in place, but nothing syncs or rotates +them any more. Prefer `credential-source: gcp` where a repo can use it. + +Inventory: [plan-2-inventory PR](https://github.com/Gforce-Innovation-Kft/gforce-google-cloud/pull/7). diff --git a/skills-lock.json b/skills-lock.json index a019979..10bf414 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -1,59 +1,65 @@ { - "version": 1, + "version": 2, "skills": { "code-review": { "source": "mattpocock/skills", "sourceType": "github", "skillPath": "skills/engineering/code-review/SKILL.md", - "computedHash": "4a17d9d3e0fc87ae48544d371a820fac5a4a78f4c05e7e6b3229094fbf8a7e26" + "computedHash": "2829c0c38fef6b3a3130912236a2a9fa5f3d0f47ee824014e164d8376690393a" }, "dx-org-manage": { "source": "forcedotcom/sf-skills", "sourceType": "github", "skillPath": "skills/dx-org-manage/SKILL.md", - "computedHash": "d2b1dffe6376a3d1f04492d7880e15664c86a21ab5d01a13da49ecadbd5cf954" + "computedHash": "172364d42f46fd2ab149b3cd1f8ad42882ae0adc3aa9821ca2d20554f785861d" }, "dx-org-permission-set-assign": { "source": "forcedotcom/sf-skills", "sourceType": "github", "skillPath": "skills/dx-org-permission-set-assign/SKILL.md", - "computedHash": "b1d3727d4352e05d1e02ad8be5d5db0ac4fb688c605eea8d7d76c9acb8754d9d" + "computedHash": "408b52deb56f683b451f930ba460008170e03e066f7485d57f1513d9139bffb6" }, "dx-pkg-post-install-configure": { "source": "forcedotcom/sf-skills", "sourceType": "github", "skillPath": "skills/dx-pkg-post-install-configure/SKILL.md", - "computedHash": "30dd5bea1d8ed3a73c319ed04b766f208629dd7a8cd788b66302352495895792" + "computedHash": "ded5280e8495c3943ec206a1440b98be6fdec09f1f5607e2bcfdee832ea84c99" }, "gforce-github-actions": { "source": "Gforce-Innovation-Kft/gforce-ai", "sourceType": "github", "skillPath": "skills/gforce-github-actions/SKILL.md", - "computedHash": "ae7dfd1e8fcb6393c932b682b6d8dad9e392bbc364d70a8aaa39920fa09a94f5" + "computedHash": "7b08e4945ce8423d8b839e21bcb1190057a0b168d524f8cb15b9a76b98266b92" + }, + "gforce-google-cloud": { + "source": "Gforce-Innovation-Kft/gforce-ai", + "sourceType": "github", + "skillPath": "skills/gforce-google-cloud/SKILL.md", + "computedHash": "15c8395eba34ff1e9d0aa42ea2f1de50b60a0e148f0324f91c23da33576e936b" }, "github-actions-docs": { "source": "xixu-me/skills", "sourceType": "github", "skillPath": "skills/github-actions-docs/SKILL.md", - "computedHash": "e9ac24e79715ef3060bd4ae376052f8d863b830604e69cb513b39764bf47ee53" + "computedHash": "ec59228f9aa91ea81587cdb4d135d7bdd007ec406ecf9c4a34f6fa8409bac201" }, "github-actions-templates": { "source": "wshobson/agents", "sourceType": "github", "skillPath": "plugins/cicd-automation/skills/github-actions-templates/SKILL.md", - "computedHash": "93e92fd674dea084628e95f9c48af806f9672ac08f815c84f85adedb55742c59" + "computedHash": "a64693f40a4fbc1177efeb42b5f2a406c74d68bb40488a5bcaafd1537bbd0225" }, "receiving-code-review": { "source": "obra/superpowers", "sourceType": "github", "skillPath": "skills/receiving-code-review/SKILL.md", - "computedHash": "3f56080356c62e4f74a183d7371686babc661e41a952dc67a97c2c76fe8a9329" + "computedHash": "bf074c0ec315a619a8dc02fe9b94c4b47313fe0c1a41a666d44b414f30db6a86" }, "requesting-code-review": { "source": "obra/superpowers", "sourceType": "github", "skillPath": "skills/requesting-code-review/SKILL.md", - "computedHash": "cde520e9118d7e6b74b5ab0123cff2f68d9c07bddb3f82c997753b6126600aed" + "computedHash": "50740895d28e7f549f0111d83e4d1aded60658fdca14e883f118fc6af12ab315" } } }