Skip to content

Commit e53e3e5

Browse files
committed
fix: harden local setup and login automation
1 parent 5128603 commit e53e3e5

40 files changed

Lines changed: 757 additions & 72 deletions

.changeset/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ The repo is currently in changesets **prerelease mode** with the `alpha` tag (se
2929
- `changeset version` cuts versions like `0.1.0-alpha.0`, `0.1.0-alpha.1`, …
3030
- Public packages are in one fixed group, so an alpha train uses the same
3131
version across `@zitadel/cli`, SDKs, components, and generated API packages.
32-
- `changeset publish` publishes them under the **`alpha`** npm dist-tag, **not** `latest`. So `npm install @zitadel/cli` keeps resolving the last stable release; consumers opt into prereleases with `@zitadel/cli@alpha`.
32+
- `changeset publish` publishes them under the **`alpha`** npm dist-tag. During the public alpha, the release workflow also promotes only `@zitadel/cli` to `latest` so bare `npx @zitadel/cli` reaches the supported tester workflow. Other public packages stay opt-in via `@alpha` or exact alpha versions.
3333
- A package that has never had a stable release is published to `latest` on its first publish (changesets behaviour), then to `alpha` thereafter until it has a stable release.
3434

3535
To leave alpha and cut a stable `latest` release:
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@zitadel/cli": patch
3+
"@zitadel/components": patch
4+
---
5+
6+
Harden local setup guidance, Next 16 scaffolding, and login form automation.

.github/workflows/ci.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,26 @@ jobs:
864864
# repository is public.
865865
NPM_CONFIG_PROVENANCE: "false"
866866

867+
- name: Promote CLI alpha to npm latest
868+
if: ${{ steps.changesets.outputs.published == 'true' }}
869+
env:
870+
NPM_CONFIG_PROVENANCE: "false"
871+
run: |
872+
set -euo pipefail
873+
version="$(node -p "require('./apps/cli/package.json').version")"
874+
node --input-type=module - "$version" <<'NODE'
875+
const version = process.argv[2];
876+
if (!/^\d+\.\d+\.\d+-alpha\.\d+$/.test(version)) {
877+
throw new Error(`expected an alpha CLI version, got ${version}`);
878+
}
879+
NODE
880+
# Temporary public-alpha exception: the CLI is the tester entrypoint,
881+
# so bare `npx @zitadel/cli` must reach the supported local workflow.
882+
# Keep `alpha` as the canonical prerelease tag, and do not move
883+
# `latest` for SDKs/components, Docker images, or GitHub Releases.
884+
npm dist-tag add "@zitadel/cli@$version" alpha
885+
npm dist-tag add "@zitadel/cli@$version" latest
886+
867887
- name: Restore changesets after publish decision
868888
run: git restore -- .changeset
869889

apps/cli-journey-e2e/scripts/prepare-next-app.mjs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export async function prepareNextApp(options = {}) {
1818
const appDir = resolve(env.JOURNEY_APP_DIR ?? join(outputDir, "myapp"));
1919
const registryUrl = env.JOURNEY_REGISTRY_URL ?? defaultRegistryUrl;
2020
const appUrl = env.JOURNEY_APP_URL ?? defaultAppUrl;
21+
const zitadelPort = optionalPort(env.JOURNEY_ZITADEL_PORT, "JOURNEY_ZITADEL_PORT");
2122
const fs = {
2223
appendFile: options.appendFile ?? appendFile,
2324
mkdir: options.mkdir ?? mkdir,
@@ -46,7 +47,7 @@ export async function prepareNextApp(options = {}) {
4647
outputDir,
4748
runCapture: runCaptureFn,
4849
step: "doctor",
49-
stepArgs: ["doctor"],
50+
stepArgs: withPort(["doctor"], zitadelPort),
5051
writeFile: fs.writeFile,
5152
});
5253
startJson = await runCliJsonStep({
@@ -56,7 +57,7 @@ export async function prepareNextApp(options = {}) {
5657
outputDir,
5758
runCapture: runCaptureFn,
5859
step: "start",
59-
stepArgs: ["start"],
60+
stepArgs: withPort(["start"], zitadelPort),
6061
writeFile: fs.writeFile,
6162
});
6263
setupJson = await runCliJsonStep({
@@ -225,6 +226,20 @@ function npmEnvironment(env, registryUrl) {
225226
};
226227
}
227228

229+
function optionalPort(value, name) {
230+
if (!value) return undefined;
231+
const port = Number(value);
232+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
233+
throw new Error(`${name} must be a TCP port, got ${value}`);
234+
}
235+
return port;
236+
}
237+
238+
function withPort(args, port) {
239+
if (!port) return args;
240+
return [...args, "--port", String(port)];
241+
}
242+
228243
function runCapture(command, args, options) {
229244
return new Promise((resolveRun, reject) => {
230245
const child = spawn(command, args, {

apps/cli-journey-e2e/scripts/prepare-next-app.test.mjs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ test("prepares the customer local setup journey in the app root", async () => {
2020
JOURNEY_REGISTRY_URL: registryUrl,
2121
JOURNEY_SDK_NEXT_PACKAGE: "@zitadel/sdk-next",
2222
JOURNEY_WORK_DIR: workDir,
23+
JOURNEY_ZITADEL_PORT: "18080",
2324
ZITADEL_LOCAL_IMAGE: image,
2425
},
2526
logMetadata: false,
@@ -32,7 +33,7 @@ test("prepares the customer local setup journey in the app root", async () => {
3233
await mkdir(join(options.cwd, ".zitadel/local"), { recursive: true });
3334
await writeFile(
3435
join(options.cwd, ".zitadel/local/runtime.json"),
35-
`${JSON.stringify({ server_url: "http://localhost:8080" })}\n`,
36+
`${JSON.stringify({ server_url: "http://localhost:18080" })}\n`,
3637
);
3738
}
3839
return {
@@ -45,21 +46,25 @@ test("prepares the customer local setup journey in the app root", async () => {
4546

4647
const appDir = join(workDir, "myapp");
4748
assert.equal(metadata.appDir, appDir);
48-
assert.equal(metadata.localRuntimeUrl, "http://localhost:8080");
49+
assert.equal(metadata.localRuntimeUrl, "http://localhost:18080");
4950
assert.deepEqual(
5051
calls.map((call) => call.args),
5152
[
5253
[
5354
"--yes",
5455
"@zitadel/cli@alpha",
5556
"doctor",
57+
"--port",
58+
"18080",
5659
"--non-interactive",
5760
"--json",
5861
],
5962
[
6063
"--yes",
6164
"@zitadel/cli@alpha",
6265
"start",
66+
"--port",
67+
"18080",
6368
"--non-interactive",
6469
"--json",
6570
],
@@ -202,11 +207,12 @@ async function writeGeneratedApp(appDir, registryUrl) {
202207

203208
function okEnvelope(args) {
204209
const command = args[2];
210+
const port = args.includes("--port") ? args[args.indexOf("--port") + 1] : "8080";
205211
if (command === "start") {
206-
return { status: "ok", data: { urls: { api: "http://localhost:8080" } } };
212+
return { status: "ok", data: { urls: { api: `http://localhost:${port}` } } };
207213
}
208214
if (command === "setup") {
209-
return { status: "ok", data: { server: "http://localhost:8080" } };
215+
return { status: "ok", data: { server: `http://localhost:${port}` } };
210216
}
211217
return { status: "ok", data: { ok: true } };
212218
}

apps/cli-journey-e2e/scripts/run-local.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const composeLogPath = join(diagnosticsDir, "compose.log");
4545
const composeProjectName = `zitadel-journey-${process.pid}-${Date.now()}`;
4646
const registryPort = await resolvePort("JOURNEY_REGISTRY_PORT");
4747
const appPort = await resolvePort("JOURNEY_APP_PORT", 3000);
48+
const zitadelPort = await resolvePort("JOURNEY_ZITADEL_PORT");
4849
const registryUrl = `http://127.0.0.1:${registryPort}`;
4950
const appUrl = `http://localhost:${appPort}`;
5051
const cliPackage = await packageName("apps/cli");
@@ -101,6 +102,7 @@ try {
101102
env: {
102103
...process.env,
103104
JOURNEY_APP_URL: appUrl,
105+
JOURNEY_ZITADEL_PORT: String(zitadelPort),
104106
JOURNEY_REGISTRY_URL: registryUrl,
105107
JOURNEY_WORK_DIR: workDir,
106108
NPM_CONFIG_USERCONFIG: verdaccioNpmrcPath,

apps/cli-journey-e2e/src/user-journey.spec.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect, test, type Page } from "@playwright/test";
1+
import { expect, test, type Locator, type Page } from "@playwright/test";
22

33
test.describe.configure({ mode: "serial" });
44

@@ -196,11 +196,11 @@ async function expectRegistrationChoice(page: Page): Promise<void> {
196196
}
197197

198198
async function fillEmail(page: Page, email: string): Promise<void> {
199-
await page.getByLabel(/email/i).first().fill(email);
199+
await fieldControl(page, "email", /email/i).fill(email);
200200
}
201201

202202
async function fillEmailIfVisible(page: Page, email: string): Promise<void> {
203-
const emailField = page.getByLabel(/email/i).first();
203+
const emailField = fieldControl(page, "email", /email/i);
204204
if (await emailField.isVisible().catch(() => false)) {
205205
await emailField.fill(email);
206206
}
@@ -224,11 +224,11 @@ async function fillFieldIfVisible(
224224
}
225225

226226
async function fillPassword(page: Page, password: string): Promise<void> {
227-
await page.getByLabel(/password/i).first().fill(password);
227+
await fieldControl(page, "password", /password/i).fill(password);
228228
}
229229

230230
async function isPasswordVisible(page: Page): Promise<boolean> {
231-
return page.getByLabel(/password/i).first().isVisible().catch(() => false);
231+
return fieldControl(page, "password", /password/i).isVisible().catch(() => false);
232232
}
233233

234234
async function expectSessionCleared(page: Page): Promise<void> {
@@ -253,7 +253,12 @@ async function clickAction(
253253
): Promise<void> {
254254
let locator = page.getByRole("button", { name }).or(page.getByRole("link", { name }));
255255
for (const actionName of actionNames) {
256-
locator = actionLocator(page, actionName).or(locator);
256+
locator = page
257+
.getByTestId(`zitadel-action-${actionName}-button`)
258+
.or(page.getByTestId(`zitadel-action-${actionName}`))
259+
.or(page.getByTestId(`zitadel-action-${actionName}-link`))
260+
.or(actionLocator(page, actionName))
261+
.or(locator);
257262
}
258263
await locator.first().click();
259264
}
@@ -262,6 +267,15 @@ function actionLocator(page: Page, actionName: string) {
262267
return page.locator(`zl-button[action="${actionName}"], [data-action="${actionName}"]`);
263268
}
264269

270+
function fieldControl(page: Page, fieldName: string, label: RegExp): Locator {
271+
return page
272+
.getByTestId(`zitadel-field-${fieldName}`)
273+
.locator("input")
274+
.or(page.locator(`zl-field[name="${fieldName}"]`).locator("input"))
275+
.or(page.getByLabel(label))
276+
.first();
277+
}
278+
265279
function logoutLocator(page: Page) {
266280
return page
267281
.locator("zitadel-logout .signout-btn")

apps/cli/README.md

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ npx @zitadel/cli@alpha start
77
npx @zitadel/cli@alpha setup --server local
88
```
99

10+
During the public alpha, bare `npx @zitadel/cli` resolves to the same tested
11+
alpha CLI. Use `@alpha` or an exact `0.1.0-alpha.N` selector in bug reports and
12+
automation when reproducibility matters.
13+
1014
> **Beta.** This is the **next-generation Zitadel**, a ground-up rewrite of the platform. It is distinct from the established Zitadel at [github.com/zitadel/zitadel](https://github.com/zitadel/zitadel). APIs and CLI flags will change.
1115
1216
## Requirements
@@ -33,12 +37,13 @@ back to `ghcr.io/zitadel/nextgen:latest`. Override with `--image` or
3337
`ZITADEL_LOCAL_IMAGE` for advanced debugging.
3438
`setup --server local` creates a project on that local server, asks which
3539
framework to scaffold when the directory is fresh, writes the Next.js app into
36-
the current directory, scaffolds `app/login`, `app/register`, and
37-
`middleware.ts`, writes `.env.local` and `.zitadel/`, and installs dependencies
38-
with the detected package manager. Pass `--skip-install` to install them
39-
yourself. The project's default user schema and login flow are provisioned
40-
server-side at creation time, so the CLI does not scaffold or upload them. Open
41-
`http://localhost:3000/login` to see the login page.
40+
the current directory, scaffolds `app/login`, `app/register`, and `proxy.ts`
41+
for Next 16+ or `middleware.ts` for older Next versions, writes `.env.local`
42+
and `.zitadel/`, and installs dependencies with the detected package manager.
43+
Pass `--skip-install` to install them yourself. The project's default user
44+
schema and login flow are provisioned server-side at creation time, so the CLI
45+
does not scaffold or upload them. Open `http://localhost:3000/login` to see the
46+
login page.
4247

4348
For a reproducible tester report, use the exact alpha train from the GitHub
4449
Release:
@@ -58,6 +63,8 @@ passkey can sign in with either credential.
5863

5964
- `zitadel doctor` — verify the local Docker runtime and generated project files
6065
- `zitadel status` — summarise the local Docker runtime and project
66+
- `zitadel plan` — validate config and preview sync changes without mutation
67+
- `zitadel apply` — validate and upload repo config to Zitadel
6168
- `zitadel eject` — remove what setup wrote (alias: `zitadel uninstall`)
6269
- `zitadel start|stop|logs|reset` — manage the local Docker runtime
6370

@@ -68,12 +75,14 @@ passkey can sign in with either credential.
6875

6976
<!-- commands -->
7077

78+
- [`zitadel apply`](#zitadel-apply)
7179
- [`zitadel autocomplete [SHELL]`](#zitadel-autocomplete-shell)
7280
- [`zitadel commands`](#zitadel-commands)
7381
- [`zitadel doctor`](#zitadel-doctor)
7482
- [`zitadel eject`](#zitadel-eject)
7583
- [`zitadel help [COMMAND]`](#zitadel-help-command)
7684
- [`zitadel logs`](#zitadel-logs)
85+
- [`zitadel plan`](#zitadel-plan)
7786
- [`zitadel reset`](#zitadel-reset)
7887
- [`zitadel search`](#zitadel-search)
7988
- [`zitadel setup`](#zitadel-setup)
@@ -84,6 +93,33 @@ passkey can sign in with either credential.
8493
- [`zitadel version`](#zitadel-version)
8594
- [`zitadel which`](#zitadel-which)
8695

96+
## `zitadel apply`
97+
98+
Validate and upload repo config to the platform.
99+
100+
```
101+
USAGE
102+
$ zitadel apply [--json] [-c <value>] [-s <value>] [-n] [-f] [--dry-run] [--verbose] [--debug]
103+
[-e development|preview|production]
104+
105+
FLAGS
106+
-c, --cwd=<value> Project directory to operate on.
107+
-e, --environment=<option> Target environment (default: development).
108+
<options: development|preview|production>
109+
-f, --force Overwrite protected files on conflict.
110+
-n, --non-interactive Disable prompts. Required when scripting or running as an agent.
111+
-s, --server=<value> Override the resolved server URL.
112+
--debug Debug logging.
113+
--dry-run Preview without mutating files or the platform.
114+
--verbose Verbose logging.
115+
116+
GLOBAL FLAGS
117+
--json Format output as json.
118+
119+
DESCRIPTION
120+
Validate and upload repo config to the platform.
121+
```
122+
87123
## `zitadel autocomplete [SHELL]`
88124

89125
Display autocomplete installation instructions.
@@ -246,6 +282,33 @@ DESCRIPTION
246282
Show local Zitadel server logs.
247283
```
248284

285+
## `zitadel plan`
286+
287+
Validate config without mutation and preview the sync diff.
288+
289+
```
290+
USAGE
291+
$ zitadel plan [--json] [-c <value>] [-s <value>] [-n] [-f] [--dry-run] [--verbose] [--debug]
292+
[-e development|preview|production]
293+
294+
FLAGS
295+
-c, --cwd=<value> Project directory to operate on.
296+
-e, --environment=<option> Target environment (default: development).
297+
<options: development|preview|production>
298+
-f, --force Overwrite protected files on conflict.
299+
-n, --non-interactive Disable prompts. Required when scripting or running as an agent.
300+
-s, --server=<value> Override the resolved server URL.
301+
--debug Debug logging.
302+
--dry-run Preview without mutating files or the platform.
303+
--verbose Verbose logging.
304+
305+
GLOBAL FLAGS
306+
--json Format output as json.
307+
308+
DESCRIPTION
309+
Validate config without mutation and preview the sync diff.
310+
```
311+
249312
## `zitadel reset`
250313

251314
Delete the local Zitadel server runtime and data.

apps/cli/SKILLS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ parse the result rather than scraping human output.
2727
npx @zitadel/cli@alpha <command> --non-interactive --json
2828
```
2929

30+
During the public alpha, bare `npx @zitadel/cli` is promoted to the same tested
31+
alpha CLI so discovery works for first-time users. Prefer `@alpha` or an exact
32+
`0.1.0-alpha.N` selector in agent scripts and bug reports for reproducibility.
33+
3034
## Reading the envelope
3135

3236
Each invocation prints one JSON object:

apps/cli/src/commands/apply.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,6 @@ import { readZitadelSecret } from "../lib/project";
1919
*/
2020
export default class Apply extends BaseCommand {
2121
static override description = "Validate and upload repo config to the platform.";
22-
// Temporarily hidden while we collapse the dev workflow around `setup`'s
23-
// auto-apply. The logic stays wired up so re-exposing this command is a
24-
// one-line flip when we settle on the surface area.
25-
static override hidden = true;
2622
static override flags = {
2723
environment: Flags.string({
2824
char: "e",

0 commit comments

Comments
 (0)