fix(database): separate user lifecycle ownership from team membership #1841
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: ci | |
| on: | |
| pull_request: | |
| push: | |
| branches: [main] | |
| permissions: | |
| contents: read | |
| # Cancel an in-flight run when a newer commit is pushed to the same PR/branch. | |
| # Main runs may publish releases, so do not cancel them once started. | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| detect-alpha-release: | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| runs-on: depot-ubuntu-24.04-4 | |
| timeout-minutes: 5 | |
| outputs: | |
| should_release: ${{ steps.detect.outputs.should_release }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Detect release-relevant changes | |
| id: detect | |
| env: | |
| BEFORE: ${{ github.event.before }} | |
| run: | | |
| set -euo pipefail | |
| base="$BEFORE" | |
| if [ -z "$base" ] || [[ "$base" =~ ^0+$ ]] || ! git cat-file -e "$base^{commit}" 2>/dev/null; then | |
| base="$(git rev-parse HEAD^ 2>/dev/null || true)" | |
| fi | |
| if [ -n "$base" ]; then | |
| git diff --name-only "$base" HEAD > "$RUNNER_TEMP/release-changed-files" | |
| else | |
| git diff-tree --no-commit-id --name-only -r HEAD > "$RUNNER_TEMP/release-changed-files" | |
| fi | |
| node --input-type=module <<'NODE' | |
| import { appendFileSync, readFileSync } from "node:fs"; | |
| import { PUBLIC_PACKAGE_MANIFESTS } from "./scripts/release-alpha-train.mjs"; | |
| const changed = readFileSync(`${process.env.RUNNER_TEMP}/release-changed-files`, "utf8") | |
| .split("\n") | |
| .map((line) => line.trim()) | |
| .filter(Boolean); | |
| const publicPackageReleasePaths = new Set( | |
| PUBLIC_PACKAGE_MANIFESTS.flatMap((manifestPath) => [ | |
| manifestPath, | |
| manifestPath.replace(/package\.json$/, "CHANGELOG.md"), | |
| ]), | |
| ); | |
| const releaseRelevant = changed.filter( | |
| (path) => path.startsWith(".changeset/") || publicPackageReleasePaths.has(path), | |
| ); | |
| const shouldRelease = releaseRelevant.length > 0; | |
| console.log(`release-relevant changes: ${releaseRelevant.join(", ") || "none"}`); | |
| appendFileSync(process.env.GITHUB_OUTPUT, `should_release=${String(shouldRelease)}\n`); | |
| NODE | |
| openapi-lint: | |
| runs-on: depot-ubuntu-24.04-4 | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up node | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: .nvmrc | |
| - name: Lint OpenAPI spec | |
| run: npx --yes @redocly/cli@2.31.5 lint api/openapi/openapi-spec.yaml --format=github-actions | |
| go-unit-test: | |
| runs-on: depot-ubuntu-24.04-8 | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-go@v6 | |
| with: | |
| go-version-file: go.mod | |
| cache: true | |
| - name: Verify gofmt formatting | |
| run: | | |
| unformatted="$(git ls-files -z -- '*.go' | xargs -0 -r gofmt -l)" | |
| if [ -n "$unformatted" ]; then | |
| printf '%s\n' "$unformatted" | |
| exit 1 | |
| fi | |
| - run: go generate ./... | |
| - name: Verify generated code is up to date | |
| run: | | |
| git diff --stat --exit-code -- . | |
| test -z "$(git status --porcelain)" | |
| - run: go vet ./... | |
| # Unit tests only. The DB-backed tests live behind the `postgres_integration` / | |
| # `spanner_integration` build tags and run in the dedicated jobs below, so | |
| # this job needs neither the postgres runtime deps nor the binary cache. | |
| - run: go test -v -timeout=10m ./... | |
| go-integration-test-postgres: | |
| runs-on: depot-ubuntu-24.04-4 | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-go@v6 | |
| with: | |
| go-version-file: go.mod | |
| cache: true | |
| # Postgres integration tests run against a Docker testcontainer (mirroring | |
| # the spanner job); the Depot Ubuntu runner ships Docker, so no embedded-postgres | |
| # runtime deps or Maven binary cache are needed here. | |
| - name: Postgres integration tests | |
| run: | | |
| mkdir -p test-output/go | |
| set -o pipefail | |
| go test -json -v -tags postgres_integration -timeout=10m ./... | tee test-output/go/postgres-integration.json | |
| - name: Upload Postgres test log on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: postgres-integration-test-log | |
| path: test-output/go/postgres-integration.json | |
| if-no-files-found: ignore | |
| retention-days: 7 | |
| go-integration-test-spanner: | |
| runs-on: depot-ubuntu-24.04-4 | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-go@v6 | |
| with: | |
| go-version-file: go.mod | |
| cache: true | |
| # Spanner tests use a Docker testcontainer emulator, not the embedded | |
| # postgres binary, so no Maven binary cache is needed here. | |
| - run: go test -v -tags spanner_integration -timeout=10m ./... | |
| goreleaser-snapshot: | |
| runs-on: depot-ubuntu-24.04-8 | |
| timeout-minutes: 20 | |
| needs: [go-unit-test, go-integration-test-postgres, go-integration-test-spanner, node-check] | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Prune npm package tags for GoReleaser | |
| run: | | |
| # Changesets creates scoped package tags such as @zitadel/sdk-nuxt@0.1.0-alpha.0. | |
| # GoReleaser should derive server versions only from server release tags. | |
| git tag -l '@zitadel/*' | while read -r tag; do | |
| git tag -d "$tag" | |
| done | |
| - name: Set up Go | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version-file: go.mod | |
| cache: true | |
| - name: Set up pnpm | |
| uses: pnpm/action-setup@v5 | |
| with: | |
| run_install: false | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: pnpm | |
| - name: Set up QEMU | |
| uses: docker/setup-qemu-action@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Run GoReleaser (snapshot) | |
| uses: goreleaser/goreleaser-action@v7 | |
| with: | |
| distribution: goreleaser | |
| version: "~> v2" | |
| args: release --snapshot --clean --skip=publish,sign | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Export Docker image for smoke tests | |
| run: | | |
| # Goreleaser builds linux/amd64 and linux/arm64 with the same tag; pin amd64 | |
| # for the linux/amd64 smoke jobs (see quickstart-smoke). | |
| IMAGE_ID="" | |
| IMAGE_REF="" | |
| for id in $(docker images ghcr.io/zitadel/nextgen --format '{{.ID}}' | sort -u); do | |
| arch=$(docker image inspect -f '{{.Architecture}}' "$id") | |
| ref=$(docker image inspect -f '{{index .RepoTags 0}}' "$id") | |
| case "$ref" in *snapshot*) ;; *) continue ;; esac | |
| if [ "$arch" = "amd64" ]; then | |
| IMAGE_ID="$id" | |
| IMAGE_REF="$ref" | |
| break | |
| fi | |
| done | |
| if [ -z "$IMAGE_ID" ]; then | |
| echo "::error::no linux/amd64 snapshot image found for ghcr.io/zitadel/nextgen" | |
| docker images ghcr.io/zitadel/nextgen | |
| exit 1 | |
| fi | |
| # Save by a stable local tag: docker save by ID drops repo tags from the tarball. | |
| CI_IMAGE="ghcr.io/zitadel/nextgen:ci-snapshot-smoke-amd64" | |
| docker tag "$IMAGE_ID" "$CI_IMAGE" | |
| echo "$CI_IMAGE" > dist/nextgen-image.tag | |
| docker save "$CI_IMAGE" -o dist/nextgen-image.tar | |
| - name: Upload GoReleaser snapshot artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: goreleaser-snapshot | |
| path: dist/** | |
| if-no-files-found: error | |
| retention-days: 7 | |
| go-smoke-test-embedded-postgres: | |
| runs-on: depot-ubuntu-24.04-4 | |
| timeout-minutes: 15 | |
| needs: [goreleaser-snapshot] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Download GoReleaser snapshot | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: goreleaser-snapshot | |
| path: goreleaser-dist | |
| # Boots the Goreleaser-built binary directly on the embedded-postgres | |
| # default connector. This proves the single binary starts, migrates, and | |
| # handles SIGTERM; Docker image and quick-start surfaces are covered below. | |
| - name: Install embedded-postgres runtime deps | |
| run: sudo apt-get update -qq && sudo apt-get install -y -qq libicu-dev libssl-dev | |
| - name: Cache embedded-postgres binary | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.embedded-postgres-go | |
| key: embedded-postgres-${{ runner.os }}-${{ hashFiles('go.sum') }} | |
| - name: Extract linux/amd64 server binary | |
| run: | | |
| TARBALL="$(find goreleaser-dist -name 'nextgen_*_linux_amd64.tar.gz' | head -1)" | |
| test -n "$TARBALL" | |
| tar -xzf "$TARBALL" -C . | |
| test -x ./nextgen | |
| chmod +x ./nextgen | |
| - name: Smoke test (start, migrate, graceful exit 0) | |
| run: | | |
| ./nextgen > server.log 2>&1 & | |
| pid=$! | |
| # "server listening" is logged only after embedded PG is up, migrations | |
| # ran, and services wired; the generous timeout covers a cold Maven download. | |
| ready=0 | |
| for i in $(seq 1 240); do | |
| if grep -q "server listening for requests" server.log; then ready=1; break; fi | |
| if ! kill -0 "$pid" 2>/dev/null; then | |
| echo "::error::server exited before becoming ready"; cat server.log; exit 1 | |
| fi | |
| sleep 1 | |
| done | |
| if [ "$ready" -ne 1 ]; then | |
| echo "::error::timed out waiting for server readiness"; cat server.log | |
| kill -TERM "$pid" 2>/dev/null || true; exit 1 | |
| fi | |
| test -f nextgen-data/server-encryption-key | |
| kill -TERM "$pid" | |
| code=0; wait "$pid" || code=$? | |
| cat server.log | |
| [ "$code" -eq 0 ] || { echo "::error::non-zero exit after SIGTERM: $code"; exit 1; } | |
| echo "embedded-postgres smoke test passed (start, migrate, graceful exit 0)" | |
| node-check: | |
| runs-on: depot-ubuntu-24.04-8 | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up pnpm | |
| uses: pnpm/action-setup@v5 | |
| with: | |
| run_install: false | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: pnpm | |
| - name: Install dependencies | |
| run: corepack pnpm install --frozen-lockfile | |
| - name: Node checks | |
| run: corepack pnpm nx run-many -t lint,typecheck,build,test | |
| changeset-check: | |
| # Gate PRs: fail when a PR changes a public npm package without adding a | |
| # changeset, so no consumer-visible change merges without a release note. | |
| # Private packages (apps, demos, mocks, lint, design-tokens, ui-react, ...) | |
| # are never published and so do not require a changeset. Publishing itself | |
| # runs on pushes to main in the release-alpha-train job below. | |
| # | |
| # The check lives in scripts/check-changeset-required.mjs so the publishable | |
| # package list and rules are testable and shared, rather than duplicated in | |
| # inline shell. | |
| # | |
| # Skip the Changesets "Version Packages" PR: it legitimately edits public | |
| # package manifests/changelogs and *removes* changesets (consuming them), | |
| # which would otherwise trip this gate. | |
| if: github.event_name == 'pull_request' && github.head_ref != 'changeset-release/main' | |
| runs-on: depot-ubuntu-24.04-4 | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| # Full history so we can diff against the PR base branch. | |
| fetch-depth: 0 | |
| - name: Require a changeset for public package changes | |
| run: | | |
| git fetch --no-tags origin "${{ github.base_ref }}" | |
| node scripts/check-changeset-required.mjs --base "origin/${{ github.base_ref }}" | |
| quickstart-smoke: | |
| runs-on: depot-ubuntu-24.04-4 | |
| timeout-minutes: 20 | |
| needs: [goreleaser-snapshot] | |
| env: | |
| GHCR_READONLY_TOKEN: ${{ secrets.GHCR_READONLY_TOKEN }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Download GoReleaser snapshot | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: goreleaser-snapshot | |
| path: goreleaser-dist | |
| - name: Load snapshot Docker image | |
| run: | | |
| test -f goreleaser-dist/nextgen-image.tar | |
| test -f goreleaser-dist/nextgen-image.tag | |
| docker load -i goreleaser-dist/nextgen-image.tar | |
| docker image inspect -f '{{.Os}}/{{.Architecture}}' "$(cat goreleaser-dist/nextgen-image.tag)" | grep -q '^linux/amd64$' | |
| - name: Prepare quick-start compose stack | |
| run: | | |
| mkdir -p nextgen_quickstart | |
| cp docs/operations/docker-compose.yaml nextgen_quickstart/docker-compose.yaml | |
| IMAGE="$(cat goreleaser-dist/nextgen-image.tag)" | |
| { | |
| echo "NEXTGEN_IMAGE=${IMAGE}" | |
| echo "NEXTGEN_PORT=8080" | |
| } > nextgen_quickstart/.env | |
| docker compose -f nextgen_quickstart/docker-compose.yaml --env-file nextgen_quickstart/.env config >/dev/null | |
| - name: Optional GHCR auth-path smoke check | |
| if: ${{ env.GHCR_READONLY_TOKEN != '' }} | |
| env: | |
| GH_USER: ${{ github.repository_owner }} | |
| run: | | |
| echo "$GHCR_READONLY_TOKEN" | docker login ghcr.io -u "$GH_USER" --password-stdin | |
| docker logout ghcr.io | |
| - name: GHCR auth-path smoke check skipped | |
| if: ${{ env.GHCR_READONLY_TOKEN == '' }} | |
| run: echo "Skipping GHCR login smoke check because GHCR_READONLY_TOKEN is not configured." | |
| - name: Start stack and verify HTTP endpoints | |
| run: | | |
| set -euo pipefail | |
| # Separate from the customer local setup journey: this keeps the | |
| # documented compose fallback valid for operators/manual quick-start | |
| # usage. | |
| COMPOSE=(docker compose -f nextgen_quickstart/docker-compose.yaml --env-file nextgen_quickstart/.env) | |
| "${COMPOSE[@]}" up -d --wait | |
| ready=0 | |
| for _ in $(seq 1 60); do | |
| if curl -fsS http://localhost:8080/healthz >/dev/null; then | |
| ready=1 | |
| break | |
| fi | |
| sleep 1 | |
| done | |
| if [ "$ready" -ne 1 ]; then | |
| echo "::error::timed out waiting for /healthz" | |
| docker image inspect -f 'platform={{.Os}}/{{.Architecture}}' "$(cat goreleaser-dist/nextgen-image.tag)" || true | |
| "${COMPOSE[@]}" logs nextgen || true | |
| exit 1 | |
| fi | |
| if ! curl -fsS -o /dev/null -w "%{http_code}\n" http://localhost:8080/ui/console/ | grep -q '^200$'; then | |
| echo "::error::/ui/console/ did not return 200" | |
| "${COMPOSE[@]}" logs nextgen || true | |
| exit 1 | |
| fi | |
| if ! curl -fsS -o /dev/null -w "%{http_code}\n" http://localhost:8080/ui/login/ | grep -q '^200$'; then | |
| echo "::error::/ui/login/ did not return 200" | |
| "${COMPOSE[@]}" logs nextgen || true | |
| exit 1 | |
| fi | |
| echo "quickstart compose smoke test passed" | |
| - name: Tear down compose stack | |
| if: always() | |
| run: | | |
| if [ -f nextgen_quickstart/.env ]; then | |
| docker compose -f nextgen_quickstart/docker-compose.yaml --env-file nextgen_quickstart/.env down -v | |
| else | |
| echo "Skipping compose teardown because nextgen_quickstart/.env was not created." | |
| fi | |
| node-e2e: | |
| runs-on: depot-ubuntu-24.04-4 | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up pnpm | |
| uses: pnpm/action-setup@v5 | |
| with: | |
| run_install: false | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: pnpm | |
| - name: Install dependencies | |
| run: corepack pnpm install --frozen-lockfile | |
| # The Playwright cache is keyed by the resolved version in | |
| # pnpm-lock.yaml so a Playwright bump invalidates it automatically. | |
| # Browsers live under ~/.cache/ms-playwright on Linux. | |
| - name: Restore Playwright browsers | |
| id: playwright-cache | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.cache/ms-playwright | |
| key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} | |
| # `pnpm exec` from the workspace root cannot find the `playwright` | |
| # binary on a clean CI checkout because @playwright/test is a dep of | |
| # the e2e packages only, not of root. Scoping the exec with --filter | |
| # routes through that workspace's node_modules/.bin. | |
| - name: Install Playwright browsers | |
| if: steps.playwright-cache.outputs.cache-hit != 'true' | |
| run: >- | |
| corepack pnpm --filter @zitadel/demo-next-e2e | |
| exec playwright install --with-deps chromium | |
| - name: Install Playwright system deps | |
| if: steps.playwright-cache.outputs.cache-hit == 'true' | |
| run: >- | |
| corepack pnpm --filter @zitadel/demo-next-e2e | |
| exec playwright install-deps chromium | |
| # Boots the demo dev servers + api-mock through Playwright's webServer | |
| # config; Nx rebuilds @zitadel/components first via ^build. | |
| # console-e2e has no specs yet — it's intentionally excluded until | |
| # Playwright would otherwise fail with "no tests found". | |
| # | |
| # demo-next-e2e and demo-nuxt-e2e use distinct api-mock ports | |
| # (4000 / 4001) so Nx can run them in parallel without EADDRINUSE. | |
| - name: End-to-end tests | |
| run: >- | |
| corepack pnpm nx run-many -t e2e | |
| -p @zitadel/demo-next-e2e,@zitadel/demo-nuxt-e2e | |
| - name: Upload Playwright reports on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: playwright-report | |
| path: | | |
| apps/demo-next-e2e/test-output/playwright/report/** | |
| apps/demo-nuxt-e2e/test-output/playwright/report/** | |
| apps/demo-next-e2e/test-output/playwright/output/** | |
| apps/demo-nuxt-e2e/test-output/playwright/output/** | |
| if-no-files-found: ignore | |
| retention-days: 7 | |
| npm-pack-smoke: | |
| runs-on: depot-ubuntu-24.04-4 | |
| timeout-minutes: 15 | |
| needs: node-check | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up pnpm | |
| uses: pnpm/action-setup@v5 | |
| with: | |
| run_install: false | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: pnpm | |
| - name: Install dependencies | |
| run: corepack pnpm install --frozen-lockfile | |
| - name: Build packages | |
| run: corepack pnpm nx run-many -t build | |
| - name: Smoke test built CLI | |
| run: | | |
| node apps/cli/bin/run.js --version | |
| node apps/cli/bin/run.js commands | |
| - name: Dry-run npm packs | |
| run: | | |
| for dir in apps/cli packages/api packages/components packages/sdk-core packages/sdk-next packages/sdk-nuxt packages/sdk-react packages/sdk-vue packages/sdk-angular; do | |
| corepack pnpm --dir "$dir" pack --dry-run | |
| done | |
| - name: Create npm package artifacts | |
| run: | | |
| mkdir -p "$RUNNER_TEMP/zitadel-npm-packages" | |
| for dir in apps/cli packages/api packages/components packages/sdk-core packages/sdk-next packages/sdk-nuxt packages/sdk-react packages/sdk-vue packages/sdk-angular; do | |
| corepack pnpm --dir "$dir" pack --pack-destination "$RUNNER_TEMP/zitadel-npm-packages" | |
| done | |
| ls -lh "$RUNNER_TEMP/zitadel-npm-packages" | |
| - name: Verify npm package artifacts | |
| run: node apps/cli-journey-e2e/scripts/verify-tarballs.mjs "$RUNNER_TEMP/zitadel-npm-packages" | |
| - name: Upload npm package artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: npm-packages | |
| path: ${{ runner.temp }}/zitadel-npm-packages/*.tgz | |
| if-no-files-found: error | |
| retention-days: 7 | |
| consumer-journey-e2e: | |
| runs-on: depot-ubuntu-24.04-4 | |
| timeout-minutes: 35 | |
| needs: [goreleaser-snapshot, npm-pack-smoke] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up pnpm | |
| uses: pnpm/action-setup@v5 | |
| with: | |
| run_install: false | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: pnpm | |
| - name: Install dependencies | |
| run: corepack pnpm install --frozen-lockfile | |
| - name: Download GoReleaser snapshot | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: goreleaser-snapshot | |
| path: goreleaser-dist | |
| - name: Download npm package artifacts | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: npm-packages | |
| path: ${{ runner.temp }}/zitadel-npm-packages | |
| - name: Restore Playwright browsers | |
| id: playwright-cache | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.cache/ms-playwright | |
| key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} | |
| - name: Install Playwright browsers | |
| if: steps.playwright-cache.outputs.cache-hit != 'true' | |
| run: >- | |
| corepack pnpm --filter @zitadel/cli-journey-e2e | |
| exec playwright install --with-deps chromium | |
| - name: Install Playwright system deps | |
| if: steps.playwright-cache.outputs.cache-hit == 'true' | |
| run: >- | |
| corepack pnpm --filter @zitadel/cli-journey-e2e | |
| exec playwright install-deps chromium | |
| - name: Start temporary npm registry | |
| env: | |
| VERDACCIO_CONFIG: ${{ runner.temp }}/verdaccio/config.yaml | |
| VERDACCIO_LOG: ${{ runner.temp }}/verdaccio.log | |
| VERDACCIO_NPMRC: ${{ runner.temp }}/verdaccio.npmrc | |
| VERDACCIO_STORAGE: ${{ runner.temp }}/verdaccio/storage | |
| run: | | |
| mkdir -p "$(dirname "$VERDACCIO_CONFIG")" "$VERDACCIO_STORAGE" | |
| cat > "$VERDACCIO_CONFIG" <<EOF | |
| storage: $VERDACCIO_STORAGE | |
| uplinks: | |
| npmjs: | |
| url: https://registry.npmjs.org/ | |
| packages: | |
| '@zitadel/*': | |
| access: \$all | |
| publish: \$all | |
| unpublish: \$all | |
| '@*/*': | |
| access: \$all | |
| publish: \$all | |
| unpublish: \$all | |
| proxy: npmjs | |
| '**': | |
| access: \$all | |
| publish: \$all | |
| unpublish: \$all | |
| proxy: npmjs | |
| logs: | |
| - { type: stdout, format: pretty, level: http } | |
| EOF | |
| npm exec --yes verdaccio@6.2.2 -- --config "$VERDACCIO_CONFIG" --listen 127.0.0.1:4873 > "$VERDACCIO_LOG" 2>&1 & | |
| echo "VERDACCIO_PID=$!" >> "$GITHUB_ENV" | |
| ready=0 | |
| for _ in $(seq 1 60); do | |
| if curl -fsS http://127.0.0.1:4873/-/ping >/dev/null; then | |
| ready=1 | |
| break | |
| fi | |
| sleep 1 | |
| done | |
| if [ "$ready" -ne 1 ]; then | |
| echo "::error::timed out waiting for Verdaccio" | |
| cat "$VERDACCIO_LOG" | |
| exit 1 | |
| fi | |
| { | |
| echo "registry=http://127.0.0.1:4873/" | |
| echo "//127.0.0.1:4873/:_authToken=journey-token" | |
| echo "always-auth=true" | |
| } > "$VERDACCIO_NPMRC" | |
| - name: Publish current npm packages to temporary registry | |
| env: | |
| JOURNEY_REGISTRY_URL: http://127.0.0.1:4873 | |
| NPM_CONFIG_USERCONFIG: ${{ runner.temp }}/verdaccio.npmrc | |
| run: >- | |
| node apps/cli-journey-e2e/scripts/publish-tarballs.mjs | |
| "$RUNNER_TEMP/zitadel-npm-packages" | |
| - name: Load snapshot Docker image | |
| run: | | |
| test -f goreleaser-dist/nextgen-image.tar | |
| test -f goreleaser-dist/nextgen-image.tag | |
| docker load -i goreleaser-dist/nextgen-image.tar | |
| docker image inspect -f '{{.Os}}/{{.Architecture}}' "$(cat goreleaser-dist/nextgen-image.tag)" | grep -q '^linux/amd64$' | |
| echo "ZITADEL_LOCAL_IMAGE=$(cat goreleaser-dist/nextgen-image.tag)" >> "$GITHUB_ENV" | |
| - name: Prepare fresh Next.js app | |
| id: prepare-app | |
| env: | |
| JOURNEY_REGISTRY_URL: http://127.0.0.1:4873 | |
| JOURNEY_WORK_DIR: ${{ runner.temp }}/zitadel-cli-journey | |
| run: node apps/cli-journey-e2e/scripts/prepare-next-app.mjs | |
| - name: Start generated Next.js app | |
| run: | | |
| cd "$JOURNEY_APP_DIR" | |
| npm run dev -- --hostname localhost --port 3000 > "$RUNNER_TEMP/next-app.log" 2>&1 & | |
| echo "NEXT_APP_PID=$!" >> "$GITHUB_ENV" | |
| for _ in $(seq 1 90); do | |
| if curl -fsS http://localhost:3000/login >/dev/null; then | |
| exit 0 | |
| fi | |
| sleep 1 | |
| done | |
| echo "::error::timed out waiting for generated Next.js app" | |
| cat "$RUNNER_TEMP/next-app.log" | |
| exit 1 | |
| - name: Run consumer journey Playwright tests | |
| run: >- | |
| corepack pnpm --filter @zitadel/cli-journey-e2e | |
| exec playwright test --config playwright.config.mts | |
| - name: Collect diagnostics | |
| if: failure() | |
| run: | | |
| DIAG_DIR="$RUNNER_TEMP/consumer-journey-diagnostics" | |
| APP_DIR="$RUNNER_TEMP/zitadel-cli-journey/myapp" | |
| mkdir -p "$DIAG_DIR/generated-app" | |
| if [ -d "$APP_DIR" ]; then | |
| ( | |
| cd "$APP_DIR" | |
| npm_config_registry=http://127.0.0.1:4873 \ | |
| npm_config_yes=true \ | |
| npm_config_audit=false \ | |
| npm_config_fund=false \ | |
| npx --yes @zitadel/cli@alpha logs --tail 400 --non-interactive --json \ | |
| > "$DIAG_DIR/logs.json" 2> "$DIAG_DIR/logs.stderr.log" | |
| ) || true | |
| fi | |
| cp "$RUNNER_TEMP/next-app.log" "$DIAG_DIR/" 2>/dev/null || true | |
| cp "$RUNNER_TEMP/verdaccio.log" "$DIAG_DIR/" 2>/dev/null || true | |
| cp "$RUNNER_TEMP/zitadel-cli-journey/doctor.json" "$DIAG_DIR/" 2>/dev/null || true | |
| cp "$RUNNER_TEMP/zitadel-cli-journey/doctor.stderr.log" "$DIAG_DIR/" 2>/dev/null || true | |
| cp "$RUNNER_TEMP/zitadel-cli-journey/start.json" "$DIAG_DIR/" 2>/dev/null || true | |
| cp "$RUNNER_TEMP/zitadel-cli-journey/start.stderr.log" "$DIAG_DIR/" 2>/dev/null || true | |
| cp "$RUNNER_TEMP/zitadel-cli-journey/setup.json" "$DIAG_DIR/" 2>/dev/null || true | |
| cp "$RUNNER_TEMP/zitadel-cli-journey/setup.stderr.log" "$DIAG_DIR/" 2>/dev/null || true | |
| cp "$RUNNER_TEMP/zitadel-cli-journey/metadata.json" "$DIAG_DIR/" 2>/dev/null || true | |
| cp "$APP_DIR/.zitadel/local/runtime.json" "$DIAG_DIR/runtime.json" 2>/dev/null || true | |
| cp "$APP_DIR/package.json" "$DIAG_DIR/generated-app/" 2>/dev/null || true | |
| cp "$APP_DIR/package-lock.json" "$DIAG_DIR/generated-app/" 2>/dev/null || true | |
| - name: Upload consumer journey diagnostics | |
| if: failure() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: consumer-journey-diagnostics | |
| path: | | |
| apps/cli-journey-e2e/test-output/playwright/report/** | |
| apps/cli-journey-e2e/test-output/playwright/output/** | |
| ${{ runner.temp }}/consumer-journey-diagnostics/** | |
| if-no-files-found: ignore | |
| retention-days: 7 | |
| - name: Tear down consumer journey services | |
| if: always() | |
| run: | | |
| if [ -n "${NEXT_APP_PID:-}" ]; then kill "$NEXT_APP_PID" 2>/dev/null || true; fi | |
| APP_DIR="$RUNNER_TEMP/zitadel-cli-journey/myapp" | |
| if [ -d "$APP_DIR" ]; then | |
| ( | |
| cd "$APP_DIR" | |
| npm_config_registry=http://127.0.0.1:4873 \ | |
| npm_config_yes=true \ | |
| npm_config_audit=false \ | |
| npm_config_fund=false \ | |
| npx --yes @zitadel/cli@alpha reset --force --non-interactive --json | |
| ) || true | |
| fi | |
| if [ -n "${VERDACCIO_PID:-}" ]; then kill "$VERDACCIO_PID" 2>/dev/null || true; fi | |
| ci-success: | |
| if: always() | |
| runs-on: depot-ubuntu-24.04-4 | |
| timeout-minutes: 5 | |
| needs: | |
| - openapi-lint | |
| - go-unit-test | |
| - go-integration-test-postgres | |
| - go-integration-test-spanner | |
| - node-check | |
| - changeset-check | |
| - node-e2e | |
| - goreleaser-snapshot | |
| - npm-pack-smoke | |
| - go-smoke-test-embedded-postgres | |
| - quickstart-smoke | |
| - consumer-journey-e2e | |
| steps: | |
| - name: Check CI gate results | |
| env: | |
| NEEDS_JSON: ${{ toJson(needs) }} | |
| run: | | |
| node <<'NODE' | |
| const needs = JSON.parse(process.env.NEEDS_JSON); | |
| const allowedSkipped = new Set(["changeset-check"]); | |
| const failed = Object.entries(needs).filter(([name, job]) => { | |
| if (job.result === "success") return false; | |
| if (job.result === "skipped" && allowedSkipped.has(name)) return false; | |
| return true; | |
| }); | |
| if (failed.length > 0) { | |
| for (const [name, job] of failed) { | |
| console.error(`${name}: ${job.result}`); | |
| } | |
| process.exit(1); | |
| } | |
| console.log("CI gate passed"); | |
| NODE | |
| release-alpha-train: | |
| if: always() && github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-alpha-release.result == 'success' && needs.ci-success.result == 'success' && needs.detect-alpha-release.outputs.should_release == 'true' | |
| runs-on: depot-ubuntu-24.04-8 | |
| needs: [detect-alpha-release, ci-success] | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| packages: write | |
| id-token: write | |
| steps: | |
| # Mint a short-lived installation token for the release GitHub App so the | |
| # changesets PR/commits trigger CI (GITHUB_TOKEN-authored ones do not). | |
| - name: Generate a token for the release app | |
| id: app-token | |
| uses: actions/create-github-app-token@v2 | |
| with: | |
| app-id: ${{ secrets.RELEASE_APP_ID }} | |
| private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ steps.app-token.outputs.token }} | |
| - name: Set up pnpm | |
| uses: pnpm/action-setup@v5 | |
| with: | |
| run_install: false | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: .nvmrc | |
| registry-url: "https://registry.npmjs.org" | |
| cache: pnpm | |
| # Trusted publishing requires npm >= 11.5.1; the pinned pnpm delegates | |
| # the actual publish to the npm CLI, so the runner's npm must be current. | |
| - name: Update npm for trusted publishing (OIDC) | |
| run: npm install -g npm@latest | |
| - name: Install dependencies | |
| run: corepack pnpm install --frozen-lockfile | |
| - name: Build packages | |
| run: corepack pnpm nx run-many -t build | |
| - name: Prune empty changesets before publish decision | |
| run: node scripts/release-alpha-train.mjs prune-empty-changesets | |
| - name: Create release PR or publish to npm | |
| id: changesets | |
| uses: changesets/action@v1 | |
| with: | |
| version: corepack pnpm changeset version | |
| publish: corepack pnpm changeset publish | |
| title: "chore: version packages" | |
| commit: "chore: version packages" | |
| createGithubReleases: false | |
| env: | |
| # App token (not GITHUB_TOKEN) so the Version Packages PR triggers CI. | |
| GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} | |
| # Empty so the action does not pass an undefined token; OIDC handles auth. | |
| NPM_TOKEN: "" | |
| # npm provenance is only supported for public source repositories. | |
| # Keep trusted publishing via OIDC, but disable provenance until this | |
| # repository is public. | |
| NPM_CONFIG_PROVENANCE: "false" | |
| - name: Restore changesets after publish decision | |
| run: git restore -- .changeset | |
| - name: Inspect alpha release train candidate | |
| id: alpha-status | |
| env: | |
| PUBLISHED: ${{ steps.changesets.outputs.published }} | |
| run: | | |
| set -euo pipefail | |
| alpha_env="$RUNNER_TEMP/alpha-release-status.env" | |
| node scripts/release-alpha-train.mjs status --published "$PUBLISHED" --remote false | tee "$alpha_env" | |
| cat "$alpha_env" >> "$GITHUB_OUTPUT" | |
| - name: Login to GHCR | |
| if: ${{ steps.alpha-status.outputs.should_complete == 'true' }} | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Prepare alpha release train | |
| if: ${{ steps.alpha-status.outputs.should_complete == 'true' }} | |
| id: alpha | |
| env: | |
| PUBLISHED: ${{ steps.changesets.outputs.published }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| alpha_env="$RUNNER_TEMP/alpha-release.env" | |
| node scripts/release-alpha-train.mjs prepare --published "$PUBLISHED" --out-dir "$RUNNER_TEMP/alpha-release" | tee "$alpha_env" | |
| cat "$alpha_env" >> "$GITHUB_OUTPUT" | |
| - name: Refresh CLI npm dist-tags | |
| if: ${{ steps.alpha.outputs.version != '' }} | |
| env: | |
| NPM_CONFIG_PROVENANCE: "false" | |
| VERSION: ${{ steps.alpha.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| version="$VERSION" | |
| node --input-type=module - "$version" <<'NODE' | |
| const version = process.argv[2]; | |
| if (!/^\d+\.\d+\.\d+-alpha\.\d+$/.test(version)) { | |
| throw new Error(`expected an alpha CLI version, got ${version}`); | |
| } | |
| NODE | |
| # Temporary public-alpha exception: the CLI is the tester entrypoint, | |
| # so bare `npx @zitadel/cli` must reach the supported local workflow. | |
| # Keep `alpha` as the canonical prerelease tag, and do not move | |
| # `latest` for SDKs/components, Docker images, or GitHub Releases. | |
| npm dist-tag add "@zitadel/cli@$version" alpha || echo "::warning::Unable to refresh @zitadel/cli@$version npm alpha dist-tag" | |
| npm dist-tag add "@zitadel/cli@$version" latest || echo "::warning::Unable to promote @zitadel/cli@$version to npm latest dist-tag" | |
| - name: Set up QEMU | |
| if: ${{ steps.alpha.outputs.run_goreleaser == 'true' }} | |
| uses: docker/setup-qemu-action@v4 | |
| - name: Set up Docker Buildx | |
| if: ${{ steps.alpha.outputs.run_goreleaser == 'true' }} | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Create and push Go release tag | |
| if: ${{ steps.alpha.outputs.create_tag == 'true' }} | |
| env: | |
| TAG: ${{ steps.alpha.outputs.tag }} | |
| TITLE: ${{ steps.alpha.outputs.title }} | |
| run: | | |
| set -euo pipefail | |
| git config user.name "zitadel-release" | |
| git config user.email "noreply@zitadel.com" | |
| git tag -a "$TAG" -m "$TITLE" | |
| git push origin "$TAG" | |
| - name: Check out existing Go release tag | |
| if: ${{ steps.alpha.outputs.run_goreleaser == 'true' && steps.alpha.outputs.tag_exists == 'true' }} | |
| env: | |
| TAG: ${{ steps.alpha.outputs.tag }} | |
| run: git checkout --detach "$TAG" | |
| - name: Prune npm package tags for GoReleaser | |
| if: ${{ steps.alpha.outputs.run_goreleaser == 'true' }} | |
| run: | | |
| set -euo pipefail | |
| git tag -l '@zitadel/*' | while read -r tag; do | |
| git tag -d "$tag" | |
| done | |
| - name: Run GoReleaser | |
| if: ${{ steps.alpha.outputs.run_goreleaser == 'true' }} | |
| uses: goreleaser/goreleaser-action@v7 | |
| with: | |
| distribution: goreleaser | |
| version: "~> v2" | |
| args: release --clean | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Update GitHub Release notes | |
| if: ${{ steps.alpha.outputs.update_release == 'true' }} | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ steps.alpha.outputs.tag }} | |
| TITLE: ${{ steps.alpha.outputs.title }} | |
| NOTES_PATH: ${{ steps.alpha.outputs.notes_path }} | |
| run: | | |
| set -euo pipefail | |
| gh release edit "$TAG" \ | |
| --draft \ | |
| --prerelease \ | |
| --latest=false \ | |
| --title "$TITLE" \ | |
| --notes-file "$NOTES_PATH" |