TECH -- Platform per-client deploy: tag/release version pinning (ID-113.3)
TECH — Platform per-client deploy: tag/release version pinning (ID-113.3)
Section titled “TECH — Platform per-client deploy: tag/release version pinning (ID-113.3)”Task: ID-113 — Platform per-client deploy: tag/release version pinning design + standup.
Subtask: {113.3} TECH.
Author date: 15/06/2026.
Status: SPEC — technical plan. Fresh-Planner artefact (Q-PLANNER-2): authored by a
different instance from the {113.2} PRODUCT author and the {113.1} RESEARCH author;
both predecessors read in full. This TECH maps 1:1 to PRODUCT’s twelve PI-N invariants
(one “Proposed changes” block per PI, each independently verifiable), resolves the two
TECH-owned open decisions (OQ-113-2 upgrade ordering, OQ-113-3 forward-only rollback),
confirms the exact Vercel pin mechanism against current config, defines the cut/promote
runbook, and covers the on-prem/Coolify semver tag-pinning (bl-301 tie-in). UK English
throughout (DD/MM/YYYY, “colour”, “organisation”, “behaviour”). ASCII only.
OQ-113-1 is RESOLVED upstream (Orchestrator, config-evidenced; ratified by Liam, see ledger ID-113.2 journal): Option A — client-agnostic version-named release branches (
release/vX.Y.Z) fast-forwarded tovX.Y.Ztags; each per-client Vercel project pins its Production Branch to a release branch and opts in via its dashboard to the SAME repo-named"Vercel - knowledge-hub: ci-summary"deploy-gate check. This TECH does NOT re-open the mechanism; it specifies how to implement it.
Context
Section titled “Context”This Task adds the release/version-pinning layer on top of the ratified ID-95
per-client topology (own Vercel project + own Supabase + branding from the client’s own
tenant_config at build time). PRODUCT (PRODUCT.md) defines the behaviour; this TECH is
the implementation plan. There is almost no new application CODE here: the layer is
overwhelmingly git ref convention + Vercel/Coolify project settings + a CI workflow
delta + a private docs-site crib + a runbook. The single application-source touch point
is a build-time semver stamp (see PI-1 / PI-8 below). Everything else is process and
configuration, which is why the verification surface is git/Vercel/Coolify state and
operator runbook behaviour, not a runtime call graph.
Code-intelligence orientation
Section titled “Code-intelligence orientation”Per the code-intelligence discipline, orientation ran before authoring. The GitNexus index
is STALE (last indexed at 69b7bd0); per the dispatch brief, exact file:line claims were
grounded with bun scripts/ast-dataflow-cli.ts / grep / direct file reads rather than
the stale graph, with GitNexus reserved for graph-level orientation. The graph-level
orientation (re-cited verbatim from the ratified {113.1}/{113.2} predecessors, which
ran it live) returned "processes": [] for the release/versioning concept —
versioning/release is a config-and-CI concern, not an indexed runtime call graph — and
surfaced only the build-time branding leaves (runClientBrandingFetch,
renderClientBrandingMap), both confirmed leaf build-time scripts with no runtime callers.
The release/versioning surface itself is genuinely greenfield in the repo:
gitnexus orientation: no existing symbols match -- greenfield surface.
Grounding (file:line verified live for this TECH, not paraphrased from predecessors):
vercel.json:1-91— carriesbuildCommand/installCommand/framework(:3-5),regions: ["lhr1"](:41), securityheaders,functionsmaxDuration map, andcrons. Nogit/branch/productionBranchkey anywhere — confirming the pin is a Vercel project dashboard setting, not a repo file. This grounds PI-3 (the pin is not committed) and PI-5 (no client identity invercel.json).package.json:11,13,15,16,59—build=next build(:11);prebuildchainsgenerate:skills -> generate:content-type-values -> generate:client-branding -> generate:branding(:13);generate:client-branding=fetch-client-branding.ts(:15);generate:branding=generate-client-branding-map.ts(:16);build:vercelis the full Vercel orchestration ending innext build(:59). This is where a build-time semver stamp attaches (PI-1/PI-8).lib/client-config.ts:604-627—loadBranding(idOverride?)resolvesCLIENT_BRANDING_MAP[id] ?? CLIENT_BRANDING_MAP.default(:605-606), schema-validates, contrast-checks, andBRANDINGis computed once at module init (:629). No runtime Supabase query — config is baked into the build. Grounds PI-8/PI-11 (served behaviour is fixed at build time; no runtime version axis).ci.yml:1358-1368(WP-G4.7) — the deploy-gate stepNotify Vercel -- ci-summary status,if: always(),uses: vercel/repository-dispatch/actions/status@30f760c..., withname: "Vercel - knowledge-hub: ci-summary"(:1367, verified verbatim). The inline comment confirms: “project must opt in to the named check via the Vercel dashboard”. This is the deploy-gate hazard surface for PI-9 and the multi-project opt-in mechanism for OQ-113-1’s resolution.onprem-deploy.yml:104-108—env.IMAGE = ghcr.io/${{ github.repository_owner }}/kh-cocoindex-pipeline(:107),env.TAG = sha-${{ github.sha }}(:108), with the documented prior-tag rollback (“re-point Coolify to a prior sha- tag; no rebuild”,:104-106).onprem-deploy.yml:239-247— the actual publish:pack build "${IMAGE}:${TAG}" --path ... --cache-image "${IMAGE}:buildcache" ... --publishthenecho "Published ${IMAGE}:${TAG}". This is the precise site where a secondvX.Y.Zimage tag is added (PI-10). The Coolify bump is a PATCH ofCOCOINDEX_IMAGE_TAGatonprem-deploy.yml:343-367(prod) /:448-472(staging); the workflow triggers onpush: [main, staging](:92-96), branch-keyed.supabase/migrations/20260613090000_id95_5_tenant_config.sql:36-42—tenant_configsingleton (id boolean PRIMARY KEY DEFAULT true), RLS-closed, service-role-only; config-as-data, never set in migrations. Grounds PI-11.- Ref state (verified live):
git tag --listshows onlyv0.2.0-task-view(a vendored sub-project tag) — no platformvX.Y.Ztags;git branch -rshows norelease/*branches. Greenfield convention; no existing-ref migration required (PI-1/PI-2/PI-5).
Empirical-verification note (Q-EX2 scope). This TECH cites no external-library API
symbols requiring an import-and-call check. The verification surface is git ref / Vercel
project-setting / Coolify image-tag configuration / GitHub Actions workflow YAML and
operator runbook behaviour. The two mechanism-dependent empirical claims (Vercel Production
Branch can pin an arbitrary release/* per project; one repo-named ci-summary check can
gate N per-client projects via per-project dashboard opt-in) were resolved as OQ-113-1 by
the Orchestrator from config evidence (ci.yml:1358-1368 + vercel.json + the
vercel/repository-dispatch action semantics) and ratified by Liam before this TECH. The
only residual live-dashboard check (multi-project opt-in sanity check at first standup) is
carried as a NON-blocking runbook step, not a TECH blocker.
TECH-owned open decisions: resolution
Section titled “TECH-owned open decisions: resolution”PRODUCT carried OQ-113-2 and OQ-113-3 as “TECH proposes; operator ratifies”. This TECH PROPOSES both; they remain operator-ratifiable. (OQ-113-1 was Orchestrator-resolved before TECH; OQ-113-4/5/6 are operator-preference and resolved below where this TECH can pick a default, else carried.)
OQ-113-2 RESOLVED (proposed): migrate-DB-first, then repoint-Vercel
Section titled “OQ-113-2 RESOLVED (proposed): migrate-DB-first, then repoint-Vercel”Decision: migrate the client’s Supabase to the target schema BEFORE repointing the Vercel Production Branch. Justification, grounded in the build-time model:
- Forward-only migrations are additive-by-discipline (see OQ-113-3) and old code tolerates
the new schema. KH migrations are forward-only (no
down); the project convention is that a migration adds columns/tables/functions without removing what the currently-pinned (older) code reads. So a client running release A against a schema already migrated to B’s shape continues to work: A’s code reads the columns it always read; B’s new columns sit unused until B’s code ships. The migrate-first window is therefore a compatibility window, not a downtime window — the live (older) deploy keeps serving correctly while the DB is ahead. - The reverse ordering (repoint-first) creates a genuine skew fault. If Vercel is
repointed to B before the DB is migrated, B’s code (which may read a column/table B’s
migration introduces) runs against a schema that lacks it — a hard runtime failure
(missing column / missing RPC) for the duration of the window. Because branding/config and
code are baked at build time (
lib/client-config.ts:604-627, no runtime negotiation), there is no graceful-degradation path: B’s build expects B’s schema. - The cutover window is bounded and observable. Migrate-first means: (1) apply the migration set to the client’s Supabase, (2) verify schema parity, (3) repoint + redeploy, (4) smoke-test. The only window where schema is “ahead” of code is steps 1-3, during which the OLD deploy serves correctly. This is the safe direction.
Consequence for PI-12: the crib upgrade-history row records the ordering taken (migrate-first) and the migration delta, so schema/code pairing is never left silently skewed. The runbook (below) enforces migrate-first as an ordered checklist.
Carried sub-decision -> OQ-113-7 (new, below): a migration that is NOT old-code-tolerant (a destructive/renaming migration) breaks the migrate-first safety argument. This TECH constrains such migrations out of the normal path via OQ-113-3 + the new OQ-113-7.
OQ-113-3 RESOLVED (proposed): roll-forward-only is the supported posture; rollback-unsafe migrations are gated to MAJOR + flagged irreversible
Section titled “OQ-113-3 RESOLVED (proposed): roll-forward-only is the supported posture; rollback-unsafe migrations are gated to MAJOR + flagged irreversible”Supported rollback posture:
-
Code-only rollback is always supported and trivial. Repoint the Vercel Production Branch back to the prior
release/vX.Y.(Z-1)+ redeploy; repoint CoolifyCOCOINDEX_IMAGE_TAGto the priorvX.Y.Ztag, no rebuild (the existingonprem-deploy.yml:104-106ergonomics). This is the supported path when the release was migration-additive (the common case). -
Across a forward-only DB migration, the supported posture is ROLL-FORWARD-ONLY. KH migrations have no
down; therefore a DB schema change is NOT auto-reverted. If a release that included a forward-only migration misbehaves, the supported remediation is to cut and roll FORWARD to avX.Y.(Z+1)PATCH that fixes the defect at the new schema — NOT to roll the schema back. -
Migration-additive discipline keeps code rollback clean. To keep posture (1) viable across most upgrades, migrations SHOULD be additive/back-compatible (add, do not remove/rename in the same release the consuming code ships). When additive, code rollback (1) is fully safe because the prior code tolerates the superset schema.
-
A rollback-UNSAFE migration (destructive/renaming) is gated to a MAJOR release and flagged irreversible in the crib. Where a destructive migration is unavoidable, it is a MAJOR bump (PI-1’s MAJOR = breaking/non-rollback-safe), and the crib upgrade-history row records the irreversibility explicitly (no clean code-rollback exists past that point).
NOT supported (stated explicitly per the brief):
- Automatic DB rollback /
downmigrations — NOT supported. KH has no down-migration mechanism; nothing in this layer adds one. - Snapshot-restore as a routine rollback step — NOT supported as the primary path. A point-in-time restore of the client’s Supabase is an emergency, manual, out-of-band operation (data-loss window for anything written after the snapshot); it is named here as a last-resort escape hatch ONLY, never the designed rollback. The designed rollback is code-repoint (1) for additive releases and roll-forward (2) across forward-only migrations.
- Compensating-migration as an automated step — NOT supported. A compensating migration (a new forward migration that undoes a prior one’s effect) is, mechanically, just a roll-forward PATCH (posture 2); it is allowed as such but there is no special “compensating rollback” machinery.
Consequence for PI-1 / PI-12: MAJOR is the version class for any non-rollback-safe change; the crib records irreversibility. Couples to OQ-113-7.
Proposed changes (1:1 against PRODUCT’s PI-N)
Section titled “Proposed changes (1:1 against PRODUCT’s PI-N)”Each block names the concrete mechanism, the files/settings touched, and how the change is verifiable. Most blocks are config/process; the few that touch source name exact files.
PI-1 — immutable semver vX.Y.Z cut at known-good main (tag = source of truth)
Section titled “PI-1 — immutable semver vX.Y.Z cut at known-good main (tag = source of truth)”Mechanism (git + CI precondition): A release is git tag vX.Y.Z <main-sha> at a main
commit whose "Vercel - knowledge-hub: ci-summary" deploy-gate (ci.yml:1367) is green.
The tag is the immutable source of truth and is never moved; a fresh defect ships as a new
PATCH tag, never a retag. The cut runbook (below) records the green-ci-summary
precondition as step 1.
Optional source touch (build-time version stamp): to make a deploy self-report its
version (supports PI-8 verification and operator smoke-tests), add a build-time injected
NEXT_PUBLIC_RELEASE_VERSION (read from the vX.Y.Z ref at build) surfaced on an existing
health/version endpoint. This is the ONLY application-source change in the Task and is
OPTIONAL (carried as OQ-113-8); the layer is correct without it (the Vercel dashboard
Production Branch already answers “what version” — Option A’s stated advantage).
Verify: every vX.Y.Z tag resolves to a main ancestor (git merge-base --is-ancestor);
the cut runbook entry records the green ci-summary precondition; no tag is ever re-pointed
(audited by git tag immutability + the changelog crib).
PI-2 — releases cut from main only
Section titled “PI-2 — releases cut from main only”Mechanism (git discipline, enforced by runbook + verification): the cut runbook tags
only a main first-parent ancestor; never staging (deploy-only) and never an unmerged
worker branch. Optional CI guard: a release-tag-push workflow that asserts
git merge-base --is-ancestor <tag-sha> origin/main and fails the tag push otherwise
(carried as OQ-113-9, low priority — the runbook check is sufficient at current scale).
Verify: git merge-base --is-ancestor <release-commit> main succeeds for every release;
no vX.Y.Z tag points at a commit absent from main.
PI-3 — client deploy pinned to a release, never main-tracking
Section titled “PI-3 — client deploy pinned to a release, never main-tracking”Mechanism (Vercel project setting — Option A): each knowledge-hub-{client} Vercel
project sets Settings -> Git -> Production Branch = release/vX.Y.Z (the client-agnostic
version-named release branch). This is a dashboard setting, NOT a vercel.json key
(vercel.json carries no branch config, :1-91). A merge to main does not touch any
release/* branch, so a pinned client never redeploys on a main merge. The release branch
is fast-forward-only to its vX.Y.Z tag (the tag is the source of truth; the branch is the
Vercel handle).
Verify: a no-op merge to main triggers no client production redeploy; each client
project’s Production Branch resolves to exactly one release/vX.Y.Z (one Vercel dashboard
line per client); no client project tracks main or a rolling branch tip.
PI-4 — upgrade is a deliberate repoint; never automatic
Section titled “PI-4 — upgrade is a deliberate repoint; never automatic”Mechanism: upgrade = an operator changes the client project’s Production Branch from
release/vA to release/vB (one dashboard dropdown / one Vercel API call) + redeploy. No
automation advances it: cutting release/v(Z+1) creates a NEW branch and never touches any
existing client project’s Production Branch setting. There is no webhook, cron, or CI step
in this design that repoints a client.
Verify: cutting vX.Y.(Z+1) leaves every existing client still serving its prior pinned
version; the only path that changes a served version is the recorded repoint step in the
upgrade runbook; no automated-upgrade mechanism exists anywhere in the design.
PI-5 — every pin ref in the public repo is client-agnostic / version-named
Section titled “PI-5 — every pin ref in the public repo is client-agnostic / version-named”Mechanism: release refs are release/vX.Y.Z (branch) and vX.Y.Z (tag) — VERSION
only, never client identity. The client->version association lives ONLY in the private
docs-site crib (PI-7). This extends the ID-95 PI-7 no-client-literal discipline to the
release layer and is subject to the same private denylist ID-95 PI-9 enforces on migrations.
Verify: git for-each-ref over the public repo’s branches+tags greps clean of any
client-name/client-domain token (private denylist); no pin ref name identifies a client; no
committed file in the public repo maps a client to a version.
PI-6 — public repo cannot answer “which client runs this version?”
Section titled “PI-6 — public repo cannot answer “which client runs this version?””Mechanism: the public repo holds release artefacts (refs/tags/code) but NOT the client->version association, which is private (PI-7). This layer adds zero ID-68-flip blockers: it is a client-agnostic process/config layer on an already client-agnostic repo.
Verify: no committed artefact in the public repo maps client->version; client identities
appear only in the private docs-site crib; git ls-files in the public repo shows no
client->version map.
PI-7 — private docs-site crib is the single source of truth for client->version
Section titled “PI-7 — private docs-site crib is the single source of truth for client->version”Mechanism (new private docs-site artefact): create ONE committed crib in the PRIVATE
docs-site holding the client->version table + append-only upgrade history. Location
decision (resolves OQ-113-5): co-locate with the existing per-client ops crib at
${KH_PRIVATE_DOCS_DIR}/src/content/docs/runbooks/client-app-deploy.md is the natural
single-ops-surface, BUT to keep the per-client pin table append-only and scannable, this
TECH places the table in a dedicated sibling
${KH_PRIVATE_DOCS_DIR}/src/content/docs/runbooks/per-client-release-pins.md and
cross-links it bidirectionally with client-app-deploy.md. (Single authoritative location;
co-located neighbourhood.) Columns (per row, client-keyed): client, Vercel project, pinned
release vX.Y.Z, Supabase project ref, pinned on-prem COCOINDEX_IMAGE_TAG (where
applicable), last-upgrade date; plus a per-client append-only upgrade-history list
(from->to version, date, migration delta, ordering taken). The cut/upgrade runbook updates
the crib row in the SAME step as the repoint.
Verify: exactly one crib location holds the client->version table; every live client pin
(Vercel Production Branch + Coolify COCOINDEX_IMAGE_TAG) matches its crib row; a repoint
without a same-step crib update is a runbook-checklist failure; the crib lives in the
private docs-site (git ls-files in the public repo shows none).
PI-8 — served behaviour fully determined by pinned version + own tenant_config
Section titled “PI-8 — served behaviour fully determined by pinned version + own tenant_config”Mechanism (no change — assertion grounded in build-time model): code is fixed by the
pinned release/vX.Y.Z (Vercel build of that ref) and config/branding by the client’s own
Supabase tenant_config baked at build time (lib/client-config.ts:604-627,
loadBranding reads CLIENT_BRANDING_MAP only, no runtime Supabase query). No third
version axis; no runtime version negotiation. The optional NEXT_PUBLIC_RELEASE_VERSION
stamp (PI-1) makes the code-version axis observable.
Verify: a client deploy at a known pin renders branding hydrated from its own
tenant_config (ID-95 PI-11 AC-E3 sequence), code matching the pinned vX.Y.Z; no runtime
Supabase version query in the resolve path.
PI-9 — release/rename/repoint never silently un-gates production
Section titled “PI-9 — release/rename/repoint never silently un-gates production”Mechanism (ordered deploy-gate verification — echoes ID-95 {95.10}): the deploy-gate
status is the single repo-named "Vercel - knowledge-hub: ci-summary"
(ci.yml:1367, posted via vercel/repository-dispatch, if: always(), client-agnostic).
Per the resolved OQ-113-1, each per-client Vercel project opts in to that SAME named check
via its dashboard (Settings -> Git -> required checks); the per-project opt-in is a runbook
step, NOT a client-named check (PI-7-clean). Because the check name is repo-named and shared,
N per-client projects gate on one CI dispatch — but a project rename or required-check
mis-name on EITHER side (the CI dispatch name: OR the Vercel dashboard required-check
string) silently un-gates that project. The runbook makes the deploy-gate verification an
ORDERED step that precedes any rename/repoint.
Verify: after any release/rename/repoint the named required-check still gates the client
project’s production deploys on BOTH the CI dispatch side (ci.yml:1367 unchanged) and the
Vercel dashboard side; the runbook records the deploy-gate verification as a completed step
strictly BEFORE any rename/repoint; a deliberately mismatched check name blocks the prod
deploy (not silently allowed through).
PI-10 — same semver pins the on-prem/Coolify pipeline image (bl-301 tie-in)
Section titled “PI-10 — same semver pins the on-prem/Coolify pipeline image (bl-301 tie-in)”Mechanism (CI workflow delta + Coolify setting):
- Add a
vX.Y.Zimage tag at release cut. Todayonprem-deploy.yml:239-247publishes${IMAGE}:${TAG}whereTAG = sha-${{ github.sha }}(:108). The versioning extension tags the SAME built image withvX.Y.Zin addition to thesha-tag. Two implementation options (TECH names both; the cut runbook picks):- (a) Re-tag-and-push at cut (no rebuild): on a release-tag-push event,
docker pull ${IMAGE}:sha-<sha>(the already-published image for the tagged commit),docker tagit${IMAGE}:vX.Y.Z,docker push. Preferred — reuses the content-addressable image; the semver tag is a pure alias of the known-goodsha-image. - (b) Add
${IMAGE}:vX.Y.Zto thepack build ... --publishtag set when the build runs on a release ref. Rejected as primary because the existing build ismain/stagingpush-keyed (onprem-deploy.yml:92-96), not release-keyed; (a) avoids re-architecting the trigger.
- (a) Re-tag-and-push at cut (no rebuild): on a release-tag-push event,
- Pin Coolify
COCOINDEX_IMAGE_TAGtovX.Y.Z. A client’s (and, per bl-301, the platform’s own) Coolify deployment setsCOCOINDEX_IMAGE_TAGto a deliberatevX.Y.Z(the same PATCH-in-place mechanism atonprem-deploy.yml:343-367, but to a semver tag, set as a deliberate operator/runbook step rather than the auto-bump-to-sha-<sha>that themain-push path performs). The upgrade flow is then identical in SHAPE to Vercel: a deliberate repoint to a newer version, preserving the prior-tag rollback ergonomics (:104-106).
Verify: a release cut produces a vX.Y.Z-tagged pipeline image as well as the sha- tag
(both pullable from ghcr.io); a client’s COCOINDEX_IMAGE_TAG resolves to a vX.Y.Z semver
(not a rolling main sha) for any client whose contract includes ingestion; the prior-tag
rollback still works (repoint Coolify to a prior vX.Y.Z, no rebuild).
PI-11 — branding/config from the client’s own tenant_config regardless of version
Section titled “PI-11 — branding/config from the client’s own tenant_config regardless of version”Mechanism (no change — non-regression assertion): the version-pinning layer touches
refs/tags/Vercel-settings/Coolify-tags only; it does NOT change the branding source path.
Every pinned version still hydrates branding at build time from the client’s own Supabase
tenant_config (scripts/fetch-client-branding.ts -> lib/branding/clients/<id>.json ->
generate-client-branding-map.ts -> lib/client-config.ts loadBranding), fail-closed
(ID-95 PI-10/PI-11). No version introduces a per-client code path, a committed branding
artefact, or a runtime branding query.
Verify: a client deploy at any pinned vX.Y.Z resolves branding via the ID-95 build-time
fetch (no client-branding/ dir in any repo; no if (client === 'x') branch in app source;
AC-E3 branded-render check passes); the pinning layer never touches the branding source path.
PI-12 — every client upgrade auditable from the crib’s append-only history
Section titled “PI-12 — every client upgrade auditable from the crib’s append-only history”Mechanism: the crib upgrade-history (PI-7) is append-only — each upgrade appends a row (from->to version, date, migration delta, ordering taken per OQ-113-2). The cut/upgrade runbook’s final step appends this row in the same act as the repoint, so schema/code pairing is never left silently skewed. “Which client moved to which version, and when” is one crib lookup.
Verify: the crib upgrade history is append-only (no row overwritten); each upgrade row carries from/to version + date + migration delta + ordering; an upgrade leaving schema/code skewed without a recorded remediation is a runbook-checklist failure.
The cut / promote runbook (TECH-formalised; lands in the docs-site)
Section titled “The cut / promote runbook (TECH-formalised; lands in the docs-site)”The runbook below is the operational deliverable (lands at
runbooks/per-client-release-pins.md alongside the crib, or client-app-deploy.md — one
ops surface). It formalises RESEARCH section 5 with the resolved orderings baked in.
Cut a release (platform owner, at known-good main)
Section titled “Cut a release (platform owner, at known-good main)”- Verify
mainknown-good: the"Vercel - knowledge-hub: ci-summary"gate is green at the target sha (PI-1 precondition). - Choose the semver bump: MAJOR = breaking client-facing OR non-rollback-safe migration (per OQ-113-3); MINOR = additive/back-compatible; PATCH = fix-only. Record rationale in the changelog crib.
git tag vX.Y.Z <known-good-main-sha>and push the tag (immutable source of truth, PI-1).- Create/fast-forward
release/vX.Y.Zto the tag commit; push (Vercel handle, FF-only, PI-3). - If the release touches the ingestion pipeline: re-tag the
sha-<sha>image asvX.Y.Zand push (PI-10 option (a)). - Record the release in the changelog crib (version, sha, date, notable changes, migration deltas, rollback-safety classification per OQ-113-3).
Upgrade a client (deliberate; migrate-DB-first per OQ-113-2)
Section titled “Upgrade a client (deliberate; migrate-DB-first per OQ-113-2)”- Pre-flight: review migration deltas between the client’s current pin and target
vX.Y.Z; confirm rollback-safety classification (OQ-113-3); confirm the migration set is old-code-tolerant (OQ-113-7) or flag the upgrade irreversible. - Migrate the client’s Supabase to the target schema FIRST (OQ-113-2). Verify schema parity. The OLD deploy keeps serving correctly during this window (compatibility window, not downtime).
- Deploy-gate verification BEFORE any repoint/rename (PI-9, ordered): confirm the
ci-summaryrequired check is intact on BOTH the CI dispatch side and the client project’s Vercel dashboard side. - Repoint the client’s Vercel Production Branch to
release/vX.Y.Z; trigger redeploy (push to the release branch, or Vercel redeploy). - If the client runs the ingestion pipeline: repoint Coolify
COCOINDEX_IMAGE_TAGtovX.Y.Z(PI-10) + Coolify deploy. - Verify: deploy-gate passed for the release branch; smoke-test the client URL (branded
render, AC-E3; optional
NEXT_PUBLIC_RELEASE_VERSIONmatchesvX.Y.Z). - Update the crib row in the SAME step (PI-7/PI-12): new pinned version + upgrade date, appending to the append-only history (from->to, date, migration delta, ordering taken).
- Rollback (if needed): code-repoint to prior
release/vX.Y.(Z-1)+ redeploy (and Coolify prior-tag) for additive releases; roll FORWARD to a fix PATCH across a forward-only migration (OQ-113-3). Record the remediation in the crib.
Testing and validation
Section titled “Testing and validation”The verification surface is git/Vercel/Coolify/docs-site state and operator runbook
behaviour, not a runtime test suite (no new application logic ships except the optional
version stamp). Each PI maps to a concrete, mostly operator-executable check; bun run test
covers only the optional stamp if added.
| Invariant | Verification |
|---|---|
| PI-1 | git merge-base --is-ancestor <tag> main; tag immutability (no retag in git reflog/changelog); cut-runbook records green ci-summary. |
| PI-2 | every vX.Y.Z resolves to a main ancestor; optional OQ-113-9 CI guard fails a non-main tag push. |
| PI-3 | no-op main merge triggers no client redeploy; each Vercel project Production Branch = one release/vX.Y.Z; none tracks main. |
| PI-4 | cutting v(Z+1) leaves existing clients on their pins; no automated-upgrade mechanism in the design. |
| PI-5 | git for-each-ref greps clean of client tokens (private denylist); no client->version file in the public repo. |
| PI-6 | git ls-files (public) shows no client->version map; client identities only in private crib. |
| PI-7 | exactly one crib location; live Vercel + Coolify pins match crib rows; repoint-without-crib-update is a checklist failure. |
| PI-8 | branded render at a known pin (AC-E3); code matches vX.Y.Z; no runtime Supabase version query (grep resolve path). |
| PI-9 | post-rename/repoint the named check gates on both sides; a mismatched name BLOCKS prod (negative test on a throwaway project at standup). |
| PI-10 | release cut produces both vX.Y.Z + sha- tags (pullable); client COCOINDEX_IMAGE_TAG is a semver; prior-tag rollback works. |
| PI-11 | branded render via ID-95 build-time fetch at any pin; no client-branding/ dir, no if (client === 'x'); AC-E3 passes. |
| PI-12 | crib history append-only (no overwrite); each row has from/to + date + delta + ordering. |
If the optional NEXT_PUBLIC_RELEASE_VERSION stamp (OQ-113-8) ships: add a
behaviour-first test (reference test-philosophy.md) asserting the version endpoint returns
the build-injected vX.Y.Z; run via bun run test (never bun test).
Risks and mitigations
Section titled “Risks and mitigations”- Deploy-gate silent un-gate (PI-9, the headline hazard). A Vercel project rename or a
dashboard required-check mis-name silently un-gates prod. Mitigation: ordered
deploy-gate verification BEFORE any rename/repoint (runbook step 3), echoing ID-95
{95.10}; a negative test at standup (deliberately mismatched name must BLOCK). - Schema/code skew on the wrong upgrade ordering (OQ-113-2). Repoint-first against an un-migrated DB is a hard runtime failure with no graceful path (build-time-baked config). Mitigation: migrate-DB-first is the enforced runbook ordering; the OLD deploy serves during the compatibility window.
- Forward-only migration with no clean rollback (OQ-113-3 / OQ-113-7). A destructive migration past which code-rollback is unsafe. Mitigation: gate such migrations to MAJOR, flag irreversible in the crib, default to additive/old-code-tolerant migrations; roll FORWARD to a fix PATCH rather than rolling the schema back. Snapshot-restore is a last-resort escape hatch ONLY, never the designed path.
- Tag/branch drift (
vX.Y.Ztag vsrelease/vX.Y.Zbranch). Two refs at one commit can diverge. Mitigation: the branch is fast-forward-only to the tag (cut runbook step 4); the tag is the source of truth. - bl-301 sequencing. If the platform-owned cocoindex pipeline (bl-301) stands up before
this convention is set, it pins a rolling
mainsha and must be re-pinned. Mitigation: ship the convention (this Task) before bl-301 standup; bl-301 adopts semver-tag pinning from day one. No hard ordering, but noted in the crib.
Open decisions for ratification (residual + new)
Section titled “Open decisions for ratification (residual + new)”OQ-113-1 resolved upstream (Option A, Liam-ratified). OQ-113-2/OQ-113-3 proposed above (operator-ratifiable). The following are carried/new for Liam:
- OQ-113-4 (carried, lower priority) — upgrade cadence / staleness signal. Purely platform-owner-initiated, or a client-facing “N versions behind” signal? Affects whether the crib needs a “latest available” column + staleness policy. Default proposed: platform-owner-initiated only; no staleness column at v1.
- OQ-113-5 RESOLVED (proposed) — crib location. Dedicated
runbooks/per-client-release-pins.md, bidirectionally cross-linked with the existingrunbooks/client-app-deploy.md. Operator may instead fold it intoclient-app-deploy.mdas one surface; PI-7 requires only one authoritative location. Operator confirms. - OQ-113-6 (carried) — pre-release / staging channel. Do clients ever pin to a
pre-release (
release/vX.Y.Z-rc.1) for UAT before a GA upgrade? Affects the naming convention (semver pre-release suffix). Default proposed: GA-only at v1; the-rc.Nsuffix is reserved (semver-compatible) for future use, no machinery built now. - OQ-113-7 (NEW) — old-code-tolerance contract for migrate-first. The migrate-DB-first safety argument (OQ-113-2) assumes the migration is old-code-tolerant (additive). Should this be a hard release-classification gate (every release’s migration set is classified “old-code-tolerant: yes/no” at cut, with “no” forcing a coordinated cutover + MAJOR)? Proposed: yes — classify at cut (runbook step 6); couples to OQ-113-3.
- OQ-113-8 (NEW, optional) — build-time
NEXT_PUBLIC_RELEASE_VERSIONstamp. Ship the optional version stamp (the only application-source change) so a deploy self-reports itsvX.Y.Zon a health/version endpoint? Proposed: nice-to-have, not required (the Vercel dashboard Production Branch already answers “what version”); defer to a follow-up unless Liam wants in-app observability now. - OQ-113-9 (NEW, low priority) — CI guard on release-tag pushes. Add a workflow
asserting a pushed
vX.Y.Ztag is amainancestor (PI-2 hard enforcement) vs relying on the runbook check? Proposed: runbook check suffices at current scale; add the guard when release frequency rises.