Skip to content

Commit 99af296

Browse files
authored
ci: reduce duplicate release workflow runs (#273)
## Summary - limit `release-npm` main-branch runs to release-relevant files - wait for the matching `ci.yml` push run to pass before Changesets can publish - extend the alpha release-plan guard and docs so the new workflow shape stays intentional ## Validation - `corepack pnpm nx test @zitadel/cli -- tests/unit/scripts/check-alpha-release-plan.test.ts` - `corepack pnpm run check -- --only release`
1 parent 08b7ab4 commit 99af296

9 files changed

Lines changed: 503 additions & 313 deletions

File tree

.changeset/README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ corepack pnpm changeset version # strips the -alpha suffix
4141

4242
## Publishing (npm trusted publishing / OIDC)
4343

44-
The [`.github/workflows/release-npm.yml`](../.github/workflows/release-npm.yml) workflow runs the [changesets GitHub Action](https://github.com/changesets/action). Pushing changesets to `main` opens a "Version Packages" PR aggregating all pending changesets; merging that PR bumps versions, updates `CHANGELOG.md` files, and publishes to npm (under the `alpha` dist-tag while in prerelease mode).
44+
The `release-alpha-train` job in [`.github/workflows/ci.yml`](../.github/workflows/ci.yml) runs the [changesets GitHub Action](https://github.com/changesets/action). Pushing changesets to `main` opens a "Version Packages" PR aggregating all pending changesets; merging that PR bumps versions, updates `CHANGELOG.md` files, waits for the CI aggregate gate, and publishes to npm (under the `alpha` dist-tag while in prerelease mode).
4545

4646
Publishing authenticates with **npm trusted publishing (OIDC)** — there is **no `NPM_TOKEN`** secret. Before the first automated publish, a maintainer must, once per public package:
4747

@@ -50,7 +50,7 @@ Publishing authenticates with **npm trusted publishing (OIDC)** — there is **n
5050
- Provider: **GitHub Actions**
5151
- Organization/owner: `zitadel`
5252
- Repository: `nextgen`
53-
- Workflow filename: `release-npm.yml` (exact, case-sensitive)
53+
- Workflow filename: `ci.yml` (exact, case-sensitive)
5454
3. Optionally, under **Publishing access**, require 2FA and disallow tokens so only this workflow can publish.
5555

5656
While this repository is private, the workflow keeps npm provenance disabled
@@ -59,11 +59,12 @@ short-lived OIDC credentials, but npm only accepts public provenance
5959
attestations from public source repositories. Re-enable provenance when
6060
`zitadel/nextgen` is public.
6161

62-
Changesets does not build the Go server binary. During alpha, `release-npm.yml`
63-
uses the lockstep npm version as the release train version, creates `v<version>`,
64-
and then runs GoReleaser so the server image and binaries publish into the same
65-
GitHub Release. The manual [`release.yml`](../.github/workflows/release.yml)
66-
workflow remains a server snapshot/fallback path. See
62+
Changesets does not build the Go server binary. During alpha, the `release-alpha-train`
63+
job uses the lockstep npm version as the release train version, creates
64+
`v<version>`, and then runs GoReleaser so the server image and binaries publish
65+
into the same GitHub Release. The manual
66+
[`release.yml`](../.github/workflows/release.yml) workflow remains a server
67+
snapshot/fallback path. See
6768
[docs/adrs/002-multi-package-release-strategy.md](../docs/adrs/002-multi-package-release-strategy.md)
6869
and
6970
[docs/adrs/023-lockstep-alpha-release-train.md](../docs/adrs/023-lockstep-alpha-release-train.md).

.changeset/release-main-ci-wait.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

.github/workflows/ci.yml

Lines changed: 245 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,62 @@ permissions:
99
contents: read
1010

1111
# Cancel an in-flight run when a newer commit is pushed to the same PR/branch.
12+
# Main runs may publish releases, so do not cancel them once started.
1213
concurrency:
1314
group: ci-${{ github.workflow }}-${{ github.ref }}
14-
cancel-in-progress: true
15+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
1516

1617
jobs:
18+
detect-alpha-release:
19+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
20+
runs-on: depot-ubuntu-24.04-4
21+
timeout-minutes: 5
22+
outputs:
23+
should_release: ${{ steps.detect.outputs.should_release }}
24+
steps:
25+
- uses: actions/checkout@v6
26+
with:
27+
fetch-depth: 0
28+
29+
- name: Detect release-relevant changes
30+
id: detect
31+
env:
32+
BEFORE: ${{ github.event.before }}
33+
run: |
34+
set -euo pipefail
35+
base="$BEFORE"
36+
if [ -z "$base" ] || [[ "$base" =~ ^0+$ ]] || ! git cat-file -e "$base^{commit}" 2>/dev/null; then
37+
base="$(git rev-parse HEAD^ 2>/dev/null || true)"
38+
fi
39+
if [ -n "$base" ]; then
40+
git diff --name-only "$base" HEAD > "$RUNNER_TEMP/release-changed-files"
41+
else
42+
git diff-tree --no-commit-id --name-only -r HEAD > "$RUNNER_TEMP/release-changed-files"
43+
fi
44+
45+
node --input-type=module <<'NODE'
46+
import { appendFileSync, readFileSync } from "node:fs";
47+
import { PUBLIC_PACKAGE_MANIFESTS } from "./scripts/release-alpha-train.mjs";
48+
49+
const changed = readFileSync(`${process.env.RUNNER_TEMP}/release-changed-files`, "utf8")
50+
.split("\n")
51+
.map((line) => line.trim())
52+
.filter(Boolean);
53+
const publicPackageReleasePaths = new Set(
54+
PUBLIC_PACKAGE_MANIFESTS.flatMap((manifestPath) => [
55+
manifestPath,
56+
manifestPath.replace(/package\.json$/, "CHANGELOG.md"),
57+
]),
58+
);
59+
const releaseRelevant = changed.filter(
60+
(path) => path.startsWith(".changeset/") || publicPackageReleasePaths.has(path),
61+
);
62+
const shouldRelease = releaseRelevant.length > 0;
63+
64+
console.log(`release-relevant changes: ${releaseRelevant.join(", ") || "none"}`);
65+
appendFileSync(process.env.GITHUB_OUTPUT, `should_release=${String(shouldRelease)}\n`);
66+
NODE
67+
1768
openapi-lint:
1869
runs-on: depot-ubuntu-24.04-4
1970
timeout-minutes: 5
@@ -260,7 +311,7 @@ jobs:
260311
# changeset, so no consumer-visible change merges without a release note.
261312
# Private packages (apps, demos, mocks, lint, design-tokens, ui-react, ...)
262313
# are never published and so do not require a changeset. Publishing itself
263-
# runs on pushes to main in release-npm.yml.
314+
# runs on pushes to main in the release-alpha-train job below.
264315
#
265316
# The check lives in scripts/check-changeset-required.mjs so the publishable
266317
# package list and rules are testable and shared, rather than duplicated in
@@ -701,3 +752,195 @@ jobs:
701752
) || true
702753
fi
703754
if [ -n "${VERDACCIO_PID:-}" ]; then kill "$VERDACCIO_PID" 2>/dev/null || true; fi
755+
756+
ci-success:
757+
if: always()
758+
runs-on: depot-ubuntu-24.04-4
759+
timeout-minutes: 5
760+
needs:
761+
- openapi-lint
762+
- go-unit-test
763+
- go-integration-test-postgres
764+
- go-integration-test-spanner
765+
- node-check
766+
- changeset-check
767+
- node-e2e
768+
- goreleaser-snapshot
769+
- npm-pack-smoke
770+
- go-smoke-test-embedded-postgres
771+
- quickstart-smoke
772+
- consumer-journey-e2e
773+
steps:
774+
- name: Check CI gate results
775+
env:
776+
NEEDS_JSON: ${{ toJson(needs) }}
777+
run: |
778+
node <<'NODE'
779+
const needs = JSON.parse(process.env.NEEDS_JSON);
780+
const allowedSkipped = new Set(["changeset-check"]);
781+
const failed = Object.entries(needs).filter(([name, job]) => {
782+
if (job.result === "success") return false;
783+
if (job.result === "skipped" && allowedSkipped.has(name)) return false;
784+
return true;
785+
});
786+
787+
if (failed.length > 0) {
788+
for (const [name, job] of failed) {
789+
console.error(`${name}: ${job.result}`);
790+
}
791+
process.exit(1);
792+
}
793+
794+
console.log("CI gate passed");
795+
NODE
796+
797+
release-alpha-train:
798+
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-alpha-release.outputs.should_release == 'true'
799+
runs-on: depot-ubuntu-24.04-8
800+
needs: [detect-alpha-release, ci-success]
801+
permissions:
802+
contents: write
803+
pull-requests: write
804+
packages: write
805+
id-token: write
806+
steps:
807+
# Mint a short-lived installation token for the release GitHub App so the
808+
# changesets PR/commits trigger CI (GITHUB_TOKEN-authored ones do not).
809+
- name: Generate a token for the release app
810+
id: app-token
811+
uses: actions/create-github-app-token@v2
812+
with:
813+
app-id: ${{ secrets.RELEASE_APP_ID }}
814+
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
815+
816+
- name: Checkout
817+
uses: actions/checkout@v6
818+
with:
819+
fetch-depth: 0
820+
token: ${{ steps.app-token.outputs.token }}
821+
822+
- name: Set up pnpm
823+
uses: pnpm/action-setup@v5
824+
with:
825+
run_install: false
826+
827+
- name: Set up Node.js
828+
uses: actions/setup-node@v6
829+
with:
830+
node-version-file: .nvmrc
831+
registry-url: "https://registry.npmjs.org"
832+
cache: pnpm
833+
834+
# Trusted publishing requires npm >= 11.5.1; the pinned pnpm delegates
835+
# the actual publish to the npm CLI, so the runner's npm must be current.
836+
- name: Update npm for trusted publishing (OIDC)
837+
run: npm install -g npm@latest
838+
839+
- name: Install dependencies
840+
run: corepack pnpm install --frozen-lockfile
841+
842+
- name: Build packages
843+
run: corepack pnpm nx run-many -t build
844+
845+
- name: Create release PR or publish to npm
846+
id: changesets
847+
uses: changesets/action@v1
848+
with:
849+
version: corepack pnpm changeset version
850+
publish: corepack pnpm changeset publish
851+
title: "chore: version packages"
852+
commit: "chore: version packages"
853+
createGithubReleases: false
854+
env:
855+
# App token (not GITHUB_TOKEN) so the Version Packages PR triggers CI.
856+
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
857+
# Empty so the action does not pass an undefined token; OIDC handles auth.
858+
NPM_TOKEN: ""
859+
# npm provenance is only supported for public source repositories.
860+
# Keep trusted publishing via OIDC, but disable provenance until this
861+
# repository is public.
862+
NPM_CONFIG_PROVENANCE: "false"
863+
864+
- name: Inspect alpha release train candidate
865+
id: alpha-status
866+
env:
867+
PUBLISHED: ${{ steps.changesets.outputs.published }}
868+
run: |
869+
set -euo pipefail
870+
alpha_env="$RUNNER_TEMP/alpha-release-status.env"
871+
node scripts/release-alpha-train.mjs status --published "$PUBLISHED" --remote false | tee "$alpha_env"
872+
cat "$alpha_env" >> "$GITHUB_OUTPUT"
873+
874+
- name: Login to GHCR
875+
if: ${{ steps.alpha-status.outputs.should_complete == 'true' }}
876+
uses: docker/login-action@v4
877+
with:
878+
registry: ghcr.io
879+
username: ${{ github.actor }}
880+
password: ${{ secrets.GITHUB_TOKEN }}
881+
882+
- name: Prepare alpha release train
883+
if: ${{ steps.alpha-status.outputs.should_complete == 'true' }}
884+
id: alpha
885+
env:
886+
PUBLISHED: ${{ steps.changesets.outputs.published }}
887+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
888+
run: |
889+
set -euo pipefail
890+
alpha_env="$RUNNER_TEMP/alpha-release.env"
891+
node scripts/release-alpha-train.mjs prepare --published "$PUBLISHED" --out-dir "$RUNNER_TEMP/alpha-release" | tee "$alpha_env"
892+
cat "$alpha_env" >> "$GITHUB_OUTPUT"
893+
894+
- name: Set up QEMU
895+
if: ${{ steps.alpha.outputs.run_goreleaser == 'true' }}
896+
uses: docker/setup-qemu-action@v4
897+
898+
- name: Set up Docker Buildx
899+
if: ${{ steps.alpha.outputs.run_goreleaser == 'true' }}
900+
uses: docker/setup-buildx-action@v4
901+
902+
- name: Create and push Go release tag
903+
if: ${{ steps.alpha.outputs.create_tag == 'true' }}
904+
env:
905+
TAG: ${{ steps.alpha.outputs.tag }}
906+
TITLE: ${{ steps.alpha.outputs.title }}
907+
run: |
908+
set -euo pipefail
909+
git config user.name "zitadel-release"
910+
git config user.email "noreply@zitadel.com"
911+
git tag -a "$TAG" -m "$TITLE"
912+
git push origin "$TAG"
913+
914+
- name: Prune npm package tags for GoReleaser
915+
if: ${{ steps.alpha.outputs.run_goreleaser == 'true' }}
916+
run: |
917+
set -euo pipefail
918+
git tag -l '@zitadel/*' | while read -r tag; do
919+
git tag -d "$tag"
920+
done
921+
922+
- name: Run GoReleaser
923+
if: ${{ steps.alpha.outputs.run_goreleaser == 'true' }}
924+
uses: goreleaser/goreleaser-action@v7
925+
with:
926+
distribution: goreleaser
927+
version: "~> v2"
928+
args: release --clean
929+
env:
930+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
931+
932+
- name: Update GitHub Release notes
933+
if: ${{ steps.alpha.outputs.update_release == 'true' }}
934+
env:
935+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
936+
TAG: ${{ steps.alpha.outputs.tag }}
937+
TITLE: ${{ steps.alpha.outputs.title }}
938+
NOTES_PATH: ${{ steps.alpha.outputs.notes_path }}
939+
run: |
940+
set -euo pipefail
941+
gh release edit "$TAG" \
942+
--draft \
943+
--prerelease \
944+
--latest=false \
945+
--title "$TITLE" \
946+
--notes-file "$NOTES_PATH"

0 commit comments

Comments
 (0)