CI Secrets Checklist
The Gotcha: Silent Empty Strings
When a GitHub Actions workflow references a secret via ${{ secrets.SECRET_NAME }}, GitHub Actions does not error if the secret is missing. Instead, it silently returns an empty string. This is by design — secrets are treated as variable expansions, not as config validation.
Impact: Code that depends on a secret will receive an empty string instead of a real value. Unless your code explicitly checks for this and fails loudly, the workflow continues and the error surfaces later:
- A service fails to authenticate (empty API key)
- A test doesn't run (empty test email)
- A deployment step uses the wrong configuration (empty URL, defaults to localhost)
Example: OBJ-519 (May 14, 2026)
PR #828 blocked CI for ~14 hours because SMOKE_CLERK_EMAIL was missing from GitHub Actions secrets. The smoke test workflow referenced it on line 78 of playwright-smoke.yml:
env:
SMOKE_CLERK_EMAIL: ${{ secrets.SMOKE_CLERK_EMAIL }}The auth.staging.setup.ts setup project checked for it and threw:
Error: SMOKE_CLERK_EMAIL is required for the staging setup project (a +clerk_test staging Clerk user)But this error only surfaced when the first real PR ran the workflow. The problem had existed since Phase 91 (which introduced the reference) and was silently returning an empty string on every sandbox/CI validation run. GitHub Actions' actionlint linter passed (it checks syntax, not secret existence). The staging.yml and production.yml workflows never ran, so no Docker image was built. When the PR merged and playwright-smoke.yml fired for the first time, the missing secret caused a complete CI failure.
Root cause: The acceptance criteria for Phase 91 did not include "confirm secret exists AND smoke workflow passes on a live CI run." The planner had marked SMOKE_CLERK_EMAIL as [ASSUMED] — Planner must confirm this is provisioned in the phase notes, but the assumption was never verified.
The Rule
Any new secret-dependent workflow step needs an acceptance criterion:
"Confirm secret
Xis present in Settings → Secrets and variables → Actions and a live CI run (PR or tag push) passes."
This rules out:
- ❌ Workflows that are validated only with
actionlintor other linters (they check syntax, not existence) - ❌ Workflows that are validated only on scheduled/manual dispatch with no real PR runs
- ❌ Workflows that have an
[ASSUMED]note in planning docs that was never verified
Responsible party: The developer/planner who wires the secret into the workflow is responsible for confirming it exists in the repo settings and for ensuring a live CI run validates it.
Current Required Secrets (as of v4.8.4)
Smoke Tests
Required for playwright-smoke.yml (both staging and production smoke runs):
SMOKE_CLERK_EMAIL— A+clerk_teststaging Clerk user email. Format:anything+clerk_test@example.com. Used byionic_frontend/tests/smoke-playwright/setup/auth.staging.setup.tsto sign in and establish session state for test runs. Required.SMOKE_FALLBACK_EMAIL— A fallback test email for certain test suites. Used by smoke specs that require multiple user accounts. Required (though may be the same address asSMOKE_CLERK_EMAILin simple setups).
Setup:
- Create a
+clerk_testtest user in the staging Clerk instance (or re-use an existing one) - Add
SMOKE_CLERK_EMAIL(e.g.,test+clerk_test@objectuve.com) to GitHub Actions secrets - Add
SMOKE_FALLBACK_EMAIL(e.g.,test-fallback+clerk_test@objectuve.com) to GitHub Actions secrets - Verify by running the
Playwright Smoke Testsworkflow manually or opening a PR
Production Deployment
Required for production.yml:
SENTRY_DSN_API— Sentry DSN for backend error tracking. Format:https://key@sentry-instance/id. Used by Rails API to report exceptions. Required.LITELLM_URL_PRODUCTION— LiteLLM proxy URL for production. Format:https://litellm.internal/.... Used by Agent Runner to route LLM calls. Required.GCP_SERVICE_ACCOUNT— GCP service account JSON (for infrastructure deployments). Required for deploying to GCP. Passed togoogle-github-actions/auth.GCP_WORKLOAD_IDENTITY_PROVIDER— GCP Workload Identity Federation provider. Required for OIDC-based auth to GCP. Passed togoogle-github-actions/auth.
Staging Deployment
Required for staging.yml:
SENTRY_DSN_API— Sentry DSN for backend error tracking (same DSN may be shared with production or a separate staging instance). Required.LITELLM_URL_STAGING— LiteLLM proxy URL for staging. Format:https://litellm-staging.internal/.... Used by staging Agent Runner to route LLM calls. Required.GCP_SERVICE_ACCOUNT— Same GCP service account used by production; required for staging Cloud Run deployments.GCP_WORKLOAD_IDENTITY_PROVIDER— Same Workload Identity provider; required for OIDC-based GCP auth on staging.
Sentry Release Tracking (staging.yml, production.yml, admin.yml)
Used by scripts/sentry-create-release.sh to create a Sentry release + deploy marker for client/api/admin after each successful deploy. Fail-soft — a missing token or repo Variable emits a ::warning:: and never fails the deploy. See OBJ-2099.
SENTRY_AUTH_TOKEN— Sentry auth token (org-scoped,project:read+project:releases+event:read). Secret. Not yet provisioned — until it is, the release/deploy-marker steps no-op with a warning.SENTRY_ORG— Sentry organization slug (objectuve). Repo Variable. Set.SENTRY_PROJECT_CLIENT— Sentry project slug for the ionic frontend (objectuve-client). Repo Variable. Set.SENTRY_PROJECT_API— Sentry project slug for the Rails API (objectuve-api). Repo Variable. Set.SENTRY_PROJECT_ADMIN— Sentry project slug for the admin dashboard (objectuve-admin). Repo Variable. Not yet provisioned — until it is, admin's release/deploy-marker step no-ops with a warning (client/API are unaffected, since each project is independently skippable).
Release Health Watch (staging.yml)
SENTRY_ISSUES_API_TOKEN— Sentry API token, distinct credential fromSENTRY_AUTH_TOKENabove (needsevent:read, notproject:releases). Used by therelease-health-watchjob'sscripts/check-release-health.mjsto poll for a new, unresolved, release-attributed Sentry error in thedeploy+0 → deploy+RELEASE_HEALTH_WINDOW_MINUTESwindow after a staging deploy (OBJ-2264/OBJ-2267 — see deployment.md § Release-health watch coverage window for the coverage-window contract). Provisioned — the job previously fail-softed on every run with::warning::SENTRY_ISSUES_API_TOKEN not provisioned(staging.yml:1344-1345); live coverage is unblocked as of this secret's provisioning (OBJ-3593).
Mobile CI (Optional)
Required for mobile-android.yml and mobile-ios.yml (manual dispatch only):
ANDROID_KEYSTORE_BASE64— Android keystore encoded as base64ANDROID_KEY_ALIAS— Keystore key aliasANDROID_KEY_PASSWORD— Keystore key passwordANDROID_KEYSTORE_PASSWORD— Keystore passwordGOOGLE_PLAY_JSON_KEY_BASE64— Google Play service account key, base64-encoded. Used bymobile-android.ymlfor Android release publishing to the Play Store. Required for Android distribution builds.APPLE_ID— Apple ID for App Store ConnectASC_KEY_ID— App Store Connect key IDASC_ISSUER_ID— App Store Connect issuer IDASC_KEY_CONTENT— App Store Connect private key (base64)MATCH_GIT_URL— Fastlane Match Git repositoryMATCH_PASSWORD— Fastlane Match passwordMATCH_GIT_PRIVATE_KEY— SSH private key for Fastlane Match Git repo access. Used bymobile-ios.ymlfor iOS code signing certificate/profile management. Required for iOS distribution builds.APPLE_TEAM_ID— Apple Developer Team IDGOOGLE_IOS_CLIENT_ID— iOS OAuth client ID from Google Cloud Console (must be the iOS client type, not Web or Android). Resolvesionic_frontend/ios/google-signin.xcconfig'sGOOGLE_IOS_CLIENT_ID_OVERRIDE, which feedsInfo.plist'sGIDClientID. Not yet provisioned — until it is,mobile-ios.yml's pre-flight step fails anytestflight/appstoredispatch before archive/upload, naming this secret. Missing it is what shipped native Google Sign-In 100% broken in production from 2026-08-16 (SentryOBJECTUVE-CLIENT-C2) with no CI signal, because the override had an empty default and nothing ever set it — see gotchas.md § An empty xcconfig*_DEFAULTwith an unwired*_OVERRIDEships silently broken to every real build. Required for iOS distribution builds. (OBJ-2772)GOOGLE_IOS_REVERSED_CLIENT_ID— The same iOS OAuth client's reversed-ID form, used as theCFBundleURLSchemesentry (Info.plist:101) that the Google Sign-In SDK's OAuth return path calls back into. Resolvesgoogle-signin.xcconfig'sGOOGLE_IOS_REVERSED_CLIENT_ID_OVERRIDE. Same provisioning source and same missing-secret gate asGOOGLE_IOS_CLIENT_IDabove — both or neither. Not yet provisioned. Required for iOS distribution builds. (OBJ-2772)
(Only required if you're building and distributing mobile apps via Fastlane.)
Smoke Test Authentication (Optional)
Read by playwright-smoke.yml's Run Playwright Smoke step, rooted at ionic_frontend/tests/smoke-playwright/:
ACHIEVEMENTS_EMPTY_EMAIL— Email for a fallback-auth account (smoke-achievements-empty@objectuve.com) seeded with zero unlocked achievements. Signs in via aFallback <FALLBACK_AUTH_SECRET>:<email>token (helpers/env.ts#buildFallbackToken), not Clerk. Used byspecs/gamification/11-achievements-empty.spec.ts(and the overlapping legacyspecs/gamification/achievements.spec.ts) to assert the empty-state hero, "Begin here" card, and Inner Drawer row. Optional — each spec doestest.skip(!email, ...), so an absent value skips rather than fails.ACHIEVEMENTS_POPULATED_EMAIL— Same fallback-auth pattern as above, for an account (smoke-achievements-populated@objectuve.com) seeded with unlocked achievements and active goal progress. Used byspecs/gamification/12-achievements-populated.spec.ts(and the same legacyachievements.spec.ts) to assert the populated hero, "Next on the shelf" card, and achievements grid. Optional, same skip behavior.CLERK_SECRET_KEY— Clerk backend secret key, used by smoke setup to create test users on-the-fly if they don't exist. Should match the staging Clerk instance's backend secret. Optional for production smoke runs (fall back toSMOKE_FALLBACK_EMAIL); required for staging smoke setups.CLERK_SECRET_KEY_PROD— Same role asCLERK_SECRET_KEYabove, scoped to the production Clerk instance.playwright-smoke.ymlselects between the two bysmoke_env:secrets.CLERK_SECRET_KEY_PRODwhen the run targets production,secrets.CLERK_SECRET_KEYotherwise (playwright-smoke.yml:410).
GitHub & Third-Party Integration
GITHUB_TOKEN— GitHub Actions default token (auto-provided, no manual setup needed). Read access for checkout, write access for PR comments.GCP_SERVICE_ACCOUNT— GCP service account JSON (for infrastructure deployments). Required if deploying to GCP. Passed togoogle-github-actions/authinproduction.ymlandstaging.yml.GCP_WORKLOAD_IDENTITY_PROVIDER— GCP Workload Identity Federation provider. Required for OIDC-based auth to GCP. Passed togoogle-github-actions/auth.
(These are infrastructure-level and typically set once per repository. Check GitHub's Actions secrets UI for current values.)
Load Tests (load-test.yml, monthly cron)
RAILS_WEBHOOK_SECRET— HMAC-SHA256 signing secret Agent Runner uses to sign its result callback toPOST /webhooks/ai-workforce(agent_runner/src/webhook/sender.ts,AUTH_MODE=secret). The L4 script (load_tests/l4_ai_workforce_webhook.js) exercises this same signed-webhook path directly. Required — the workflow's own "Verify required secrets" step (load-test.yml:55-69) fails the job fast with a named error if it's absent, rather than letting the L4 step fail later with a less specific one.
Scheduled Release Train (scheduled-release.yml)
SLACK_BUDGET_ALERT_WEBHOOK_URL— Slack incoming webhook URL, reused from the same secret backing production/staging Rails alerts. Used by the "Alert — dead or misconfigured release train" step to notify on a dormant (missingRELEASE_TOKEN) or failed (invalidRELEASE_TOKEN) run (OBJ-1349). Fail-soft — an absent value just skips the alert with a::warning::, never fails the workflow. See multica-github-auth.md § Diagnosing a dead release train for the incident this closed — that doc's own "Nobody was notified" note still says this secret is unprovisioned and the alert is a no-op, which is now stale (flagged separately, not fixed here — out of this task's file scope).
How to Add a New Secret
When adding a new secret to a workflow:
Add the secret to GCP Secret Manager or another secret provider:
bashecho "value" | gcloud secrets create my-secret-name --data-file=-Add it to GitHub Actions secrets:
- Go to Settings → Secrets and variables → Actions
- Click New repository secret
- Name:
MY_SECRET_NAME(SCREAMING_SNAKE_CASE) - Value: the secret value
- Click Add secret
Reference it in the workflow:
yamlenv: MY_ENV_VAR: ${{ secrets.MY_SECRET_NAME }}Create a test/verification step:
- If the secret is used by a test, add a check:
if [ -z "$MY_ENV_VAR" ]; then echo "ERROR: MY_SECRET_NAME not set"; exit 1; fi - If the secret is used by a service deploy, test the deployment on staging first
- Document the secret in this file (so future contributors know about it)
- If the secret is used by a test, add a check:
Verify on CI:
- Open a PR or run the workflow manually
- Confirm the workflow passes and the secret is used correctly
- Do NOT merge until verified
Checklist: Before Deploying a Workflow Change
- [ ] All new secrets are present in GitHub Actions Settings → Secrets and variables → Actions
- [ ] Workflow references are spelled correctly (exact match to secret name, case-sensitive)
- [ ] At least one live CI run (PR or tag push) has passed with the secrets in place
- [ ] The code that uses the secret has an explicit check (e.g.,
if (!process.env.VARIABLE) throw new Error(...)) - [ ] This file has been updated with the new secret name and purpose (so future contributors know)
Additional References
- Deployment pipeline details: See deployment.md — includes production/staging safeguards, release process, rollback procedures
- GitHub Actions workflows: See
.github/workflows/for full workflow definitions - Related issue (OBJ-519): Example of a 14-hour CI block due to missing
SMOKE_CLERK_EMAIL. Now preventable with explicit live-run verification.
Last updated: 2026-09-14