Skip to content

Commit 9e31a55

Browse files
committed
Add zitadel-preview bundle release support
1 parent fee97de commit 9e31a55

22 files changed

Lines changed: 1414 additions & 25 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@zitadel/cli": minor
3+
---
4+
5+
Add zitadel-preview manifest support for pinning tested server images and SDK package versions.
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
name: release-preview
2+
3+
# Composes already-published artifacts into a customer-facing
4+
# `zitadel-preview@0.x.y` bundle. This workflow does not publish npm packages
5+
# and does not run GoReleaser; it verifies existing artifacts and creates a
6+
# draft GitHub Release with a manifest asset.
7+
8+
on:
9+
workflow_dispatch:
10+
inputs:
11+
preview_version:
12+
description: "Preview bundle version, for example 0.1.0."
13+
required: true
14+
type: string
15+
ref:
16+
description: "Git ref to attach the preview release to."
17+
required: true
18+
default: "main"
19+
type: string
20+
server_image:
21+
description: "Immutable server image ref, for example ghcr.io/zitadel/nextgen:v0.1.0-alpha.3."
22+
required: true
23+
type: string
24+
cli_version:
25+
description: "Exact @zitadel/cli npm version."
26+
required: true
27+
type: string
28+
sdk_next_version:
29+
description: "Exact @zitadel/sdk-next npm version."
30+
required: true
31+
type: string
32+
npm_packages_json:
33+
description: "Optional JSON object of extra npm package versions, for example {\"@zitadel/sdk-react\":\"0.1.0-alpha.0\"}."
34+
required: false
35+
default: "{}"
36+
type: string
37+
release_notes_path:
38+
description: "Optional repository path to Markdown release notes to include before generated details."
39+
required: false
40+
default: ""
41+
type: string
42+
43+
permissions:
44+
contents: write
45+
packages: read
46+
47+
jobs:
48+
compose:
49+
runs-on: ubuntu-latest
50+
steps:
51+
- name: Checkout
52+
uses: actions/checkout@v6
53+
with:
54+
ref: ${{ inputs.ref }}
55+
fetch-depth: 0
56+
57+
- name: Set up Node.js
58+
uses: actions/setup-node@v6
59+
with:
60+
node-version-file: .nvmrc
61+
registry-url: "https://registry.npmjs.org"
62+
63+
- name: Set up Docker Buildx
64+
uses: docker/setup-buildx-action@v4
65+
66+
- name: Login to GHCR
67+
uses: docker/login-action@v4
68+
with:
69+
registry: ghcr.io
70+
username: ${{ github.actor }}
71+
password: ${{ secrets.GITHUB_TOKEN }}
72+
73+
- name: Generate preview manifest and notes
74+
id: preview
75+
env:
76+
PREVIEW_VERSION: ${{ inputs.preview_version }}
77+
SERVER_IMAGE: ${{ inputs.server_image }}
78+
CLI_VERSION: ${{ inputs.cli_version }}
79+
SDK_NEXT_VERSION: ${{ inputs.sdk_next_version }}
80+
NPM_PACKAGES_JSON: ${{ inputs.npm_packages_json }}
81+
RELEASE_NOTES_PATH: ${{ inputs.release_notes_path }}
82+
run: |
83+
set -euo pipefail
84+
mkdir -p dist/preview-release
85+
COMMIT="$(git rev-parse HEAD)"
86+
ARGS=(
87+
prepare
88+
--preview-version "$PREVIEW_VERSION"
89+
--commit "$COMMIT"
90+
--server-image "$SERVER_IMAGE"
91+
--cli-version "$CLI_VERSION"
92+
--sdk-next-version "$SDK_NEXT_VERSION"
93+
--additional-npm-packages-json "$NPM_PACKAGES_JSON"
94+
--repository "${GITHUB_REPOSITORY}"
95+
--out-dir "dist/preview-release"
96+
)
97+
if [ -n "$RELEASE_NOTES_PATH" ]; then
98+
ARGS+=(--release-notes-path "$RELEASE_NOTES_PATH")
99+
fi
100+
node scripts/release-preview.mjs "${ARGS[@]}" | tee dist/preview-release.env
101+
cat dist/preview-release.env >> "$GITHUB_OUTPUT"
102+
103+
- name: Create draft GitHub Release
104+
env:
105+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
106+
TAG: ${{ steps.preview.outputs.tag }}
107+
TITLE: ${{ steps.preview.outputs.title }}
108+
MANIFEST_PATH: ${{ steps.preview.outputs.manifest_path }}
109+
ASSET_NAME: ${{ steps.preview.outputs.asset_name }}
110+
NOTES_PATH: ${{ steps.preview.outputs.notes_path }}
111+
run: |
112+
set -euo pipefail
113+
gh release create "$TAG" \
114+
"$MANIFEST_PATH#$ASSET_NAME" \
115+
--draft \
116+
--target "$(git rev-parse HEAD)" \
117+
--title "$TITLE" \
118+
--notes-file "$NOTES_PATH"

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,37 @@ corepack pnpm changeset
204204
The changesets workflow opens a "Version Packages" PR. Merging that PR versions
205205
and publishes the affected packages through npm trusted publishing.
206206

207+
### Preview product bundles (`zitadel-preview`)
208+
209+
`zitadel-preview@0.x` is the customer-facing bundle layer for external testers.
210+
It does not replace the GoReleaser or Changesets workflows; it records the exact
211+
server image and npm package versions that were tested together.
212+
213+
Release ceremony:
214+
215+
1. Merge normal feature and fix PRs with changesets as usual.
216+
2. Let [`release-npm.yml`](.github/workflows/release-npm.yml) publish the npm
217+
prerelease packages.
218+
3. Run [`release.yml`](.github/workflows/release.yml) for the server image.
219+
4. Run [`release-preview.yml`](.github/workflows/release-preview.yml) with:
220+
- `preview_version`, for example `0.1.0`;
221+
- immutable `server_image`, for example `ghcr.io/zitadel/nextgen:v0.1.0-alpha.3`;
222+
- exact `cli_version` and `sdk_next_version`;
223+
- optional extra npm packages as JSON.
224+
225+
Follow the short [preview release runbook](docs/runbooks/release-preview.md)
226+
when cutting a bundle.
227+
228+
The preview workflow creates a draft GitHub Release tagged
229+
`zitadel-preview-v<version>` with a `zitadel-preview-<version>.json` manifest
230+
asset and generated tester commands:
231+
232+
```sh
233+
npx @zitadel/cli@<exact-cli-version> doctor --preview-manifest <manifest-url>
234+
npx @zitadel/cli@<exact-cli-version> start --preview-manifest <manifest-url>
235+
npx @zitadel/cli@<exact-cli-version> setup --framework next --server local --preview-manifest <manifest-url>
236+
```
237+
207238
### Local development
208239

209240
The devcontainer at [.devcontainer/](.devcontainer/) pins Go 1.26 and a PostgreSQL sidecar.

apps/cli/README.md

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,24 @@ npm run dev
2828

2929
`start` runs a Docker-backed local Zitadel server and stores runtime data
3030
under `.zitadel/local/`. It uses `ghcr.io/zitadel/nextgen:latest` unless
31-
overridden with `--image` or `ZITADEL_LOCAL_IMAGE`. `setup --server local`
32-
creates a project on that local server, scaffolds `app/login`, `app/register`, and
33-
`middleware.ts`, writes `.env.local` and `.zitadel/`, and installs dependencies
34-
with the detected package manager. Pass `--skip-install` to install them
35-
yourself. The project's default user schema and login flow are provisioned
36-
server-side at creation time, so the CLI does not scaffold or upload them.
37-
Open `http://localhost:3000/login` to see the login page.
31+
overridden with `--image`, `--preview-manifest`, or `ZITADEL_LOCAL_IMAGE`.
32+
`setup --server local` creates a project on that local server, scaffolds
33+
`app/login`, `app/register`, and `middleware.ts`, writes `.env.local` and
34+
`.zitadel/`, and installs dependencies with the detected package manager. Pass
35+
`--skip-install` to install them yourself. The project's default user schema
36+
and login flow are provisioned server-side at creation time, so the CLI does
37+
not scaffold or upload them. Open `http://localhost:3000/login` to see the
38+
login page.
39+
40+
For a `zitadel-preview` bundle, use the exact manifest URL from the GitHub
41+
Release so the CLI starts the tested server image and scaffolds the tested SDK
42+
versions:
43+
44+
```sh
45+
npx @zitadel/cli@<exact-version> doctor --preview-manifest <manifest-url>
46+
npx @zitadel/cli@<exact-version> start --preview-manifest <manifest-url>
47+
npx @zitadel/cli@<exact-version> setup --framework next --server local --preview-manifest <manifest-url>
48+
```
3849

3950
The default project flow supports password registration/login, passkey
4051
registration/login, and optional passkey setup after password registration.
@@ -138,7 +149,7 @@ Verify local runtime and project state.
138149
```
139150
USAGE
140151
$ zitadel doctor [--json] [-c <value>] [-s <value>] [-n] [-f] [--dry-run] [--verbose] [--debug] [--fix]
141-
[--image <value>] [--port <value>]
152+
[--image <value>] [--port <value>] [--preview-manifest <value>]
142153
143154
FLAGS
144155
-c, --cwd=<value> Project directory to operate on.
@@ -150,6 +161,8 @@ FLAGS
150161
--fix Re-apply missing managed files.
151162
--image=<value> Container image to check.
152163
--port=<value> [default: 8080] Local HTTP port.
164+
--preview-manifest=<value>
165+
Path or URL to a zitadel-preview manifest.
153166
--verbose Verbose logging.
154167
155168
GLOBAL FLAGS
@@ -280,7 +293,7 @@ Create a Zitadel project and scaffold local auth.
280293
```
281294
USAGE
282295
$ zitadel setup [--json] [-c <value>] [-s <value>] [-n] [-f] [--dry-run] [--verbose] [--debug]
283-
[--framework next] [--renderer react|web-component] [--skip-install]
296+
[--framework next] [--renderer react|web-component] [--skip-install] [--preview-manifest <value>]
284297
285298
FLAGS
286299
-c, --cwd=<value> Project directory to operate on.
@@ -291,6 +304,8 @@ FLAGS
291304
--dry-run Preview without mutating files or the platform.
292305
--framework=<option> Framework to target.
293306
<options: next>
307+
--preview-manifest=<value>
308+
Path or URL to a zitadel-preview manifest.
294309
--renderer=<option> Renderer (default: react).
295310
<options: react|web-component>
296311
--skip-install Do not install dependencies after setup updates package.json.
@@ -313,7 +328,7 @@ Start a local Zitadel server.
313328
```
314329
USAGE
315330
$ zitadel start [--json] [-c <value>] [-s <value>] [-n] [-f] [--dry-run] [--verbose] [--debug] [--image
316-
<value>] [--port <value>]
331+
<value>] [--port <value>] [--preview-manifest <value>]
317332
318333
FLAGS
319334
-c, --cwd=<value> Project directory to operate on.
@@ -324,6 +339,8 @@ FLAGS
324339
--dry-run Preview without mutating files or the platform.
325340
--image=<value> Container image to run.
326341
--port=<value> [default: 8080] Local HTTP port.
342+
--preview-manifest=<value>
343+
Path or URL to a zitadel-preview manifest.
327344
--verbose Verbose logging.
328345
329346
GLOBAL FLAGS

apps/cli/SKILLS.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ layer, not the envelope.
6868
- `reset` — stop/remove the managed container and delete local runtime data;
6969
requires `--force` when non-interactive.
7070

71+
For an external `zitadel-preview` bundle, pass
72+
`--preview-manifest <path-or-url>` to `doctor`, `start`, and `setup`. The
73+
manifest pins the server image and SDK package versions that were tested
74+
together. `zitadel start --image <ref>` remains the explicit image override.
75+
7176
## Golden path
7277

7378
```sh
@@ -79,6 +84,14 @@ npx @zitadel/cli@alpha plan --non-interactive --json
7984
npx @zitadel/cli@alpha apply --non-interactive --json
8085
```
8186

87+
Preview bundle invocation:
88+
89+
```sh
90+
npx @zitadel/cli@<exact-version> doctor --preview-manifest <manifest-url> --non-interactive --json
91+
npx @zitadel/cli@<exact-version> start --preview-manifest <manifest-url> --non-interactive --json
92+
npx @zitadel/cli@<exact-version> setup --framework next --server local --preview-manifest <manifest-url> --non-interactive --json
93+
```
94+
8295
Repo config is authoritative: edit `zitadel.json` or files under `.zitadel/`,
8396
then re-run `plan` and `apply`. Managed files carry a marker comment; `eject`
8497
removes only files that still carry it, preserving anything the user replaced.

apps/cli/src/commands/doctor/index.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from "../../lib/local-server/runtime";
1616
import { BaseCommand, type JsonEnvelope } from "../../lib/oclif";
1717
import { createOrca } from "../../lib/orca";
18+
import { loadPreviewManifest, previewServerImage } from "../../lib/preview-manifest";
1819
import { hasZitadelConfig } from "../../lib/project";
1920
import { publicCliCommand } from "../../lib/public-cli";
2021
import { SANITY_CHECKS, type CheckContext, type CheckOutcome } from "./checks";
@@ -40,14 +41,24 @@ export default class Doctor extends BaseCommand {
4041
fix: Flags.boolean({ description: "Re-apply missing managed files." }),
4142
image: Flags.string({ description: "Container image to check." }),
4243
port: Flags.integer({ description: "Local HTTP port.", default: DEFAULT_LOCAL_SERVER_PORT }),
44+
"preview-manifest": Flags.string({
45+
description: "Path or URL to a zitadel-preview manifest.",
46+
}),
4347
};
4448

4549
async run(): Promise<JsonEnvelope> {
4650
const { flags } = await this.parse(Doctor);
4751
const port = flags.port ?? DEFAULT_LOCAL_SERVER_PORT;
4852
await this.toMeta(flags, { resolveServer: false, source: localServerUrl(port) });
4953
const { cwd, dryRun } = this.meta;
50-
const image = flags.image ?? this.meta.env.ZITADEL_LOCAL_IMAGE ?? DEFAULT_LOCAL_SERVER_IMAGE;
54+
const preview = flags["preview-manifest"]
55+
? await loadPreviewManifest(flags["preview-manifest"], cwd)
56+
: undefined;
57+
const image =
58+
flags.image ??
59+
(preview ? previewServerImage(preview) : undefined) ??
60+
this.meta.env.ZITADEL_LOCAL_IMAGE ??
61+
DEFAULT_LOCAL_SERVER_IMAGE;
5162
const runtimeChecks = await runLocalRuntimeChecks(cwd, image, port);
5263
const hasConfig = await hasZitadelConfig(cwd);
5364
const ctx: CheckContext = { cwd, orca: createOrca(), cliVersion: this.meta.cliVersion, dryRun };
@@ -82,6 +93,12 @@ export default class Doctor extends BaseCommand {
8293
ok: failed.length === 0,
8394
image,
8495
port,
96+
preview: preview
97+
? {
98+
product: preview.product,
99+
manifest: flags["preview-manifest"],
100+
}
101+
: undefined,
85102
project: {
86103
lifecycle: hasConfig ? "configured" : "not-configured",
87104
},

apps/cli/src/commands/setup/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { BaseCommand, type JsonEnvelope } from "../../lib/oclif";
99
import { createOrca, issuerFromPort, type FrameworkFacts, type Orca } from "../../lib/orca";
1010
import { RENDERER_IDS } from "../../lib/orca/patchers/rule/next/renderers/registry";
1111
import type { PatchContext } from "../../lib/orca/patchers/types";
12+
import { loadPreviewManifest, previewNpmVersions } from "../../lib/preview-manifest";
1213
import { hasZitadelConfig, hasZitadelSecret } from "../../lib/project";
1314
import { installDependenciesForSetup } from "./install";
1415
import { PickFrameworkPrompt, SETUP_PROMPTS, type SetupAnswers } from "./prompts";
@@ -58,12 +59,19 @@ export default class Setup extends BaseCommand {
5859
"skip-install": Flags.boolean({
5960
description: "Do not install dependencies after setup updates package.json.",
6061
}),
62+
"preview-manifest": Flags.string({
63+
description: "Path or URL to a zitadel-preview manifest.",
64+
}),
6165
};
6266

6367
async run(): Promise<JsonEnvelope> {
6468
const { flags } = await this.parse(Setup);
6569
await this.toMeta(flags);
6670
const { cwd, nonInteractive, dryRun, force } = this.meta;
71+
const preview = flags["preview-manifest"]
72+
? await loadPreviewManifest(flags["preview-manifest"], cwd)
73+
: undefined;
74+
const dependencyVersions = preview ? previewNpmVersions(preview) : undefined;
6775

6876
if (await hasZitadelConfig(cwd)) {
6977
return this.emit({ status: "skipped", reason: "already-initialized" });
@@ -135,6 +143,7 @@ export default class Setup extends BaseCommand {
135143
issuer,
136144
server: answers.server,
137145
cliVersion: this.meta.cliVersion,
146+
dependencyVersions,
138147
};
139148
consola.start(`Patching project files${dryRun ? " (dry run)" : ""}`);
140149
const result = await orca.patcherFor(framework.id).patch(ctx, { cwd, dryRun, force });
@@ -201,6 +210,12 @@ export default class Setup extends BaseCommand {
201210
files_written: result.filesWritten.map((file) => relativeDisplay(cwd, file)),
202211
files_skipped: result.filesSkipped.map((file) => relativeDisplay(cwd, file)),
203212
install: installOutcome.install,
213+
preview: preview
214+
? {
215+
product: preview.product,
216+
manifest: flags["preview-manifest"],
217+
}
218+
: undefined,
204219
next_actions: installOutcome.nextActions,
205220
next_commands: installOutcome.nextCommands,
206221
},

0 commit comments

Comments
 (0)