Skip to content

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).


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 (rovrymhhffssilaftdwd prod / turayklvaunphgbgscat staging). 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.

EnvironmentMaps to SupabaseMaps to VercelUsed by
ProductionPlatform prod DB zjqbrdctesqvouboziae (CLIENT_ID=default)Production targetProduction-context GHA jobs (taxonomy-sync.yml, future ci.yml post-merge jobs)
StagingPlatform staging branch rbwqewalexrzgxtvcqrh (CLIENT_ID=default)Preview targetStaging-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 / Staging carries 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 — see specs/id-127-platform-pipeline/AMENDMENT-staging-prod-two-server.md), these SAME Production / Staging Coolify 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. A SUPABASE_* 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:

TypeStorageWorkflow accessUse for
SecretEncrypted-at-rest, masked in logs, write-only after creation${{ secrets.X }}Sensitive credentials (keys, passwords, HMAC tokens)
VariablePlain-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_URLhttps://zjqbrdctesqvouboziae.supabase.co (platform prod; was the first-client ref pre-S408)
  • SUPABASE_PUBLISHABLE_KEY — platform prod publishable key
  • SUPABASE_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 DSN
  • NEXT_PUBLIC_APP_URLhttps://kh.aisolutionhub.co.uk (platform showcase domain; was the first-client custom domain pre-S408)
  • NEXT_PUBLIC_CLIENT_IDdefault (platform tenant; was a client id pre-S408)

Staging environment secrets:

  • SUPABASE_URLhttps://rbwqewalexrzgxtvcqrh.supabase.co (platform staging branch; was the first-client staging ref pre-S408)
  • SUPABASE_PUBLISHABLE_KEY — platform staging publishable key
  • SUPABASE_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 Production
  • NEXT_PUBLIC_APP_URL — platform staging branch URL
  • NEXT_PUBLIC_CLIENT_IDdefault (platform tenant; was a client id pre-S408)
  • TEST_USER_1_EMAIL, TEST_USER_2_EMAIL, TEST_USER_3_EMAIL
  • TEST_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).

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:

LocationState
.env.local (developer machine)Required. Liam’s PAT for local CLI work + sandbox-bypass scripts. Never commit; in .gitignore.
GitHub Actions — Production environmentRequired. Used by supabase-advisors.yml (Management API advisor lint). Set via gh secret set --env Production.
GitHub Actions — Staging environmentRemoved S23. Was duplicated for symmetry; revoked once supabase-advisors.yml was confirmed Production-scope only.
GitHub Actions — repo-level secretsNEVER. 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.


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 scope

The 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.

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 }}

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.


WorkflowEnvironment(s)Notes
.github/workflows/ci.ymlProduction + Stagingquality-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.ymlProductionUses Production-scoped SUPABASE_PUBLISHABLE_KEY + SUPABASE_SERVICE_ROLE_KEY + TAXONOMY_SYNC_CALLBACK_SECRET HMAC.
.github/workflows/supabase-advisors.ymlProductionReads 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.ymlStagingRefreshes 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.ymlProduction + StagingCross-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”
Terminal window
gh secret set SECRET_NAME --env Production --body "<value>"
# Or via UI: Settings → Environments → Production → Secrets → Add secret

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):

Terminal window
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 ...`

GitHub does NOT support rename in place. Workflow:

  1. Create the new-name environment + populate secrets.
  2. Update every environment: <old-name> reference in workflow YAML to the new name.
  3. 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:

  • Preview
  • Preview – knowledge-hub
  • Preview – knowledge-hub-kpf
  • Production – knowledge-hub
  • Production – 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):

Terminal window
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}" --silent
done

After deletion, gh api repos/ai-solution-hub/canonical/environments should return 2 environments: Production and Staging.

Verify before delete:

Terminal window
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 -5
done

If 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 / Staging environments). 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 from knowledge-hub) plus a per-client Vercel project + Supabase project. See specs/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_config row + branding Storage bucket; the auth hook reads signup_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 / Staging environments 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 shared Vercel - canonical: ci-summary check 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/):

  1. Provision the client’s own managed Supabase project; push the canonical migration set; seed the storage buckets (supabase seed buckets --linked — declared in config.toml [storage.buckets.*]); narrow the Data API to the api schema.
  2. Create the client’s own Vercel project; set its env (NEXT_PUBLIC_CLIENT_ID, client Supabase URL + keys, NEXT_PUBLIC_APP_URL).
  3. Seed tenant_config + signup_policy + branding bucket via the operator CLI; wire the Before-User-Created auth hook.
  4. Pin the client’s Vercel Production Branch to a release/vX.Y.Z handle (never main) per /runbooks/per-client-release-pins/.
  5. The shared workflows (taxonomy-sync.yml, ci.yml) run on the public repo as-is — no per-client repo or environment edits required.

  • 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-sensitive environment: 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.

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).