When to use
When you need to operate, update, troubleshoot, or recover the safe-settings service on Cloud Run.
Preconditions
- Access to the
eigenoid/iac-platformrepo (for infrastructure changes). gcloudCLI with permissions on theeigenoid-prdproject.- Access to the GitHub App configuration (only for key rotation).
Overview
safe-settings runs as a container on Cloud Run in the eigenoid-prd project. All infrastructure is managed as Terraform in iac-platform (see ADR-0013).
| Field | Value |
|---|---|
| Service | eigenoid-safe-settings |
| Project | eigenoid-prd |
| Region | europe-west1 |
| Port | 3000 |
| Min instances | 1 |
| Max instances | 3 |
| IMAGE_TAG | 2.1.20-rc.3 |
| CRON | 0 0 */6 * * * |
| Cost | ~$5-10/month |
Procedure: Image updates
To update the safe-settings version:
1. Check for a new version
gh api repos/github/safe-settings/releases/latest --jq '.tag_name'
2. Update IMAGE_TAG
cd iac-platform
git checkout -b chore/update-safe-settings-image
echo "NEW_VERSION" > safe-settings/IMAGE_TAG
git add safe-settings/IMAGE_TAG
git commit -m "chore: update safe-settings to NEW_VERSION"
git push origin chore/update-safe-settings-image
3. Open a PR
The Terraform plan will show the image change in Cloud Run. Verify that:
- Only the image changes (
google_cloud_run_v2_service.safe_settings[0]) - There are no unexpected changes to secrets or config
4. Merge
The merge triggers:
- Image mirroring: copies the image from GHCR to Artifact Registry.
- Terraform apply: updates the Cloud Run revision with the new image.
5. Verify
# Verify active revision
gcloud run services describe eigenoid-safe-settings \
--region europe-west1 --project eigenoid-prd \
--format="value(status.latestReadyRevisionName)"
# Verify logs from the new container
gcloud run services logs read eigenoid-safe-settings \
--region europe-west1 --project eigenoid-prd \
--limit 10
Procedure: Secret rotation
Rotate PRIVATE_KEY (most frequent)
-
Generate a new key on the App page → Private keys → Generate a private key.
-
Create a new version in Secret Manager:
bashgcloud secrets versions add safe-settings-private-key \--data-file=/path/to/new.pem \--project=eigenoid-prd -
Re-deploy Cloud Run to pick up the new version:
bashgcloud run services update eigenoid-safe-settings \--region europe-west1 --project eigenoid-prd \--update-secrets "PRIVATE_KEY=safe-settings-private-key:latest" -
Verify: push a change to the admin repo and confirm the sync works.
-
Delete the old key on the App page → Private keys → Delete.
The private key must be raw PEM (starts with -----BEGIN RSA PRIVATE KEY-----). Probot rejects base64.
-
Update the secret in
platform-settings(used by governance workflows):bashgh secret set SETTINGS_BOT_PRIVATE_KEY \--repo eigenoid/platform-settings \< /path/to/new.pem
Rotate WEBHOOK_SECRET
-
Generate a new secret:
openssl rand -hex 32 -
Update in GitHub App webhook config: App settings → Webhook → Webhook secret
-
Update in Secret Manager:
bashecho -n "NEW_SECRET" | gcloud secrets versions add safe-settings-webhook-secret \--data-file=- --project=eigenoid-prd -
Re-deploy Cloud Run (same command as above, changing the secret name)
Rotate APP_ID
Only needed if the GitHub App is recreated. Update in Secret Manager and re-deploy.
Procedure: Webhook configuration
The GitHub App webhook must point to:
| Field | Value |
|---|---|
| URL | https://eigenoid-safe-settings-{hash}.europe-west1.run.app/api/github/webhooks |
| Content type | application/json |
| Secret | (value from Secret Manager) |
| Active | Yes |
The path must be /api/github/webhooks, not root /. If the path is wrong, safe-settings will not receive events.
Enabled events (9)
push, repository, repository_ruleset, branch_protection_rule, pull_request, check_run, check_suite, member, team
Troubleshooting
Cloud Run is not responding
- Check logs:
bashgcloud run services logs read eigenoid-safe-settings \--region europe-west1 --project eigenoid-prd --limit 50
- Check instances: if min-instances=0 (error), webhooks fail due to cold start (GitHub's 10s timeout).
- Check IAM:
invoker_iam_disabledmust betrue(the org policy blocks theallUsersbinding).
Webhook is not arriving
- Check delivery logs: App settings → Advanced → Recent Deliveries.
- Check URL: must end in
/api/github/webhooks. - Check active: the webhook must be enabled.
- Check events: all 9 listed events must be selected.
Settings are not being applied
- Check CRON: automatic sync runs every 6 hours. To force a sync, push a change to a file in
.github/of the admin repo. - Check config syntax: a YAML error in
settings.ymlorrepos/*.ymlcan silently prevent application. - Check repo permissions: safe-settings needs
Administration: Read & Writeon each repo.
Image mirroring fails
- Check the mirror SA: the mirroring SA needs
roles/artifactregistry.writer. - Check the WIF binding: the SA must be able to authenticate via WIF.
- Check the digest: on Apple Silicon,
docker pull --platform linux/amd64may pull arm64. Use an explicit sha256 digest.
Error: "A JSON web token could not be decoded" (401)
The private key is invalid or expired. Follow the PRIVATE_KEY rotation procedure above.
Emergency procedures
Image rollback
# List available revisions
gcloud run revisions list --service=eigenoid-safe-settings \
--region europe-west1 --project eigenoid-prd
# Route traffic to a previous revision
gcloud run services update-traffic eigenoid-safe-settings \
--region europe-west1 --project eigenoid-prd \
--to-revisions=REVISION_NAME=100
Disable webhook (emergency)
On the App page → Webhook → uncheck "Active". This stops all events — safe-settings will not apply config until reactivated.
Force a manual sync
cd platform-settings
echo "# sync trigger $(date)" >> .github/settings.yml
git commit -am "fix: force config sync" && git push
Verification
- Logs show a successful sync after a push to the admin repo.
- A manual change to a ruleset is reverted within seconds (drift prevention).
- CRON sync runs every 6 hours (verify in logs).
- Dry-run check appears on PRs to the admin repo.
Escalation
| Situation | Action |
|---|---|
| Cloud Run not serving | Check logs, attempt rollback. Contact @shoootyou. |
| Webhook broken | Check URL, secret, events. If unresolved, temporarily disable the webhook. |
| Config corruption | Revert the commit in platform-settings and force a sync. |