Skip to content

Per-client release pins crib

Status: Authoritative client-to-version mapping. The live Vercel Production Branch and Coolify COCOINDEX_IMAGE_TAG are 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 from knowledge-hub at 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).

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

ClientVercel projectPinned release vX.Y.ZSupabase project refPinned on-prem COCOINDEX_IMAGE_TAGLast-upgrade date
<CLIENT> (template)canonical-<CLIENT>TBD at first cut<supabase-project-ref>n/a (where not applicable)TBD
Phewcanonical-phewv1.0.0rovrymhhffssilaftdwdv1.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.0 exists (commit cb3c5919; release/v1.0.0 branch; ghcr image kh-cocoindex-pipeline:v1.0.0, aliased no-rebuild from sha-cb3c5919). Phew is pinned to it via the canonical-phew Vercel project (Production Branch → release/v1.0.0; NEXT_PUBLIC_RELEASE_VERSION=v1.0.0 set on canonical-phew only). The on-prem COCOINDEX_IMAGE_TAG for 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-platform tracks main (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 staging branch CONTINUOUSLY — it is deploy-only and is never pinned to a release/vX.Y.Z handle. The release lifecycle is therefore main → (continuous) client staging → (deliberate, pinned) client prod: a change proves on the client’s staging instance (shared staging branch + the client’s own staging Supabase/Vercel-preview — see /runbooks/client-app-deploy/ and /runbooks/staging-refresh/) BEFORE the operator cuts a vX.Y.Z and repoints the client’s production Branch to it (§4). Do not add a staging row to the pin table; staging carries no pin.

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.

ColumnContent
ClientThe <CLIENT> token (private crib only — real identifier kept here, never in the public repo)
FromRelease tag before the upgrade (e.g. vX.Y.Z)
ToRelease tag after the upgrade (e.g. vX.Y.Z)
DateDD/MM/YYYY
Migration deltaMigration files applied (list of supabase/migrations/<timestamp>_<name>.sql)
Orderingmigrate-first (per OQ-113-2) or rollback-first — must be migrate-first for forward upgrades
Rollback-safety classPer OQ-113-3: safe / risky / destructive; record the justification
Old-code-tolerancePer 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 IDSupabase snapshot ID taken immediately before migration (RATIFIED REV 1 — mandatory; see §4 step 2)

Template / example row — not a real event. Replace with real data when the first upgrade is performed.

ClientFromToDateMigration deltaOrderingRollback-safety classOld-code-toleranceSnapshot ID
<CLIENT> (example)vX.Y.ZvX.Y.ZDD/MM/YYYY20260101_000000_example.sqlmigrate-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

PI-2 guard: Only tag a commit that is a first-parent ancestor of main. NEVER tag staging (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}).

  • 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.yml tag-release workflow_dispatch job ({113.6}) must be present and the Actions environment must have packages:write permission on ghcr.io via GITHUB_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.

Terminal window
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=success

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

BumpWhen to use
MAJORBreaking client-facing change OR any migration classified rollback-safety: destructive (OQ-113-3) OR old-code-tolerant: no (OQ-113-7, step 3 below)
MINORAdditive or back-compatible change (new features, additive migrations)
PATCHFix-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.

Terminal window
# 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.Z

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

Terminal window
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-FF

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

Terminal window
gh workflow run onprem-deploy.yml \
--ref vX.Y.Z \
--field job=tag-release \
--field source_sha=<full-sha> \
--field release_version=vX.Y.Z

Verify the vX.Y.Z tag appears in ghcr.io before proceeding:

Terminal window
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-NOTICES or LICENSE file 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):

  1. Identify AGPL/GPL components in the release (check requirements.txt, deploy/coolify/ compose files, and any upstream images referenced there).
  2. Generate or refresh THIRD-PARTY-NOTICES at 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).
  3. Commit THIRD-PARTY-NOTICES to main and include it in the release/vX.Y.Z branch (cherry-pick before pointing Vercel if the commit landed after tagging).
  4. 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).

VersionSHA (short)DateNotable changesMigration deltaRollback-safety classOld-code-tolerantTHIRD-PARTY-NOTICES
vX.Y.Z<short-sha>DD/MM/YYYYBrief descriptionsupabase/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

First entry to be added when the first vX.Y.Z tag is cut.

VersionSHA (short)DateNotable changesMigration deltaRollback-safety classOld-code-tolerantTHIRD-PARTY-NOTICES
v1.0.0cb3c591918/06/2026First release. Source-available (PC-2; no project licence, root LICENSE removed). Repo rename knowledge-hubcanonical; deploy-gate check renamed Vercel - knowledge-hub: ci-summaryVercel - 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 releaseTHIRD-PARTY-NOTICES (repo root; pandoc GPL-2.0+ via pypandoc_binary, docxtpl LGPL-2.1, @vercel/analytics + dompurify MPL-2.0)

PI-3/PI-4 guard: The client’s Vercel Production Branch is ALWAYS pinned to a deliberate release/vX.Y.Z handle branch — never to main and never auto-tracking. A repoint is an explicit operator action; it never happens automatically. See §3 for where vX.Y.Z and release/vX.Y.Z come 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)

  • The target vX.Y.Z has been cut (§3) and the release/vX.Y.Z branch 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), or destructive (data-loss on rollback; upgrade is forward-only). A destructive classification 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: yes in the §3 release changelog. If old-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:

Terminal window
# 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).

Terminal window
# Apply migrations to the client's Supabase project:
supabase db push --project-ref <project-ref>

After applying, verify schema parity:

Terminal window
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 the release/vX.Y.Z branch:
Terminal window
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) to release/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 main or any auto-updating branch. The release/vX.Y.Z branch 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:

Terminal window
# 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:

  1. Confirm the Vercel - canonical: ci-summary check passed for the release/vX.Y.Z branch (re-run the step 4 check command against the live deployment SHA).

  2. Smoke-test the client URL — confirm branded render (acceptance criterion AC-E3: the client’s branding assets load correctly).

  3. Confirm the /api/health version field matches vX.Y.Z ({113.5} — SHIPPED; app/api/health/route.ts reads NEXT_PUBLIC_RELEASE_VERSION):

Terminal window
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" when NEXT_PUBLIC_RELEASE_VERSION is 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.Z on 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):

FieldValue to record
Client<CLIENT> token
FromPrevious pinned version (e.g. vX.Y.(Z-1))
TovX.Y.Z
DateDD/MM/YYYY
Migration deltaList of supabase/migrations/<timestamp>_<name>.sql applied
Orderingmigrate-first (OQ-113-2)
Rollback-safety classClassification from step 1 + justification
Old-code-toleranceClassification from step 1 + justification
Snapshot IDSnapshot ID and UTC time from step 2

9. Rollback guidance.

ScenarioAction
Additive release (rollback-safety: safe) — code rollback neededRepoint 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-migrateRoll 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.Z after 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 staging branch + the client’s own staging Supabase + Vercel preview), staging-refresh.md (parity checks), and §4. Upgrade a client (the production repoint this lane gates).

TierBranch / pinSupabaseVercelPinned?
Client stagingshared staging branch (continuous, deploy-only)client’s own staging Supabase project/branchclient’s own Vercel preview env (canonical-<CLIENT>-git-staging-<team>.vercel.app)No — tracks staging
Client prodrelease/vX.Y.Z handle (deliberate repoint)client’s own prod Supabase projectclient’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.

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.

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:

Terminal window
# 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:

  1. In each client’s Vercel project dashboard → Settings → Git → Production Branch: confirm the value is release/vX.Y.Z (where vX.Y.Z matches the §1 pin table), NOT main or any other auto-updating branch.
  2. Push a no-op (empty) commit to main:
Terminal window
git commit --allow-empty -m "chore: standup verification no-op (PI-3/PI-4 check)"
git push origin main
  1. 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:

  1. Create a throwaway Vercel project connected to the ai-solution-hub/canonical repo (or use an existing non-client project).
  2. In the throwaway project → Settings → Git → Required checks: set the required check to a deliberately wrong string, e.g. ci-summary (missing the Vercel - canonical: prefix).
  3. Push a commit that triggers CI. Observe whether Vercel allows the production deploy to proceed when the correct Vercel - canonical: ci-summary check 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}.)

  1. 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)”
Terminal window
# 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)”
Terminal window
# 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.

Terminal window
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:

  1. In the Vercel dashboard, open two or more client project settings side by side (Settings → Git → Required checks).
  2. Confirm each project shows Vercel - canonical: ci-summary as 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).

Record each run below. Append a new row per operator / per date — do not overwrite prior rows.

CheckInvariantDate (DD/MM/YYYY)ResultNotes
1 — public refs client-freePI-5/PI-616/06/2026PASS93 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-trackingPI-3/PI-4pending first standup (requires live Vercel)
3 — deploy-gate NEGATIVE TESTPI-9pending first standup (requires throwaway Vercel project)
4 — branding non-regressionPI-1116/06/2026PASS (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 fieldPI-8pending first standup (requires live Vercel; {113.5} shipped — field is live)
6 — shared ci-summary checkOQ-113-1pending first standup (requires Vercel dashboard with ≥2 client projects)
  • 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.Z tag, 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.Z repoint 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 staging instance, then cut from main and 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/health version field), and OQ-113-1 resolution