GitHub Environments Runbook
GitHub Environments Runbook
Section titled “GitHub Environments Runbook”Status: Stub (S4 close-out, kh-prod-readiness-S4). Full handover-grade expansion in Wave 5 (WP-G6.x). Owner: prod-readiness track. Audience: Liam + future client-onboarding operator. Pair with:
docs/audits/kh-production-readiness-phase-1/research/09-vercel-supabase-env-mapping-spec.md§11.5 (canonical environments decision rationale).
§1. Purpose
Section titled “§1. Purpose”Document the canonical GitHub Environments setup for the Knowledge Hub repository: which environments exist, what secrets/variables each carries, how GHA workflows reference them, and how to extend the pattern for future clients.
§2. Canonical environments (post-S408 repoint)
Section titled “§2. Canonical environments (post-S408 repoint)”REPOINTED to the platform (S408, verified live; ID-127.15). These two environments were previously mapped to the first-client Supabase refs (
rovrymhhffssilaftdwdprod /turayklvaunphgbgscatstaging). They are now repointed to the platform DBs — the platform is the canonical CI / dev / showcase tenant (NEXT_PUBLIC_CLIENT_ID=default), and a client’s CI/deploy credentials live in the client’s OWN Vercel/Supabase project, not in this shared public repo (see §8). The client refs below are RETIRED from these environments.
| Environment | Maps to Supabase | Maps to Vercel | Used by |
|---|---|---|---|
Production | Platform prod DB zjqbrdctesqvouboziae (CLIENT_ID=default) | Production target | Production-context GHA jobs (taxonomy-sync.yml, future ci.yml post-merge jobs) |
Staging | Platform staging branch rbwqewalexrzgxtvcqrh (CLIENT_ID=default) | Preview target | Staging-context GHA jobs (MCP eval Layer 4, staging E2E smoke) |
Naming convention: Capital-first letters (Production, Staging). This
matches GitHub’s auto-creation default. Critical: GitHub’s environment:
matching in workflows is case-sensitive per [GH-DOC]. A workflow saying
environment: production (lowercase) does NOT resolve to the Production
environment — it either fails or creates an unintended new environment. Always
match the exact case.
App-DB credentials vs Coolify credentials — do not conflate (S408). Each of
Production/Stagingcarries TWO unrelated credential families and they were a source of S408’s DB-config confusion: (a) the app-DB credentials (SUPABASE_URL/ keys /POSTGRES_PASSWORD) which now resolve to the platform refs above; and (b) the Coolify ingestion-pipeline deploy credentials (COOLIFY_API_TOKEN/COOLIFY_BASE_URL) which authenticate the on-prem pipeline deploy jobs. Per the two-server split + OQ-2 (no new GH environment — seespecs/id-127-platform-pipeline/AMENDMENT-staging-prod-two-server.md), these SAMEProduction/StagingCoolify creds are reused to deploy BOTH the client pipeline (Server A) and the platform pipeline (Server B); client vs platform — and platform staging vs prod — is distinguished by server + app UUID + deploy input, NOT by separate credentials or a separate environment. ASUPABASE_*value in these environments says nothing about WHICH Coolify server/app a given deploy job targets — read the deploy job’s UUID input, not the env name.
§3. Variables vs Secrets — typing rules
Section titled “§3. Variables vs Secrets — typing rules”GitHub distinguishes two storage types per environment:
| Type | Storage | Workflow access | Use for |
|---|---|---|---|
| Secret | Encrypted-at-rest, masked in logs, write-only after creation | ${{ secrets.X }} | Sensitive credentials (keys, passwords, HMAC tokens) |
| Variable | Plain-text, readable + editable | ${{ vars.X }} | Non-sensitive config (model names, public URLs, batch sizes, client ID) |
Decision rule: if leaking the value would harm someone or some system → secret. Otherwise → variable.
3.1 Secret list per environment (current state)
Section titled “3.1 Secret list per environment (current state)”Per Liam’s S4 setup (verified by
gh api repos/ai-solution-hub/canonical/environments); the SUPABASE_* values
were repointed to the platform DBs in S408 (see §2):
Production environment secrets:
SUPABASE_URL—https://zjqbrdctesqvouboziae.supabase.co(platform prod; was the first-client ref pre-S408)SUPABASE_PUBLISHABLE_KEY— platform prod publishable keySUPABASE_SERVICE_ROLE_KEY— platform prod service-role JWT (Sensitive)POSTGRES_PASSWORD— platform prod DB password (Sensitive)ANTHROPIC_API_KEY— prod Anthropic key (Sensitive; rotated post-19/04 breach)OPENAI_API_KEY— prod OpenAI key (Sensitive; rotated)FIRECRAWL_API_KEY— prod Firecrawl key (Sensitive; rotated)CRON_SECRET— prod cron auth secret (Sensitive; rotated)TAXONOMY_SYNC_CALLBACK_SECRET— prod HMAC secret (Sensitive)GITHUB_SYNC_TOKEN— fine-scoped GH PAT (Sensitive)SENTRY_AUTH_TOKEN— Sentry write token (Sensitive)NEXT_PUBLIC_SENTRY_DSN— Sentry DSNNEXT_PUBLIC_APP_URL—https://kh.aisolutionhub.co.uk(platform showcase domain; was the first-client custom domain pre-S408)NEXT_PUBLIC_CLIENT_ID—default(platform tenant; was a client id pre-S408)
Staging environment secrets:
SUPABASE_URL—https://rbwqewalexrzgxtvcqrh.supabase.co(platform staging branch; was the first-client staging ref pre-S408)SUPABASE_PUBLISHABLE_KEY— platform staging publishable keySUPABASE_SERVICE_ROLE_KEY— platform staging service-role JWT (Sensitive)POSTGRES_PASSWORD— platform staging DB password (Sensitive)ANTHROPIC_API_KEY— staging-specific Anthropic key (Sensitive)OPENAI_API_KEY— staging-specific OpenAI key (Sensitive)FIRECRAWL_API_KEY— staging-specific Firecrawl key (Sensitive)CRON_SECRET— staging cron secret (Sensitive)TAXONOMY_SYNC_CALLBACK_SECRET— staging HMAC (Sensitive; can match prod or be unique)SENTRY_AUTH_TOKEN— same as Production (Sensitive)NEXT_PUBLIC_SENTRY_DSN— same as ProductionNEXT_PUBLIC_APP_URL— platform staging branch URLNEXT_PUBLIC_CLIENT_ID—default(platform tenant; was a client id pre-S408)TEST_USER_1_EMAIL,TEST_USER_2_EMAIL,TEST_USER_3_EMAILTEST_USER_1_PASSWORD,TEST_USER_2_PASSWORD,TEST_USER_3_PASSWORD(all Sensitive)
Critical: GITHUB_SYNC_TOKEN should NOT be set on Staging — keeps a staging
instance from dispatching workflows that mutate main (per WP-ENV.1 spec §4.6).
3.2 Variable list (recommended migration from secret-typed)
Section titled “3.2 Variable list (recommended migration from secret-typed)”These are non-sensitive config; ideally typed as variables (not secrets) for clarity + readable workflow logs. Liam may have set them as secrets initially — that works, just sub-optimal:
AI_SUMMARY_MODEL(e.g.claude-sonnet-4-6)AI_EMBEDDING_MODEL(e.g.text-embedding-3-large)AI_EMBEDDING_DIMS(e.g.1024)CLASSIFICATION_BATCH_SIZE(e.g.25)SENTRY_ORG,SENTRY_PROJECT
Migration path: delete the secret + re-add as variable via GitHub Settings →
Environments → Production / Staging → Variables. Workflow YAML reference changes
from ${{ secrets.X }} to ${{ vars.X }}. This is a follow-on cleanup, not a
blocker.
3.3 GH variables vs secrets — silent failure mode
Section titled “3.3 GH variables vs secrets — silent failure mode”GitHub’s ${{ secrets.X }} and ${{ vars.X }} are NOT interchangeable. If a
value lives as a var but the workflow reads it as secrets.X, the
substitution returns an empty string with no warning. The downstream
consumer (e.g. Zod env validation at module load) sees X="" and fails with a
cryptic error far from the substitution site. Reference:
feedback_gha_secrets_vs_vars_silent_empty.
The current ci.yml block carries TEST_USER_EMAIL as ${{ vars.X }} and
TEST_USER_PASSWORD as ${{ secrets.X }} — emails are non-sensitive (var),
passwords are credentials (secret). Mismatching the typing on either side
breaks the auth-session.ts sign-in path silently.
3.4 SUPABASE_ACCESS_TOKEN — storage matrix
Section titled “3.4 SUPABASE_ACCESS_TOKEN — storage matrix”SUPABASE_ACCESS_TOKEN is a Supabase Management API PAT (personal access
token). It authenticates supabase CLI commands (supabase link,
supabase db push, supabase gen types, supabase migration list) and the
Supabase Management API (api.supabase.com/v1/...). Storage discipline post
kh-prod-readiness-S23:
| Location | State |
|---|---|
.env.local (developer machine) | Required. Liam’s PAT for local CLI work + sandbox-bypass scripts. Never commit; in .gitignore. |
GitHub Actions — Production environment | Required. Used by supabase-advisors.yml (Management API advisor lint). Set via gh secret set --env Production. |
GitHub Actions — Staging environment | Removed S23. Was duplicated for symmetry; revoked once supabase-advisors.yml was confirmed Production-scope only. |
| GitHub Actions — repo-level secrets | NEVER. Repo-level secrets are accessible to ALL workflow jobs regardless of environment: declaration. |
| Vercel project env vars (any environment) | NEVER. Vercel runtime never invokes the Supabase Management API; the runtime uses anon/service-role JWTs not PATs. |
Why Production-only on GH: the only CI consumer is the Supabase advisor lint workflow, which reads live PROD findings (security + performance). There is no Staging-side equivalent — staging branches don’t carry independent advisor outputs.
Reference: reference_supabase_access_token_storage.
§4. Workflow integration
Section titled “§4. Workflow integration”4.1 Production-context job
Section titled “4.1 Production-context job”jobs: my-job: runs-on: ubuntu-latest environment: Production # exact case-match required steps: - run: echo "$SECRET" env: SECRET: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} # resolves from Production scopeThe environment: Production line at the job level is what scopes all
${{ secrets.X }} references to the Production environment. Without it, secrets
resolve to repo-level scope (which Liam has emptied per the S4 cleanup) — the
workflow will see undefined.
4.2 Staging-context job
Section titled “4.2 Staging-context job”jobs: my-staging-job: runs-on: ubuntu-latest environment: Staging steps: - run: bun run test:mcp-eval:fc # against staging branch env: SUPABASE_URL: ${{ secrets.SUPABASE_URL }} # staging branch URL SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}4.3 Mixed-context (rare)
Section titled “4.3 Mixed-context (rare)”A workflow can NOT scope different jobs to different environments in a way that
shares secrets across them. If you need both prod + staging in one workflow,
split into two jobs each with its own environment: declaration.
4.4 Repo-level secrets (Actions: none; Dependabot namespace: mirrored)
Section titled “4.4 Repo-level secrets (Actions: none; Dependabot namespace: mirrored)”Liam’s S4 cleanup removed all repo-level Actions secrets. Any future
addition should go to an environment scope, not back to repo-level. Repo-level
Actions secrets are accessible to ALL workflow jobs regardless of
environment: — defeats per-env isolation.
Exception (session 238 — Dependabot CI parity): the separate repo-level
Dependabot secret namespace (gh secret set --app dependabot) carries 10
staging-flavour secrets, and 13 staging vars are mirrored at repo level, so
Dependabot-triggered runs (which drop the env binding via the conditional
environment: expression) still resolve credentials. See ci.md §4.4. Also
PLATFORM_PROJECT_REF lives as a repo secret (workflow-level env in ci.yml).
Exception that warrants repo-level: if a secret is genuinely identical and used by every environment (e.g. a Slack webhook for the team’s CI alerts). In that case repo-level is acceptable. Default to env-scoped.
§5. Current workflow status
Section titled “§5. Current workflow status”| Workflow | Environment(s) | Notes |
|---|---|---|
.github/workflows/ci.yml | Production + Staging | quality-precheck + quality-test (4-shard) on Production; e2e-smoke + mcp-build + mcp-eval-seed + mcp-eval (l1/l3/l4 matrix) + integration + supabase-types-parity on Staging; detect-changes / type-drift-parity / build-gate / ci-summary carry no env. Every environment: is conditional — Dependabot runs drop the binding (§4.4 + ci.md §4.4). See docs/runbooks/ci.md §2 for full topology. |
.github/workflows/taxonomy-sync.yml | Production | Uses Production-scoped SUPABASE_PUBLISHABLE_KEY + SUPABASE_SERVICE_ROLE_KEY + TAXONOMY_SYNC_CALLBACK_SECRET HMAC. |
.github/workflows/supabase-advisors.yml | Production | Reads live advisor findings via SUPABASE_ACCESS_TOKEN + PLATFORM_PROJECT_REF (see §3.4). Nightly cron + push (main/staging) + dispatch; baseline lives in the private docs-site (ops/supabase-advisor-baseline.json). |
.github/workflows/staging-reference-refresh.yml | Staging | Refreshes the FK-safe reference/lookup tables (currently 6 — see scripts/staging-reference-refresh.sh) from prod to staging (WP-CI.RES.7). Fortnightly cron + dispatch. |
.github/workflows/schema-parity.yml | Production + Staging | Cross-branch schema parity probe. |
.github/workflows/migration-revoke-guard.yml (removed) | — | Retired — the standalone REVOKE-guard workflow no longer exists; function-ACL posture is carried by the ID-115 forward migration + advisor lint. |
.github/workflows/cloud-run-deploy.yml (removed S298) | — | Retired — Cloud Run fully decommissioned; workflow deleted. The ingestion pipeline now deploys on-prem (IONOS VPS + Coolify) via .github/workflows/onprem-deploy.yml. See docs/runbooks/onprem-b1-deploy.md. |
Other side workflows (not env-scoped or self-explanatory): onprem-deploy.yml
(pipeline deploy — Coolify creds in Production/Staging),
compose-parity.yml, token-parity.yml,
identity-guard.yml, api-view-coverage.yml, docs-dispatch.yml, and the
nightly lanes (e2e-nightly, integration-nightly, eval-nightly,
pytest-nightly, cocoindex-nightly). (task-view-vendor-drift.yml retired
with the ID-165 ordna cutover.)
§6. Extending — adding a new environment / secret
Section titled “§6. Extending — adding a new environment / secret”6.1 Adding a new secret to an existing environment
Section titled “6.1 Adding a new secret to an existing environment”gh secret set SECRET_NAME --env Production --body "<value>"# Or via UI: Settings → Environments → Production → Secrets → Add secret6.2 Adding a new environment
Section titled “6.2 Adding a new environment”Discouraged unless you have a concrete need beyond Production + Staging. The
current 2-env setup is sufficient for the canonical multi-tenant pattern.
If genuinely needed (e.g. a Preview-PR env for ephemeral PR-scoped deploys via
the Supabase preview-branch integration):
gh api -X PUT repos/ai-solution-hub/canonical/environments/Preview-PR \ --silent# Then add secrets via the UI or `gh secret set --env Preview-PR ...`6.3 Renaming an environment
Section titled “6.3 Renaming an environment”GitHub does NOT support rename in place. Workflow:
- Create the new-name environment + populate secrets.
- Update every
environment: <old-name>reference in workflow YAML to the new name. - Delete the old-name environment.
§7. Cleanup of legacy non-deterministic environments
Section titled “§7. Cleanup of legacy non-deterministic environments”GitHub auto-created several environments before the S4 cleanup:
PreviewPreview – knowledge-hubPreview – knowledge-hub-kpfProduction – knowledge-hubProduction – knowledge-hub-kpf
Per gh api (verified S4): these have no secrets attached. Safe to delete; they
are UI clutter.
Deletion command (Liam to run when ready):
for env in \ "Preview" \ "Preview – knowledge-hub" \ "Preview – knowledge-hub-kpf" \ "Production – knowledge-hub" \ "Production – knowledge-hub-kpf"; do gh api -X DELETE "repos/ai-solution-hub/canonical/environments/${env}" --silentdoneAfter deletion, gh api repos/ai-solution-hub/canonical/environments should
return 2 environments: Production and Staging.
Verify before delete:
for env in "Preview" "Preview – knowledge-hub" "Preview – knowledge-hub-kpf" "Production – knowledge-hub" "Production – knowledge-hub-kpf"; do echo "=== $env ===" gh api "repos/ai-solution-hub/canonical/environments/${env}/secrets" 2>&1 | head -5doneIf any environment shows secrets — DO NOT DELETE. Audit first.
§8. Multi-tenant fit (ratified ID-95 model — single public repo)
Section titled “§8. Multi-tenant fit (ratified ID-95 model — single public repo)”SUPERSEDED model removed (ID-127.15). This section previously described a per-client GitHub-REPO-fork model (“fork or clone the repo into a new repository for the client”, each fork carrying its own
Production/Stagingenvironments). That model was never built and is DEAD. The ratified ID-95 per-client topology is a SINGLE public application repo (ai-solution-hub/canonical, renamed fromknowledge-hub) plus a per-client Vercel project + Supabase project. Seespecs/id-95-per-client-topology/(PRODUCT/TECH) and/runbooks/client-app-deploy/for the operational standup.
The model is one public repo for everyone + one Supabase project per client +
one Vercel project per client. A client is added by adding a Vercel project and
a Supabase project — never a per-client repo, branch, or code path (ID-95 PI-1).
Naming stays generic: NO client prefix in env-var names (e.g. SUPABASE_URL,
NOT <CLIENT>_SUPABASE_URL).
Where per-client config lives — NOT in this repo’s GH environments:
- Build/runtime config (
NEXT_PUBLIC_CLIENT_ID, the client Supabase URL + keys,NEXT_PUBLIC_APP_URL) lives in the client’s OWN Vercel project env, set per-instance — see/runbooks/client-app-deploy/§3c. - Branding + signup policy live as data IN the client’s own Supabase
project (
public.tenant_configrow +brandingStorage bucket; the auth hook readssignup_policy) — see/runbooks/client-app-deploy/§2. NOTHING client-identifying is committed to the public repo. - There is NO per-client GitHub environment. The
Production/Stagingenvironments in THIS repo are the platform CI/dev/showcase environments (§2, repointed to the platform DBs S408) — they are not a client’s environments. A client’s CI gate is the sharedVercel - canonical: ci-summarycheck on the public repo, consumed by the client’s own Vercel project (see/runbooks/per-client-release-pins/).
When onboarding a future client (summary — full steps in
/runbooks/client-app-deploy/):
- Provision the client’s own managed Supabase project; push the canonical
migration set; seed the storage buckets (
supabase seed buckets --linked— declared inconfig.toml [storage.buckets.*]); narrow the Data API to theapischema. - Create the client’s own Vercel project; set its env (
NEXT_PUBLIC_CLIENT_ID, client Supabase URL + keys,NEXT_PUBLIC_APP_URL). - Seed
tenant_config+signup_policy+ branding bucket via the operator CLI; wire the Before-User-Created auth hook. - Pin the client’s Vercel Production Branch to a
release/vX.Y.Zhandle (nevermain) per/runbooks/per-client-release-pins/. - The shared workflows (
taxonomy-sync.yml,ci.yml) run on the public repo as-is — no per-client repo or environment edits required.
§9. Cross-references
Section titled “§9. Cross-references”docs/audits/kh-production-readiness-phase-1/research/09-vercel-supabase-env-mapping-spec.md§6 (GHA env-var matrix) + §11.5 (canonical environments decision).docs/audits/kh-production-readiness-phase-1/research/11-supabase-vercel-integration-sync-behaviour.md(Vercel-side sync model — informs why Vercel + GH should be kept in lockstep on env-scoped secrets)..github/workflows/taxonomy-sync.yml— current canonical example of a Production-scoped workflow.- [GH-DOC]
https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-deployments/managing-environments-for-deployment— case-sensitiveenvironment:matching, environment lifecycle. - [GH-DOC]
https://docs.github.com/en/rest/deployments/environments— REST API for environment management. - [GH-DOC]
https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions— secrets vs variables distinction.
§10. W5 expansion checklist
Section titled “§10. W5 expansion checklist”Items to tighten when this graduates from stub to handover doc:
- Audit Liam’s actual secret-vs-variable typing post-S4 + sync this doc to match.
- Capture screenshots of the GH Settings → Environments UI for each of
Production+Staging(showing populated secrets). - Document required-reviewers + deployment-branch-policies for each environment (Liam decision needed — TBD W5).
- Add a “Disaster recovery” section: what to do if a secret is compromised (rotate at provider → update GH secret → redeploy).
- Add audit-log review pattern (
gh api repos/.../actions/secrets/...shows last-updated timestamp). - Cross-link to incident response playbook (W5 deliverable).