diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index de3dec7ff4..8cc068553b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -148,7 +148,7 @@ jobs: (github.event_name == 'merge_group' || inputs.force || github.event.pull_request.draft == false) - name: Run end-to-end tests (shard ${{ matrix.shard }}/3) + name: Run CLI end-to-end tests (shard ${{ matrix.shard }}/3) runs-on: blacksmith-8vcpu-ubuntu-2404 strategy: fail-fast: false @@ -186,16 +186,39 @@ jobs: run: pnpm exec turbo run supabase#build - name: Run end-to-end tests - run: pnpm exec turbo run test:e2e:run --only --concurrency=1 -- --shard=${{ matrix.shard }}/3 + run: pnpm exec turbo run test:e2e:run --only --concurrency=1 --filter=supabase --filter=@supabase/cli-e2e -- --shard=${{ matrix.shard }}/3 env: CLI_HARNESS_TARGET: ts-legacy SUPABASE_GO_BINARY: ${{ github.workspace }}/apps/cli-go/supabase-go - # Summary job that gates branch protection. The matrix `test-e2e` job - # produces per-shard check names (`Run end-to-end tests (shard N/3)`), so - # this job preserves the original `Run end-to-end tests` check name that - # branch protection rules already require. It succeeds iff every shard - # succeeded (or skipped — `success()` is true for skipped jobs). + test-stack-e2e: + if: | + !startsWith(github.head_ref, 'release-notes/') && + (github.event_name == 'merge_group' || + inputs.force || + github.event.pull_request.draft == false) + name: Run stack end-to-end tests (${{ matrix.runtime }}) + runs-on: blacksmith-8vcpu-ubuntu-2404 + strategy: + fail-fast: false + matrix: + runtime: [native, container] + steps: + - name: Checkout + uses: useblacksmith/checkout@6fd481652155169ed4d2f25ebaf97464f685175f # v1.0.0-beta + + - name: Setup + uses: ./.github/actions/setup + with: + dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} + + - name: Run stack end-to-end tests + run: pnpm --filter @supabase/stack test:e2e:run + env: + SUPABASE_STACK_E2E_RUNTIME: ${{ matrix.runtime }} + + # Summary job that gates branch protection. It preserves the original check + # name and succeeds iff every CLI shard and stack runtime succeeded. test-e2e-summary: if: | always() && @@ -204,13 +227,13 @@ jobs: inputs.force || github.event.pull_request.draft == false) name: Run end-to-end tests - needs: test-e2e + needs: [test-e2e, test-stack-e2e] runs-on: ubuntu-latest steps: - name: Verify all shards succeeded run: | - if [ "${{ needs.test-e2e.result }}" = "failure" ] || [ "${{ needs.test-e2e.result }}" = "cancelled" ]; then - echo "::error ::One or more e2e shards failed: ${{ needs.test-e2e.result }}" + if [ "${{ needs.test-e2e.result }}" = "failure" ] || [ "${{ needs.test-e2e.result }}" = "cancelled" ] || [ "${{ needs.test-stack-e2e.result }}" = "failure" ] || [ "${{ needs.test-stack-e2e.result }}" = "cancelled" ]; then + echo "::error ::One or more e2e jobs failed: cli=${{ needs.test-e2e.result }}, stack=${{ needs.test-stack-e2e.result }}" exit 1 fi - echo "All e2e shards reported: ${{ needs.test-e2e.result }}" + echo "All e2e jobs reported: cli=${{ needs.test-e2e.result }}, stack=${{ needs.test-stack-e2e.result }}" diff --git a/.gitignore b/.gitignore index 5a892f3aba..a1d49ff0cf 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,10 @@ coverage/ !.env.example .claude/ .agents/.repos/effect-v3 +.repos/slim-services/ .worktrees/ .supabase/ +erl_crash.dump # Stray `supabase` project dir created by running the CLI at the repo root # (e.g. supabase/.temp/linked-project.json). This monorepo has no top-level # Supabase project — real fixtures live under apps/cli-e2e/fixtures/. @@ -23,6 +25,7 @@ packages/cli-*/bin/ # Turbo .turbo/ +apps/cli/.supabase/ # Transient render dir created by packages/api/scripts/generated-output-sync.unit.test.ts packages/api/.generated-output-sync-*/ diff --git a/.gitmodules b/.gitmodules index ec13d73342..084b300883 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,9 +13,6 @@ [submodule ".repos/cheffect"] path = .repos/cheffect url = https://github.com/tim-smart/cheffect.git -[submodule ".repos/process-compose"] - path = .repos/process-compose - url = https://github.com/F1bonacc1/process-compose.git [submodule ".repos/t3code"] path = .repos/t3code url = https://github.com/pingdotgg/t3code.git diff --git a/.oxlintrc.json b/.oxlintrc.json index 6d05fcc7b3..38b82bb597 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -8,8 +8,6 @@ ".repos", "apps/cli-go", "apps/cli-e2e/fixtures", - "packages/stack", - "packages/process-compose", "**/testdata", "**/dist", "**/coverage", diff --git a/.repos/process-compose b/.repos/process-compose deleted file mode 160000 index a4038d6698..0000000000 --- a/.repos/process-compose +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a4038d669818c35fc68fc7fc240b39e371ce0e7a diff --git a/AGENTS.md b/AGENTS.md index 9e313dc366..45a1f55572 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,13 +12,12 @@ Bun monorepo with workspaces under `apps/` and `packages/`. - `apps/docs` — internal Next.js docs site - `packages/api` — typed Supabase Management API client - `packages/config` — config schema and generated types -- `packages/process-compose` — process orchestration library - `packages/stack` — programmatic local Supabase stack runtime - `packages/cli-*` — platform-specific published CLI binary wrappers ## Package Structure -Use `packages/process-compose` as the reference for internal TypeScript/Bun workspaces such as `apps/cli`, `packages/api`, `packages/config`, `packages/process-compose`, and `packages/stack`. +Use the existing internal TypeScript/Bun workspaces as references for package structure and scripts. These workspaces should generally follow this structure: @@ -29,7 +28,7 @@ These workspaces should generally follow this structure: - Standard scripts: `test`, `types:check` - Standard devDependencies: `@tsconfig/bun`, `@types/bun`, `typescript` -Generic linting (`oxlint`), formatting (`oxfmt`), and unused-code analysis (`knip`) are repo-wide, not per-package: the tools are root devDependencies configured by `.oxlintrc.json`, `.oxfmtrc.json`, and `knip.json` at the repo root (knip's config maps each workspace under its `workspaces` key). Effect-specific linting is incrementally scoped to `packages/stack` and `packages/process-compose` through `.oxlintrc.effect.json`; run it with the root `lint:effect:check` or `lint:effect:fix` scripts. The root `check:all`/`fix:all` scripts are the sole repo-wide quality entrypoints and use Turbo to orchestrate the root-owned generic `lint:*`/`fmt:*`/`knip:*` scripts and package `types:check` targets; `fix:all` runs the Effect lint fix after those generic fixes complete. Package-local work can run `pnpm types:check` and the package's test scripts; `pnpm exec oxlint`, `pnpm exec oxfmt`, and `pnpm exec knip-bun` from the repo root also work directly. +Generic linting (`oxlint`), formatting (`oxfmt`), and unused-code analysis (`knip`) are repo-wide, not per-package: the tools are root devDependencies configured by `.oxlintrc.json`, `.oxfmtrc.json`, and `knip.json` at the repo root (knip's config maps each workspace under its `workspaces` key). Effect-specific linting is scoped to `packages/stack` through `.oxlintrc.effect.json`; run it with the root `lint:effect:check` or `lint:effect:fix` scripts. The root `check:all`/`fix:all` scripts are the sole repo-wide quality entrypoints and use Turbo to orchestrate the root-owned generic `lint:*`/`fmt:*`/`knip:*` scripts and package `types:check` targets; `fix:all` runs the Effect lint fix after those generic fixes complete. Package-local work can run `pnpm types:check` and the package's test scripts; `pnpm exec oxlint`, `pnpm exec oxfmt`, and `pnpm exec knip-bun` from the repo root also work directly. Expected exceptions: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c37e341d0d..39ad83f70e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -92,7 +92,6 @@ That pulls `.repos/effect/`, which is the local source of truth for Effect v4 AP |-- packages/ | |-- api/ # Typed Supabase Management API client | |-- config/ # Supabase config schema and generated types -| |-- process-compose/ # Effect-based process orchestration library | |-- stack/ # Programmatic local Supabase stack runtime | `-- cli-*/ # Platform-specific CLI binary packages |-- tools/ # Repository tooling (release scripts, etc.) @@ -115,7 +114,6 @@ That pulls `.repos/effect/`, which is the local source of truth for Effect v4 AP | `packages/api` | Auto-generated TypeScript client for the Supabase Management API. | | `packages/cli-test-helpers` | CLI test harness library — `createHarness`/`exec` API for spawning TS Legacy and TS Next CLI subprocesses in tests. | | `packages/config` | JSON Schema and generated TypeScript types for Supabase configuration. | -| `packages/process-compose` | TypeScript/Bun port of `process-compose` used for multi-service orchestration. | | `packages/stack` | Programmatic local Supabase stack used by the CLI and other tooling. | | `packages/cli-darwin-arm64` | Published native CLI binary wrapper for macOS arm64. | | `packages/cli-darwin-x64` | Published native CLI binary wrapper for macOS x64. | @@ -140,7 +138,7 @@ pnpm run fix:all # run all fixers across every project ### Standard package scripts -Standard TypeScript workspaces (`apps/cli-e2e`, `apps/cli`, `packages/api`, `packages/cli-test-helpers`, `packages/config`, `packages/process-compose`, `packages/stack`) declare their package scripts explicitly. Test suites vary by package: unit tests are standard, while integration and e2e tests exist only where applicable. +Standard TypeScript workspaces (`apps/cli-e2e`, `apps/cli`, `packages/api`, `packages/cli-test-helpers`, `packages/config`, `packages/stack`) declare their package scripts explicitly. Test suites vary by package: unit tests are standard, while integration and e2e tests exist only where applicable. | Script | What it does | | ------------------ | -------------------------------------- | diff --git a/apps/cli/AGENTS.md b/apps/cli/AGENTS.md index 60c28994cb..eb5a7b19d3 100644 --- a/apps/cli/AGENTS.md +++ b/apps/cli/AGENTS.md @@ -397,7 +397,7 @@ export class LegacyThingMissingError extends Data.TaggedError("LegacyThingMissin - **An instance-dependent getter must stay valid when its fields are absent** — the drift guard evaluates it against a field-less probe. - **A plain `Error` subclass (no `_tag`) also declares its fingerprint identifier**: `static readonly [ErrorActionabilityFingerprintId] = ""`, matching the export name exactly. Tagged errors skip this — their fingerprint comes from the tag. The static identifier is what keeps `error:` fingerprints stable in minified release builds, where `constructor.name` is renamed. -**Errors defined outside `apps/cli/src`** (`@supabase/stack`, `@supabase/config`, `@supabase/process-compose`, `@supabase/api`, `effect`) cannot carry a declaration. Add a structural adapter keyed by `_tag` to `externalActionabilityByTag` in that same module, branching on the producer's typed fields. +**Errors defined outside `apps/cli/src`** (`@supabase/stack`, `@supabase/config`, `@supabase/api`, `effect`) cannot carry a declaration. Add a structural adapter keyed by `_tag` to `externalActionabilityByTag` in that same module, branching on the producer's typed fields. `error-actionability-coverage.unit.test.ts` enforces this. It scans every `TaggedError("Tag")`, every `*Error("Tag")` factory, and every `class X extends Error` under `apps/cli/src`, and fails when a class is unexported, has no own declaration, or is untagged without its matching static fingerprint identifier. A failure there is the guard working: classify the new error rather than loosening the guard, because `unknown` in production telemetry must mean a genuinely unforeseen failure, not one nobody categorized. diff --git a/apps/cli/scripts/build-binary.integration.test.ts b/apps/cli/scripts/build-binary.integration.test.ts index 8073bee193..81babaf38f 100644 --- a/apps/cli/scripts/build-binary.integration.test.ts +++ b/apps/cli/scripts/build-binary.integration.test.ts @@ -3,6 +3,9 @@ import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { spawn } from "node:child_process"; +import { once } from "node:events"; +import { NATIVE_PROCESS_DISPATCH_SENTINEL } from "@supabase/stack/internal/supervisor"; const fixturePath = fileURLToPath( new URL("../tests/fixtures/compiled-libpg-query.ts", import.meta.url), @@ -10,6 +13,9 @@ const fixturePath = fileURLToPath( const versionFixturePath = fileURLToPath( new URL("../tests/fixtures/compiled-cli-version.ts", import.meta.url), ); +const nativeDispatchFixturePath = fileURLToPath( + new URL("../tests/fixtures/compiled-native-dispatch.ts", import.meta.url), +); const temporaryDirectories: string[] = []; afterEach(async () => { @@ -95,4 +101,73 @@ describe("compiled binary assets", () => { expect(probeExitCode, stderr).toBe(0); expect(stdout.trim()).toBe("7.8.9-beta.1"); }, 20_000); + + test("dispatches the embedded native launcher from a compiled binary", async () => { + const directory = await mkdtemp(path.join(tmpdir(), "supabase-compiled-native-")); + temporaryDirectories.push(directory); + const executable = path.join(directory, "native-dispatch-probe"); + const reap = async (child: ReturnType | undefined): Promise => { + if (child === undefined || child.exitCode !== null || child.signalCode !== null) return; + await new Promise((resolve) => { + const done = () => { + child.off("exit", done); + child.off("error", done); + resolve(); + }; + child.once("exit", done); + child.once("error", done); + child.kill("SIGKILL"); + }); + }; + + let build: ReturnType | undefined; + let child: ReturnType | undefined; + let buildStderr: Promise | undefined; + let stdout: Promise | undefined; + let stderr: Promise | undefined; + try { + build = spawn( + "bun", + ["build", nativeDispatchFixturePath, "--compile", `--outfile=${executable}`], + { stdio: ["ignore", "pipe", "pipe"] }, + ); + buildStderr = new Response(build.stderr!).text(); + const [buildExitCode] = (await once(build, "exit")) as [number | null, NodeJS.Signals | null]; + expect(buildExitCode, await buildStderr).toBe(0); + + child = spawn(executable, [NATIVE_PROCESS_DISPATCH_SENTINEL], { + cwd: directory, + env: {}, + stdio: ["ignore", "pipe", "pipe", "pipe", "pipe"], + }); + stdout = new Response(child.stdout!).text(); + stderr = new Response(child.stderr!).text(); + const payload = JSON.stringify({ + executable: process.execPath, + args: ["-e", "process.stdout.write('native launcher dispatch ok\\n')"], + }); + const payloadFd = child.stdio[4]; + if ( + payloadFd == null || + typeof payloadFd === "number" || + !("end" in payloadFd) || + typeof payloadFd.end !== "function" + ) { + throw new Error("compiled native dispatch probe did not expose payload fd"); + } + payloadFd.end(payload); + const [exitCode] = (await once(child, "exit")) as [number | null, NodeJS.Signals | null]; + + expect(exitCode, await stderr).toBe(0); + expect((await stdout).trim()).toBe("native launcher dispatch ok"); + } finally { + await reap(child); + await reap(build); + await Promise.all([ + buildStderr?.catch(() => ""), + stdout?.catch(() => ""), + stderr?.catch(() => ""), + ]); + } + }, 30_000); }); diff --git a/apps/cli/src/legacy/commands/config/pull/pull.command.ts b/apps/cli/src/legacy/commands/config/pull/pull.command.ts index e38bcf16c4..392017b179 100644 --- a/apps/cli/src/legacy/commands/config/pull/pull.command.ts +++ b/apps/cli/src/legacy/commands/config/pull/pull.command.ts @@ -39,9 +39,7 @@ const config = { export type LegacyConfigPullFlags = CliCommand.Command.Config.Infer; -// Exported so integration tests can drive the exact wiring -// `Command.withHandler` uses below (same precedent as `legacyConfigDiffHandler`). -export const legacyConfigPullHandler = (flags: LegacyConfigPullFlags) => +const legacyConfigPullHandler = (flags: LegacyConfigPullFlags) => legacyConfigPull(flags).pipe( // `--project-ref` accepts branch names here (CLI-2167 vocabulary), so its // value is only safe to log verbatim when it is actually ref-shaped — a diff --git a/apps/cli/src/legacy/commands/config/pull/pull.format.ts b/apps/cli/src/legacy/commands/config/pull/pull.format.ts index f512dffc25..f95b565d7b 100644 --- a/apps/cli/src/legacy/commands/config/pull/pull.format.ts +++ b/apps/cli/src/legacy/commands/config/pull/pull.format.ts @@ -43,7 +43,7 @@ export const LEGACY_CONFIG_PULL_PAYLOAD_VERSION = 1; * change `legacyPlanConfigPull` planned to write still ends up unwritten when * the run is a dry run or the user declined. */ -export type LegacyConfigPullChangeSkipReason = LegacyConfigPullSkipReason | "declined" | "dry_run"; +type LegacyConfigPullChangeSkipReason = LegacyConfigPullSkipReason | "declined" | "dry_run"; /** * The run's actual outcome, known only after the confirmation prompt (or diff --git a/apps/cli/src/legacy/commands/config/pull/pull.plan.ts b/apps/cli/src/legacy/commands/config/pull/pull.plan.ts index f3edf07f4a..12dbf78fe3 100644 --- a/apps/cli/src/legacy/commands/config/pull/pull.plan.ts +++ b/apps/cli/src/legacy/commands/config/pull/pull.plan.ts @@ -47,12 +47,12 @@ export type LegacyConfigPullSkipReason = | "unwritable" | "would_invalidate"; -export interface LegacyConfigPullSkip { +interface LegacyConfigPullSkip { readonly change: ConfigChange; readonly reason: LegacyConfigPullSkipReason; } -export interface LegacyConfigPullPlannedWrite { +interface LegacyConfigPullPlannedWrite { readonly change: ConfigChange; /** `change.path`, prefixed with `["remotes", label]` when the destination * is a `[remotes.*]` block — the exact path `applyConfigEdits` edits. */ @@ -60,7 +60,7 @@ export interface LegacyConfigPullPlannedWrite { readonly value: ConfigEditValue; } -export type LegacyConfigPullWarningKind = +type LegacyConfigPullWarningKind = | "dual_scope" | "duplicates_root" | "array_drift" diff --git a/apps/cli/src/legacy/commands/config/pull/pull.scope.ts b/apps/cli/src/legacy/commands/config/pull/pull.scope.ts index 8a46f1a276..3351047e36 100644 --- a/apps/cli/src/legacy/commands/config/pull/pull.scope.ts +++ b/apps/cli/src/legacy/commands/config/pull/pull.scope.ts @@ -39,11 +39,11 @@ import { legacySanitizeInlineName } from "../../../shared/legacy-http-errors.ts" * 5. Otherwise: the config root. */ -export interface LegacyConfigPullDestinationRoot { +interface LegacyConfigPullDestinationRoot { readonly kind: "root"; } -export interface LegacyConfigPullDestinationRemote { +interface LegacyConfigPullDestinationRemote { readonly kind: "remote"; /** * The `[remotes.