Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions .agents/skills/gforce-google-cloud/SKILL.md
Original file line number Diff line number Diff line change
@@ -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 `<workload>-<component>`, secret
`<system>-<purpose>`, 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/<image>`.

## 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/<repo>`) | 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=<root>.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`.
31 changes: 31 additions & 0 deletions .agents/skills/gforce-google-cloud/overrides.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
1 change: 1 addition & 0 deletions .claude/skills/gforce-google-cloud
29 changes: 20 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -124,7 +134,7 @@ Rationale: [`docs/architecture.md`](docs/architecture.md).
<!-- skills-tooling -->
## 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
Expand All @@ -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. |
Expand Down
7 changes: 4 additions & 3 deletions docs/claude-actions-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)

Expand Down
51 changes: 51 additions & 0 deletions docs/gcloud-migration.md
Original file line number Diff line number Diff line change
@@ -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).
Loading
Loading