From 895e1b1c96dbd22fd1ab35d2cdbd405f98ff8e9f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 4 Sep 2026 11:52:01 +0000 Subject: [PATCH 1/3] feat(migration): add migrate subcommand and opt-in --migrate flag Add `nextgen migrate` so schema changes can run and exit without starting the HTTP server. Server start no longer migrates unless `--migrate` is set; zitadel start and the image CMD still pass that flag so local and container zero-config paths keep applying schema. Co-authored-by: Silvan --- .changeset/nextgen-migrate-command.md | 5 ++ CONTRIBUTING.md | 18 ++-- Dockerfile | 4 +- apps/cli/src/lib/local-server/binary.ts | 8 +- .../unit/lib/local-server/binary.test.ts | 4 +- .../unit/lib/local-server/docker.test.ts | 4 + cmd/server/migrate.go | 32 +++++++ cmd/server/migrate_test.go | 83 +++++++++++++++++++ cmd/server/server.go | 60 ++++++++++---- docs/operations/docker-compose.yaml | 1 + docs/quick-start/docker-compose.md | 1 + examples/bootstrap-users/README.md | 2 +- scripts/run-server-debug.mjs | 16 +++- scripts/run-server.mjs | 14 +++- 14 files changed, 219 insertions(+), 33 deletions(-) create mode 100644 .changeset/nextgen-migrate-command.md create mode 100644 cmd/server/migrate.go create mode 100644 cmd/server/migrate_test.go diff --git a/.changeset/nextgen-migrate-command.md b/.changeset/nextgen-migrate-command.md new file mode 100644 index 000000000..54869c3a4 --- /dev/null +++ b/.changeset/nextgen-migrate-command.md @@ -0,0 +1,5 @@ +--- +"@zitadel/server": minor +--- + +Operators can run `nextgen migrate` to apply schema changes and exit without starting the HTTP server. `server` no longer migrates on start unless you pass `--migrate`; `zitadel start` and the published image still migrate. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f2955c60f..3b2f6c85e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,7 +43,7 @@ For changes to the Go server, APIs, or database layer. | Install dependencies | `corepack pnpm install --frozen-lockfile` | | Verify my toolchain | `moon run workspace:doctor` | | Start the server (builds console + login-ui, then Go) | `moon run workspace:server` | -| Start the server (skip UI builds) | `go run . server` | +| Start the server (skip UI builds) | `go run . server --migrate` | | Debug and attach VSCode | `moon run workspace:server-debug` | | Regenerate Go/OpenAPI artifacts | `moon run server:generate` | | Verify committed generated output | `moon run server:check-generate` | @@ -60,19 +60,21 @@ The Go binary embeds production builds of two frontend apps: - `apps/login-ui` → `internal/staticui/login/dist/` `moon run workspace:server` builds both apps before starting Go. If you have not -changed any frontend code, skip the UI builds with `go run . server`. +changed any frontend code, skip the UI builds with `go run . server --migrate`. To build the UIs manually (when bypassing the wrapper): ```sh moon run console:build login-ui:build -go run . server +go run . server --migrate ``` With no database configured, the server uses SQLite at `/zitadel.db`. Override with `-c docs/operations/nextgen.example.yaml`, `NEXTGEN_DATABASE_SQLITE`, or `NEXTGEN_DATABASE_POSTGRES` when you want a -path or DSN you manage. +path or DSN you manage. Schema migrations run when you pass `--migrate`; +`moon run workspace:server` adds that flag. To apply schema and exit without +serving, run `go run . migrate`. Open http://localhost:8080/ui/console/ and http://localhost:8080/ui/login/ @@ -82,7 +84,7 @@ Use `server-debug` to build the binary with debug symbols and disabled inlining, then attach VSCode's Go debugger by PID: ```sh -moon run workspace:server-debug -- server --user-file examples/bootstrap-users/demo-admin.json +moon run workspace:server-debug -- server --migrate --user-file examples/bootstrap-users/demo-admin.json ``` The task prints the `go build` invocation and the PID of the running process. The @@ -91,7 +93,7 @@ three `-X` values are stamped from the current HEAD, and `` below stands fo ``` [server-debug] build: go build -gcflags 'all=-N -l' -ldflags '-X .version=debug+ -X .commit= -X .date=' -o dist/server/nextgen-debug . -[server-debug] run: ./dist/server/nextgen-debug server --user-file examples/bootstrap-users/demo-admin.json +[server-debug] run: ./dist/server/nextgen-debug server --migrate --user-file examples/bootstrap-users/demo-admin.json [server-debug] PID 98765 — VSCode: Run ▸ Start Debugging ▸ "Attach to Process" ``` @@ -143,7 +145,7 @@ the two SPAs bundled directly into the server binary. | Open the component workbench (Storybook) | `moon run storybook:dev` → http://localhost:6006 | | Start the console dev server | `moon run console:dev` → http://localhost:5174 | | Start the login-UI dev server | `moon run login-ui:dev` → http://localhost:5175 | -| Build both apps and test with the Go server | `moon run console:build login-ui:build` then `go run . server` | +| Build both apps and test with the Go server | `moon run console:build login-ui:build` then `go run . server --migrate` | | Run lint, type checks, and tests | `moon ci :lint :typecheck :build :test` | Run `corepack pnpm install --frozen-lockfile` first. @@ -165,7 +167,7 @@ schema and default login/register flow definition. **1. Start the API-only backend:** ```sh -NEXTGEN_SERVER_CONSOLE_ENABLED=false NEXTGEN_SERVER_LOGIN_ENABLED=false go run . server +NEXTGEN_SERVER_CONSOLE_ENABLED=false NEXTGEN_SERVER_LOGIN_ENABLED=false go run . server --migrate ``` This skips the embedded UI dist checks, so it works before you have built diff --git a/Dockerfile b/Dockerfile index 92510ed4d..b35bf6cbf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,4 +23,6 @@ USER 65532:65532 VOLUME ["/var/lib/zitadel/nextgen-data"] EXPOSE 8080 ENTRYPOINT ["/usr/local/bin/nextgen"] -# Root cobra command is already `server` (see main.go); no extra argv. +# Default argv applies schema migrations then serves. Override with +# `migrate` (apply and exit) or drop `--migrate` once a migrate job has run. +CMD ["--migrate"] diff --git a/apps/cli/src/lib/local-server/binary.ts b/apps/cli/src/lib/local-server/binary.ts index 280c51e43..e14d9206f 100644 --- a/apps/cli/src/lib/local-server/binary.ts +++ b/apps/cli/src/lib/local-server/binary.ts @@ -64,7 +64,7 @@ export async function startBinaryRuntime(spec: BinaryRunSpec): Promise { return new Promise((resolveDelay) => setTimeout(resolveDelay, ms)); } + +const MIGRATE_FLAG = "--migrate"; + +function withMigrateFlag(args: string[]): string[] { + return args.includes(MIGRATE_FLAG) ? args : [...args, MIGRATE_FLAG]; +} diff --git a/apps/cli/tests/unit/lib/local-server/binary.test.ts b/apps/cli/tests/unit/lib/local-server/binary.test.ts index 6f62b7562..8da5c6846 100644 --- a/apps/cli/tests/unit/lib/local-server/binary.test.ts +++ b/apps/cli/tests/unit/lib/local-server/binary.test.ts @@ -35,11 +35,13 @@ describe("local server binary helpers", () => { serverUrl: "http://localhost:8091", }); - const [, , options] = vi.mocked(spawn).mock.calls[0] as unknown as [ + const [command, args, options] = vi.mocked(spawn).mock.calls[0] as unknown as [ string, string[], { env: NodeJS.ProcessEnv }, ]; + expect(command).toBe("/tmp/fake-nextgen-server"); + expect(args).toEqual(["--migrate"]); expect(options.env.NEXTGEN_SERVER_ADDRESS).toBe(":8091"); expect(options.env.NEXTGEN_SERVER_PUBLIC_BASE).toBe("http://localhost:8091"); }); diff --git a/apps/cli/tests/unit/lib/local-server/docker.test.ts b/apps/cli/tests/unit/lib/local-server/docker.test.ts index 41e458c3e..f83a9880d 100644 --- a/apps/cli/tests/unit/lib/local-server/docker.test.ts +++ b/apps/cli/tests/unit/lib/local-server/docker.test.ts @@ -18,6 +18,10 @@ describe("local server Docker helpers", () => { expect(dockerfile).toContain(`ENV NEXTGEN_SERVER_DATA_DIR=${CONTAINER_DATA_DIR}`); // The declared USER is what makes the default location unwritable. expect(dockerfile).toContain("USER 65532:65532"); + // Bare `docker run` must still migrate; the image CMD is how smoke-container + // and `zitadel start --runtime docker` keep applying schema after --migrate + // defaulted off on the binary. + expect(dockerfile).toContain('CMD ["--migrate"]'); }); it("builds the single-container run command without an explicit encryption key", () => { diff --git a/cmd/server/migrate.go b/cmd/server/migrate.go new file mode 100644 index 000000000..8dc4f34b1 --- /dev/null +++ b/cmd/server/migrate.go @@ -0,0 +1,32 @@ +package server + +import ( + "context" + "log/slog" + + "github.com/spf13/cobra" +) + +func newMigrateCommand(configPath *string) *cobra.Command { + return &cobra.Command{ + Use: "migrate", + Short: "Apply database migrations and exit", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, _ []string) error { + cfg, err := loadConfig(*configPath) + if err != nil { + return err + } + return migrateDatabase(cmd.Context(), cfg) + }, + } +} + +func migrateDatabase(ctx context.Context, cfg Config) error { + pool, err := startDatabase(ctx, cfg, true) + if err != nil { + return err + } + slog.Info("database migrations applied") + return pool.Close(ctx) +} diff --git a/cmd/server/migrate_test.go b/cmd/server/migrate_test.go new file mode 100644 index 000000000..8ec62d3ff --- /dev/null +++ b/cmd/server/migrate_test.go @@ -0,0 +1,83 @@ +package server + +import ( + "bytes" + "context" + "database/sql" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + _ "modernc.org/sqlite" +) + +func TestCommandHelpListsMigrate(t *testing.T) { + cmd := NewCommand() + var out bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&out) + cmd.SetArgs([]string{"--help"}) + require.NoError(t, cmd.Execute()) + + got := out.String() + assert.Contains(t, got, "migrate") + assert.Contains(t, got, "Apply database migrations and exit") +} + +func TestMigrateCommandAppliesSchemaIdempotently(t *testing.T) { + dataDir, configPath := tempServerConfig(t) + + for range 2 { + cmd := NewCommand() + cmd.SetArgs([]string{"migrate", "--config", configPath}) + require.NoError(t, cmd.Execute()) + } + + assert.True(t, sqliteHasGooseTable(t, defaultSQLitePath(dataDir))) +} + +func TestStartDatabaseSkipsMigrationsUnlessRequested(t *testing.T) { + dataDir, configPath := tempServerConfig(t) + cfg, err := loadConfig(configPath) + require.NoError(t, err) + + ctx := context.Background() + skipped, err := startDatabase(ctx, cfg, false) + require.NoError(t, err) + require.NoError(t, skipped.Close(ctx)) + + dbPath := defaultSQLitePath(dataDir) + assert.False(t, sqliteHasGooseTable(t, dbPath), "goose table should be absent without --migrate") + + applied, err := startDatabase(ctx, cfg, true) + require.NoError(t, err) + require.NoError(t, applied.Close(ctx)) + assert.True(t, sqliteHasGooseTable(t, dbPath)) +} + +func tempServerConfig(t *testing.T) (dataDir, configPath string) { + t.Helper() + dataDir = t.TempDir() + t.Setenv("NEXTGEN_SERVER_DATA_DIR", dataDir) + configPath = filepath.Join(t.TempDir(), "nextgen.yaml") + require.NoError(t, os.WriteFile(configPath, nil, 0o600)) + return dataDir, configPath +} + +func sqliteHasGooseTable(t *testing.T, dbPath string) bool { + t.Helper() + db, err := sql.Open("sqlite", dbPath) + require.NoError(t, err) + defer db.Close() + + var name string + err = db.QueryRow(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'goose_db_version'`).Scan(&name) + if err == sql.ErrNoRows { + return false + } + require.NoError(t, err) + return name == "goose_db_version" +} diff --git a/cmd/server/server.go b/cmd/server/server.go index 8e938d270..42e198add 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -48,26 +48,44 @@ import ( func NewCommand() *cobra.Command { var configPath string var userFiles []string + var applyMigrations bool - cmd := &cobra.Command{ - Use: "server", - Short: "Run the server", - RunE: func(cmd *cobra.Command, _ []string) error { - cfg, err := loadConfig(configPath) - if err != nil { - return err - } - return run(cmd.Context(), cfg, userFiles) - }, + runServer := func(cmd *cobra.Command, _ []string) error { + cfg, err := loadConfig(configPath) + if err != nil { + return err + } + return run(cmd.Context(), cfg, userFiles, applyMigrations) } - cmd.Flags().StringVarP(&configPath, "config", "c", "", "Path to YAML configuration file") - cmd.Flags().StringArrayVar(&userFiles, "user-file", nil, "Bootstrap user JSON file (repeatable)") + root := &cobra.Command{ + Use: "nextgen", + Short: "Run the server", + SilenceUsage: true, + RunE: runServer, + } + root.PersistentFlags().StringVarP(&configPath, "config", "c", "", "Path to YAML configuration file") + addServerFlags(root, &applyMigrations, &userFiles) - return cmd + serverCmd := &cobra.Command{ + Use: "server", + Short: "Run the server", + SilenceUsage: true, + RunE: runServer, + } + addServerFlags(serverCmd, &applyMigrations, &userFiles) + + root.AddCommand(serverCmd) + root.AddCommand(newMigrateCommand(&configPath)) + return root +} + +func addServerFlags(cmd *cobra.Command, applyMigrations *bool, userFiles *[]string) { + cmd.Flags().BoolVar(applyMigrations, "migrate", false, "Apply database migrations before serving") + cmd.Flags().StringArrayVar(userFiles, "user-file", nil, "Bootstrap user JSON file (repeatable)") } -func run(ctx context.Context, cfg Config, userFiles []string) error { +func run(ctx context.Context, cfg Config, userFiles []string, applyMigrations bool) error { var err error sfs := &ShutdownFuncs{} defer func() { @@ -99,7 +117,7 @@ func run(ctx context.Context, cfg Config, userFiles []string) error { setUpLogging(cfg.Instrumentation.Log, metrics.LoggerProvider()) - pool, err := startDatabase(ctx, cfg) + pool, err := startDatabase(ctx, cfg, applyMigrations) if err != nil { return err } @@ -605,16 +623,24 @@ func buildHTTPMux(cfg ServerConfig, reqIdGen middleware.RequestIDGenerator, apiH // ----------------------------- STORAGE -------------------------------------- -func startDatabase(ctx context.Context, cfg Config) (database.Pool, error) { +func connectDatabase(ctx context.Context, cfg Config) (database.Pool, error) { dialect, err := buildDatabaseDialect(cfg) if err != nil { return nil, err } - pool, err := database.Connect(ctx, dialect) + return database.Connect(ctx, dialect) +} + +func startDatabase(ctx context.Context, cfg Config, applyMigrations bool) (database.Pool, error) { + pool, err := connectDatabase(ctx, cfg) if err != nil { return nil, err } + if !applyMigrations { + return pool, nil + } if err := pool.Migrate(ctx); err != nil { + _ = pool.Close(ctx) return nil, err } return pool, nil diff --git a/docs/operations/docker-compose.yaml b/docs/operations/docker-compose.yaml index 236f50da3..87a0f8e01 100644 --- a/docs/operations/docker-compose.yaml +++ b/docs/operations/docker-compose.yaml @@ -36,6 +36,7 @@ services: # Optional: mount bootstrap users (see examples/bootstrap-users/) # command: # - server + # - --migrate # - --user-file # - /bootstrap/demo-admin.json diff --git a/docs/quick-start/docker-compose.md b/docs/quick-start/docker-compose.md index 05e96fc2a..3839a65dd 100644 --- a/docs/quick-start/docker-compose.md +++ b/docs/quick-start/docker-compose.md @@ -43,6 +43,7 @@ To load demo users on startup, uncomment the `user-file` volume and command over ```sh docker compose run --rm nextgen server \ + --migrate \ --user-file /bootstrap/demo-admin.json ``` diff --git a/examples/bootstrap-users/README.md b/examples/bootstrap-users/README.md index 8b0482b3e..c3fd430ab 100644 --- a/examples/bootstrap-users/README.md +++ b/examples/bootstrap-users/README.md @@ -5,7 +5,7 @@ Load pre-defined users when starting the Go server. Each JSON file describes one ## Run ```sh -go run . server -c \ +go run . server --migrate -c \ --user-file examples/bootstrap-users/demo-admin.json ``` diff --git a/scripts/run-server-debug.mjs b/scripts/run-server-debug.mjs index 3bc641474..1c1fb5466 100644 --- a/scripts/run-server-debug.mjs +++ b/scripts/run-server-debug.mjs @@ -6,7 +6,7 @@ import { forwardedArgs, formatCommand, run } from "./dev-process.mjs"; import { assertServerBuildPackage, gitInfo, serverLdflags } from "./server-build.mjs"; const repoRoot = fileURLToPath(new URL("..", import.meta.url)); -const args = forwardedArgs(); +const args = withServerMigrateArgs(forwardedArgs()); const out = "dist/server/nextgen-debug"; // -gcflags "all=-N -l": disable compiler optimisations (-N) and inlining (-l) so the @@ -25,7 +25,9 @@ try { } else { process.stderr.write(`\n[server-debug] build: ${formatCommand("go", buildArgs)}\n`); process.stderr.write(`[server-debug] run: ${formatCommand(`./${out}`, args)}\n\n`); - await run("moon", ["run", "console:build", "login-ui:build"], { cwd: repoRoot }); + if (args[0] !== "migrate") { + await run("moon", ["run", "console:build", "login-ui:build"], { cwd: repoRoot }); + } await run("go", buildArgs, { cwd: repoRoot }); await runBinary(`./${out}`, args, repoRoot); } @@ -86,6 +88,16 @@ function isHelp(a) { return a.includes("--help") || a.includes("-h") || a[0] === "help"; } +function withServerMigrateArgs(args) { + if (isHelp(args) || args.includes("--migrate")) { + return args; + } + if (args[0] === "migrate" || args[0] === "completion") { + return args; + } + return [...args, "--migrate"]; +} + function helpArgs(a) { return a[0] === "help" ? ["--help", ...a.slice(1)] : a; } diff --git a/scripts/run-server.mjs b/scripts/run-server.mjs index a9075de38..1d1ead61b 100644 --- a/scripts/run-server.mjs +++ b/scripts/run-server.mjs @@ -4,10 +4,10 @@ import { fileURLToPath } from "node:url"; import { forwardedArgs, run } from "./dev-process.mjs"; const repoRoot = fileURLToPath(new URL("..", import.meta.url)); -const args = forwardedArgs(); +const args = withServerMigrateArgs(forwardedArgs()); try { - if (!isHelp(args)) { + if (!isHelp(args) && args[0] !== "migrate") { await run("moon", ["run", "console:build", "login-ui:build"], { cwd: repoRoot }); } await run("go", ["run", ".", ...args], { cwd: repoRoot }); @@ -19,3 +19,13 @@ try { function isHelp(args) { return args.includes("--help") || args.includes("-h") || args[0] === "help"; } + +function withServerMigrateArgs(args) { + if (isHelp(args) || args.includes("--migrate")) { + return args; + } + if (args[0] === "migrate" || args[0] === "completion") { + return args; + } + return [...args, "--migrate"]; +} From e33f0ac98de3f278421f44cfc5cc108b72ea787f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 4 Sep 2026 17:13:55 +0000 Subject: [PATCH 2/3] fix(migration): honor log config and silence duplicate cobra errors Wire migrate through setUpLogging and StreamRuntime so Cloud Run jobs respect instrumentation.log. Debug-log pool.Close failures without failing the process. Silence Cobra errors on root, server, and migrate. Skip UI builds for completion in the run-server wrappers. Tighten migrate tests per review. Co-authored-by: Silvan --- cmd/server/migrate.go | 34 +++++++++++++++++++++++++++++----- cmd/server/migrate_test.go | 24 ++++++++++++++++++------ cmd/server/server.go | 18 ++++++++++-------- scripts/run-server-debug.mjs | 2 +- scripts/run-server.mjs | 2 +- 5 files changed, 59 insertions(+), 21 deletions(-) diff --git a/cmd/server/migrate.go b/cmd/server/migrate.go index 8dc4f34b1..2c9d8dd81 100644 --- a/cmd/server/migrate.go +++ b/cmd/server/migrate.go @@ -2,16 +2,21 @@ package server import ( "context" + "fmt" "log/slog" "github.com/spf13/cobra" + + "github.com/zitadel/nextgen/internal/instrumentation/zlog" + "github.com/zitadel/nextgen/internal/instrumentation/zotel" ) func newMigrateCommand(configPath *string) *cobra.Command { return &cobra.Command{ - Use: "migrate", - Short: "Apply database migrations and exit", - SilenceUsage: true, + Use: "migrate", + Short: "Apply database migrations and exit", + SilenceUsage: true, + SilenceErrors: true, RunE: func(cmd *cobra.Command, _ []string) error { cfg, err := loadConfig(*configPath) if err != nil { @@ -23,10 +28,29 @@ func newMigrateCommand(configPath *string) *cobra.Command { } func migrateDatabase(ctx context.Context, cfg Config) error { + metrics, err := zotel.NewOtelMetrics(ctx, zotel.MetricsConfig{ + ServiceName: cfg.Instrumentation.ServiceName, + TraceIdFraction: cfg.Instrumentation.Trace.Fraction, + TraceExporter: cfg.Instrumentation.Trace.Exporter, + MetricExporter: cfg.Instrumentation.Metric.Exporter, + LogExporter: cfg.Instrumentation.Log.Exporter, + }) + if err != nil { + return fmt.Errorf("failed to create otel metrics: %w", err) + } + defer func() { + _ = metrics.Shutdown(context.WithoutCancel(ctx)) + }() + setUpLogging(cfg.Instrumentation.Log, metrics.LoggerProvider()) + ctx = zlog.WithLoggingContext(ctx, zlog.WithStream(slog.Default(), zlog.StreamRuntime)) + pool, err := startDatabase(ctx, cfg, true) if err != nil { return err } - slog.Info("database migrations applied") - return pool.Close(ctx) + zlog.Info(ctx, "database migrations applied") + if err := pool.Close(ctx); err != nil { + zlog.WithError(ctx, err).Debug("closing database pool after migrate") + } + return nil } diff --git a/cmd/server/migrate_test.go b/cmd/server/migrate_test.go index 8ec62d3ff..9b227c006 100644 --- a/cmd/server/migrate_test.go +++ b/cmd/server/migrate_test.go @@ -2,7 +2,6 @@ package server import ( "bytes" - "context" "database/sql" "os" "path/filepath" @@ -22,9 +21,20 @@ func TestCommandHelpListsMigrate(t *testing.T) { cmd.SetArgs([]string{"--help"}) require.NoError(t, cmd.Execute()) - got := out.String() - assert.Contains(t, got, "migrate") - assert.Contains(t, got, "Apply database migrations and exit") + var migrateCmd, serverCmd bool + for _, c := range cmd.Commands() { + switch c.Name() { + case "migrate": + migrateCmd = true + assert.Equal(t, "Apply database migrations and exit", c.Short) + case "server": + serverCmd = true + require.NotNil(t, c.Flags().Lookup("migrate"), "expected --migrate on server") + } + } + assert.True(t, migrateCmd, "expected a command named migrate") + assert.True(t, serverCmd, "expected a command named server") + require.NotNil(t, cmd.Flags().Lookup("migrate"), "expected --migrate on root") } func TestMigrateCommandAppliesSchemaIdempotently(t *testing.T) { @@ -33,7 +43,9 @@ func TestMigrateCommandAppliesSchemaIdempotently(t *testing.T) { for range 2 { cmd := NewCommand() cmd.SetArgs([]string{"migrate", "--config", configPath}) - require.NoError(t, cmd.Execute()) + executed, err := cmd.ExecuteC() + require.NoError(t, err) + assert.Equal(t, "migrate", executed.Name()) } assert.True(t, sqliteHasGooseTable(t, defaultSQLitePath(dataDir))) @@ -44,7 +56,7 @@ func TestStartDatabaseSkipsMigrationsUnlessRequested(t *testing.T) { cfg, err := loadConfig(configPath) require.NoError(t, err) - ctx := context.Background() + ctx := t.Context() skipped, err := startDatabase(ctx, cfg, false) require.NoError(t, err) require.NoError(t, skipped.Close(ctx)) diff --git a/cmd/server/server.go b/cmd/server/server.go index 42e198add..c40f1cc75 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -59,19 +59,21 @@ func NewCommand() *cobra.Command { } root := &cobra.Command{ - Use: "nextgen", - Short: "Run the server", - SilenceUsage: true, - RunE: runServer, + Use: "nextgen", + Short: "Run the server", + SilenceUsage: true, + SilenceErrors: true, + RunE: runServer, } root.PersistentFlags().StringVarP(&configPath, "config", "c", "", "Path to YAML configuration file") addServerFlags(root, &applyMigrations, &userFiles) serverCmd := &cobra.Command{ - Use: "server", - Short: "Run the server", - SilenceUsage: true, - RunE: runServer, + Use: "server", + Short: "Run the server", + SilenceUsage: true, + SilenceErrors: true, + RunE: runServer, } addServerFlags(serverCmd, &applyMigrations, &userFiles) diff --git a/scripts/run-server-debug.mjs b/scripts/run-server-debug.mjs index 1c1fb5466..47400cdac 100644 --- a/scripts/run-server-debug.mjs +++ b/scripts/run-server-debug.mjs @@ -25,7 +25,7 @@ try { } else { process.stderr.write(`\n[server-debug] build: ${formatCommand("go", buildArgs)}\n`); process.stderr.write(`[server-debug] run: ${formatCommand(`./${out}`, args)}\n\n`); - if (args[0] !== "migrate") { + if (args[0] !== "migrate" && args[0] !== "completion") { await run("moon", ["run", "console:build", "login-ui:build"], { cwd: repoRoot }); } await run("go", buildArgs, { cwd: repoRoot }); diff --git a/scripts/run-server.mjs b/scripts/run-server.mjs index 1d1ead61b..30849623e 100644 --- a/scripts/run-server.mjs +++ b/scripts/run-server.mjs @@ -7,7 +7,7 @@ const repoRoot = fileURLToPath(new URL("..", import.meta.url)); const args = withServerMigrateArgs(forwardedArgs()); try { - if (!isHelp(args) && args[0] !== "migrate") { + if (!isHelp(args) && args[0] !== "migrate" && args[0] !== "completion") { await run("moon", ["run", "console:build", "login-ui:build"], { cwd: repoRoot }); } await run("go", ["run", ".", ...args], { cwd: repoRoot }); From 3a995171b8cab14147de2f278f7f0506ba98ce2a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 4 Sep 2026 17:28:26 +0000 Subject: [PATCH 3/3] fix(cli): record injected --migrate in local server status Compute spawn args once so BinaryRuntimeMetadata.command matches the process that was started, not the pre-injection argv. Co-authored-by: Silvan --- apps/cli/src/lib/local-server/binary.ts | 5 +++-- apps/cli/tests/unit/lib/local-server/binary.test.ts | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/cli/src/lib/local-server/binary.ts b/apps/cli/src/lib/local-server/binary.ts index e14d9206f..d3ed94eef 100644 --- a/apps/cli/src/lib/local-server/binary.ts +++ b/apps/cli/src/lib/local-server/binary.ts @@ -64,7 +64,8 @@ export async function startBinaryRuntime(spec: BinaryRunSpec): Promise { vi.stubEnv("ZITADEL_SERVER_BINARY", "/tmp/fake-nextgen-server"); const dir = await mkdtemp(join(tmpdir(), "zitadel-binary-test-")); - await startBinaryRuntime({ + const runtime = await startBinaryRuntime({ cliVersion: "0.0.0-test", dataDir: join(dir, "data"), logPath: join(dir, "logs", "server.log"), @@ -42,6 +42,7 @@ describe("local server binary helpers", () => { ]; expect(command).toBe("/tmp/fake-nextgen-server"); expect(args).toEqual(["--migrate"]); + expect(runtime.command).toBe("/tmp/fake-nextgen-server --migrate"); expect(options.env.NEXTGEN_SERVER_ADDRESS).toBe(":8091"); expect(options.env.NEXTGEN_SERVER_PUBLIC_BASE).toBe("http://localhost:8091"); });