From b172e721be6dd57d12d2a03dbbcf31df41eea6aa Mon Sep 17 00:00:00 2001 From: Stuart Date: Mon, 5 Oct 2026 15:20:26 +1100 Subject: [PATCH 1/2] fix(ci): request a role that grants storage.buckets.get, and allow a manual retry storage.objectAdmin was documented as sufficient for the source bucket. It is not: it grants object permissions only and contains no storage.buckets.* at all, while the deploy reads the bucket's metadata before it writes anything. Verified against the live IAM role definitions - objectAdmin has 32 permissions, none of them buckets.get, and the workflow still fails on exactly that after the grant. legacyBucketReader supplies storage.buckets.get, so the pair is the narrow fix; storage.admin (which has buckets.get and buckets.list) covers both in one. Also adds workflow_dispatch. A deploy can fail for reasons outside the code - a missing grant, a Cloud Build outage - and until now the only way to retry was to push a no-op commit to main, which is a bad thing to teach on the branch that deploys production. --- .github/workflows/deploy-runner.yml | 4 ++++ DEPLOYMENT.md | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy-runner.yml b/.github/workflows/deploy-runner.yml index db86562..8c0efbf 100644 --- a/.github/workflows/deploy-runner.yml +++ b/.github/workflows/deploy-runner.yml @@ -10,6 +10,10 @@ on: branches: [main] paths: - "cloud-run/**" + # Deploys can fail for reasons outside the code (a missing IAM grant, a Cloud + # Build outage). Without this the only way to retry was to push a no-op + # commit, which is worse than useless on the branch that deploys production. + workflow_dispatch: # The deploy script needs `gcloud` (auth + setup-gcloud below). No write access # to this repository itself is required. diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index a7a8624..011b303 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -186,7 +186,10 @@ changes will deploy nothing: | `roles/iam.serviceAccountUser` | act as the runtime service account | | `roles/cloudbuild.builds.editor` | run the build | | `roles/artifactregistry.writer` | push the built image (else `artifactregistry.repositories.get` is denied) | - | `roles/storage.objectAdmin` | read/write the source bucket `run-sources--` (else `storage.buckets.get` is denied) | + | `roles/storage.objectAdmin` | read/write the objects in the source bucket `run-sources--` | + | `roles/storage.legacyBucketReader` | read the bucket's own metadata (else `storage.buckets.get` is denied) | + + `storage.objectAdmin` is **not** sufficient on its own: it grants object permissions only and contains no `storage.buckets.*` at all, while the deploy reads the bucket before it writes anything. Granting the pair scoped to the bucket is the narrow option; `roles/storage.admin` covers both if you would rather grant once. Scope the last two to the repository and bucket if you prefer, but a partial set fails at deploy time with a message naming only the next missing From 1412bc27229ecd777e4d27b3e187765407bce300 Mon Sep 17 00:00:00 2001 From: Stuart Date: Mon, 5 Oct 2026 15:32:22 +1100 Subject: [PATCH 2/2] docs: storage.admin is the grant that works, not a pair of narrower roles Recommendation three kept being wrong because I reasoned from role names instead of the permission lists. Measured: objectAdmin has 32 permissions, none of them storage.buckets.*; legacyBucketReader supplies buckets.get but not buckets.list; a source deploy needs buckets.get, buckets.list and objects.create. Only storage.admin supplies all three, so the failures arrive one at a time and each one looks like the previous guess was simply the wrong role. --- DEPLOYMENT.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 011b303..3b9490a 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -186,14 +186,16 @@ changes will deploy nothing: | `roles/iam.serviceAccountUser` | act as the runtime service account | | `roles/cloudbuild.builds.editor` | run the build | | `roles/artifactregistry.writer` | push the built image (else `artifactregistry.repositories.get` is denied) | - | `roles/storage.objectAdmin` | read/write the objects in the source bucket `run-sources--` | - | `roles/storage.legacyBucketReader` | read the bucket's own metadata (else `storage.buckets.get` is denied) | - - `storage.objectAdmin` is **not** sufficient on its own: it grants object permissions only and contains no `storage.buckets.*` at all, while the deploy reads the bucket before it writes anything. Granting the pair scoped to the bucket is the narrow option; `roles/storage.admin` covers both if you would rather grant once. - - Scope the last two to the repository and bucket if you prefer, but a partial - set fails at deploy time with a message naming only the next missing - permission — which is how this list was corrected twice. + | `roles/storage.admin` | the source bucket: read its metadata (`storage.buckets.get`, `storage.buckets.list`) and write the objects in it | + + `roles/storage.admin` is the one to grant, and the narrower storage roles do + **not** compose into a substitute. Measured against the live role definitions: + `storage.objectAdmin` has 32 permissions and not one is `storage.buckets.*`; + `storage.legacyBucketReader` supplies `storage.buckets.get` but not + `storage.buckets.list`. A source deploy needs `buckets.get`, `buckets.list` + and `objects.create`, and only `storage.admin` supplies all three — so the + failures arrive one permission at a time and each looks like the last one + being wrong. 2. In **Settings → Secrets and variables → Actions**, create a repository secret named `CCC_FFAI_RUNNER_SA_KEY_JSON` whose value is the key's full JSON (including the surrounding `{ }`).