From 606eea58e2eb6f9bff6b47f4a33e6c70c1a9eb35 Mon Sep 17 00:00:00 2001 From: yaowenc2 Date: Wed, 12 Aug 2026 12:42:22 -0700 Subject: [PATCH 1/2] chore: mark Neon paths retired in messaging; keep the code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neon is no longer used by any environment — postgres is 100% insta-db backed. This sweep changes wording only; no code path, test, or command is removed. User-visible (must not name providers we don't use): - db limits/volume 'no manageable instance' info() lines drop 'Neon-backed', keep the reason - 'services upgrade' help text: compute-only, points postgres at 'insta db limits' (the platform rejects postgres upgrades with 400) Internal (must say it explicitly): - comments in db.ts/metrics.ts, CONTRIBUTING.md, and test/limits.test.ts now state Neon is no longer used and the handling code is retained, not live Co-Authored-By: Claude Opus 5 (1M context) --- CONTRIBUTING.md | 3 ++- src/commands/db.ts | 30 ++++++++++++++++++------------ src/commands/metrics.ts | 4 +++- src/index.ts | 2 +- test/limits.test.ts | 13 ++++++++----- 5 files changed, 32 insertions(+), 20 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index baee0d8..ab90d89 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -94,7 +94,8 @@ persisted config, so a local control plane gives you an end-to-end loop without cloud infrastructure. The platform has a `dev:fake` mode that swaps in fake provider adapters, so it needs no -Neon, Fly or Tigris credentials: +Fly or Tigris credentials (nor Neon's — Neon is no longer used by any environment; its +adapter code is retained, not live): ```bash # 1. Postgres for the control plane itself diff --git a/src/commands/db.ts b/src/commands/db.ts index 9523fe9..53e0002 100644 --- a/src/commands/db.ts +++ b/src/commands/db.ts @@ -7,8 +7,9 @@ type Opts = { branch?: string; group?: string; json?: boolean } // Toggle a postgres service between scale-to-zero (the default: instance suspends when idle, // cold-starts on the next connection) and always-on (instance stays warm; idle RAM bills at // actual usage). Thin wrapper over PATCH /database/settings {scaleToZero} — insta-db-backed -// postgres only; Neon-backed services manage their own autosuspend and the platform returns an -// error for them. +// postgres only. Legacy Neon path: Neon is no longer used by any environment (postgres is 100% +// insta-db) and this code is retained, not live — Neon-backed services managed their own +// autosuspend and the platform returned an error for them. export async function dbAlwaysOn(mode: string, opts: Opts): Promise { if (mode !== 'on' && mode !== 'off') throw new Error('mode must be on|off') const api = await ApiClient.load() @@ -46,10 +47,12 @@ export function fmtMib(mib: number): string { } // The read outcome, as a seam. rawRequest THROWS ApiError on any status >= 400 (api.ts — it only -// differs from request in returning {status,body} below 400, for 202 branching), so the Neon case -// and the friendly wrapping must live in a catch, not in status branching on the return value — -// branches on res.status >= 400 after rawRequest are unreachable. Takes the client as an argument -// so tests drive it with a stub, per this repo's pure-seam convention. +// differs from request in returning {status,body} below 400, for 202 branching), so the soft +// no-instance case and the friendly wrapping must live in a catch, not in status branching on the +// return value — branches on res.status >= 400 after rawRequest are unreachable. That soft case is +// the legacy Neon path: Neon is no longer used by any environment; the handling is retained, not +// live. Takes the client as an argument so tests drive it with a stub, per this repo's pure-seam +// convention. export type DbInstanceRead = { kind: 'ok'; body: any } | { kind: 'no-instance' } export async function fetchDbInstance( @@ -62,7 +65,8 @@ export async function fetchDbInstance( return { kind: 'ok', body: res.body } } catch (e) { // The platform answers a provider-shaped 502 for services with no manageable instance - // (Neon-backed): a soft case, not a failure. Everything else stays an error — an expired + // (the legacy Neon path — Neon is no longer used by any environment; this branch is retained, + // not live): a soft case, not a failure. Everything else stays an error — an expired // token must not render as "no ceiling set" — but wrapped so the user sees what failed. if (e instanceof ApiError && e.status === 502) return { kind: 'no-instance' } if (e instanceof ApiError) throw new Error(`reading the instance failed (${e.status}): ${e.message}`) @@ -85,7 +89,7 @@ export async function dbLimits(opts: Opts & { cpu?: string; memory?: string }): if (!opts.cpu && !opts.memory) { const read = await fetchDbInstance(api, p.projectId, suffix) if (read.kind === 'no-instance') { - info(`postgres ${opts.group ?? 'default'}: no manageable instance (Neon-backed services manage their own resources)`) + info(`postgres ${opts.group ?? 'default'}: no manageable instance (this service manages its own resources)`) return } if (opts.json) return printJson(read.body) @@ -160,9 +164,11 @@ export function dbStatsLines(group: string, body: any): string[] { // Point-in-time stats snapshot for a postgres service: connections vs the server's ceiling, cache // hit rate, database size. Read-only. insta-db-backed: a suspended instance answers from the -// provider's control plane (shown as "(suspended)" with structural zeros), never dialed. -// Neon-backed: the platform reads over a direct SQL connection, so a one-shot call may wake a -// suspended endpoint — acceptable for an explicit command, which is why nothing here polls. +// provider's control plane (shown as "(suspended)" with structural zeros), never dialed. That is +// every environment today — the Neon-backed contrast below is historical: Neon is no longer used +// anywhere, and the code that handled it is retained, not live. Neon-backed: the platform read +// over a direct SQL connection, so a one-shot call could wake a suspended endpoint — acceptable +// for an explicit command, which is why nothing here polls. export async function dbStats(opts: Opts): Promise { const api = await ApiClient.load() const p = await requireProject() @@ -202,7 +208,7 @@ export async function dbVolume(opts: Opts & { size?: string }): Promise { if (!opts.size) { const read = await fetchDbInstance(api, p.projectId, suffix) if (read.kind === 'no-instance') { - info(`postgres ${opts.group ?? 'default'}: no manageable instance (Neon-backed services manage their own storage)`) + info(`postgres ${opts.group ?? 'default'}: no manageable instance (this service manages its own storage)`) return } if (opts.json) return printJson(read.body) diff --git a/src/commands/metrics.ts b/src/commands/metrics.ts index de23f37..9868fc9 100644 --- a/src/commands/metrics.ts +++ b/src/commands/metrics.ts @@ -99,7 +99,9 @@ function printDimensions(dims: Dim[]): void { // insta usage — usage across the 5 billing dimensions (cpu/memory/volume/egress/storage) for the // current billing cycle. Shows the whole ORG by default (with a per-project breakdown); pass --proj -// [id] for a single project (the linked one, or a given id). Billed dimensions, not raw fly/neon meters. +// [id] for a single project (the linked one, or a given id). Billed dimensions, not raw provider +// meters. (Historical: those were fly/neon meters — Neon is no longer used by any environment, +// though the adapter code is retained, not live.) export async function usage(opts: { from?: string; to?: string; json?: boolean; proj?: string | boolean }): Promise { const api = await ApiClient.load() const p = await requireProject() diff --git a/src/index.ts b/src/index.ts index ed8488c..c11f23e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -137,7 +137,7 @@ svc.command('set-access ').description('Set a storage serv .option('--json').action(guard((type, name, access, o) => services.servicesSetAccess(type, name, access, o))) svc.command('scale [region]').description('Set a compute service machine count (paid plans only)') .option('--json').option('--branch ', 'branch (default: current)').action(guard((type, name, number, region, o) => services.servicesScale(type, name, number, region, o))) -svc.command('upgrade ').description('Change a compute/postgres service spec (paid plans only)') +svc.command('upgrade ').description('Change a compute service spec (paid plans only; for postgres resources use `insta db limits`)') .option('--json').option('--branch ', 'branch (default: current)').action(guard((type, name, spec, o) => services.servicesUpgrade(type, name, spec, o))) svc.command('secrets ').description("List a service's secret names") .option('--branch ').option('--json').action(guard((type, name, o) => services.servicesSecrets(type, name, o))) diff --git a/test/limits.test.ts b/test/limits.test.ts index c63d275..fe04b9f 100644 --- a/test/limits.test.ts +++ b/test/limits.test.ts @@ -79,10 +79,12 @@ describe('fmtMib (display must not claim a ceiling the API did not set)', () => }) }) -// The round-3 Critical: rawRequest THROWS on >=400, so the Neon soft-path must live in a catch — -// status-branching on its return value was unreachable dead code and a Neon read crashed with a -// raw ApiError. These drive the seam with a stub client, which is exactly the test that would -// have caught it (the 502 branch was never taken by any test). +// The round-3 Critical: rawRequest THROWS on >=400, so the no-instance soft-path must live in a +// catch — status-branching on its return value was unreachable dead code and the read crashed with +// a raw ApiError. These drive the seam with a stub client, which is exactly the test that would +// have caught it (the 502 branch was never taken by any test). That soft path is the legacy Neon +// path: Neon is no longer used by any environment, so these stay as retained coverage for code +// that is not live. describe('fetchDbInstance (the read seam)', () => { const stub = (fn: () => Promise) => ({ rawRequest: fn }) as any @@ -91,7 +93,8 @@ describe('fetchDbInstance (the read seam)', () => { expect(read).toEqual({ kind: 'ok', body: { cpuMilli: 4000 } }) }) - it('maps the provider-shaped 502 (Neon-backed) to the soft no-instance case', async () => { + // Legacy Neon path — Neon is no longer used by any environment; this test is retained coverage. + it('maps the provider-shaped 502 (legacy Neon-backed) to the soft no-instance case', async () => { const read = await fetchDbInstance(stub(async () => { throw new ApiError(502, 'provider request failed') }), 'p1', '') expect(read).toEqual({ kind: 'no-instance' }) }) From 388c0ebf87a6b322f5762cad71843bc28ecc018e Mon Sep 17 00:00:00 2001 From: yaowenc2 Date: Wed, 12 Aug 2026 13:02:04 -0700 Subject: [PATCH 2/2] docs(help): say postgres upgrades are rejected, not just redirected (cubic P2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two reviewers independently misread the previous wording as implying postgres upgrade still works. It does not — the platform 400s it. Co-Authored-By: Claude Opus 5 (1M context) --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index c11f23e..35c8e49 100644 --- a/src/index.ts +++ b/src/index.ts @@ -137,7 +137,7 @@ svc.command('set-access ').description('Set a storage serv .option('--json').action(guard((type, name, access, o) => services.servicesSetAccess(type, name, access, o))) svc.command('scale [region]').description('Set a compute service machine count (paid plans only)') .option('--json').option('--branch ', 'branch (default: current)').action(guard((type, name, number, region, o) => services.servicesScale(type, name, number, region, o))) -svc.command('upgrade ').description('Change a compute service spec (paid plans only; for postgres resources use `insta db limits`)') +svc.command('upgrade ').description('Change a compute service spec (paid plans only). Postgres upgrades are rejected by the platform — use `insta db limits` instead') .option('--json').option('--branch ', 'branch (default: current)').action(guard((type, name, spec, o) => services.servicesUpgrade(type, name, spec, o))) svc.command('secrets ').description("List a service's secret names") .option('--branch ').option('--json').action(guard((type, name, o) => services.servicesSecrets(type, name, o)))