Skip to content

Commit f9710d7

Browse files
authored
ci: fix alpha release train gate (#276)
## Summary - Fix `release-alpha-train` so it uses `always()` and then explicitly requires `detect-alpha-release` and `ci-success` to succeed before publishing. - Keep the release-plan guard aligned so future workflow edits must preserve the explicit gate, including the guard being scoped to the `release-alpha-train` job block. - Add coverage for the main-branch skip case and the false-positive workflow match case. ## Validation - Observed [`ci` run 27441077450](https://github.com/zitadel/nextgen/actions/runs/27441077450): CI/smoke gates passed, `detect-alpha-release` found release-relevant package files, but `release-alpha-train` was skipped. - PR CI run [`27444586592`](https://github.com/zitadel/nextgen/actions/runs/27444586592) passed. `release-alpha-train` is skipped there as expected for pull requests. - `corepack pnpm install --frozen-lockfile` - `corepack pnpm nx test @zitadel/cli` - `corepack pnpm nx test @zitadel/cli -- tests/unit/scripts/check-alpha-release-plan.test.ts` - `corepack pnpm nx lint @zitadel/source --skip-nx-cache` - `go test -v -tags spanner_integration -timeout=10m ./internal/api/integration_test -run TestListFlowDefinitions -count=1` - `corepack pnpm run check -- --only release` - `git diff --check` ## Release notes / changeset - Empty changeset added: `.changeset/fix-alpha-release-gate.md`. - No package API or runtime behavior changes in this PR. The expected release impact is that the next main CI run can execute the already-versioned alpha train instead of inheriting the intentional `changeset-check` skip. ## Notes - The linked run's head commit has `@zitadel/cli` at `0.1.0-alpha.3`, while npm still reports the `alpha` dist-tag at `0.1.0-alpha.2`, so that run did not publish alpha.3. - I also corrected the existing `v0.1.0-alpha.2` GitHub Release metadata to `prerelease=true` and `latest=false` after observing it was marked as a normal latest release. - `release-alpha-train` still uses the 8-core Depot runner because the same job can proceed from npm publishing into QEMU/Buildx plus GoReleaser. If we want to optimize runner size later, the clean split is npm publishing on 4-core and GoReleaser on 8-core.
1 parent b445881 commit f9710d7

4 files changed

Lines changed: 66 additions & 5 deletions

File tree

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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -795,7 +795,7 @@ jobs:
795795
NODE
796796
797797
release-alpha-train:
798-
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-alpha-release.outputs.should_release == 'true'
798+
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'
799799
runs-on: depot-ubuntu-24.04-8
800800
needs: [detect-alpha-release, ci-success]
801801
permissions:

apps/cli/tests/unit/scripts/check-alpha-release-plan.test.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ const publicPackageNames = [
2525
"@zitadel/sdk-angular",
2626
];
2727

28+
const releaseJobCondition =
29+
" 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'";
30+
const inheritedSkipReleaseJobCondition =
31+
" if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-alpha-release.outputs.should_release == 'true'";
32+
2833
beforeAll(async () => {
2934
checkAlphaReleasePlanModule = (await import(
3035
new URL("../../../../../scripts/check-alpha-release-plan.mjs", import.meta.url).href
@@ -128,6 +133,34 @@ describe("check-alpha-release-plan script", () => {
128133
).rejects.toThrow("post-npm alpha train steps must not be gated only on Changesets publishing");
129134
});
130135

136+
it("rejects a release job condition that can inherit intentional main-branch skips", async () => {
137+
const { cwd, statusPath } = await fixtureRepo({
138+
ciWorkflow: validCiWorkflow().replace(
139+
releaseJobCondition,
140+
inheritedSkipReleaseJobCondition,
141+
),
142+
});
143+
144+
await expect(
145+
checkAlphaReleasePlanModule.checkAlphaReleasePlan({ cwd, statusPath }),
146+
).rejects.toThrow("must explicitly require release relevance and a successful CI gate");
147+
});
148+
149+
it("rejects a release job condition that only appears on a different job", async () => {
150+
const { cwd, statusPath } = await fixtureRepo({
151+
ciWorkflow: validCiWorkflow()
152+
.replace(releaseJobCondition, inheritedSkipReleaseJobCondition)
153+
.replace(
154+
" ci-success:\n steps:",
155+
[" ci-success:", releaseJobCondition, " steps:"].join("\n"),
156+
),
157+
});
158+
159+
await expect(
160+
checkAlphaReleasePlanModule.checkAlphaReleasePlan({ cwd, statusPath }),
161+
).rejects.toThrow("must explicitly require release relevance and a successful CI gate");
162+
});
163+
131164
it("rejects a legacy standalone release workflow", async () => {
132165
const { cwd, statusPath } = await fixtureRepo({
133166
legacyReleaseWorkflow: "name: release-npm\n",
@@ -200,7 +233,7 @@ function validCiWorkflow(): string {
200233
" - run: |",
201234
' const allowedSkipped = new Set(["changeset-check"]);',
202235
" release-alpha-train:",
203-
" if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-alpha-release.outputs.should_release == 'true'",
236+
releaseJobCondition,
204237
" needs: [detect-alpha-release, ci-success]",
205238
" permissions:",
206239
" contents: write",

scripts/check-alpha-release-plan.mjs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,11 @@ export async function validateReleaseTooling(cwd, readFileFn = readFileDefault)
145145
"release-alpha-train:",
146146
"ci.yml must contain the alpha release train job",
147147
);
148-
assertContains(
148+
assertJobContains(
149149
ciWorkflow,
150-
"if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-alpha-release.outputs.should_release == 'true'",
151-
"release-alpha-train must only run on release-relevant main pushes",
150+
"release-alpha-train",
151+
" 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'",
152+
"release-alpha-train must explicitly require release relevance and a successful CI gate",
152153
);
153154
assertContains(
154155
ciWorkflow,
@@ -234,6 +235,31 @@ function assertContains(input, expected, message) {
234235
}
235236
}
236237

238+
function assertJobContains(input, jobName, expected, message) {
239+
const block = workflowJobBlock(input, jobName);
240+
if (!block || !block.includes(expected)) {
241+
throw new Error(message);
242+
}
243+
}
244+
245+
function workflowJobBlock(input, jobName) {
246+
const headerPattern = new RegExp(`^ ${escapeRegExp(jobName)}:\\s*$`, "m");
247+
const header = headerPattern.exec(input);
248+
if (!header) {
249+
return undefined;
250+
}
251+
252+
const nextJobPattern = /^ {2}[A-Za-z0-9_-]+:\s*$/gm;
253+
nextJobPattern.lastIndex = header.index + header[0].length;
254+
const nextJob = nextJobPattern.exec(input);
255+
const end = nextJob ? nextJob.index : input.length;
256+
return input.slice(header.index, end);
257+
}
258+
259+
function escapeRegExp(input) {
260+
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
261+
}
262+
237263
function assertNotContains(input, expected, message) {
238264
if (input.includes(expected)) {
239265
throw new Error(message);

0 commit comments

Comments
 (0)