Skip to content

feat(stack): prepare slim resources on demand - #6250

Open
jgoux wants to merge 9 commits into
developfrom
juliengoux/cli-2123-stack-prepare-slim-resources-safely-on-demand-or-upfront
Open

feat(stack): prepare slim resources on demand#6250
jgoux wants to merge 9 commits into
developfrom
juliengoux/cli-2123-stack-prepare-slim-resources-safely-on-demand-or-upfront

Conversation

@jgoux

@jgoux jgoux commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • introduce package-owned off, lazy, and eager preparation policies with enabled dependency closure and coalesced work
  • prepare native slim-service archives through verified manifests, exact checksums, in-process zstd decompression, host gates, and atomic cache publication
  • use exact canonical GHCR resources and activate PostgREST lazily as the first end-to-end tracer bullet
  • select one exclusive execution mode, persist it with the managed stack, and use the selected Docker or Podman executable consistently
  • preserve typed lifecycle failures across the daemon boundary and make overlapping reload, preparation, and disposal outcomes deterministic
  • collect service logs from the selected runtime socket when usable (readable and writable), degrade safely when it is unavailable, and reject incompatible native glibc artifacts before launch

Context

This keeps resource preparation, runtime selection, and activation inside @supabase/stack, with the CLI remaining a thin consumer. An omitted mode selects Docker when Docker or Podman is usable and otherwise selects native mode. Explicit native or Docker choices are strict, preparation never falls back to the other mode, and managed stacks pin their persisted selection once claimed, including after a failed startup. Changing modes requires deleting and recreating the stack and its managed data.

Stack creation stays side-effect free beyond runtime detection and port reservation. Eager resources are prepared at startup, while lazy resources are prepared through the same activation path used by proxy and programmatic callers. Concurrent work is coalesced, disposal completes waiters with typed failures before cancellation, and cached paths cannot start services after disposal begins. Download completion restores prior public state atomically without overwriting a newer lifecycle transition. Concurrent Functions and Edge Runtime reloads preserve each committed state.

Docker and Podman remain exclusive runtime selections. Vector consumes the selected runtime socket when one is usable (readable and writable); a socket-less host uses internal Vector logs instead of crash-looping the service, and Podman never implicitly adopts the Docker default socket.

The catalog uses the frozen service versions, including Postgres 17.6.1.163, and preserves exact published container tags. The canonical Postgres image starts through its published non-root entrypoint and database bootstrap is modeled as an observable one-shot dependency before consumers become ready. Docker-only services remain container-backed until they have a concrete native runtime consumer.

@jgoux

jgoux commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Automated review triage is complete for this batch.

Fixed:

  • socket-less Docker or Podman now uses internal Vector logs instead of crash-looping the stack
  • a Podman-selected stack no longer implicitly mounts the Docker default socket
  • download completion restores public state atomically and cannot overwrite a newer lifecycle transition
  • the stale Downloading coverage now exercises real download events, including a concurrent explicit stop

Deliberately deferred:

  • graceful stop cancelling an in-progress cold start remains a product lifecycle decision; disposal is the force-cancelling boundary
  • layer-scope single-flight alignment, version-prefix leniency, a catalog mandatory trait, and archive streaming remain outside this tracer-bullet scope

This is the final automated-fix pass for this PR. The remaining items need human product and architecture review rather than another mechanical review loop.

@jgoux

jgoux commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Final automated-review disposition:

  • Applied usable-socket checks before Vector selects container logs, label=disable for every explicitly selected Podman socket, and an atomic Downloading projection transition.
  • Added focused coverage for inaccessible-socket degradation and an explicit Docker-compatible Podman socket override.
  • Deliberately did not add an exhaustive XDG/per-UID path matrix or warning plumbing. Those path permutations do not add meaningful user-surface confidence, and a warning belongs at a later UX boundary.
  • The uncached core run exposed a separate test-only managed-control flake: its 2-second guard left little margin over a deliberate ~1.5-second retry schedule. The guard is now 5 seconds, still well below the real 35-second parent handshake; production timing is unchanged.

This is the final automated-review pass. The remaining recorded deferrals are unchanged; the PR is ready for human review.

@jgoux
jgoux marked this pull request as ready for review August 19, 2026 21:11
@jgoux
jgoux requested a review from a team as a code owner August 19, 2026 21:11
@github-actions

Copy link
Copy Markdown
Contributor

Supabase CLI preview

npx --yes https://pkg.pr.new/supabase/cli/supabase@05b8067fb17b61a99cf8d6c0f93f48b227f08d29

Preview package for commit 05b8067.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

// A fully staged replacement is now available, so an incomplete
// destination can be reclaimed without risking the last usable
// cache entry. Retry publication once; persistent filesystem
// failures still surface instead of looping forever.
yield* fs.remove(cacheDir, { recursive: true, force: true });
const retry = yield* fs.rename(stagingDir, cacheDir).pipe(Effect.result);

P2 Badge Avoid deleting a concurrently published cache

When two processes repair the same incomplete cache, both can fail the initial rename and observe the destination as incomplete; if one publishes after the other's check, this unconditional removal deletes the newly complete directory that the winner may already have returned to a starting service. That produces intermittent missing executables despite successful resolution. Use a cross-process lock or a replacement protocol that cannot remove a destination after another contender has published it.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +21 to +24
const managedStackLaunchSchema = Schema.Union([
Schema.Struct({
mode: Schema.Literal("native"),
...managedStackLaunchFields,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Continue decoding legacy managed launch records

Existing managed stack documents remain formatVersion: 1, but stacks created by the previous default contain launch.mode: "auto"; this narrowed union rejects those documents before the supervisor can select and persist a concrete runtime. After upgrading, normal start, status, stop, and delete operations therefore classify the user's existing stack as corrupt. Decode and migrate the prior auto representation before requiring the new concrete launch shape.

Useful? React with 👍 / 👎.

Comment on lines +684 to 688
if (yield* isCompleteCache(cacheDir, release, info)) {
return {
path: cacheDir,
downloaded: false,
} satisfies ResolveBinaryResult;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Revalidate host compatibility on cache hits

When cacheRoot is shared or copied between Linux hosts, this fast path returns an artifact validated against the downloading host's glibc rather than the current host's version. Because the completion marker stores no manifest floor and validateManifest only runs during extraction, a cache produced on newer glibc can bypass the new compatibility gate and fail only when the service is spawned. Persist the compatibility metadata and validate it for every reuse.

Useful? React with 👍 / 👎.

Comment on lines +56 to +62
/** Normalizes a version string to the catalog's canonical stored form. */
export function normalizeServiceVersion(service: ServiceName, version: string): string {
const trimmed = version.trim();
const prefix = IMAGE_TAG_PREFIX[service];

if (prefix === "v") {
return trimmed.replace(/^v/i, "");
}

if (prefix === undefined && DEFAULT_VERSIONS[service].startsWith("v")) {
return /^v/i.test(trimmed) ? `v${trimmed.slice(1)}` : `v${trimmed}`;
}

return trimmed;
const normalized = version.trim();
const tagPrefix = serviceMetadata(service).artifact.docker.tagPrefix;
return tagPrefix !== undefined && normalized.startsWith(tagPrefix)
? normalized.slice(tagPrefix.length)
: normalized;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Migrate legacy version strings before exact-tag resolution

Existing project-link snapshots, local override files, and pinned manifests were written using the previous canonical form, which removed the leading v from services such as PostgREST, Auth, and Edge Runtime. This normalizer now preserves those legacy values, while the new release and GHCR resolvers treat the stored value as the exact tag, so an existing postgrest: "16.1" becomes a request for postgrest-16.1 or :16.1 instead of the published v16.1 artifact. Migrate persisted legacy values before applying the exact-tag convention.

Useful? React with 👍 / 👎.

Comment on lines +117 to 119
const containerRuntime =
document.launch?.mode === "docker" ? (document.launch.containerRuntime ?? "docker") : null;
const acquisition = yield* manager.acquireControl(stackId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clean up legacy Docker stacks without launch metadata

For an existing managed Docker stack whose optional launch field is absent, this now infers containerRuntime as null, so every dead-owner and failed-stack stop path skips dockerForceRemove and then records the stack as stopped. Those documents were valid previously, when stop always attempted exact-name Docker cleanup, so upgrading after a daemon crash can leave containers running and ports occupied. Preserve best-effort Docker cleanup or migrate the runtime when legacy launch metadata is missing.

Useful? React with 👍 / 👎.

Comment on lines +333 to +338
const attempt = runPullCommand(spawner, runtime, image).pipe(
Effect.retry({
while: (error) => shouldRetryPull(error.detail),
schedule: Schedule.recurs(1).pipe(
Schedule.addDelay(() => Effect.succeed(Duration.millis(500))),
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound image-pull retries by elapsed time

For transient registry failures, this retry policy allows exactly one retry regardless of how little wall-clock time has elapsed, so a brief pair of rate-limit, timeout, or connection-reset responses can fail stack preparation after roughly 500 ms. Use a wall-clock deadline with spaced retries rather than an attempt-count budget so loaded CI and slow user networks receive a stable recovery window.

AGENTS.md reference: AGENTS.md:L170-L172

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant