Skip to content

Commit e8f5e67

Browse files
committed
fix(cli): preserve saved stack settings on restart
1 parent 0a6769b commit e8f5e67

5 files changed

Lines changed: 301 additions & 137 deletions

File tree

apps/cli/docs/stack-commands.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ native runtimes.
1212
| `supabase stack start` | Create or resume the project's stack. |
1313
| `supabase stack status` | Show identity, readiness, and drift, or export connection variables with `--env`. |
1414
| `supabase stack logs` | Read retained or live stack logs. |
15-
| `supabase stack restart` | Prepare the current project configuration, then restart an existing stack. |
15+
| `supabase stack restart` | Restart an existing stack using its saved effective configuration. |
1616
| `supabase stack stop` | Stop a stack while retaining its data. |
1717

1818
Use each command's `--help` for its available targeting and runtime options.
@@ -152,12 +152,15 @@ including `--workdir` and `SUPABASE_WORKDIR`, and prefers JSON when both files e
152152

153153
## Service selection and shutdown
154154

155-
`supabase stack restart` applies the current project configuration to an existing stack. It
156-
prepares required artifacts before stopping, then stops and starts the same stack identity. Stack
157-
data and durable identity are preserved. Select a stack with `--stack <name>` or `--stack-id <id>`;
158-
the selected stack's persisted project root supplies the configuration for ID targets. Restart does
159-
not retain one-off `start` flags such as `--exclude`, `--eager`, or `--preparation`; those settings
160-
fall back to the project's configuration and runtime defaults.
155+
`supabase stack restart` reuses an existing stack's saved effective configuration. It stops and
156+
starts the same stack identity, preserving its data. Normal startup may still download missing
157+
artifacts according to the saved preparation policy. Select a stack with `--stack <name>` or
158+
`--stack-id <id>`. The restart handler does not reload project configuration. Set
159+
`SUPABASE_EXPERIMENTAL_STACK=1` when restarting by ID outside the project or with invalid project
160+
configuration, so feature routing does not depend on that configuration. Start flags such as `--exclude`, `--eager`, or
161+
`--preparation` remain in the saved stack configuration; a later normal `start` reloads the project
162+
configuration and current flags.
163+
An unconfigured stack must be initialized with `supabase stack start` before it can be restarted.
161164

162165
`supabase stack start --exclude studio,analytics -x mail` disables those services in the effective
163166
start configuration without changing the project file. Valid names are `rest`, `auth`, `realtime`,

apps/cli/src/commands/experimental/stack/restart/SIDE_EFFECTS.md

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,21 @@ configuration or `SUPABASE_EXPERIMENTAL_STACK=1`. It has no top-level alias.
66
## Files Read
77

88
Reads the selected stack's descriptor and `<SUPABASE_HOME>/managed/stacks/<id>/state.json`,
9-
plus owner metadata in `control.json` when present. Configuration comes from the
10-
selected descriptor's project root: `supabase/config.toml` or `supabase/config.json`,
11-
project environment input through the config loader, configured signing material,
12-
and enabled function dotenv files under `supabase/functions/`.
9+
plus owner metadata in `control.json` when present. Restart does not load project
10+
configuration; it uses the selected stack's saved effective configuration and preparation policy.
11+
Feature routing and workdir discovery may still read `supabase/config.toml` or
12+
`supabase/config.json`. Use `SUPABASE_EXPERIMENTAL_STACK=1` with `--stack-id` when
13+
project configuration is invalid or unavailable. Runtime startup can read files
14+
referenced by the saved definition, such as signing material.
1315

1416
## Files Written
1517

1618
The CLI does not rewrite project configuration. The stack package updates its
1719
state record, owner metadata, runtime files, logs, and service data beneath the
18-
selected stack directory. Preparation may populate the package's artifact cache
19-
or the container engine's image store. Restart preserves the stack ID and data;
20-
it never calls create or destroy.
20+
selected stack directory. Restart preserves the stack ID and data; it never calls
21+
create or destroy. There is no explicit `prepare()` call. Runtime startup reuses
22+
cached artifacts and may fetch missing ones; the saved `background` preparation
23+
policy can also prefetch enabled lazy services, while `on-demand` skips that prefetch.
2124

2225
## API Routes
2326

@@ -29,18 +32,18 @@ Artifact URLs and registry requests depend on the selected runtime and releases.
2932

3033
- `SUPABASE_HOME`: managed state location; defaults to the user's `.supabase` directory.
3134
- `HOME`: participates in default home resolution.
32-
- Environment references in project configuration and function dotenv files are
33-
resolved by the shared config loader. Their secret values are not emitted.
35+
- Project configuration and function dotenv overrides are not reloaded by the
36+
restart handler. Saved secret values are not emitted.
3437
- Standard CLI settings, output, and telemetry environment controls apply through
3538
the existing CLI layers; restart adds no command-specific environment variables.
3639

3740
## Exit Codes
3841

39-
| Code | Condition |
40-
| ----- | ---------------------------------------------------------------------------------------------------- |
41-
| `0` | The selected stack restarted successfully. |
42-
| `1` | Invalid flags, missing stack/configuration, or a configuration, preparation, stop, or start failure. |
43-
| `130` | The CLI waiter was interrupted. |
42+
| Code | Condition |
43+
| ----- | --------------------------------------------------------- |
44+
| `0` | The selected stack restarted successfully. |
45+
| `1` | Invalid flags, missing stack, or a stop or start failure. |
46+
| `130` | The CLI waiter was interrupted. |
4447

4548
## Telemetry Events Fired
4649

@@ -63,11 +66,11 @@ Legacy `-o/--output` is rejected with guidance to use `--output-format`.
6366
## Notes
6467

6568
Targets one existing stack through `--stack`, `--stack-id`, or the current
66-
project. Configuration validation and preparation precede stop. A preparation
67-
failure leaves the running stack untouched; stop failure prevents start; start
68-
failure leaves the same stack stopped and available for recovery. Interrupting
69+
project. The command stops and starts without an explicit configuration. Stop failure prevents
70+
start; start failure leaves the same stack stopped and available for recovery. Interrupting
6971
the CLI waiter follows the package's owner lifecycle contract and does not invoke
7072
destroy from the command handler.
73+
An unconfigured stack must be initialized with `supabase stack start` before it can be restarted.
7174

72-
Restart does not retain one-off `start` flags such as `--exclude`, `--eager`, or
73-
`--preparation`; it uses the current project configuration and runtime defaults.
75+
Restart reuses saved one-off `start` flags such as `--exclude`, `--eager`, and
76+
`--preparation`; a normal `start` reloads project configuration and current flags.

apps/cli/src/commands/experimental/stack/restart/restart.command.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import { withCommandTelemetry } from "../../../../telemetry/command-telemetry.ts
55
import { stackRestart } from "./restart.handler.ts";
66

77
const config = {
8-
stack: Flag.string("stack").pipe(Flag.withDescription("Restart a named stack."), Flag.optional),
8+
stack: Flag.string("stack").pipe(
9+
Flag.withDescription("Restart a named stack (defaults to the current project stack)."),
10+
Flag.optional,
11+
),
912
stackId: Flag.string("stack-id").pipe(
1013
Flag.withDescription("Restart an existing stack by id."),
1114
Flag.optional,
@@ -16,7 +19,7 @@ export type StackRestartFlags = CliCommand.Command.Config.Infer<typeof config>;
1619

1720
export const stackRestartCommand = Command.make("restart", config).pipe(
1821
Command.withDescription(
19-
"Restart an existing managed local Supabase stack using the current project configuration, prepared before stop.",
22+
"Restart an existing managed local Supabase stack using its saved configuration, including the previous start's service selection and preparation policy.",
2023
),
2124
Command.withShortDescription("Restart a managed local stack"),
2225
Command.withExamples([

apps/cli/src/commands/experimental/stack/restart/restart.handler.ts

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Effect, Match, Option } from "effect";
2-
import type { StackError, StackId } from "@supabase/stack/effect";
2+
import type { StackDescriptor, StackError, StackId } from "@supabase/stack/effect";
33
import { Output } from "../../../../shared/output/output.service.ts";
44
import { OutputFlag } from "../../../../command-internal/global-flags.ts";
55
import { CommandSettings } from "../../../../config/command-settings.service.ts";
@@ -13,7 +13,6 @@ import {
1313
validateStackId,
1414
validateStackTarget,
1515
} from "../stack.shared.ts";
16-
import { loadStackConfig } from "../stack-config.ts";
1716
import type { StackRestartFlags } from "./restart.command.ts";
1817
import { StackCommandRestartError } from "./restart.errors.ts";
1918

@@ -32,7 +31,7 @@ const mapStackError = (error: StackError) => {
3231
Match.tag("PortUnavailableError", "PortAllocationError", () => ({
3332
reason: "port" as const,
3433
suggestion:
35-
"Free the conflicting port or update the local stack port configuration, then retry.",
34+
"Free the conflicting port and retry, or stop the stack and use supabase stack start to apply updated project port configuration.",
3635
})),
3736
Match.tag(
3837
"InvalidStackConfigError",
@@ -72,6 +71,14 @@ const mapStackError = (error: StackError) => {
7271
reason: "artifact" as const,
7372
suggestion: "Retry the stack restart with --debug if the artifact cannot be prepared.",
7473
})),
74+
Match.tag("StackRuntimeError", () => ({
75+
reason: "unknown" as const,
76+
suggestion: "Retry the stack restart with --debug and inspect the runtime diagnostics.",
77+
})),
78+
Match.tag("StackCleanupError", () => ({
79+
reason: "unknown" as const,
80+
suggestion: "Retry the stack restart with --debug and inspect cleanup diagnostics.",
81+
})),
7582
Match.orElse(() => ({ reason: "unknown" as const })),
7683
);
7784
return new StackCommandRestartError({
@@ -96,13 +103,12 @@ export const stackRestart = Effect.fn("experimental.stack.restart")(function* (
96103
stackId: Option.getOrUndefined(flags.stackId),
97104
}).pipe(Effect.mapError(mapTargetError));
98105

99-
let target: { readonly projectRoot: string; readonly id: StackId };
106+
let id: StackId;
107+
let desiredLifecycle: StackDescriptor["desiredLifecycle"];
100108
if (Option.isSome(flags.stackId)) {
101-
const validId = yield* validateStackId(flags.stackId.value).pipe(
102-
Effect.mapError(mapTargetError),
103-
);
104-
const inspection = yield* api.inspectStack(validId).pipe(Effect.mapError(mapStackError));
105-
target = { projectRoot: inspection.descriptor.projectRoot, id: inspection.descriptor.id };
109+
id = yield* validateStackId(flags.stackId.value).pipe(Effect.mapError(mapTargetError));
110+
const inspection = yield* api.inspectStack(id).pipe(Effect.mapError(mapStackError));
111+
desiredLifecycle = inspection.descriptor.desiredLifecycle;
106112
} else {
107113
const found = yield* api
108114
.findStack({
@@ -120,30 +126,22 @@ export const stackRestart = Effect.fn("experimental.stack.restart")(function* (
120126
? "Choose an existing --stack name or omit --stack for the current project."
121127
: "Run supabase stack start first.",
122128
});
123-
target = { projectRoot: found.value.projectRoot, id: found.value.id };
129+
id = found.value.id;
130+
desiredLifecycle = found.value.desiredLifecycle;
124131
}
125-
const config = yield* loadStackConfig(target.projectRoot).pipe(
126-
Effect.mapError(
127-
(error) =>
128-
new StackCommandRestartError({
129-
reason: "invalid-config",
130-
message: error.message,
131-
cause: error,
132-
}),
133-
),
134-
);
135-
const stack = yield* api.openStack(target.id).pipe(Effect.mapError(mapStackError));
136-
const task = yield* output.task("Preparing local Supabase stack...");
137-
yield* stack.prepare({ config }).pipe(
138-
Effect.mapError(mapStackError),
139-
Effect.tapError((error) => task.fail(error.message)),
140-
);
141-
yield* task.message("Restarting local Supabase stack...");
132+
if (desiredLifecycle === "unconfigured")
133+
return yield* new StackCommandRestartError({
134+
reason: "lifecycle",
135+
message: "The selected stack has not been configured yet.",
136+
suggestion: "Run supabase stack start to configure the stack first.",
137+
});
138+
const stack = yield* api.openStack(id).pipe(Effect.mapError(mapStackError));
139+
const task = yield* output.task("Restarting local Supabase stack...");
142140
yield* stack.stop.pipe(
143141
Effect.mapError(mapStackError),
144142
Effect.tapError((error) => task.fail(error.message)),
145143
);
146-
const status = yield* stack.start({ config }).pipe(
144+
const status = yield* stack.start().pipe(
147145
Effect.mapError(mapStackError),
148146
Effect.tapError((error) => task.fail(error.message)),
149147
Effect.tap(() => task.clear()),

0 commit comments

Comments
 (0)