Per-client release pins crib
Per-client release pins crib
Section titled “Per-client release pins crib”Status: Authoritative client-to-version mapping. The live Vercel Production Branch and Coolify
COCOINDEX_IMAGE_TAGare the enforcement mechanism; this crib records what is pinned where and when. Kept consistent by the cut-release runbook (§3. Cut a release) and the upgrade-a-client runbook (§4. Upgrade a client). Owner: platform (Liam). Audience: whoever cuts a release or upgrades a client instance.PI-5/PI-6 guard: This crib lives ONLY in the private docs-site. Client identities NEVER enter the public application repo (
ai-solution-hub/canonical, renamed fromknowledge-hubat v1.0.0). Every client reference in the public repo uses the<CLIENT>token only.Pair with: client-app-deploy.md (instance stand-up), §3. Cut a release (this document), §4. Upgrade a client (this document), §4A. Per-client staging → prod promotion lane (this document), and §5. Standup verification (this document).
1. Per-client pin table
Section titled “1. Per-client pin table”One row per live client. Add a row when a client instance is stood up for the
first time; update the row in-place when a client is upgraded (record the
historical upgrade in §2). The “Pinned release” column records the vX.Y.Z tag
(from the public KH repo) that the client’s Vercel Production Branch and on-prem
image are locked to. “Pinned on-prem COCOINDEX_IMAGE_TAG” is the corresponding
pipeline image digest/tag set in Coolify for that client’s VPS (n/a for
cloud-only instances).
| Client | Vercel project | Pinned release vX.Y.Z | Supabase project ref | Pinned on-prem COCOINDEX_IMAGE_TAG | Last-upgrade date |
|---|---|---|---|---|---|
<CLIENT> (template) | canonical-<CLIENT> | TBD at first cut | <supabase-project-ref> | n/a (where not applicable) | TBD |
| Phew | canonical-phew | v1.0.0 | rovrymhhffssilaftdwd | v1.0.0 (set at id-45 re-ingest / handover) | 18/06/2026 |
v1.0.0 cut (18/06/2026). The first platform release tag
v1.0.0exists (commitcb3c5919;release/v1.0.0branch; ghcr imagekh-cocoindex-pipeline:v1.0.0, aliased no-rebuild fromsha-cb3c5919). Phew is pinned to it via thecanonical-phewVercel project (Production Branch →release/v1.0.0;NEXT_PUBLIC_RELEASE_VERSION=v1.0.0set on canonical-phew only). The on-premCOCOINDEX_IMAGE_TAGfor Phew is finalised at the id-45 re-ingest / handover — the Coolify config currently runs under the aisolutionhub domain but is the client’s pipeline; the domain flips to kh.phew.org.uk at handover.Platform is NOT a client. kh.aisolutionhub.co.uk / Vercel
canonical-platformtracksmain(our own dev / CI / showcase on the Platform Supabase DB) and is never pinned to a release tag — so it has no row in this table.Only PROD is pinned; STAGING is continuous (S408 / ID-127.15). The pin in this table is the client’s production anchor only. A client’s staging tier tracks the shared
stagingbranch CONTINUOUSLY — it is deploy-only and is never pinned to arelease/vX.Y.Zhandle. The release lifecycle is thereforemain→ (continuous) client staging → (deliberate, pinned) client prod: a change proves on the client’s staging instance (sharedstagingbranch + the client’s own staging Supabase/Vercel-preview — see/runbooks/client-app-deploy/and/runbooks/staging-refresh/) BEFORE the operator cuts avX.Y.Zand repoints the client’s production Branch to it (§4). Do not add a staging row to the pin table; staging carries no pin.
2. Append-only upgrade history
Section titled “2. Append-only upgrade history”CRITICAL — append-only (PI-12). Rows in this section are NEVER overwritten or deleted. Each upgrade produces one new row per client. To correct an error in a prior row, append a correction-note row immediately after it; do not edit the original.
The purpose of this section is to give an auditable trail of: what version each client was on before and after each upgrade, what migrations ran, the ordering taken, the rollback-safety classification, and the old-code-tolerance test result — so that any future operator can reconstruct the state of a client instance at any point in time.
Column reference
Section titled “Column reference”| Column | Content |
|---|---|
| Client | The <CLIENT> token (private crib only — real identifier kept here, never in the public repo) |
| From | Release tag before the upgrade (e.g. vX.Y.Z) |
| To | Release tag after the upgrade (e.g. vX.Y.Z) |
| Date | DD/MM/YYYY |
| Migration delta | Migration files applied (list of supabase/migrations/<timestamp>_<name>.sql) |
| Ordering | migrate-first (per OQ-113-2) or rollback-first — must be migrate-first for forward upgrades |
| Rollback-safety class | Per OQ-113-3: safe / risky / destructive; record the justification |
| Old-code-tolerance | Per OQ-113-7: tolerant / intolerant; tolerant means the previous version’s code can run against the new schema without error (pre-upgrade overlap window) |
| Snapshot ID | Supabase snapshot ID taken immediately before migration (RATIFIED REV 1 — mandatory; see §4 step 2) |
History table
Section titled “History table”Template / example row — not a real event. Replace with real data when the first upgrade is performed.
| Client | From | To | Date | Migration delta | Ordering | Rollback-safety class | Old-code-tolerance | Snapshot ID |
|---|---|---|---|---|---|---|---|---|
<CLIENT> (example) | vX.Y.Z | vX.Y.Z | DD/MM/YYYY | 20260101_000000_example.sql | migrate-first (OQ-113-2) | safe — additive column only, no destructive DDL (OQ-113-3) | tolerant — old code ignores new nullable column (OQ-113-7) | <snapshot-id> taken DD/MM/YYYY HH:MM UTC |
3. Cut a release
Section titled “3. Cut a release”PI-2 guard: Only tag a commit that is a first-parent ancestor of
main. NEVER tagstaging(deploy-only branch), a feature branch, or an unmerged worker branch. Tagging the wrong ref produces an immutable bad anchor — prevention is manual at this step.OQ-113-9 (CI tag-ancestry guard) is DEFERRED. No automated CI check is authored in this wave to enforce the PI-2 lineage constraint. The manual step below (step 1 and step 4) is the sole enforcement mechanism until OQ-113-9 is implemented.
PI-9: Verify the deploy gate BEFORE any rename or repoint operation (consistent with ID-95 {95.10}).
Prerequisites
Section titled “Prerequisites”- Write access to
ai-solution-hub/canonical(to push tags and branches). - Write access to the private docs-site (to update this crib).
- For pipeline releases: the
onprem-deploy.ymltag-releaseworkflow_dispatchjob ({113.6}) must be present and the Actions environment must havepackages:writepermission onghcr.ioviaGITHUB_TOKEN.
1. Verify main is known-good (PI-1 precondition).
Confirm the Vercel - canonical: ci-summary check is GREEN at the
target SHA. This is the deploy-gate check posted by ci.yml (step
“Notify Vercel — ci-summary status”) — it aggregates all parallel CI jobs.
gh run list --branch main --limit 5# Identify the target SHA. Then:gh api repos/ai-solution-hub/canonical/commits/<sha>/check-runs \ --jq '.check_runs[] | select(.name == "Vercel - canonical: ci-summary") | {status, conclusion}'# Expected: status=completed, conclusion=successDo not proceed if the check is absent, pending, or failed. A red gate means a
defect is in main; fix and re-verify before tagging.
2. Choose the semver bump.
| Bump | When to use |
|---|---|
| MAJOR | Breaking client-facing change OR any migration classified rollback-safety: destructive (OQ-113-3) OR old-code-tolerant: no (OQ-113-7, step 3 below) |
| MINOR | Additive or back-compatible change (new features, additive migrations) |
| PATCH | Fix-only — no schema change, no API change |
3. Classify the migration set: old-code-tolerant field (BLOCKING — release does not proceed without this recorded).
RATIFIED REV 3 — BLOCKING release-cut field (OQ-113-7, elevated from proposed to BLOCKING). This classification is what makes the migrate-DB-first strategy safe in the upgrade-a-client runbook ({113.9}): if
old-code-tolerant: no, the old client code cannot safely run against the new schema during the migration window, forcing a coordinated cutover and a MAJOR bump. The cut does NOT proceed until this field is determined and recorded in step 8.
For each migration file in the release delta:
tolerant— the previous version’s application code can run against the new schema without error (e.g. the migration adds a nullable column; old code simply ignores it).intolerant— the previous version’s application code would error against the new schema (e.g. a column was renamed or removed; a NOT NULL constraint was added without a default that old inserts would satisfy).
If any migration in the set is intolerant, the release classification is
old-code-tolerant: no, which FORCES:
- Bump to MAJOR (regardless of whether the change would otherwise be MINOR).
- Coordinated cutover at upgrade time (see {113.9}).
Record the classification and justification in step 8 (crib changelog).
4. Tag the release.
# Confirm the target SHA is a first-parent ancestor of main (PI-2):git merge-base --is-ancestor <sha> main && echo "OK — on main lineage" || echo "STOP — not on main"
git tag vX.Y.Z <known-good-main-sha>git push origin vX.Y.ZThe tag is immutable once pushed (PI-1). NEVER retag an existing vX.Y.Z. If a
defect is found after tagging, ship the fix as a new PATCH (vX.Y.(Z+1)).
5. Create or fast-forward the release/vX.Y.Z Vercel handle branch.
git checkout -b release/vX.Y.Z <known-good-main-sha>git push origin release/vX.Y.Z# If the branch already exists (should not for a new vX.Y.Z — indicates a prior aborted cut):git push origin release/vX.Y.Z --force-with-lease # FF-only; fails if non-FFThe tag is the source of truth (PI-3); the release/vX.Y.Z branch is the
Vercel handle only. The branch must always point to exactly the tag commit. Never
commit directly to a release/ branch.
6. Pipeline image: retag sha-<sha> as vX.Y.Z (pipeline releases only).
Skip this step if the release contains no on-prem pipeline changes (i.e. no
changes to scripts/cocoindex_pipeline/, requirements.txt, or
deploy/coolify/).
If the release does include pipeline changes, the sha-<sha> image was built and
pushed to ghcr.io by onprem-deploy.yml at CI time ({113.6}, PI-10 option (a)).
Retag it as vX.Y.Z via the onprem-deploy.yml tag-release workflow_dispatch
job:
- Input
source_sha: the full commit SHA whosesha-<sha>image is to be retagged. - Input
release_version:vX.Y.Z.
The job pulls the existing sha-<sha> image, tags it vX.Y.Z, and pushes — pure
alias, no rebuild. The COCOINDEX_IMAGE_TAG in Coolify is NOT changed by this
step; the upgrade-a-client runbook ({113.9}) handles the per-client image switch
at upgrade time.
gh workflow run onprem-deploy.yml \ --ref vX.Y.Z \ --field job=tag-release \ --field source_sha=<full-sha> \ --field release_version=vX.Y.ZVerify the vX.Y.Z tag appears in ghcr.io before proceeding:
gh api /orgs/ai-solution-hub/packages/container/kh-cocoindex-pipeline/versions \ --jq '.[] | select(.metadata.container.tags[] | contains("vX.Y.Z")) | .id'7. AGPL / copyleft discharge: add or refresh THIRD-PARTY-NOTICES (RATIFIED REV 5).
No
THIRD-PARTY-NOTICESorLICENSEfile exists anywhere in the KH repo as of 16/06/2026 (verified at ID-113.8 authoring). This step is required on every release cut regardless of D1 status — it is not deferred.
For any copyleft (AGPL or GPL) dependency or image shipped per-client in this release (including any upstream image pulled into the on-prem compose):
- Identify AGPL/GPL components in the release (check
requirements.txt,deploy/coolify/compose files, and any upstream images referenced there). - Generate or refresh
THIRD-PARTY-NOTICESat the repo root:- List each copyleft component, its licence (AGPL-3.0 / GPL-3.0 / etc.), its upstream source URL, and the version shipped.
- Include a Corresponding-Source pointer: the URL where the source code for the shipped version can be obtained (e.g. the upstream GitHub release tag).
- Commit
THIRD-PARTY-NOTICEStomainand include it in therelease/vX.Y.Zbranch (cherry-pick before pointing Vercel if the commit landed after tagging). - Record the notices artefact location in the crib changelog (step 8).
If no copyleft components are present in this release, record that finding
explicitly in step 8 (THIRD-PARTY-NOTICES: n/a — no AGPL/copyleft components in vX.Y.Z).
8. Record the release in the crib changelog.
Update the per-client pin table (§1) if any client is being immediately pinned to this release. Then append a row to the release changelog table below (create it here on first use — the template is provided).
| Version | SHA (short) | Date | Notable changes | Migration delta | Rollback-safety class | Old-code-tolerant | THIRD-PARTY-NOTICES |
|---|---|---|---|---|---|---|---|
vX.Y.Z | <short-sha> | DD/MM/YYYY | Brief description | supabase/migrations/<timestamp>_<name>.sql (or none) | safe / risky / destructive + justification (OQ-113-3) | yes / no + justification (OQ-113-7) | Path in repo, or n/a — no AGPL/copyleft |
Release changelog
Section titled “Release changelog”First entry to be added when the first
vX.Y.Ztag is cut.
| Version | SHA (short) | Date | Notable changes | Migration delta | Rollback-safety class | Old-code-tolerant | THIRD-PARTY-NOTICES |
|---|---|---|---|---|---|---|---|
v1.0.0 | cb3c5919 | 18/06/2026 | First release. Source-available (PC-2; no project licence, root LICENSE removed). Repo rename knowledge-hub→canonical; deploy-gate check renamed Vercel - knowledge-hub: ci-summary→Vercel - canonical: ci-summary (PI-9). ID-68 client-IP scrub baseline. Pipeline image ghcr.io/ai-solution-hub/kh-cocoindex-pipeline:v1.0.0 (aliased from sha-cb3c5919…, no rebuild). | none — first cut (squash baseline 20260617130000) | n/a — first release (no prior tag) | n/a — first release | THIRD-PARTY-NOTICES (repo root; pandoc GPL-2.0+ via pypandoc_binary, docxtpl LGPL-2.1, @vercel/analytics + dompurify MPL-2.0) |
4. Upgrade a client
Section titled “4. Upgrade a client”PI-3/PI-4 guard: The client’s Vercel Production Branch is ALWAYS pinned to a deliberate
release/vX.Y.Zhandle branch — never tomainand never auto-tracking. A repoint is an explicit operator action; it never happens automatically. See §3 for wherevX.Y.Zandrelease/vX.Y.Zcome from.PI-9: Verify the deploy gate BEFORE any repoint or rename operation (consistent with ID-95 {95.10}).
Upgrade ordering is fixed: migrate the database FIRST, then repoint Vercel. Reverse ordering is a hard runtime skew fault — the application binary has build-time-baked configuration with no graceful path if the schema is ahead of the code. (OQ-113-2)
Prerequisites
Section titled “Prerequisites”- The target
vX.Y.Zhas been cut (§3) and therelease/vX.Y.Zbranch exists in the public KH repo. - Write access to the client’s Supabase project (to apply migrations and take snapshots).
- Write access to the client’s Vercel project (to repoint the Production Branch).
- If the client runs ingestion: access to the client’s Coolify instance (to
update
COCOINDEX_IMAGE_TAG). - Write access to this private docs-site crib (to record the upgrade in §1 and §2 in the same step as the repoint — see step 8 below).
1. Pre-flight: review the migration delta and classify upgrade safety.
Review every migration file in the delta from the client’s current pin to the
target vX.Y.Z (the migration delta is recorded in the §3 release changelog for
that version):
- Rollback-safety (OQ-113-3): classify as
safe(additive, easily reverted),risky(reverted with care), ordestructive(data-loss on rollback; upgrade is forward-only). Adestructiveclassification does NOT block the upgrade but MUST be flagged explicitly — the operator acknowledges that rollback after migration is not possible and that the rev-1 snapshot (step 2) is the only pre-migration restore path. - Old-code-tolerance (OQ-113-7 / RATIFIED REV 3): confirm the release was
classified
old-code-tolerant: yesin the §3 release changelog. Ifold-code-tolerant: no, the previous application code cannot safely serve against the new schema during the migration window — a coordinated cutover is required (schedule a maintenance window; do not proceed with the standard migrate-first / serve-old-code approach below).
Do not proceed to step 2 until these two classifications are confirmed and recorded in your upgrade notes for the §2 history row.
2. (BLOCKING — RATIFIED REV 1) Take a Supabase snapshot of the client’s project before any migration.
This step is MANDATORY and BLOCKING. The upgrade MUST NOT proceed without a confirmed snapshot. No snapshot = no migration.
This is the explicit pre-upgrade safety net introduced by RATIFIED REV 1, which revises OQ-113-3 from roll-forward-only: the snapshot is the last-resort emergency restore path for data written after the snapshot (data-loss window for writes after the snapshot — emergency use only; see step 9 for rollback guidance).
Take the snapshot via the Supabase dashboard or CLI for the client’s project:
# Via Supabase CLI (replace <project-ref> with the client's Supabase project ref from §1):supabase snapshots create --project-ref <project-ref># Note the snapshot ID and timestamp returned.Record the snapshot ID and the UTC time taken. You will need both for the §2 history row (step 8).
3. (MIGRATE-DB-FIRST — OQ-113-2) Apply the migration set to the client’s Supabase project.
Apply the migration files in the release delta to the client’s Supabase project.
The old application code continues serving during this compatibility window —
this is safe only when old-code-tolerant: yes (confirmed in step 1).
# Apply migrations to the client's Supabase project:supabase db push --project-ref <project-ref>After applying, verify schema parity:
supabase db diff --project-ref <project-ref># Expected: no diff (all migrations applied cleanly).Ordering constraint: The old deploy continues serving during this window (forward-only additive migrations ⇒ compatibility window, NOT downtime). Do NOT repoint Vercel before confirming schema parity — reverse ordering (repoint first, migrate second) is a hard runtime skew fault because the application binary has build-time-baked configuration with no graceful degradation path.
4. (PI-9 / BLOCKING) Verify the deploy gate before any repoint.
Before repointing the client’s Vercel Production Branch, confirm that the
Vercel - canonical: ci-summary required check is intact on BOTH:
- The CI dispatch side: the
ci.yml“Notify Vercel — ci-summary status” step posts this check name verbatim (Vercel - canonical: ci-summary— re-verified 22/07/2026; line numbers drift, grep for the name). Confirm the check appears on therelease/vX.Y.Zbranch:
gh api repos/ai-solution-hub/canonical/commits/<release-branch-tip-sha>/check-runs \ --jq '.check_runs[] | select(.name == "Vercel - canonical: ci-summary") | {status, conclusion}'# Expected: status=completed, conclusion=success- The client’s Vercel project dashboard: navigate to the project → Settings →
Git → Required checks. Confirm the required check string reads exactly
Vercel - canonical: ci-summary. A mismatch (e.g. a stale check name from a prior rename) silently un-gates production — the Vercel deployment proceeds even if CI has failed. (This echoes the deploy-gate pattern established by ID-95 {95.10}.)
Do not proceed to step 5 if either check is absent, pending, failed, or mismatched.
5. Repoint the client’s Vercel Production Branch to release/vX.Y.Z.
In the client’s Vercel project dashboard:
- Navigate to Settings → Git → Production Branch.
- Change the Production Branch from the current
release/vX.Y.(Z-1)torelease/vX.Y.Z. - Trigger a Vercel redeploy (PI-4 — deliberate, never automatic).
PI-3/PI-4: The repoint is always deliberate. Vercel is never left tracking
mainor any auto-updating branch. Therelease/vX.Y.Zbranch is a static handle that points to the tagged commit only (per §3 step 5).
6. (Pipeline clients only) Repoint Coolify COCOINDEX_IMAGE_TAG to vX.Y.Z.
Skip this step if the client does not run on-prem ingestion (i.e. their §1 pin
table row has n/a in the COCOINDEX_IMAGE_TAG column).
For pipeline clients, update COCOINDEX_IMAGE_TAG to vX.Y.Z in the client’s
Coolify instance. This uses the same PATCH-in-place mechanism as the
onprem-deploy.yml prod deploy-cocoindex job (PI-10), but applied manually to
a deliberate semver rather than a SHA tag:
# PATCH COCOINDEX_IMAGE_TAG in Coolify to the new semver (replace vars as appropriate):curl -s -X PATCH "${COOLIFY_BASE_URL}/api/v1/applications/${COOLIFY_APP_UUID}/envs" \ -H "Authorization: Bearer ${COOLIFY_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"key":"COCOINDEX_IMAGE_TAG","value":"vX.Y.Z","is_buildtime":true,"is_runtime":true}'Then trigger a Coolify deploy for the client’s pipeline application. Confirm the deploy completes successfully before proceeding.
7. Verify: deploy gate, smoke test, and version field.
After the Vercel redeploy and (where applicable) Coolify deploy have completed:
-
Confirm the
Vercel - canonical: ci-summarycheck passed for therelease/vX.Y.Zbranch (re-run the step 4 check command against the live deployment SHA). -
Smoke-test the client URL — confirm branded render (acceptance criterion AC-E3: the client’s branding assets load correctly).
-
Confirm the
/api/healthversionfield matchesvX.Y.Z({113.5} — SHIPPED;app/api/health/route.tsreadsNEXT_PUBLIC_RELEASE_VERSION):
curl <client-url>/api/health | jq .version# Expected: "vX.Y.Z"{113.5} is live — this is a hard verification step. The route returns
"unknown"whenNEXT_PUBLIC_RELEASE_VERSIONis not set on the deployment (e.g. an unpinned/platform deploy); a pinned client deploy MUST have the env set at pin time (§1 —NEXT_PUBLIC_RELEASE_VERSION=vX.Y.Zon the client’s Vercel project), so an"unknown"here on a client domain means the env was not set with the repoint — fix the env, do not skip the check.
8. (SAME-STEP — PI-7/PI-12) Update the crib in the same act as the repoint.
A repoint without a same-step crib update is a checklist FAILURE.
In the same act as completing the Vercel repoint (step 5):
- Append a new row to the §2 upgrade-history table (append-only — never edit
prior rows). Include: client token, From version, To version, date (DD/MM/YYYY),
migration delta, ordering taken (
migrate-first), rollback-safety class and justification, old-code-tolerance and justification, and the snapshot ID from step 2. - Update the client’s §1 pin-table row in-place: set “Pinned release vX.Y.Z”
to the new version and “Last-upgrade date” to today’s date (DD/MM/YYYY). If the
client runs ingestion, also update “Pinned on-prem
COCOINDEX_IMAGE_TAG”.
The §2 columns to populate match the column reference defined in §2 above, with the addition of the Snapshot ID column (RATIFIED REV 1):
| Field | Value to record |
|---|---|
| Client | <CLIENT> token |
| From | Previous pinned version (e.g. vX.Y.(Z-1)) |
| To | vX.Y.Z |
| Date | DD/MM/YYYY |
| Migration delta | List of supabase/migrations/<timestamp>_<name>.sql applied |
| Ordering | migrate-first (OQ-113-2) |
| Rollback-safety class | Classification from step 1 + justification |
| Old-code-tolerance | Classification from step 1 + justification |
| Snapshot ID | Snapshot ID and UTC time from step 2 |
9. Rollback guidance.
| Scenario | Action |
|---|---|
Additive release (rollback-safety: safe) — code rollback needed | Repoint Vercel Production Branch back to release/vX.Y.(Z-1) and redeploy. For pipeline clients, repoint Coolify COCOINDEX_IMAGE_TAG to the prior tag. The additive schema is forward-compatible; no schema rollback is needed. Record the remediation in a correction-note row in §2 (append-only). |
Forward-only migration (rollback-safety: destructive) — defect found post-migrate | Roll FORWARD: ship a fix as vX.Y.(Z+1) (new PATCH per §3) and upgrade the client to it. Do NOT attempt to revert the schema — the migration is irreversible. Record the incident and the forward-fix version in §2. |
| Emergency data restore needed (last resort) | Restore from the pre-upgrade Supabase snapshot taken in step 2. Warning: any writes made to the client’s project after the snapshot was taken will be lost. This is an emergency-only path. Contact Supabase support if the project-level restore UI is not available. Record the restore event in §2 with a correction-note row. |
After any rollback or emergency restore, re-verify the client’s §1 pin-table row reflects the actual running version and update “Last-upgrade date” accordingly.
4A. Per-client staging → prod promotion lane
Section titled “4A. Per-client staging → prod promotion lane”Status: The intended release lane (S408 / ID-127.15). §4 above repoints a client’s production Branch straight to a new
release/vX.Y.Zafter a snapshot. This section records the SAFER lane Liam intends as the default: a change is proved on the client’s staging instance FIRST, then promoted to the client’s production pin. Staging is continuous; only prod is pinned (§1).Pair with: client-app-deploy.md (the client STAGING tier — shared
stagingbranch + the client’s own staging Supabase + Vercel preview), staging-refresh.md (parity checks), and §4. Upgrade a client (the production repoint this lane gates).
The two tiers
Section titled “The two tiers”| Tier | Branch / pin | Supabase | Vercel | Pinned? |
|---|---|---|---|---|
| Client staging | shared staging branch (continuous, deploy-only) | client’s own staging Supabase project/branch | client’s own Vercel preview env (canonical-<CLIENT>-git-staging-<team>.vercel.app) | No — tracks staging |
| Client prod | release/vX.Y.Z handle (deliberate repoint) | client’s own prod Supabase project | client’s own Vercel production target (client domain) | Yes — §1 pin table |
A client’s staging instance is wired to the client’s OWN staging Supabase (NOT
the platform/shared staging app) and is fed by the client’s own staging
ingestion pipeline — see /runbooks/client-app-deploy/ for the staging-tier
wiring + the pipeline-run webhook model.
Promotion steps
Section titled “Promotion steps”1. Prove the candidate on the client’s staging instance.
The candidate change is already on the shared staging branch (continuous
deploy). Confirm the client’s staging Vercel preview
(canonical-<CLIENT>-git-staging-<team>.vercel.app) is green against the
client’s staging Supabase, and run the staging parity checks
(/runbooks/staging-refresh/ §4.3 — application_types = 6, api anon-exposure
= 1 of 179). Exercise the client-facing flows that the release touches.
2. Cut the release from main (NOT from staging).
Once staging has proved the change, follow §3. Cut a release
to tag vX.Y.Z against a known-good main SHA (PI-2 — NEVER tag staging;
staging is the continuous deploy-only branch, never a release anchor) and
create the release/vX.Y.Z handle branch.
3. Promote to the client’s production pin via §4. Upgrade a client.
Run the §4 upgrade lane against the client’s PRODUCTION project: pre-flight
classify → snapshot (BLOCKING) → migrate-DB-first → deploy-gate verify →
repoint the client’s production Branch to release/vX.Y.Z → smoke-test →
same-step crib update (§1 + §2). The snapshot remains mandatory at the
production repoint even though staging proved the change — staging and prod are
physically separate Supabase projects, so the prod snapshot is the only
pre-migration restore path for prod data.
Why staging-first, not straight-to-prod. §4 on its own permits a snapshot-guarded direct prod repoint (acceptable for an emergency PATCH). The staging-first lane is the DEFAULT for routine releases: it catches schema / branding / flow regressions on the client’s own staging surface before any production data is migrated, and it exercises the same
staging-branch build the platform CI already gates — so a client promotion never ships a change that has not run end-to-end against a client-shaped staging instance.
5. Standup verification
Section titled “5. Standup verification”When to run: Once at the first standup after going live with per-client release pins, and again after any rename or repoint operation. Six checks in total — two are statically runnable from a repo checkout (Checks 1 and 4); four require a live Vercel/deploy environment and are operator-run at standup (Checks 2, 3, 5, 6).
Purpose: Confirm the PI invariants hold end-to-end in the live configuration — not just in the runbook. A checklist that was never run is a checklist that was never verified.
Check 1 — PI-5/PI-6: public repo refs are client-identity-free
Section titled “Check 1 — PI-5/PI-6: public repo refs are client-identity-free”Pin references are version-named only (release/vX.Y.Z, vX.Y.Z). No
client name or client domain token may appear in any branch name, tag name, or
tracked file in the public ai-solution-hub/canonical repo.
Commands:
# List all refs (branches + tags) and confirm none contain a client identifier.# Use the private denylist to define what counts as a "client token" —# replace <CLIENT> and <client-domain> below with real identifiers from this crib:git branch --list --all | grep -Ei '<CLIENT>|<client-domain>' \ && echo "VIOLATION — client token found in branch refs" \ || echo "CLEAN — no client tokens in branch refs"
git log --oneline --decorate --all | grep 'tag:' | grep -Ei '<CLIENT>|<client-domain>' \ && echo "VIOLATION — client token found in tags" \ || echo "CLEAN — no client tokens in tags"
# Confirm no client→version map file is committed to the public repo:git ls-files | grep -Ei '<CLIENT>|<client-domain>|client-branding/' \ && echo "VIOLATION — client file found in public tree" \ || echo "CLEAN — no client files in public tree"Expected / pass result: All three commands return CLEAN. Any VIOLATION line
is a blocker — the client identity has leaked into the public repo and must be
removed (force-push to purge history is acceptable for this specific case if the
leak is in a non-main branch; contact Liam if the leak is on main or in a
tag).
Check 2 — PI-3/PI-4: client production deploys are pin-not-main-tracking
Section titled “Check 2 — PI-3/PI-4: client production deploys are pin-not-main-tracking”A no-op merge to main must NOT trigger a client production redeploy, and each
client’s Vercel project Production Branch must resolve to exactly one
release/vX.Y.Z handle branch.
Requires live Vercel environment — operator runs at standup.
Steps:
- In each client’s Vercel project dashboard → Settings → Git → Production Branch:
confirm the value is
release/vX.Y.Z(wherevX.Y.Zmatches the §1 pin table), NOTmainor any other auto-updating branch. - Push a no-op (empty) commit to
main:
git commit --allow-empty -m "chore: standup verification no-op (PI-3/PI-4 check)"git push origin main- Observe the Vercel dashboard for each client project. No production deployment should be triggered — only the base repo’s own Vercel project (if any) should see a preview/staging build.
Expected / pass result: No client project shows a new Production deployment in
response to the main push. Each client’s Production Branch field shows exactly
one release/vX.Y.Z.
Check 3 — PI-9: deploy-gate NEGATIVE TEST (headline hazard)
Section titled “Check 3 — PI-9: deploy-gate NEGATIVE TEST (headline hazard)”Requires a throwaway Vercel project — operator runs at standup. This is the headline hazard check: a misconfigured required-check name silently un-gates production.
Deliberately misconfigure the required check on a throwaway Vercel project and confirm the production deploy is BLOCKED — not silently allowed.
Steps:
- Create a throwaway Vercel project connected to the
ai-solution-hub/canonicalrepo (or use an existing non-client project). - In the throwaway project → Settings → Git → Required checks: set the required
check to a deliberately wrong string, e.g.
ci-summary(missing theVercel - canonical:prefix). - Push a commit that triggers CI. Observe whether Vercel allows the production
deploy to proceed when the correct
Vercel - canonical: ci-summarycheck passes but the required-check field does not match it.
Expected / pass result: The production deploy is BLOCKED — Vercel treats the required check as unmet when the name does not match exactly. If the deploy proceeds, the gate is silently misconfigured and Check 3 is a FAIL. In that case, the required-check string in every live client project must be audited immediately. (This echoes ID-95 {95.10}.)
- After confirming the BLOCK, restore the throwaway project’s required-check field
to
Vercel - canonical: ci-summary(or delete the throwaway project).
Check 4 — PI-11: branding non-regression (static + deploy)
Section titled “Check 4 — PI-11: branding non-regression (static + deploy)”4a. Static (run from repo checkout)
Section titled “4a. Static (run from repo checkout)”# No client-branding/ directory in the public repo:git ls-files | grep -i 'client-branding/' \ && echo "VIOLATION — client-branding/ dir in public tree" \ || echo "CLEAN — no client-branding/ dir"
# No hardcoded if-client branch in app source:grep -rn 'if.*client\s*===\s*['"'"'"]' app/ lib/ components/ hooks/ \ && echo "VIOLATION — hardcoded client branch found" \ || echo "CLEAN — no hardcoded client branch"
# loadBranding reads the build-baked CLIENT_BRANDING_MAP only — no runtime Supabase call:grep -n 'supabase\|\.from\|\.select' lib/client-config.ts \ && echo "REVIEW — potential Supabase call in client-config.ts" \ || echo "CLEAN — no Supabase calls in loadBranding module"Expected / pass result: All three return CLEAN. loadBranding
(lib/client-config.ts lines 618–641) reads CLIENT_BRANDING_MAP[id] (build-baked
from lib/branding/client-branding-map.generated.ts) and clientEnv.NEXT_PUBLIC_CLIENT_ID
(inlined by SWC at build time) — no runtime Supabase version query.
4b. Deploy (operator runs at standup — requires live client URL)
Section titled “4b. Deploy (operator runs at standup — requires live client URL)”# Confirm the client URL renders with its OWN tenant_config branding (AC-E3):# Open <client-url> in a browser and verify:# - The page title, logo, and brand colours match the client's tenant_config.# - No "Knowledge Hub" default branding is shown.# - Browser devtools → Network: no /api/branding or similar runtime fetch.Expected / pass result: Branded render hydrated from the client’s own
tenant_config; no default branding visible; no runtime branding fetch.
Check 5 — PI-8: /api/health version field matches pin
Section titled “Check 5 — PI-8: /api/health version field matches pin”{113.5} SHIPPED — the field is live (
app/api/health/route.ts). Operator runs at standup.
curl <client-url>/api/health | jq .version# Expected: "vX.Y.Z" — where vX.Y.Z matches the client's §1 pin table row.Expected / pass result: The version field equals the vX.Y.Z value in the
client’s §1 pin table. A mismatch means the deployed code does not correspond to
the recorded pin — investigate before proceeding with any upgrade. "unknown"
means NEXT_PUBLIC_RELEASE_VERSION is unset on that deployment — a config
failure on a pinned client deploy, not a pass.
Check 6 — OQ-113-1 resolution: multiple per-client projects share one ci-summary check
Section titled “Check 6 — OQ-113-1 resolution: multiple per-client projects share one ci-summary check”Requires the Vercel dashboard — operator runs at standup.
Confirm that multiple per-client Vercel projects can each independently configure
the same Vercel - canonical: ci-summary required check without conflict.
Steps:
- In the Vercel dashboard, open two or more client project settings side by side (Settings → Git → Required checks).
- Confirm each project shows
Vercel - canonical: ci-summaryas the required check, and that each project independently gates its own production deployments using the same check name.
Expected / pass result: All client projects show the same check name; each project’s deployment history shows it was independently gated. No “check name conflict” warning from Vercel. This resolves OQ-113-1 (open question on whether shared check names across multiple Vercel projects for the same repo are permitted).
Results table
Section titled “Results table”Record each run below. Append a new row per operator / per date — do not overwrite prior rows.
| Check | Invariant | Date (DD/MM/YYYY) | Result | Notes |
|---|---|---|---|---|
| 1 — public refs client-free | PI-5/PI-6 | 16/06/2026 | PASS | 93 local refs, 49 remote origin refs audited; all branch/tag names are task-, session-, bot-, or semver-named; 0 client identity tokens; no release/vX.Y.Z branches yet (correct for pre-first-release); git ls-files shows no client-branding/ dir and no committed client→version map. 4 existing tags: v0.2.0-task-view (task-view tooling), s262-id32-worker-final, s262-32.16-acceptance-gate, archive/kh-knowledge-platform — none are client-named. |
| 2 — pin not main-tracking | PI-3/PI-4 | — | pending first standup (requires live Vercel) | — |
| 3 — deploy-gate NEGATIVE TEST | PI-9 | — | pending first standup (requires throwaway Vercel project) | — |
| 4 — branding non-regression | PI-11 | 16/06/2026 | PASS (static) | git ls-files | grep -i 'client-branding/' → empty (CLEAN); grep -rn 'if.*client\s*===\s*' across app/, lib/, components/, hooks/ → no matches (CLEAN); grep -n 'supabase|\.from|\.select' lib/client-config.ts → no matches (CLEAN). loadBranding (lib/client-config.ts:618–641) reads CLIENT_BRANDING_MAP[id] (build-baked; lib/branding/client-branding-map.generated.ts contains default entry only, no client identities) — no runtime Supabase call. Deploy half (AC-E3 branded render) pending first standup. |
| 5 — /api/health version field | PI-8 | — | pending first standup (requires live Vercel; {113.5} shipped — field is live) | — |
| 6 — shared ci-summary check | OQ-113-1 | — | pending first standup (requires Vercel dashboard with ≥2 client projects) | — |
Related runbooks
Section titled “Related runbooks”- client-app-deploy.md — stand-up a new client instance (Vercel project, Supabase project, branding seed, auth hook)
- §3. Cut a release (this document) — cut a
vX.Y.Ztag, create the Vercel handle branch, retag the pipeline image, discharge AGPL, and record in this crib - §4. Upgrade a client (this document) — apply a
release/vX.Y.Zrepoint to a live client instance: pre-flight, snapshot, migrate-first, deploy-gate verification, repoint, smoke-test, and same-step crib update - §4A. Per-client staging → prod promotion lane
(this document) — the staging-first default lane: prove on the client’s
continuous
staginginstance, then cut frommainand promote to the client’s pinned production via §4 - §5. Standup verification (this document) — run once
at first standup and after any rename/repoint: six checks covering PI-5/6, PI-3/4,
PI-9 (deploy-gate negative test), PI-11 (branding non-regression), PI-8
(
/api/healthversion field), and OQ-113-1 resolution