Skip to content

Commit 5b2115a

Browse files
avalletecursoragent
andcommitted
fix(cli): warn when gen types ignores --network-id
Native generation cannot join a Docker network. Surface a docker-run workaround and classify native IPv6 dial failures so the pooler retry still runs. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 8ce5cfa commit 5b2115a

8 files changed

Lines changed: 123 additions & 12 deletions

File tree

AGENTS.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -216,9 +216,6 @@ Inside Effect code, compose schemas through their Effect APIs:
216216

217217
## Code Quality
218218

219-
Never `git commit` or `git push` until lint and `types:check` have been run and passed for the change. Targeted unit/integration tests are not a substitute — CI Check code quality runs `pnpm check:all` (`types:check`, oxlint, oxfmt, knip). Before commit or push, from each changed TypeScript workspace run `pnpm types:check`, and from the repo root run `pnpm exec oxlint` (or `pnpm check:all`). If those fail, fix them before committing.
220-
After every `git push` to a branch that has a PR, check GitHub CI for that PR (`gh pr checks` / `gh run list`) and report whether it is green. If it is not, diagnose and fix; do not leave a red PR as done.
221-
222219
Run repo-wide quality checks from the repository root with `pnpm check:all` or `pnpm fix:all`; these root scripts are the only quality entrypoints and delegate orchestration to Turbo. For package-local work, run `pnpm types:check` and the applicable package test scripts from the workspace you changed. Do not consider a task complete until all relevant scripts pass.
223220
Do not waive or defer failing checks in a changed workspace as "pre-existing". If a required check fails, fix it before closing the task. Only treat a failure as an external blocker when it cannot be resolved within the workspace, and in that case call it out explicitly.
224221
If you run a root quality command such as `pnpm check:all`, you own all failing checks it reports for the duration of the task, even if the failing files look unrelated. Do not leave the repository with unresolved failing checks after running the command.

apps/cli/src/legacy/commands/gen/types/SIDE_EFFECTS.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ treated as disable. When the connection string carries no explicit
6060
`connect_timeout`, a positive `--query-timeout` is also used as the connect
6161
timeout — `0` leaves the driver's default (10s remote, 2s local). `--local`
6262
connects to the host-mapped database port from `supabase/config.toml`
63-
(`db.port`).
63+
(`db.port`). Remote connections that use a `supabase_admin` or `cli_login_*`
64+
role step down to `postgres` via the shared driver (`SET SESSION ROLE
65+
postgres`) before introspection.
6466

6567
For a remote target whose DSN carries no explicit `sslmode`, a raw TCP
6668
`SSLRequest` probe (the shared pg-delta probe, default 10s timeout) is opened
@@ -77,8 +79,9 @@ embedded bundle.
7779

7880
`--network-id` / `SUPABASE_NETWORK_ID` are unused: generation no longer runs
7981
inside a container, so a hostname reachable only on a Docker network will not
80-
resolve. `--local` uses the published host port instead; `--db-url` must be
81-
host-reachable.
82+
resolve. An explicit `--network-id` prints a warning with a
83+
`docker run --network … npx supabase gen types` workaround. `--local` uses the
84+
published host port instead; `--db-url` must be host-reachable.
8285

8386
## Subprocesses
8487

apps/cli/src/legacy/commands/gen/types/types.handler.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
legacyReadDbToml,
2727
} from "../../../shared/legacy-db-config.toml-read.ts";
2828
import type { LegacyPgConnInput } from "../../../shared/legacy-db-connection.service.ts";
29+
import { legacyPflagStringValue } from "../../../shared/legacy-pflag-reconcile.ts";
2930
import { legacyTempPaths } from "../../../shared/legacy-temp-paths.ts";
3031
import { LegacyLinkedProjectCache } from "../../../telemetry/legacy-linked-project-cache.service.ts";
3132
import { LegacyTelemetryState } from "../../../telemetry/legacy-telemetry-state.service.ts";
@@ -42,6 +43,7 @@ import {
4243
defaultSchemas,
4344
localDbContainerId,
4445
localDbPassword,
46+
legacyGenTypesNetworkIdUnusedWarning,
4547
parseQueryTimeoutSeconds,
4648
} from "./types.shared.ts";
4749

@@ -444,6 +446,11 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le
444446
}
445447
}
446448

449+
const networkIdOverride = legacyPflagStringValue(occurrences, "network-id");
450+
if (Option.isSome(networkIdOverride)) {
451+
yield* output.warn(legacyGenTypesNetworkIdUnusedWarning(networkIdOverride.value));
452+
}
453+
447454
if (flags.local) {
448455
const config = yield* legacyReadDbToml(fs, path, cliSettings.workdir);
449456
yield* legacyApplyProjectEnv(

apps/cli/src/legacy/commands/gen/types/types.integration.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,48 @@ describe("legacy gen types", () => {
10021002
});
10031003
});
10041004

1005+
it.live("warns that --network-id is unused and still generates", () => {
1006+
const dbUrl = "postgresql://postgres:postgres@db:5432/postgres";
1007+
const { layer, generator, out } = setup({
1008+
args: ["gen", "types", "--db-url", dbUrl, "--network-id", "mycompose_default"],
1009+
});
1010+
1011+
return Effect.gen(function* () {
1012+
yield* legacyGenTypes(defaultFlags({ dbUrl: Option.some(dbUrl) })).pipe(
1013+
Effect.provide(layer),
1014+
);
1015+
1016+
expect(generator.calls).toHaveLength(1);
1017+
expect(out.messages).toContainEqual(
1018+
expect.objectContaining({
1019+
type: "warn",
1020+
message: expect.stringContaining("docker run --rm --network mycompose_default"),
1021+
}),
1022+
);
1023+
expect(out.messages).toContainEqual(
1024+
expect.objectContaining({
1025+
type: "warn",
1026+
message: expect.stringContaining("npx --yes supabase gen types"),
1027+
}),
1028+
);
1029+
});
1030+
});
1031+
1032+
it.live("does not warn about --network-id when the flag is omitted", () => {
1033+
const dbUrl = "postgresql://postgres:postgres@127.0.0.1:5432/postgres";
1034+
const { layer, out } = setup({
1035+
args: ["gen", "types", "--db-url", dbUrl],
1036+
});
1037+
1038+
return Effect.gen(function* () {
1039+
yield* legacyGenTypes(defaultFlags({ dbUrl: Option.some(dbUrl) })).pipe(
1040+
Effect.provide(layer),
1041+
);
1042+
1043+
expect(out.messages.filter((message) => message.type === "warn")).toEqual([]);
1044+
});
1045+
});
1046+
10051047
for (const scenario of nonTypescriptProjectRefScenarios) {
10061048
it.live(`generates ${scenario.lang} types from a project ref through the DB resolver`, () => {
10071049
const { layer, out, api, linkedProjectCache, dbConfig, generator } = setup({

apps/cli/src/legacy/commands/gen/types/types.shared.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,16 @@ export function applyProbedSslMode(
8787
...(sslrootcert !== undefined && sslrootcert.length > 0 ? { sslrootcert } : {}),
8888
};
8989
}
90+
91+
/**
92+
* `--network-id` cannot attach the in-process generator to a Docker network.
93+
* Point at a host-reachable DSN, or run the CLI inside that network.
94+
*/
95+
export function legacyGenTypesNetworkIdUnusedWarning(networkId: string): string {
96+
const network = networkId.length > 0 ? networkId : "<network-id>";
97+
return (
98+
"--network-id is unused: gen types no longer runs inside a container and cannot join a Docker network.\n" +
99+
"To reach a hostname that exists only on that network:\n" +
100+
` docker run --rm --network ${network} node:lts npx --yes supabase gen types --db-url <url>`
101+
);
102+
}

apps/cli/src/legacy/commands/gen/types/types.unit.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
applyProbedSslMode,
88
applyQueryTimeouts,
99
defaultSchemas,
10+
legacyGenTypesNetworkIdUnusedWarning,
1011
localDbPassword,
1112
parseQueryTimeoutSeconds,
1213
} from "./types.shared.ts";
@@ -162,6 +163,19 @@ describe("schema and password helpers", () => {
162163
});
163164
});
164165

166+
describe("legacyGenTypesNetworkIdUnusedWarning", () => {
167+
it("names the unused flag and the docker run + npx workaround", () => {
168+
const warning = legacyGenTypesNetworkIdUnusedWarning("mycompose_default");
169+
expect(warning).toContain("--network-id is unused");
170+
expect(warning).toContain("docker run --rm --network mycompose_default");
171+
expect(warning).toContain("npx --yes supabase gen types --db-url <url>");
172+
});
173+
174+
it("uses a placeholder when the flag value is empty", () => {
175+
expect(legacyGenTypesNetworkIdUnusedWarning("")).toContain("--network <network-id>");
176+
});
177+
});
178+
165179
describe("oxfmt binding pin", () => {
166180
it("stays on the oxfmt version postgrest-typegen resolves", () => {
167181
const cliPackageJson = fileURLToPath(new URL("../../../../../package.json", import.meta.url));

apps/cli/src/legacy/shared/legacy-connect-errors.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,13 @@ export function legacyIpv6Suggestion(): string {
3131
// `ipv6LiteralPattern`: an IPv6 address in brackets
3232
// (Go dial form) or parens (libpq form). Run against the original-case message.
3333
const IPV6_LITERAL_PATTERN = /(?:\[[0-9a-fA-F:]+\]|\([0-9a-fA-F:]+\))/;
34-
// Node's dial-failure shape (`connect ENETUNREACH 2600:…:5432`). The port may be
35-
// followed by whitespace, end-of-string, or a closing paren — the connect-failure
36-
// message renders the driver cause parenthesized (pgconn `dial error (…)` form).
37-
const NODE_ENETUNREACH_PATTERN = /\benetunreach\s+([0-9a-fA-F:]+):\d+(?:[\s)]|$)/i;
34+
// Node's dial-failure shape (`connect EHOSTUNREACH 2600:…:5432`). The port may
35+
// be followed by whitespace, end-of-string, or a closing paren — the
36+
// connect-failure message renders the driver cause parenthesized (pgconn
37+
// `dial error (…)` form). ENETUNREACH / EHOSTUNREACH / EADDRNOTAVAIL are the
38+
// IPv6-unreachable errnos; LegacyDbConnectError keeps only this rendered text.
39+
const NODE_IPV6_DIAL_PATTERN =
40+
/\b(?:enetunreach|ehostunreach|eaddrnotavail)\s+([0-9a-fA-F:]+):\d+(?:[\s)]|$)/i;
3841

3942
/**
4043
* Port of `isIPv6ConnectivityError`. Lower-cases the
@@ -48,8 +51,8 @@ export function legacyIsIPv6ConnectivityError(message: string): boolean {
4851
if (lower.includes("address family for hostname not supported")) return true;
4952
if (lower.includes("no address associated with hostname")) return true;
5053
if (lower.includes("network is unreachable")) return true;
51-
const nodeEnetunreachMatch = NODE_ENETUNREACH_PATTERN.exec(message);
52-
if (nodeEnetunreachMatch?.[1] !== undefined) return isIPv6(nodeEnetunreachMatch[1]);
54+
const nodeIpv6DialMatch = NODE_IPV6_DIAL_PATTERN.exec(message);
55+
if (nodeIpv6DialMatch?.[1] !== undefined) return isIPv6(nodeIpv6DialMatch[1]);
5356
if (lower.includes("no route to host") || lower.includes("cannot assign requested address")) {
5457
return IPV6_LITERAL_PATTERN.test(message);
5558
}

apps/cli/src/legacy/shared/legacy-connect-errors.unit.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,21 @@ describe("legacyIsIPv6ConnectivityError", () => {
8585
).toBe(true);
8686
});
8787

88+
it("classifies Node EHOSTUNREACH and EADDRNOTAVAIL stderr for IPv6 literals", () => {
89+
expect(legacyIsIPv6ConnectivityError("connect EHOSTUNREACH 2600:1f18::1:5432")).toBe(true);
90+
expect(legacyIsIPv6ConnectivityError("connect EADDRNOTAVAIL 2a05:d014::1:5432")).toBe(true);
91+
expect(legacyIsIPv6ConnectivityError("connect EHOSTUNREACH 10.0.0.1:5432")).toBe(false);
92+
expect(legacyIsIPv6ConnectivityError("connect EADDRNOTAVAIL 10.0.0.1:5432")).toBe(false);
93+
});
94+
95+
it("classifies Node EHOSTUNREACH inside the parenthesized connect-failure rendering", () => {
96+
expect(
97+
legacyIsIPv6ConnectivityError(
98+
"failed to connect to `host=db.x.supabase.co user=postgres database=postgres`: dial error (connect EHOSTUNREACH 2600:1f18::1:5432)",
99+
),
100+
).toBe(true);
101+
});
102+
88103
it("does not classify unrelated errors", () => {
89104
expect(legacyIsIPv6ConnectivityError("permission denied for schema public")).toBe(false);
90105
expect(legacyIsIPv6ConnectivityError("")).toBe(false);
@@ -601,6 +616,23 @@ describe("legacyIsIPv6ConnectivityErrorCause", () => {
601616
).toBe(true);
602617
});
603618

619+
it("classifies the native rendered EHOSTUNREACH connect error without structured fields", () => {
620+
expect(
621+
legacyIsIPv6ConnectivityErrorCause(
622+
new Error(
623+
"failed to connect to postgres: failed to connect to `host=db.x.supabase.co user=postgres database=postgres`: dial error (connect EHOSTUNREACH 2600:1f18::1:5432)",
624+
),
625+
),
626+
).toBe(true);
627+
expect(
628+
legacyIsIPv6ConnectivityErrorCause(
629+
new Error(
630+
"failed to connect to postgres: failed to connect to `host=db.x.supabase.co user=postgres database=postgres`: dial error (connect EADDRNOTAVAIL 2a05:d014::1:5432)",
631+
),
632+
),
633+
).toBe(true);
634+
});
635+
604636
it("classifies the native rendered ENOTFOUND connect error", () => {
605637
expect(
606638
legacyIsIPv6ConnectivityErrorCause(

0 commit comments

Comments
 (0)