import { execFile } from "node:child_process"; import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; import { createServer as createHttpsServer } from "node:https"; import { tmpdir } from "node:os"; import { delimiter, dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import { describe, expect, it } from "vitest"; import { uiP0CiMatrix, uiP0Groups } from "../lib/playwright/suites.ts"; const execFileAsync = promisify(execFile); const e2eRoot = dirname(dirname(fileURLToPath(import.meta.url))); const workspaceRoot = dirname(e2eRoot); const ciWorkflowPath = join(workspaceRoot, ".github", "workflows", "ci.yml"); const commentWorkflowPath = join(workspaceRoot, ".github", "workflows", "comment.atom.yml"); const autofixWorkflowPath = join(workspaceRoot, ".github", "workflows", "autofix.atom.yml"); const reportWorkflowPath = join(workspaceRoot, ".github", "workflows", "report.atom.yml"); const bakePluginPreviewsWorkflowPath = join(workspaceRoot, ".github", "workflows", "bake-plugin-previews.yml"); const bakePluginPreviewsPrWorkflowPath = join(workspaceRoot, ".github", "workflows", "bake-plugin-previews-pr.yml"); const dockerImageWorkflowPath = join(workspaceRoot, ".github", "workflows", "docker-image.yml"); const backportAutomergeWorkflowPath = join(workspaceRoot, ".github", "workflows", "backport-automerge.yml"); const bakePreviewsAutomergeWorkflowPath = join( workspaceRoot, ".github", "workflows", "bake-plugin-previews-automerge.yml", ); const bakePreviewsWorkflowPath = join(workspaceRoot, ".github", "workflows", "bake-plugin-previews.yml"); const bakePreviewsReleaseWorkflowPath = join( workspaceRoot, ".github", "workflows", "bake-plugin-previews-release.yml", ); const finalizeReleaseWorkflowPath = join(workspaceRoot, ".github", "workflows", "finalize-release.yml"); const handoffScriptPath = join(workspaceRoot, ".github", "scripts", "handoff.py"); const releaseBetaWorkflowPath = join(workspaceRoot, ".github", "workflows", "release-beta.yml"); const releaseBetaSelfHostedWorkflowPath = join(workspaceRoot, ".github", "workflows", "release-beta-s.yml"); const releasePreviewWorkflowPath = join(workspaceRoot, ".github", "workflows", "release-preview.yml"); const releasePrereleaseWorkflowPath = join(workspaceRoot, ".github", "workflows", "release-prerelease.yml"); const releaseStableWorkflowPath = join(workspaceRoot, ".github", "workflows", "release-stable.yml"); const releaseStableNotesScriptPath = join(workspaceRoot, ".github", "scripts", "release", "github", "stable-notes.sh"); const releasePreviewScriptPath = join(workspaceRoot, "tools", "release", "src", "metadata", "prepare-preview.ts"); const releaseStableScriptPath = join(workspaceRoot, "tools", "release", "src", "metadata", "prepare-stable.ts"); const releaseBetaScriptPath = join(workspaceRoot, "tools", "release", "src", "metadata", "prepare-beta.ts"); const packagedPackageJsonPath = join(workspaceRoot, "apps", "packaged", "package.json"); const scopesScriptPath = join(workspaceRoot, "scripts", "scopes.ts"); const runnersScriptPath = join(workspaceRoot, ".github", "scripts", "runners.py"); const notifyDailyFeishuWorkflowPath = join(workspaceRoot, ".github", "workflows", "notify-daily-feishu.yml"); const cutReleaseWorkflowPath = join(workspaceRoot, ".github", "workflows", "cut-release.yml"); const cutPatchReleaseWorkflowPath = join(workspaceRoot, ".github", "workflows", "cut-patch-release.yml"); const feishuNoticeScriptPath = join(workspaceRoot, "tools", "release", "src", "notifications", "feishu-notice.ts"); const landingPageDailyFeishuWorkflowPath = join(workspaceRoot, ".github", "workflows", "landing-page-daily-feishu.yml"); const landingPageProductionWorkflowPath = join(workspaceRoot, ".github", "workflows", "landing-page-production.yml"); const landingPageDailyFeishuScriptPath = join(workspaceRoot, ".github", "scripts", "landing-page-daily-feishu.ts"); const releasePublishMetadataScriptPath = join( workspaceRoot, "tools", "release", "src", "storage", "publish-metadata.ts", ); const releaseBetaPosixBuildScriptPath = join(workspaceRoot, "tools", "release", "scripts", "build-platform.sh"); const releaseBetaWindowsBuildScriptPath = join(workspaceRoot, "tools", "release", "scripts", "build-platform.ps1"); const releaseBetaPlatformPublishScriptPath = join( workspaceRoot, "tools", "release", "src", "storage", "publish-platform.ts", ); function sectionBetween(content: string, start: string, end: string): string { const startIndex = content.indexOf(start); expect(startIndex).toBeGreaterThanOrEqual(0); const endIndex = content.indexOf(end, startIndex + start.length); expect(endIndex).toBeGreaterThan(startIndex); return content.slice(startIndex, endIndex); } function extractWorkflowRunScript(workflow: string, stepName: string): string { const marker = ` - name: ${stepName}`; const start = workflow.indexOf(marker); expect(start).toBeGreaterThanOrEqual(0); const next = workflow.indexOf("\n - ", start + marker.length); const step = workflow.slice(start, next === -1 ? workflow.length : next); const runStart = step.indexOf(" run: |\n"); expect(runStart).toBeGreaterThanOrEqual(0); return step .slice(runStart + " run: |\n".length) .split("\n") .map((line) => line.replace(/^ /, "")) .join("\n") .trimEnd(); } function parseGithubOutput(raw: string): Record { return Object.fromEntries( raw .split(/\r?\n/) .filter(Boolean) .map((line) => { const sep = line.indexOf("="); expect(sep).toBeGreaterThan(0); return [line.slice(0, sep), line.slice(sep + 1)]; }), ); } async function gitPatchId(mode: "--stable" | "--verbatim", diff: string): Promise { return await new Promise((resolve, reject) => { const child = execFile("git", ["patch-id", mode], { cwd: workspaceRoot, encoding: "utf8" }, (error, stdout, stderr) => { if (error) { reject(Object.assign(error, { stdout, stderr })); return; } resolve(stdout.trim().split(/\s+/)[0] ?? ""); }); child.stdin?.end(diff); }); } // Pull the two jq programs that the `validate` job's "Check workspace validation jobs" step runs // (the blanket failure scan and the required-jobs scan) straight out of ci.yml, so the gate logic // under test stays the real workflow source rather than a reimplementation. Both are invoked as // `jq -r --arg event "$EVENT_NAME" ''` and contain no single quotes, so the program is the // text between the opening and the next `'`. function extractValidateGateJqPrograms(workflow: string): { failures: string; requiredMisses: string } { const validate = sectionBetween(workflow, " validate:", " runtime_summary:"); const programs = [...validate.matchAll(/jq -r --arg event "\$EVENT_NAME" '([\s\S]*?)'/g)].map((match) => match[1] ?? ""); expect(programs).toHaveLength(2); return { failures: programs[0] ?? "", requiredMisses: programs[1] ?? "" }; } function runValidateGateJq(program: string, eventName: string, needs: unknown): Promise { return new Promise((resolve, reject) => { const child = execFile("jq", ["-r", "--arg", "event", eventName, program], { encoding: "utf8" }, (error, stdout, stderr) => { if (error) { reject(Object.assign(error, { stdout, stderr })); return; } resolve(stdout.trim()); }); child.stdin?.end(JSON.stringify(needs)); }); } // Mirrors the bash gate decision: the step exits non-zero when either jq program emits any line. async function validateGatePasses(workflow: string, eventName: string, needs: unknown): Promise { const { failures, requiredMisses } = extractValidateGateJqPrograms(workflow); const [failureLines, missLines] = await Promise.all([ runValidateGateJq(failures, eventName, needs), runValidateGateJq(requiredMisses, eventName, needs), ]); return failureLines === "" && missLines === ""; } async function runReleaseStableForFailure(env: Record): Promise { try { await execFileAsync(process.execPath, ["--experimental-strip-types", releaseStableScriptPath], { cwd: workspaceRoot, env: { ...process.env, GITHUB_REPOSITORY: "nexu-io/open-design", GITHUB_SHA: "0123456789abcdef0123456789abcdef01234567", OPEN_DESIGN_RELEASE_CHANNEL: "stable", ...env, }, }); } catch (error) { const failed = error as { stderr?: string; stdout?: string }; return `${failed.stdout ?? ""}${failed.stderr ?? ""}`; } throw new Error("release-stable script unexpectedly succeeded"); } async function readPackagedVersion(): Promise { const packageJson = JSON.parse(await readFile(packagedPackageJsonPath, "utf8")) as { version?: unknown }; if (typeof packageJson.version !== "string" || packageJson.version.length === 0) { throw new Error("apps/packaged/package.json must define a version"); } return packageJson.version; } async function runScopesPrint(eventName: string, eventPayload: unknown, changedFiles: string[] = []): Promise> { const tempDir = await mkdtemp(join(tmpdir(), "od-scopes-")); const eventPath = join(tempDir, "event.json"); const ghPath = join(tempDir, "gh"); const ghCmdPath = join(tempDir, "gh.cmd"); await writeFile(eventPath, JSON.stringify(eventPayload)); const script = `#!/usr/bin/env node process.stdout.write(${JSON.stringify(changedFiles.join("\n"))}); if (${JSON.stringify(changedFiles.length > 0)}) process.stdout.write("\\n"); `; await writeFile(ghPath, script); await chmod(ghPath, 0o755); await writeFile(ghCmdPath, `@echo off\r\n"${process.execPath}" "${ghPath}" %*\r\n`); try { const fakePath = `${tempDir}${delimiter}${process.env.PATH ?? ""}`; const { stdout } = await execFileAsync(process.execPath, ["--experimental-strip-types", scopesScriptPath, "print"], { cwd: workspaceRoot, env: { ...process.env, GITHUB_EVENT_NAME: eventName, GITHUB_EVENT_PATH: eventPath, GITHUB_REPOSITORY: "nexu-io/open-design", GITHUB_SHA: "0123456789abcdef0123456789abcdef01234567", OPEN_DESIGN_GH_NODE_SCRIPT: ghPath, Path: fakePath, PATH: fakePath, }, }); return JSON.parse(stdout) as Record; } finally { await rm(tempDir, { recursive: true, force: true }); } } async function runRunners(mode?: string): Promise> { const env = { ...process.env }; if (mode === undefined) { delete env.OD_CI_RUNNER_MODE; } else { env.OD_CI_RUNNER_MODE = mode; } delete env.GITHUB_OUTPUT; const { stdout } = await execFileAsync("python3", [runnersScriptPath], { cwd: workspaceRoot, env, }); return Object.fromEntries( stdout .trim() .split("\n") .filter((line) => line.length > 0) .map((line) => { const separatorIndex = line.indexOf("="); expect(separatorIndex).toBeGreaterThan(0); return [line.slice(0, separatorIndex), line.slice(separatorIndex + 1)]; }), ); } function runnerOutput(profiles: Record, key: string): string { const value = profiles[key]; if (value === undefined) { throw new Error(`Missing runner profile output: ${key}`); } return value; } function runnerDecision(profiles: Record): { schema_version: number; mode: string } { return JSON.parse(runnerOutput(profiles, "decision")) as { schema_version: number; mode: string }; } function runnerRunsOn(profiles: Record): Record { return JSON.parse(runnerOutput(profiles, "runs_on")) as Record; } async function writeFakeGhBin(binDir: string, releases: unknown[]): Promise { const ghPath = join(binDir, "gh"); const ghCmdPath = join(binDir, "gh.cmd"); await writeFile( ghPath, `#!/usr/bin/env node if (process.argv[2] === "api" && /^repos\\/[^/]+\\/[^/]+\\/releases\\?/.test(process.argv[3] ?? "")) { const url = new URL(process.argv[3], "https://api.github.com/"); const page = url.searchParams.get("page") ?? "1"; process.stdout.write(JSON.stringify(page === "1" ? ${JSON.stringify(releases)} : [])); process.exit(0); } console.error("unexpected gh invocation: " + process.argv.slice(2).join(" ")); process.exit(1); `, ); await chmod(ghPath, 0o755); await writeFile(ghCmdPath, `@echo off\r\n"${process.execPath}" "%~dp0gh" %*\r\n`); } describe("packaged smoke workflow", () => { it("[P2] keeps packaged smoke outside the main CI gate", async () => { const workflow = await readFile(ciWorkflowPath, "utf8"); expect(workflow).not.toContain("packaged_smoke_"); expect(workflow).not.toContain("Build PR mac artifacts"); expect(workflow).not.toContain("Build PR windows artifacts"); expect(workflow).not.toContain("Build PR linux headless artifacts"); expect(workflow).not.toContain("Smoke PR mac packaged runtime"); expect(workflow).not.toContain("Smoke PR windows packaged runtime"); expect(workflow).not.toContain("Smoke PR linux headless packaged runtime"); expect(workflow).not.toContain("OD_PACKAGED_E2E_"); expect(workflow).not.toContain("actions/cache/save"); }); it("[P2] runs Windows launcher payload archive validation when tools-pack is touched", async () => { const workflow = await readFile(ciWorkflowPath, "utf8"); const job = sectionBetween(workflow, " windows_tools_pack_payload_tests:", " web_workspace_tests:"); const validate = sectionBetween(workflow, " validate:", " if [ -n \"$failures\" ]; then"); expect(job).toContain("fromJSON(needs.runners.outputs.runs_on).windows_tools"); expect(job).toContain("toJSON(fromJSON(needs.runners.outputs.runs_on).windows_tools)"); expect(job).toContain("needs.scopes.outputs.run_windows_tools_pack_payload_tests == 'true'"); expect(job).toContain("pnpm --filter @open-design/tools-pack exec vitest run tests/launcher-payload.test.ts"); expect(validate).toContain("windows_tools_pack_payload_tests"); }); it("[P2] limits manual blob guard checks to changed files against main", async () => { const workflow = await readFile(ciWorkflowPath, "utf8"); const blobGuard = sectionBetween(workflow, " static_gate:", " nix_validation:"); expect(blobGuard).toContain('${{ github.event_name }}" = "workflow_dispatch"'); expect(blobGuard).toContain("repos/${{ github.repository }}/compare/main...${{ github.sha }}"); expect(blobGuard).toContain("select(.status != \"removed\") | .filename"); }); it("[P2] keeps merge queue as the authoritative post-PR validation path", async () => { const [ciWorkflow, dockerWorkflow, commentWorkflow, autofixWorkflow, reportWorkflow] = await Promise.all([ readFile(ciWorkflowPath, "utf8"), readFile(dockerImageWorkflowPath, "utf8"), readFile(commentWorkflowPath, "utf8"), readFile(autofixWorkflowPath, "utf8"), readFile(reportWorkflowPath, "utf8"), ]); const ciTrigger = sectionBetween(ciWorkflow, "on:", "\npermissions:"); const ciBlobGuard = sectionBetween(ciWorkflow, " static_gate:", " nix_validation:"); const dockerTrigger = sectionBetween(dockerWorkflow, "on:", "\njobs:"); expect(ciTrigger).toContain("pull_request:"); expect(ciTrigger).toContain("merge_group:"); expect(ciTrigger).toContain("workflow_dispatch:"); expect(ciTrigger).not.toContain("push:"); expect(ciBlobGuard).not.toContain('${{ github.event_name }}" = "push"'); expect(dockerTrigger).toContain("workflow_call:"); expect(dockerTrigger).toContain("tags: ['v*.*.*']"); expect(dockerTrigger).not.toContain("branches: [main]"); expect(dockerTrigger).not.toContain("- main"); expect(commentWorkflow).toContain("workflows: [ci]"); expect(commentWorkflow).toContain("github.event.workflow_run.event == 'pull_request'"); expect(autofixWorkflow).toContain("workflows: [ci]"); expect(autofixWorkflow).toContain("github.event.workflow_run.event == 'pull_request'"); expect(autofixWorkflow).not.toContain("ci-nix"); expect(reportWorkflow).toContain("workflows: [ci]"); expect(reportWorkflow).toContain("github.event.workflow_run.event == 'pull_request'"); }); it("[P2] gates the backport auto-merge follow-up as a trusted workflow_run consumer", async () => { const workflow = await readFile(backportAutomergeWorkflowPath, "utf8"); // Triggered only by ci completing, and only for a pull_request run on a backport-* branch — // never a push / manual dispatch, so a non-PR run can't reach the App-token or merge steps. expect(workflow).toContain("workflows: [ci]"); expect(workflow).toContain("github.event.workflow_run.event == 'pull_request'"); expect(workflow).toContain("startsWith(github.event.workflow_run.head_branch, 'backport-')"); // It mints the privileged release App token, hence the identity + SHA gates below. expect(workflow).toContain("actions/create-github-app-token"); expect(workflow).toContain("secrets.RELEASE_BOT_APP_ID"); // Only a non-draft, same-repo PR authored by the release bot, targeting release/v*, is merged. expect(workflow).toContain("steps.pr.outputs.author == 'app/open-design-release-bot'"); expect(workflow).toContain("steps.pr.outputs.cross == 'false'"); expect(workflow).toContain("steps.pr.outputs.draft == 'false'"); expect(workflow).toContain('startswith("release/v")'); // A human commit pushed on top of the cherry-pick (conflict resolution, CI fix) is never // reviewed, so auto-approve/merge require pristine == 'true': the backport cumulative diff // must be patch-equivalent to the source PR's reviewed merge commit on main. Author/committer // identity is a spoofable git header, so the gate must not trust github-actions[bot] metadata. expect(workflow).toContain("steps.pr.outputs.pristine == 'true'"); expect(workflow).toContain("Checkout trusted repository history"); expect(workflow).toContain("fetch-depth: 0"); expect(workflow).toContain("Backport of #([0-9]+)"); expect(workflow).toContain("source_patch_id"); expect(workflow).toContain("backport_patch_id"); expect(workflow).toContain("git patch-id --verbatim"); expect(workflow).not.toContain("git patch-id --stable"); expect(workflow).toContain("+refs/heads/$(printf '%s' \"$row\" | jq -r '.baseRefName')"); expect(workflow).toContain("+refs/pull/$num/head:refs/remotes/pull/$num/head"); expect(workflow).toContain('backport_base="$(git merge-base "$base_oid" "$head_oid" 2>/dev/null || true)"'); expect(workflow).toContain('if [ -n "$backport_base" ]; then'); expect(workflow).toContain("git diff --binary \"$source_merge^\" \"$source_merge\""); expect(workflow).toContain("git diff --binary \"$backport_base\" \"$head_oid\""); expect(workflow).toContain("source_base\" = \"main"); expect(workflow).not.toContain('.[].committer.login // "none"'); expect(workflow).not.toContain("grep -Fvxc 'github-actions[bot]'"); // The PR is resolved from the run's authoritative workflow_run.pull_requests association // (filtered to the release/* base at exactly the run's SHA), not a branch-name guess, and the // merge is bound to that same SHA (no post-green commit merged untested). expect(workflow).toContain("github.event.workflow_run.pull_requests"); expect(workflow).toContain("select(.head.sha == $sha)"); expect(workflow).toContain("steps.pr.outputs.head_oid == github.event.workflow_run.head_sha"); expect(workflow).toContain("--match-head-commit"); expect(workflow).toContain("--squash"); expect(workflow).toContain("--delete-branch"); // To satisfy release/v*'s one-approval rule, the bot's own clean backport is auto-approved by // github-actions[bot] (pull-requests: write) — under the same author==bot gate as the merge. expect(workflow).toContain("pull-requests: write"); expect(workflow).toContain("gh pr review"); expect(workflow).toContain("--approve"); const approveStep = sectionBetween(workflow, "Approve the clean backport", "gh pr review"); expect(approveStep).toContain("steps.pr.outputs.author == 'app/open-design-release-bot'"); expect(approveStep).toContain("steps.pr.outputs.pristine == 'true'"); expect(approveStep).toContain("github.token"); expect(approveStep).toContain("github.event.workflow_run.conclusion == 'success'"); // The Feishu failure path carries the same identity + SHA gates, so a fork / non-bot // backport-* PR can't spam the release group and stale failed runs do not page after the PR // head advanced. Alert on failure and timed_out, but not routine cancelled runs. const feishuStep = sectionBetween(workflow, "Notify Feishu on failed backport CI", "python3"); expect(feishuStep).toContain("steps.pr.outputs.author == 'app/open-design-release-bot'"); expect(feishuStep).toContain("steps.pr.outputs.cross == 'false'"); expect(feishuStep).toContain("steps.pr.outputs.head_oid == github.event.workflow_run.head_sha"); expect(feishuStep).toContain( "(github.event.workflow_run.conclusion == 'failure' || github.event.workflow_run.conclusion == 'timed_out')", ); expect(feishuStep).not.toContain("'cancelled'"); }); it("[P2] gates the finalize-release follow-up as a trusted workflow_run consumer", async () => { const workflow = await readFile(finalizeReleaseWorkflowPath, "utf8"); // Reacts to release-stable completing (not release: published — release-stable promotes its // own draft with GITHUB_TOKEN, which cannot trigger workflows), plus a manual backfill. expect(workflow).toContain('workflows: ["release-stable"]'); expect(workflow).toContain("types: [completed]"); expect(workflow).toContain("workflow_dispatch:"); expect(workflow).toContain("github.event_name == 'workflow_dispatch'"); expect(workflow).toContain("github.event.workflow_run.conclusion == 'success'"); expect(workflow).toContain("github.repository == 'nexu-io/open-design'"); expect(workflow).not.toContain("github.event.workflow_run.conclusion != 'cancelled'"); // It mints the privileged release App token for label deletion, branch push, PR + merge-queue. expect(workflow).toContain("actions/create-github-app-token"); expect(workflow).toContain("secrets.RELEASE_BOT_APP_ID"); // The shipped version is resolved from the LATEST published release (release-stable marks it // --latest) or the dispatch tag — NEVER from workflow_run.head_branch, which can be the // dispatch ref rather than the built release branch when release-stable runs with a `ref` input. expect(workflow).toContain("gh release view --repo nexu-io/open-design --json tagName,isDraft,isPrerelease"); expect(workflow).not.toContain("github.event.workflow_run.head_branch"); expect(workflow).not.toContain("HEAD_BRANCH"); // Only a published (draft=false), non-prerelease, open-design-vX.Y.Z release finalizes; a // dry-run completion (latest stays the previous, already-finalized stable) must no-op. expect(workflow).toContain("isDraft"); expect(workflow).toContain("isPrerelease"); expect(workflow).toContain("^[0-9]+\\.[0-9]+\\.[0-9]+$"); expect(workflow).toContain('echo "skip=true"'); expect(workflow).toContain("steps.ver.outputs.skip != 'true'"); // Idempotent forward-only bump of the synchronized manifests (patch, not the next minor that // cut-release owns); independently-versioned packages are skipped via npm pkg set version. expect(workflow).toContain("sort -V"); expect(workflow).toContain("npm pkg set version"); expect(workflow).toContain("changed=true"); // The bump PR needs one core-maintainer approval (code-owned manifests + require_code_owner_review // on main), so finalize requests that review and arms the merge queue pinned to the pushed SHA. expect(workflow).toContain("--add-reviewer nexu-io/core-maintainers"); expect(workflow).toContain("--squash --auto --match-head-commit"); expect(workflow).toContain("head_sha=$(git rev-parse HEAD)"); expect(workflow).toContain("git ls-remote --exit-code --heads origin \"$BRANCH\""); // The shipped release's backport label is cleaned up (tolerant of an already-deleted label). expect(workflow).toContain('label="backport release/v$VERSION"'); expect(workflow).toContain("gh label delete"); }); it("[P2] resolves finalize-release shipped versions from the real workflow shell step", async () => { const workflow = await readFile(finalizeReleaseWorkflowPath, "utf8"); const script = extractWorkflowRunScript( workflow, "Resolve the shipped version (latest published stable, or the dispatch tag)", ); async function runResolve(args: { event: "workflow_dispatch" | "workflow_run"; inputTag?: string; state?: string; ghExit?: boolean; }): Promise<{ output: Record; ghArgs: string[] }> { const dir = await mkdtemp(join(tmpdir(), "od-finalize-resolve-")); const ghPath = join(dir, "gh"); const jqPath = join(dir, "jq"); const outputPath = join(dir, "github-output"); const ghLogPath = join(dir, "gh-args.log"); await writeFile( ghPath, `#!/usr/bin/env node const fs = require("node:fs"); fs.appendFileSync(process.env.FAKE_GH_LOG, process.argv.slice(2).join(" ") + "\\n"); if (process.env.FAKE_GH_EXIT === "1") process.exit(1); process.stdout.write(process.env.FAKE_GH_STATE || ""); `, ); await chmod(ghPath, 0o755); await writeFile( jqPath, `#!/usr/bin/env node const selector = process.argv[process.argv.length - 1]; let input = ""; process.stdin.on("data", (chunk) => { input += chunk; }); process.stdin.on("end", () => { const data = JSON.parse(input); const key = selector.startsWith(".") ? selector.slice(1) : selector; process.stdout.write(String(data[key]) + "\\n"); }); `, ); await chmod(jqPath, 0o755); try { await execFileAsync("bash", ["-c", script], { cwd: dir, env: { ...process.env, EVENT: args.event, INPUT_TAG: args.inputTag ?? "", GITHUB_OUTPUT: outputPath, PATH: `${dir}${delimiter}${process.env.PATH ?? ""}`, FAKE_GH_LOG: ghLogPath, FAKE_GH_STATE: args.state ?? "", FAKE_GH_EXIT: args.ghExit ? "1" : "0", }, }); const [rawOutput, rawGhArgs] = await Promise.all([ readFile(outputPath, "utf8").catch(() => ""), readFile(ghLogPath, "utf8").catch(() => ""), ]); return { output: parseGithubOutput(rawOutput), ghArgs: rawGhArgs.split(/\r?\n/).filter(Boolean), }; } finally { await rm(dir, { recursive: true, force: true }); } } await expect( runResolve({ event: "workflow_dispatch", inputTag: "0.12.0", state: JSON.stringify({ tagName: "open-design-v0.12.0", isDraft: false, isPrerelease: false }), }), ).resolves.toMatchObject({ output: { skip: "false", version: "0.12.0", next: "0.12.1", branch: "release-bot/bump-main-v0.12.1", }, ghArgs: [expect.stringContaining("release view open-design-v0.12.0")], }); await expect( runResolve({ event: "workflow_dispatch", inputTag: "open-design-v1.2.3", state: JSON.stringify({ tagName: "open-design-v1.2.3", isDraft: false, isPrerelease: false }), }), ).resolves.toMatchObject({ output: { skip: "false", version: "1.2.3", next: "1.2.4" }, ghArgs: [expect.stringContaining("release view open-design-v1.2.3")], }); for (const state of [ { tagName: "open-design-v0.12.0", isDraft: true, isPrerelease: false }, { tagName: "open-design-v0.12.0", isDraft: false, isPrerelease: true }, { tagName: "open-design-v0.12.0-beta.1", isDraft: false, isPrerelease: false }, ]) { await expect(runResolve({ event: "workflow_run", state: JSON.stringify(state) })).resolves.toMatchObject({ output: { skip: "true" }, }); } await expect(runResolve({ event: "workflow_run", ghExit: true })).resolves.toMatchObject({ output: { skip: "true" }, }); }); it("[P2] bumps only synchronized workspace manifests in finalize-release", async () => { const workflow = await readFile(finalizeReleaseWorkflowPath, "utf8"); const script = extractWorkflowRunScript(workflow, "Bump main's synchronized workspace versions"); const dir = await mkdtemp(join(tmpdir(), "od-finalize-bump-")); const outputPath = join(dir, "github-output"); const writeJson = (relativePath: string, value: unknown) => writeFile(join(dir, relativePath), `${JSON.stringify(value, null, 2)}\n`); try { await Promise.all([ mkdir(join(dir, "apps", "web"), { recursive: true }), mkdir(join(dir, "apps", "telemetry-worker"), { recursive: true }), mkdir(join(dir, "packages", "platform"), { recursive: true }), mkdir(join(dir, "packages", "components"), { recursive: true }), mkdir(join(dir, "tools", "dev"), { recursive: true }), mkdir(join(dir, "e2e"), { recursive: true }), ]); await Promise.all([ writeJson("package.json", { name: "root", version: "0.12.0", dependencies: { untouched: "0.12.0" } }), writeJson("apps/web/package.json", { name: "@open-design/web", version: "0.12.0" }), writeJson("apps/telemetry-worker/package.json", { name: "telemetry-worker", version: "0.1.0" }), writeJson("packages/platform/package.json", { name: "@open-design/platform", version: "0.12.0" }), writeJson("packages/components/package.json", { name: "@open-design/components", version: "0.5.0" }), writeJson("tools/dev/package.json", { name: "@open-design/dev", version: "0.12.0" }), writeJson("e2e/package.json", { name: "@open-design/e2e", version: "0.12.0" }), ]); await execFileAsync("bash", ["-c", script], { cwd: dir, env: { ...process.env, NEXT: "0.12.1", GITHUB_OUTPUT: outputPath }, }); await expect(readFile(outputPath, "utf8")).resolves.toContain("changed=true"); await expect(readFile(join(dir, "package.json"), "utf8").then(JSON.parse)).resolves.toMatchObject({ version: "0.12.1", dependencies: { untouched: "0.12.0" }, }); await expect(readFile(join(dir, "apps", "web", "package.json"), "utf8").then(JSON.parse)).resolves.toMatchObject({ version: "0.12.1", }); await expect(readFile(join(dir, "packages", "platform", "package.json"), "utf8").then(JSON.parse)).resolves.toMatchObject({ version: "0.12.1", }); await expect(readFile(join(dir, "tools", "dev", "package.json"), "utf8").then(JSON.parse)).resolves.toMatchObject({ version: "0.12.1", }); await expect(readFile(join(dir, "e2e", "package.json"), "utf8").then(JSON.parse)).resolves.toMatchObject({ version: "0.12.1", }); await expect(readFile(join(dir, "apps", "telemetry-worker", "package.json"), "utf8").then(JSON.parse)).resolves.toMatchObject({ version: "0.1.0", }); await expect(readFile(join(dir, "packages", "components", "package.json"), "utf8").then(JSON.parse)).resolves.toMatchObject({ version: "0.5.0", }); } finally { await rm(dir, { recursive: true, force: true }); } }); it("[P2] keeps finalize-release version bumps forward-only", async () => { const workflow = await readFile(finalizeReleaseWorkflowPath, "utf8"); const script = extractWorkflowRunScript(workflow, "Bump main's synchronized workspace versions"); const dir = await mkdtemp(join(tmpdir(), "od-finalize-bump-noop-")); const outputPath = join(dir, "github-output"); try { await writeFile(join(dir, "package.json"), `${JSON.stringify({ name: "root", version: "0.12.2" }, null, 2)}\n`); await execFileAsync("bash", ["-c", script], { cwd: dir, env: { ...process.env, NEXT: "0.12.1", GITHUB_OUTPUT: outputPath }, }); await expect(readFile(outputPath, "utf8")).resolves.toContain("changed=false"); await expect(readFile(join(dir, "package.json"), "utf8").then(JSON.parse)).resolves.toMatchObject({ version: "0.12.2", }); } finally { await rm(dir, { recursive: true, force: true }); } }); it("[P2] keeps the backport patch-equivalence gate whitespace-sensitive", async () => { const compactDiff = [ "diff --git a/script.py b/script.py", "--- a/script.py", "+++ b/script.py", "@@ -1 +1 @@", "-print(1)", "+print(2)", "", ].join("\n"); const whitespaceDiff = [ "diff --git a/script.py b/script.py", "--- a/script.py", "+++ b/script.py", "@@ -1 +1 @@", "-print(1)", "+ print(2)", "", ].join("\n"); const compactStable = await gitPatchId("--stable", compactDiff); const whitespaceStable = await gitPatchId("--stable", whitespaceDiff); const compactVerbatim = await gitPatchId("--verbatim", compactDiff); const whitespaceVerbatim = await gitPatchId("--verbatim", whitespaceDiff); expect(compactStable).toBe(whitespaceStable); expect(compactVerbatim).not.toBe(whitespaceVerbatim); }); it("[P2] gates the plugin-preview manifest auto-merge as a trusted workflow_run consumer", async () => { const workflow = await readFile(bakePreviewsAutomergeWorkflowPath, "utf8"); // Triggered only by ci completing, and only for a pull_request run on the rolling // chore/plugin-previews branch — never a push / manual dispatch, so a non-PR run can't reach // the App-token or merge steps. expect(workflow).toContain("workflows: [ci]"); expect(workflow).toContain("github.event.workflow_run.event == 'pull_request'"); expect(workflow).toContain("github.event.workflow_run.head_branch == 'chore/plugin-previews'"); // It mints the privileged release App token, hence the identity + SHA gates below. expect(workflow).toContain("actions/create-github-app-token"); expect(workflow).toContain("secrets.RELEASE_BOT_APP_ID"); // Only a non-draft, same-repo PR authored by the release bot, targeting main, is merged. expect(workflow).toContain("steps.pr.outputs.author == 'app/open-design-release-bot'"); expect(workflow).toContain("steps.pr.outputs.cross == 'false'"); expect(workflow).toContain("steps.pr.outputs.draft == 'false'"); expect(workflow).toContain('select(.base.ref == "main")'); // A human commit pushed onto the rolling branch is unreviewed, so auto-approve/merge require // pristine == 'true': the PR's changed files are EXACTLY data/plugin-previews/manifest.json. A // commit's author/committer identity is a spoofable git header and the Git Data API does not // sign commits, so neither identity nor signature can prove bot provenance; instead the diff is // bounded to manifest-only so a pushed code change can never auto-merge. Paginated across all // files; any non-manifest file fails the count. expect(workflow).toContain("steps.pr.outputs.pristine == 'true'"); expect(workflow).toContain("/files?per_page=100"); expect(workflow).toContain(".[].filename"); expect(workflow).toContain("--paginate"); expect(workflow).toContain("grep -Fvxc 'data/plugin-previews/manifest.json'"); // The PR is resolved from the run's authoritative workflow_run.pull_requests association // (filtered to the main base at exactly the run's SHA), not a branch-name guess, and the merge // is bound to that same SHA (no post-green commit merged untested). main has a merge queue, so // the merge is GitHub-native --auto (enqueue), not a direct squash. expect(workflow).toContain("github.event.workflow_run.pull_requests"); expect(workflow).toContain("select(.head.sha == $sha)"); expect(workflow).toContain("steps.pr.outputs.head_oid == github.event.workflow_run.head_sha"); expect(workflow).toContain("--match-head-commit"); expect(workflow).toContain("--squash"); expect(workflow).toContain("--auto"); // To satisfy main's one-approval rule, the bot's own clean manifest PR is auto-approved by // github-actions[bot] (pull-requests: write) — a different identity than the App author. expect(workflow).toContain("pull-requests: write"); expect(workflow).toContain("gh pr review"); expect(workflow).toContain("--approve"); const approveStep = sectionBetween(workflow, "Approve the clean manifest PR", "gh pr review"); expect(approveStep).toContain("steps.pr.outputs.author == 'app/open-design-release-bot'"); expect(approveStep).toContain("steps.pr.outputs.pristine == 'true'"); expect(approveStep).toContain("github.token"); // Approve only when the run CI actually succeeded — dropping this predicate would approve a // PR whose CI failed. expect(approveStep).toContain("github.event.workflow_run.conclusion == 'success'"); // The enqueue step carries the same identity + SHA + pristine + success gates as the approve, // and merges via the native --auto path bound to the validated SHA. Dropping the success // predicate here would enqueue a PR whose CI failed. const enqueueStep = sectionBetween(workflow, "Enqueue the clean manifest PR", "gh pr merge"); expect(enqueueStep).toContain("steps.pr.outputs.author == 'app/open-design-release-bot'"); expect(enqueueStep).toContain("steps.pr.outputs.pristine == 'true'"); expect(enqueueStep).toContain("steps.pr.outputs.head_oid == github.event.workflow_run.head_sha"); expect(enqueueStep).toContain("github.event.workflow_run.conclusion == 'success'"); // Base-freshness: main's merge queue does not require an up-to-date branch, so a bake rendered // against an older main must not be squashed in over a newer manifest. The enqueue requires the // rendered base (the PR head's first parent) to equal current main HEAD; a behind PR waits for // the next bake to refresh. expect(enqueueStep).toContain("steps.pr.outputs.base_fresh == 'true'"); expect(workflow).toContain('.parents[0].sha // ""'); expect(workflow).toContain("commits/heads/main"); // The Feishu failure path carries the same identity gates, so a fork / non-bot PR can't spam // the release group, and fires only on a CI *failure* (not success). const feishuStep = sectionBetween(workflow, "Notify Feishu on failed manifest CI", "python3"); expect(feishuStep).toContain("steps.pr.outputs.author == 'app/open-design-release-bot'"); expect(feishuStep).toContain("steps.pr.outputs.cross == 'false'"); // Page on every terminal RED state that strands the PR — failure AND timed_out (ci.yml has // timeout-minutes jobs) — but NOT cancelled, which a routine force-push of the rolling branch // produces and would otherwise page on every refresh. expect(feishuStep).toContain("github.event.workflow_run.conclusion == 'failure'"); expect(feishuStep).toContain("github.event.workflow_run.conclusion == 'timed_out'"); expect(feishuStep).not.toContain("'cancelled'"); // Bind the alert to the exact SHA that failed: the rolling branch is force-pushed, so a stale // failed run that finishes after the head advanced must not page about a SHA that is no longer // the PR head. expect(feishuStep).toContain("steps.pr.outputs.head_oid == github.event.workflow_run.head_sha"); // Sign the release webhook with the SAME secret the rest of the repo pairs with // FEISHU_RELEASE_WEBHOOK (notify-release-feishu / notify-daily-feishu / backport-automerge), // not a stray secret that resolves empty and sends an unsigned, rejected request. expect(feishuStep).toContain("secrets.FEISHU_RELEASE_WEBHOOK"); expect(feishuStep).toContain("secrets.FEISHU_RELEASE_SIGN_SECRET"); }); it("[P2] keeps the bake producer wired so the auto-merge pristine path works", async () => { // The downstream pristine/auto-merge gates only hold if the producer keeps authoring the // rolling manifest PR the right way. Lock the three producer invariants this PR depends on, so // regressing any of them fails here instead of silently breaking auto-merge. const workflow = await readFile(bakePreviewsWorkflowPath, "utf8"); // 1. The rolling PR is pushed with the release-bot App token, not GITHUB_TOKEN — a // GITHUB_TOKEN-authored push triggers no CI, so the PR could never clear main's required // `Validate workspace` check or the merge queue. expect(workflow).toContain("actions/create-github-app-token"); expect(workflow).toContain("secrets.RELEASE_BOT_APP_ID"); expect(workflow).toContain("x-access-token:${APP_TOKEN}"); // 2. The manifest commit touches ONLY data/plugin-previews/manifest.json. The reactor's // pristine gate trusts the PR file set (not a forgeable commit identity), so the producer // must keep committing exactly that one file. expect(workflow).toContain("cp \"$NEW\" \"$OLD\""); expect(workflow).toContain('git add "$OLD"'); // 3. The rolling branch is rebuilt from the checked-out HEAD (the revision the manifest was // rendered against), NOT live main — the bake can run ~90min while main advances, and basing // it on live main would publish a stale manifest on top of newer commits. Creating the branch // with `git checkout -B` from HEAD parents the commit on the rendered revision; a regression // to a live-main base (fetching refs/heads/main as the parent) fails here. expect(workflow).toContain('git checkout -B "$BRANCH"'); expect(workflow).not.toContain("git/ref/heads/main"); }); it("[P2] keeps the release-cut bake pushing its manifest as a ruleset bypass bot", async () => { // release/** is guarded by the "Protected branches (preview/*, release/v*)" ruleset whose // pull_request rule rejects a direct push (GH013) unless the pusher is a bypass actor. The // release-cut bake writes the authoritative manifest straight onto the release branch, so it // must push as open-design-bot — a bypass actor on that ruleset — not github-actions[bot], // which is NOT and gets rejected (this stranded release/v0.14.2's manifest). These three auth // invariants only work together; a refactor that drops any one silently reintroduces the // GH013 regression, so lock them here rather than rely on YAML review. const workflow = await readFile(bakePreviewsReleaseWorkflowPath, "utf8"); // 1. Checkout must NOT persist GITHUB_TOKEN: its http.extraheader would override the inline bot // token on push and re-authenticate as github-actions[bot] (the same override fixed in the // post-merge bake — #5357). expect(workflow).toContain("persist-credentials: false"); // 2. The run mints an open-design-bot token via the BOT_APP_* creds — the App that IS a bypass // actor on the release ruleset. RELEASE_BOT_APP_ID (used by the post-merge bake) is a // different App and is NOT a bypass actor, so pin the correct credentials, not just the // generic token action. expect(workflow).toContain("actions/create-github-app-token"); expect(workflow).toContain("secrets.BOT_APP_CLIENT_ID"); expect(workflow).toContain("secrets.BOT_APP_PRIVATE_KEY"); // 3. The manifest push goes through the explicit tokenized URL with that bot token, so it // authenticates as the bypass actor rather than the checkout's default credential. expect(workflow).toContain("x-access-token:${BOT_TOKEN}"); }); it("[P2] keeps PR and merge queue CI separated by hot/full validation mode", async () => { const workflow = await readFile(ciWorkflowPath, "utf8"); const scopes = sectionBetween(workflow, " scopes:", " static_gate:"); const validate = sectionBetween(workflow, " validate:", " runtime_summary:"); expect(workflow).toContain("ci_mode:"); expect(scopes).toContain("ci_mode: ${{ steps.detect.outputs.ci_mode }}"); expect(scopes).toContain("ui_p0_validation_required: ${{ steps.detect.outputs.ui_p0_validation_required }}"); expect(scopes).toContain("run_ui_p0: ${{ steps.detect.outputs.run_ui_p0 }}"); expect(workflow).toContain("needs.scopes.outputs.run_ui_p0 == 'true'"); expect(validate).toContain('when($out.run_ui_p0 == "true"; ["ui_p0_smoke", "ui_p0"])'); await expect(runScopesPrint("workflow_dispatch", { inputs: { ci_mode: "hot" } }, ["apps/web/src/app/page.tsx"])).resolves.toMatchObject({ ci_mode: "hot", run_ui_p0: true, run_nix_validation: false, }); await expect(runScopesPrint("workflow_dispatch", { inputs: {} })).resolves.toMatchObject({ ci_mode: "full", ui_p0_validation_required: true, run_docker_build: true, run_nix_validation: true, run_ui_p0: true, }); await expect(runScopesPrint("merge_group", {})).resolves.toMatchObject({ ci_mode: "full", ui_p0_validation_required: true, run_docker_build: true, run_nix_validation: true, run_ui_p0: true, }); }); it("[P2] keeps the Validate workspace gate Nix-advisory on PRs and enforced at merge", async () => { const workflow = await readFile(ciWorkflowPath, "utf8"); const validate = sectionBetween(workflow, " validate:", " runtime_summary:"); // The gate must read the event so it can treat a red nix_validation differently per surface. expect(validate).toContain("EVENT_NAME: ${{ github.event_name }}"); expect(validate).toContain('select(.key != "nix_validation" or $event != "pull_request")'); expect(validate).toContain('when($out.run_nix_validation == "true" and $event != "pull_request"; ["nix_validation"])'); const baseOutputs = { run_nix_validation: "true", run_preflight: "false", run_workspace_unit_tests: "false", run_windows_tools_pack_payload_tests: "false", run_web_workspace_tests: "false", run_e2e_vitest: "false", run_playwright_critical: "false", run_ui_p0: "false", run_playwright_visual: "false", run_docker_build: "false", }; const needsWithFailedNix = { scopes: { result: "success", outputs: baseOutputs }, static_gate: { result: "success" }, nix_validation: { result: "failure" }, }; // A stale/failed Nix hash is advisory on pull_request: the gate still passes while autofix heals it. await expect(validateGatePasses(workflow, "pull_request", needsWithFailedNix)).resolves.toBe(true); // ...but it is a hard gate at merge time and on manual full runs — fail closed, never reaching main. await expect(validateGatePasses(workflow, "merge_group", needsWithFailedNix)).resolves.toBe(false); await expect(validateGatePasses(workflow, "workflow_dispatch", needsWithFailedNix)).resolves.toBe(false); // The PR exception is scoped to Nix only — any other failed required job still fails the gate on a PR. const needsWithFailedWeb = { scopes: { result: "success", outputs: { ...baseOutputs, run_web_workspace_tests: "true" } }, static_gate: { result: "success" }, nix_validation: { result: "success" }, web_workspace_tests: { result: "failure" }, }; await expect(validateGatePasses(workflow, "pull_request", needsWithFailedWeb)).resolves.toBe(false); }); it("[P2] routes default CI through cost-sensitive runner tiers", async () => { const workflow = await readFile(ciWorkflowPath, "utf8"); const runners = sectionBetween(workflow, " runners:", " scopes:"); const scopes = sectionBetween(workflow, " scopes:", " static_gate:"); const staticGate = sectionBetween(workflow, " static_gate:", " nix_validation:"); const workspaceUnitTests = sectionBetween(workflow, " workspace_unit_tests:", " windows_tools_pack_payload_tests:"); const webWorkspaceTests = sectionBetween(workflow, " web_workspace_tests:", " e2e_vitest:"); const e2eVitest = sectionBetween(workflow, " e2e_vitest:", " playwright_critical:"); const nixValidation = sectionBetween(workflow, " nix_validation:", " preflight:"); const preflight = sectionBetween(workflow, " preflight:", " workspace_unit_tests:"); const dockerPr = sectionBetween(workflow, " docker_pr:", " validate:"); const uiP0 = sectionBetween(workflow, " ui_p0:", " playwright_visual:"); const visual = sectionBetween(workflow, " playwright_visual:", " docker_pr:"); expect(runners).toContain("runs-on: ubuntu-24.04"); expect(runners).toContain("runs_on: ${{ steps.runners.outputs.runs_on }}"); expect(runners).toContain("decision: ${{ steps.runners.outputs.decision }}"); expect(runners).toContain("python3 .github/scripts/runners.py"); expect(scopes).toContain("needs: [runners]"); expect(scopes).toContain("fromJSON(needs.runners.outputs.runs_on).control"); expect(staticGate).toContain("needs: [runners]"); expect(staticGate).toContain("fromJSON(needs.runners.outputs.runs_on).control"); expect(workspaceUnitTests).toContain("fromJSON(needs.runners.outputs.runs_on).workspace_unit"); expect(workspaceUnitTests).toContain("toJSON(fromJSON(needs.runners.outputs.runs_on).workspace_unit)"); expect(webWorkspaceTests).toContain("fromJSON(needs.runners.outputs.runs_on).js_hot"); expect(webWorkspaceTests).toContain("toJSON(fromJSON(needs.runners.outputs.runs_on).js_hot)"); expect(webWorkspaceTests).not.toContain('"od-persistent-ci"'); expect(e2eVitest).toContain("fromJSON(needs.runners.outputs.runs_on).js_hot"); expect(e2eVitest).toContain("toJSON(fromJSON(needs.runners.outputs.runs_on).js_hot)"); expect(e2eVitest).not.toContain('"od-persistent-ci"'); expect(nixValidation).toContain("fromJSON(needs.runners.outputs.runs_on).general_medium"); expect(preflight).toContain("fromJSON(needs.runners.outputs.runs_on).general_medium"); expect(preflight).toContain("toJSON(fromJSON(needs.runners.outputs.runs_on).general_medium)"); expect(dockerPr).toContain("fromJSON(needs.runners.outputs.runs_on).general_medium"); expect(uiP0).toContain("fromJSON(needs.runners.outputs.runs_on).ui_hot"); expect(uiP0).toContain("toJSON(fromJSON(needs.runners.outputs.runs_on).ui_hot)"); expect(uiP0).toContain("include: ${{ fromJSON(needs.scopes.outputs.ui_p0_matrix) }}"); expect(uiP0CiMatrix.map((entry) => entry.name)).toEqual([ "entry-settings", "project-workspace", "project-runtime", "workspace-restoration", ]); expect(uiP0Groups["project-workspace"].files).toEqual([ "ui/app.test.ts", "ui/app-design-files.test.ts", "ui/app-manual-edit.test.ts", "ui/project-management-flows.test.ts", "ui/workspace-keyboard-flows.test.ts", ]); expect(uiP0Groups["project-workspace"].workers).toBe(1); expect(visual).toContain("fromJSON(needs.runners.outputs.runs_on).visual_hot"); expect(visual).toContain("toJSON(fromJSON(needs.runners.outputs.runs_on).visual_hot)"); expect(workflow).not.toContain("needs.runners.outputs.contabo_control"); expect(workflow).not.toContain("needs.runners.outputs.hosted_or_blacksmith"); expect(workflow).not.toContain("needs.runners.outputs.blacksmith_default"); }); it("[P2] resolves CI runner profiles by mode", async () => { const defaultProfiles = await runRunners(); const defaultRunsOn = runnerRunsOn(defaultProfiles); expect(runnerDecision(defaultProfiles)).toEqual({ schema_version: 1, mode: "default" }); expect(Object.keys(defaultRunsOn).sort()).toEqual([ "control", "general_medium", "js_hot", "ui_hot", "visual_hot", "windows_tools", "workspace_unit", ]); expect(defaultRunsOn.control).toEqual([ "self-hosted", "Linux", "X64", "od-persistent-ci", "od-ci-hot-poc", ]); expect(defaultRunsOn.general_medium).toEqual(["ubuntu-24.04"]); expect(defaultRunsOn.workspace_unit).toEqual(["ubuntu-24.04"]); expect(defaultRunsOn.windows_tools).toEqual(["windows-latest"]); expect(defaultRunsOn.js_hot).toEqual(["blacksmith-4vcpu-ubuntu-2404"]); expect(defaultRunsOn.ui_hot).toEqual(["blacksmith-4vcpu-ubuntu-2404"]); expect(defaultRunsOn.visual_hot).toEqual(["blacksmith-4vcpu-ubuntu-2404"]); expect(defaultProfiles).not.toHaveProperty("contabo_control"); expect(defaultProfiles).not.toHaveProperty("hosted_or_blacksmith"); expect(defaultProfiles).not.toHaveProperty("blacksmith_default"); const performanceProfiles = await runRunners("performance"); const performanceRunsOn = runnerRunsOn(performanceProfiles); expect(runnerDecision(performanceProfiles)).toEqual({ schema_version: 1, mode: "performance" }); expect(performanceRunsOn.control).toEqual(["ubuntu-24.04"]); expect(performanceRunsOn.general_medium).toEqual(["blacksmith-4vcpu-ubuntu-2404"]); expect(performanceRunsOn.workspace_unit).toEqual(["ubuntu-24.04"]); expect(performanceRunsOn.windows_tools).toEqual(["windows-latest"]); expect(performanceRunsOn.js_hot).toEqual(["blacksmith-4vcpu-ubuntu-2404"]); expect(performanceRunsOn.ui_hot).toEqual(["blacksmith-4vcpu-ubuntu-2404"]); expect(performanceRunsOn.visual_hot).toEqual(["blacksmith-4vcpu-ubuntu-2404"]); const economicProfiles = await runRunners("economic"); const economicRunsOn = runnerRunsOn(economicProfiles); expect(runnerDecision(economicProfiles)).toEqual({ schema_version: 1, mode: "economic" }); expect(economicRunsOn.control).toEqual(["ubuntu-24.04"]); expect(economicRunsOn.general_medium).toEqual(["ubuntu-24.04"]); expect(economicRunsOn.workspace_unit).toEqual(["ubuntu-24.04"]); expect(economicRunsOn.windows_tools).toEqual(["windows-latest"]); expect(economicRunsOn.js_hot).toEqual(["ubuntu-24.04"]); expect(economicRunsOn.ui_hot).toEqual(["ubuntu-24.04"]); expect(economicRunsOn.visual_hot).toEqual(["ubuntu-24.04"]); }); it("[P2] routes CI follow-ons through generic handoff workflows", async () => { const [ciWorkflow, commentWorkflow, autofixWorkflow, reportWorkflow, handoffScript] = await Promise.all([ readFile(ciWorkflowPath, "utf8"), readFile(commentWorkflowPath, "utf8"), readFile(autofixWorkflowPath, "utf8"), readFile(reportWorkflowPath, "utf8"), readFile(handoffScriptPath, "utf8"), ]); expect(ciWorkflow).toContain("handoff.py dir comment"); expect(ciWorkflow).toContain("handoff.py dir autofix"); expect(ciWorkflow).toContain("handoff.py dir report"); expect(ciWorkflow).toContain("handoff-comment-"); expect(ciWorkflow).toContain("handoff-autofix-"); expect(ciWorkflow).toContain("handoff-report-"); expect(ciWorkflow).not.toContain("nix-hash-autofix"); expect(ciWorkflow).not.toContain("visual-pr-comment"); expect(commentWorkflow).toContain("artifact-pattern comment"); expect(commentWorkflow).toContain("merge-multiple: false"); expect(commentWorkflow).toContain("pull-requests: write"); expect(autofixWorkflow).toContain("artifact-pattern autofix"); expect(autofixWorkflow).toContain("allowed_paths"); expect(reportWorkflow).toContain("artifact-pattern report"); expect(reportWorkflow).toContain("scripts/visual-report.ts compare-pr"); expect(reportWorkflow).toContain("R2_ACCESS_KEY_ID"); expect(reportWorkflow).toContain("pull-requests: write"); expect(reportWorkflow).toContain("Visual report comment creation failed"); expect(reportWorkflow).toContain("jq -n --rawfile body"); expect(reportWorkflow).toContain("--input"); expect(reportWorkflow).not.toContain("handoff.py dir comment"); expect(reportWorkflow).not.toContain("handoff-comment-"); expect(handoffScript).toContain("def self_check()"); expect(handoffScript).toContain('"report"'); for (const workflow of [commentWorkflow, autofixWorkflow]) { expect(workflow).toContain("python3 .github/scripts/handoff.py self-check"); expect(workflow).toContain("github.event.workflow_run.event == 'pull_request'"); expect(workflow).not.toContain("nix/pnpm-deps.nix"); expect(workflow).not.toContain("visual-report"); } expect(reportWorkflow).toContain("python3 .github/scripts/handoff.py self-check"); expect(reportWorkflow).toContain("github.event.workflow_run.event == 'pull_request'"); expect(commentWorkflow).toContain("jq -n --rawfile body"); expect(commentWorkflow).toContain("--input"); for (const workflow of [commentWorkflow]) { expect(workflow).toContain("jq -n --rawfile body"); expect(workflow).toContain("--input"); expect(workflow).not.toContain("--field body=\"$(cat"); expect(workflow).not.toContain("--field \"body=$(cat"); } }); it("[P2] keeps pull-request plugin preview baking secretless and read-only", async () => { const [prWorkflow, postMergeWorkflow] = await Promise.all([ readFile(bakePluginPreviewsPrWorkflowPath, "utf8"), readFile(bakePluginPreviewsWorkflowPath, "utf8"), ]); expect(prWorkflow).toContain("permissions:\n contents: read"); expect(prWorkflow).toContain("Checkout PR head"); expect(prWorkflow).toContain("ref: ${{ github.event.pull_request.head.sha }}"); expect(prWorkflow).toContain("upload: 'false'"); expect(prWorkflow).toContain("post-merge bake will publish clips and open the rolling manifest PR"); expect(prWorkflow).not.toContain("contents: write"); expect(prWorkflow).not.toContain("PREVIEW_BAKE_TOKEN"); expect(prWorkflow).not.toContain("CLOUDFLARE_R2_REPOSITORY_ASSETS"); expect(prWorkflow).not.toContain("git push"); expect(postMergeWorkflow).toContain("permissions:\n contents: write"); expect(postMergeWorkflow).toContain("CLOUDFLARE_R2_REPOSITORY_ASSETS_AK"); // The write-capable credential the PR path lacks lives here post-merge. The rolling manifest PR // is now authored with the release-bot App (so its push triggers CI and the auto-merge reactor // can act), replacing the never-configured PREVIEW_BAKE_TOKEN fallback. expect(postMergeWorkflow).toContain("secrets.RELEASE_BOT_APP_ID"); expect(postMergeWorkflow).not.toContain("PREVIEW_BAKE_TOKEN"); }); it("[P2] preserves beta linux AppImage smoke reports for platform publication", async () => { const workflow = await readFile(releaseBetaWorkflowPath, "utf8"); const linuxBuildStep = workflow.match(/- name: Build beta linux_x64\r?\n(?:.+\r?\n)+?(?=\r?\n - name: Write linux_x64 release report)/m); expect(linuxBuildStep?.[0]).toBeDefined(); expect(linuxBuildStep?.[0]).toContain("RELEASE_TARGET: linux_x64"); expect(linuxBuildStep?.[0]).toContain("RELEASE_REPORT_DIR: ${{ runner.temp }}/release-report/linux_x64"); expect(linuxBuildStep?.[0]).toContain("bash tools/release/scripts/build-platform.sh"); expect(workflow).toContain("Write linux_x64 release report"); expect(workflow).toContain("RELEASE_REPORT_JSON_PATH: ${{ runner.temp }}/release-report/linux_x64/report.json"); expect(workflow).toContain("Prepare linux_x64 assets"); expect(workflow).toContain("Publish linux_x64 platform"); expect(workflow).toContain("Upload linux_x64 publish manifest"); expect(workflow).toContain("open-design-beta-linux-x64-publish-manifest"); expect(workflow).toContain("Download linux_x64 publish manifest"); expect(workflow).not.toContain(".github/scripts/release/assets/linux.sh"); expect(workflow).not.toContain(".github/scripts/release/r2/publish-platform.ts"); }); it("[P2] preserves stable linux AppImage smoke reports for release publication", async () => { const workflow = await readFile(releaseStableWorkflowPath, "utf8"); const linuxBuildStep = workflow.match( /- name: Build release linux artifacts\r?\n(?:.+\r?\n)+?(?=\r?\n - name: Smoke release linux AppImage runtime)/m, ); expect(linuxBuildStep?.[0]).toBeDefined(); expect(linuxBuildStep?.[0]).toContain( 'node -e \'const fs = require("node:fs"); JSON.parse(fs.readFileSync(process.argv[1], "utf8"));\' "$build_json_path"', ); expect(workflow).toContain("Smoke release linux AppImage runtime"); expect(workflow).toContain("manifest.json"); expect(workflow).toContain("tools-pack.json"); expect(workflow).toContain("Upload linux e2e spec report"); expect(workflow).toContain("open-design-release-linux-e2e-report"); expect(workflow).toContain("Download linux e2e spec report"); expectReleaseLinuxBuildPreservesEvidence(workflow, "Build release linux artifacts"); expectReleaseLinuxSmokePreservesEvidenceBeforeApt(workflow, "Smoke release linux AppImage runtime"); }); it("[P2] keeps release namespaces aligned with release channels", async () => { const [releaseStableWorkflow, releaseStableScript, releasePreviewWorkflow, releasePrereleaseWorkflow, releaseBetaWorkflow] = await Promise.all([ readFile(releaseStableWorkflowPath, "utf8"), readFile(releaseStableScriptPath, "utf8"), readFile(releasePreviewWorkflowPath, "utf8"), readFile(releasePrereleaseWorkflowPath, "utf8"), readFile(releaseBetaWorkflowPath, "utf8"), ]); expect(releaseStableScript).toContain('mac: releaseNamespace(channel, "mac"),'); expect(releaseStableScript).toContain('setOutput("namespace", namespaces.mac);'); expect(releaseStableScript).toContain('setOutput("mac_intel_namespace", namespaces.macIntel);'); expect(releaseStableScript).toContain('setOutput("win_namespace", namespaces.win);'); expect(releaseStableScript).toContain('setOutput("linux_namespace", namespaces.linux);'); expect(releaseStableWorkflow).toContain("namespace: ${{ steps.stable.outputs.namespace }}"); expect(releaseStableWorkflow).toContain("mac_intel_namespace: ${{ steps.stable.outputs.mac_intel_namespace }}"); expect(releaseStableWorkflow).toContain("win_namespace: ${{ steps.stable.outputs.win_namespace }}"); expect(releaseStableWorkflow).toContain("linux_namespace: ${{ steps.stable.outputs.linux_namespace }}"); expect(releaseStableWorkflow).toContain('--namespace "${{ needs.metadata.outputs.namespace }}"'); expect(releaseStableWorkflow).toContain("OD_PACKAGED_E2E_NAMESPACE: ${{ needs.metadata.outputs.namespace }}"); expect(releaseStableWorkflow).toContain('"--namespace", "${{ needs.metadata.outputs.win_namespace }}",'); expect(releaseStableWorkflow).toContain('OD_PACKAGED_E2E_NAMESPACE: ${{ needs.metadata.outputs.win_namespace }}'); expect(releaseStableWorkflow).toContain('--namespace "${{ needs.metadata.outputs.linux_namespace }}"'); expect(releaseStableWorkflow).toContain('"namespace": "${{ needs.metadata.outputs.linux_namespace }}",'); expect(releaseStableWorkflow).not.toMatch(/--namespace release-stable(?:-intel|-win|-linux)?\b/); expect(releaseStableWorkflow).not.toMatch(/OD_PACKAGED_E2E_NAMESPACE: release-stable(?:-win|-linux)?\b/); expect(releaseStableWorkflow).not.toMatch(/namespaces\/release-stable(?:-intel|-win|-linux)?\b/); expectChannelWorkflowNamespaces(releasePreviewWorkflow, "preview", { hasLinuxSmoke: false }); expectChannelWorkflowNamespaces(releasePrereleaseWorkflow, "prerelease", { hasLinuxSmoke: false }); expect(releaseBetaWorkflow).toContain("RELEASE_NAMESPACE: release-beta"); expect(releaseBetaWorkflow).toContain("RELEASE_NAMESPACE: release-beta-win"); expect(releaseBetaWorkflow).toContain("RELEASE_NAMESPACE: release-beta-x64"); expect(releaseBetaWorkflow).toContain("RELEASE_NAMESPACE: release-beta-linux"); expect(releaseBetaWorkflow).toContain("RELEASE_TARGET: mac_arm64"); expect(releaseBetaWorkflow).toContain("RELEASE_TARGET: win_x64"); expect(releaseBetaWorkflow).toContain("RELEASE_TARGET: mac_x64"); expect(releaseBetaWorkflow).toContain("RELEASE_TARGET: linux_x64"); const betaWinJob = sectionBetween(releaseBetaWorkflow, " build_win_x64:", " build_linux_x64:"); expect(betaWinJob).not.toContain("tools\\release\\scripts\\build-platform.ps1"); expect(betaWinJob).toContain("uses: actions/cache/restore@v5"); expect(betaWinJob).toContain("uses: actions/cache/save@v5"); expect(betaWinJob).toContain("tools-pack-win-v1-beta-$env:RUNNER_OS-"); expect(betaWinJob).toContain('"tools-pack", "win", "build"'); expect(betaWinJob).toContain("tools-pack win validate-payload"); expect(betaWinJob).toContain("pnpm exec tsx scripts/release-smoke.ts win specs/win.spec.ts"); const betaBuildScript = await readFile(releaseBetaPosixBuildScriptPath, "utf8"); expect(betaBuildScript).toContain("required RELEASE_CHANNEL"); expect(betaBuildScript).toContain('release_channel="$RELEASE_CHANNEL"'); expect(betaBuildScript).not.toContain('RELEASE_CHANNEL:-beta'); expect(betaBuildScript).toContain('OD_PACKAGED_E2E_RELEASE_CHANNEL="$release_channel"'); expect(betaBuildScript).toContain('OD_PACKAGED_E2E_RELEASE_VERSION="$RELEASE_VERSION"'); expect(betaBuildScript).toContain('OD_PACKAGED_E2E_MAC_UPDATE_FIXTURE="${update_build_json_path:+tools-serve}"'); const betaWindowsBuildScript = await readFile(releaseBetaWindowsBuildScriptPath, "utf8"); expect(betaWindowsBuildScript).toContain('throw "RELEASE_CHANNEL is required"'); expect(betaWindowsBuildScript).not.toContain('"beta" } else { $env:RELEASE_CHANNEL }'); expect(betaWindowsBuildScript).toContain('Test-JsonString $manifest.channel "channel" $ReleaseChannel'); expect(betaWindowsBuildScript).toContain('channel = $ReleaseChannel'); expect(betaWindowsBuildScript).toContain('$env:OD_PACKAGED_E2E_RELEASE_CHANNEL = $ReleaseChannel'); expect(betaWindowsBuildScript).toContain('$env:OD_PACKAGED_E2E_WIN_UPDATE_FIXTURE = "tools-serve"'); expectWindowsUpdaterSmokeContract(releaseBetaWorkflow, "beta"); expectWindowsUpdaterSmokeContract(releasePreviewWorkflow, "preview"); expectWindowsUpdaterSmokeContract(releasePrereleaseWorkflow, "prerelease"); expectWindowsUpdaterSmokeContract(releaseStableWorkflow, "stable"); }); it("[P2] prerelease publishes github.commit so its changelog has a baseline", async () => { // The Feishu release card computes its changelog as `git log ..`, // where is read from prerelease/latest/metadata.json's `.github.commit` // (notify-release-feishu.yml). That field is written by githubInfo() from the // RELEASE_COMMIT env. release-beta sets RELEASE_COMMIT on every build + publish job, // but release-prerelease historically set it on none, so every prerelease published an // empty github.commit and the card could never diff against the prior prerelease // ("首个 Prerelease 包"). Require RELEASE_COMMIT on the prerelease build + publish jobs // so the per-platform manifests and the final metadata.json carry the built commit and // stay mutually consistent (publish-metadata rejects a manifest whose commit disagrees). const [prereleaseWorkflow, betaWorkflow] = await Promise.all([ readFile(releasePrereleaseWorkflowPath, "utf8"), readFile(releaseBetaWorkflowPath, "utf8"), ]); const releaseCommitEnv = "RELEASE_COMMIT: ${{ needs.metadata.outputs.commit }}"; // Beta is the working reference that already populates the commit. expect(betaWorkflow).toContain(releaseCommitEnv); const jobBounds: Array<[string, string]> = [ [" build_mac:", " build_mac_intel:"], [" build_mac_intel:", " build_win:"], [" build_win:", " build_linux:"], [" build_linux:", " publish:"], [" publish:", " cleanup_partial_release_assets:"], ]; for (const [start, end] of jobBounds) { expect(sectionBetween(prereleaseWorkflow, start, end)).toContain(releaseCommitEnv); } }); it("[P2] keeps counted release workflow calls on a consistent ref and output contract", async () => { const [previewWorkflow, prereleaseWorkflow, previewScript] = await Promise.all([ readFile(releasePreviewWorkflowPath, "utf8"), readFile(releasePrereleaseWorkflowPath, "utf8"), readFile(releasePreviewScriptPath, "utf8"), ]); expectCountedReleaseWorkflowCallContract(previewWorkflow, "preview"); expectCountedReleaseWorkflowCallContract(prereleaseWorkflow, "prerelease"); expect(previewWorkflow).toContain("OPEN_DESIGN_PREVIEW_VERSION: ${{ inputs.release_version }}"); expect(previewWorkflow).toContain("Empty uses preview/vX.Y.Z when present, otherwise apps/packaged/package.json."); expect(previewScript).toContain("function resolvePreviewBaseVersion"); expect(previewScript).toContain('source: "apps/packaged/package.json"'); expect(previewScript).not.toContain("release-preview can only run from preview/vX.Y.Z branches"); expect(prereleaseWorkflow).toContain("OPEN_DESIGN_STABLE_VERSION: ${{ inputs.release_version }}"); expect(prereleaseWorkflow).toContain("Required when ref is not release/vX.Y.Z"); }); it("[P2] requires stable release dispatch to use the release version branch", async () => { const [workflow, script] = await Promise.all([ readFile(releaseStableWorkflowPath, "utf8"), readFile(releaseStableScriptPath, "utf8"), ]); expect(workflow).not.toContain("OPEN_DESIGN_STABLE_VERSION:"); expect(workflow).not.toContain("inputs.release_version"); expect(workflow).toContain("Stable release branch to build, for example release/v0.5.1."); expect(script).toContain("const stableReleaseBranchPattern = /^release\\/v(\\d+\\.\\d+\\.\\d+)$/;"); expect(script).toContain("function resolveStableBaseVersion"); expect(script).toContain("release-stable requires GITHUB_REF_NAME to be release/vX.Y.Z"); expect(script).toContain("function resolvePrereleaseBaseVersion"); expect(script).toContain( '${stableBaseVersion.source ?? "release base"} version ${stableBaseVersion.value} must match apps/packaged/package.json version', ); }); it("[P2] rejects stable release runs without the release version branch", async () => { const output = await runReleaseStableForFailure({ GITHUB_REF_NAME: "main", OPEN_DESIGN_STABLE_VERSION: "", }); expect(output).toContain("release-stable requires GITHUB_REF_NAME to be release/vX.Y.Z; got main"); }); it("[P2] ignores explicit stable version inputs in favor of the release branch gate", async () => { const output = await runReleaseStableForFailure({ GITHUB_REF_NAME: "main", OPEN_DESIGN_STABLE_VERSION: "0.10.1", }); expect(output).toContain("release-stable requires GITHUB_REF_NAME to be release/vX.Y.Z; got main"); }); it("[P2] reads beta metadata.json written with releaseVersion/releaseNumber field names", async () => { // The unified publisher refactor (.github/workflow/scripts/release/storage) // and the in-flight tools-release rewrite stamp beta/latest/metadata.json // with generic releaseVersion/releaseNumber fields instead of the legacy // betaVersion/betaNumber. tools-release's daily-beta reader must accept // those aliases or the scheduled build dies at metadata time. const packagedVersion = JSON.parse( await readFile(join(workspaceRoot, "apps", "packaged", "package.json"), "utf8"), ).version as string; const objects: Record = { "stable/latest/metadata.json": { channel: "stable", stableVersion: "0.0.1" }, "beta/latest/metadata.json": { baseVersion: packagedVersion, channel: "beta", releaseNumber: 4, releaseVersion: `${packagedVersion}-beta.4`, }, }; const fixture = await startStablePrereleaseMetadataServer(objects); const runnerTemp = await mkdtemp(join(tmpdir(), "od-release-beta-reader-")); const outputPath = join(runnerTemp, "outputs.txt"); try { const result = await execFileAsync(process.execPath, ["--experimental-strip-types", releaseBetaScriptPath], { cwd: workspaceRoot, env: { ...process.env, GITHUB_OUTPUT: outputPath, GITHUB_REF_NAME: "main", GITHUB_SHA: "0123456789abcdef0123456789abcdef01234567", NODE_TLS_REJECT_UNAUTHORIZED: "0", OPEN_DESIGN_BETA_METADATA_URL: `${fixture.origin}/beta/latest/metadata.json`, OPEN_DESIGN_STABLE_METADATA_URL: `${fixture.origin}/stable/latest/metadata.json`, }, maxBuffer: 1024 * 1024, }); expect(result.stdout).toContain(`[release-beta] beta version: ${packagedVersion}-beta.5`); const outputs = await readFile(outputPath, "utf8"); expect(outputs).toContain(`beta_version=${packagedVersion}-beta.5`); } finally { await fixture.close(); await rm(runnerTemp, { force: true, recursive: true }); } }); it("[P2] daily beta resolve defaults to main and preserves the ref override", async () => { // Beta is the daily R&D channel and must track the development tip (main). // Selecting the highest-semver release/vX.Y.Z branch stalls the build: once // that branch ships stable, its base version equals the latest stable and // release-beta's strictly-greater-than-stable guard rejects every run until // someone hand-bumps the retired branch. main always leads stable, so it // never hits that trap. // // Scope every assertion to the resolve job so a refactor elsewhere in the // workflow cannot keep this green while changing the build-ref control flow, // and prove both branches of that control flow: the empty-input default // builds main, and the workflow_dispatch override is still propagated. const workflow = await readFile(notifyDailyFeishuWorkflowPath, "utf8"); const resolveJob = sectionBetween(workflow, " resolve:", "\n build:"); // Override path: workflow_dispatch ref is wired in and forwarded verbatim. expect(resolveJob).toContain("OVERRIDE_REF: ${{ inputs.ref }}"); expect(resolveJob).toContain('echo "ref=$OVERRIDE_REF" >> "$GITHUB_OUTPUT"'); // Default path: an empty input builds main, never a release branch. expect(resolveJob).toContain('echo "ref=main" >> "$GITHUB_OUTPUT"'); expect(resolveJob).not.toContain("refs/heads/release/v*"); }); it("[P2] gates the Thursday patch cut on the Tuesday minor being published", async () => { // cut-patch-release is the Tuesday cut-release flow, one weekday later, with a // PATCH bump and a publish guard. Lock the three properties that make it safe: // 1. It fires Thursday and bumps patch (not minor) from the highest release branch. // 2. It only cuts when this line's minor base X.Y.0 is a PUBLISHED stable // GitHub Release (non-draft, non-prerelease) — otherwise it must NOT create // a branch or build; it posts a Feishu notice and stops. // 3. The happy path still cuts from main and pushes with the App token, so the // existing notify-release-feishu push trigger produces the prerelease + card. const [workflow, notice] = await Promise.all([ readFile(cutPatchReleaseWorkflowPath, "utf8"), readFile(feishuNoticeScriptPath, "utf8"), ]); // Thursday cron, and a patch (not minor) bump. const trigger = sectionBetween(workflow, "on:", "\npermissions:"); expect(trigger).toContain("cron: '0 1 * * 4'"); expect(workflow).toContain('V="${major}.${minor}.$((patch+1))"'); expect(workflow).not.toContain("minor+1"); // The guard target (MINOR_BASE) must derive from the FINAL version V, not from // the highest branch — otherwise a manual `version=` on another line is gated // against the wrong minor (e.g. version=0.15.1 while latest is 0.14.0 would // wrongly check open-design-v0.14.0). Assert V's own major/minor drive it. expect(workflow).toContain('vmajor=${V%%.*}; vrest=${V#*.}; vminor=${vrest%%.*}'); expect(workflow).toContain('MINOR_BASE="${vmajor}.${vminor}.0"'); expect(workflow).not.toContain('MINOR_BASE="${major}.${minor}.0"'); // The guard reads the minor base's stable release and requires it published // (neither draft nor prerelease); a missing release falls back to not published. const guard = sectionBetween(workflow, "- name: Check the Tuesday minor is published", "# ---- Skip path"); expect(guard).toContain('gh release view "$MINOR_TAG"'); expect(guard).toContain("--jq '(.isDraft or .isPrerelease) | not'"); expect(guard).toContain("|| published=false"); expect(workflow).toContain('echo "minor_tag=open-design-v$MINOR_BASE"'); // Skip path: no branch, no build — only the Feishu notice runs, gated on !published. const noticeStep = sectionBetween(workflow, "- name: Notify Feishu that the patch cut was skipped", "- name: Stop here when skipping"); expect(noticeStep).toContain("if: steps.guard.outputs.published != 'true'"); expect(noticeStep).toContain("tools/release/src/notifications/feishu-notice.ts"); // Every branch-cutting step must be gated on the guard passing: assert the // guard `if:` is the line immediately after each step name. for (const step of ["Bail out if the branch already exists", "Create branch + bump version + push", "Create backport label"]) { expect(workflow).toContain(`- name: ${step}\n if: steps.guard.outputs.published == 'true'`); } // Happy path keeps cut-release's mechanics: cut from main, App-token push. expect(workflow).toContain("ref: main"); expect(workflow).toContain("token: ${{ steps.app.outputs.token }}"); expect(workflow).toContain('git push origin "$BRANCH"'); // The version bump is a no-op whenever main already leads stable by this exact // patch (apps/packaged == $VERSION), so the release commit MUST tolerate an empty // tree — otherwise `git commit` dies on "nothing to commit" and no branch is cut. expect(workflow).toContain('git commit --allow-empty -am "chore(release): v$VERSION"'); // The notice card is a standalone poster with the same signed-webhook contract. expect(notice).toContain("msg_type: \"interactive\""); expect(notice).toContain('required("NOTICE_TITLE")'); expect(notice).toContain('required("NOTICE_BODY")'); }); it("[P2] posts an immediate branch-cut Feishu card naming the backport label on both cut workflows", async () => { // Cutting a release branch triggers a ~20-40 min prerelease build before the // download card lands, so both cut workflows post an eager "branch cut" notice // the moment the branch exists. Each must: reuse feishu-notice.ts, run AFTER the // branch push + label creation, and name the exact backport label so the team // knows which label to apply for backports. const [minorCut, patchCut] = await Promise.all([ readFile(cutReleaseWorkflowPath, "utf8"), readFile(cutPatchReleaseWorkflowPath, "utf8"), ]); for (const [label, workflow, kind] of [ ["cut-release (minor)", minorCut, "大版本 minor"], ["cut-patch-release (patch)", patchCut, "小版本 patch"], ] as const) { const step = sectionBetween(workflow, "- name: Notify Feishu that the branch was cut", "\n run:"); // Same standalone notifier + the shared release webhook/secret. expect(workflow, label).toContain("run: node --experimental-strip-types tools/release/src/notifications/feishu-notice.ts"); expect(step, label).toContain("FEISHU_WEBHOOK: ${{ secrets.FEISHU_RELEASE_WEBHOOK }}"); // Names the version's backport label in the card body. expect(step, label).toContain("backport release/v${{ steps.ver.outputs.version }}"); // Distinguishes major vs minor cut. expect(step, label).toContain(kind); // The eager card must come AFTER the branch is actually cut + pushed. expect(workflow.indexOf("- name: Notify Feishu that the branch was cut"), label) .toBeGreaterThan(workflow.indexOf('git push origin "$BRANCH"')); } // The patch workflow's eager card only fires on the happy (published) path. const patchNotice = sectionBetween(patchCut, "- name: Notify Feishu that the branch was cut", "\n run:"); expect(patchNotice).toContain("if: steps.guard.outputs.published == 'true'"); }); it("[P2] sends the daily landing PR summary to Feishu with staging deployment status", async () => { const [workflow, productionWorkflow, script] = await Promise.all([ readFile(landingPageDailyFeishuWorkflowPath, "utf8"), readFile(landingPageProductionWorkflowPath, "utf8"), readFile(landingPageDailyFeishuScriptPath, "utf8"), ]); const trigger = sectionBetween(workflow, "on:", "\npermissions:"); const productionCheckout = sectionBetween(productionWorkflow, "- name: Checkout", "- name: Setup pnpm"); expect(trigger).toContain('cron: "0 1 * * *"'); expect(trigger).not.toContain("lookback_hours:"); expect(workflow).toContain("actions: read"); expect(workflow).toContain("contents: read"); expect(workflow).toContain("pull-requests: read"); expect(workflow).toContain("github.ref == 'refs/heads/main'"); expect(workflow).toContain("FEISHU_WEBHOOK: ${{ secrets.FEISHU_LANDING_WEBHOOK || secrets.FEISHU_RELEASE_WEBHOOK }}"); expect(workflow).toContain("FEISHU_SIGN_SECRET: ${{ secrets.FEISHU_LANDING_SIGN_SECRET || secrets.FEISHU_RELEASE_SIGN_SECRET }}"); expect(workflow).toContain("node --experimental-strip-types .github/scripts/landing-page-daily-feishu.ts self-check"); expect(workflow).toContain("node --experimental-strip-types .github/scripts/landing-page-daily-feishu.ts"); expect(workflow).toContain("ref: main"); expect(workflow).toContain("fetch-depth: 0"); expect(productionCheckout).toContain("ref: ${{ github.sha }}"); expect(productionCheckout).not.toContain("ref: main"); expect(productionCheckout).toContain("Verify production checkout commit"); expect(productionCheckout).toContain('deployed_sha="$(git rev-parse HEAD)"'); expect(productionCheckout).toContain('$deployed_sha" != "$GITHUB_SHA'); expect(productionCheckout).toContain('main_sha="$(git ls-remote origin refs/heads/main'); expect(productionCheckout).toContain('$GITHUB_SHA" != "$main_sha'); expect(productionCheckout).toContain("refusing production deploy for stale workflow SHA"); expect(script).toContain('const STAGING_URL = "https://staging.open-design.ai"'); expect(script).toContain('const STAGING_WORKFLOW = "landing-page-staging.yml"'); expect(script).toContain('const PRODUCTION_WORKFLOW = "landing-page-production.yml"'); expect(script).toContain("type StagingSnapshot"); expect(script).toContain("createStagingSnapshot"); expect(script).toContain("run_started_at"); expect(script).toContain("run_attempt"); expect(script).toContain("runOperationalTime"); expect(script).toContain("staging: StagingSnapshot"); expect(script).toContain("historical staging success not to count as current staging deployment"); expect(script).toContain("rerun historical staging success to become current staging deployment"); expect(script).toContain("rerun staging header to use the rerun historical deployment"); expect(script).toContain("No successful ${PRODUCTION_WORKFLOW} run found on main"); expect(script).toContain("正式环境基线"); expect(script).toContain("待 QA 验收"); expect(script).toContain("git\", [\"merge-base\", \"--is-ancestor\""); expect(script).toContain("已自动部署到当前 staging.open-design.ai"); expect(script).toContain("正在部署到 staging.open-design.ai"); expect(script).toContain("apps/landing-page/"); expect(script).toContain(".github/workflows/landing-page-staging.yml"); }); it("[P2] supports stable metadata, prepublish, and publish dispatch modes", async () => { const [workflow, script] = await Promise.all([ readFile(releaseStableWorkflowPath, "utf8"), readFile(releaseStableScriptPath, "utf8"), ]); expect(workflow).toContain("dry_run:"); expect(workflow).toContain( "Release mode. metadata stops after promotion metadata; prepublish runs build/smoke/report/plan without publishing; publish performs the stable release.", ); expect(workflow).toContain("group: open-design-release-stable-${{ inputs.dry_run }}"); expect(workflow).toContain("type: choice"); expect(workflow).toContain("- metadata"); expect(workflow).toContain("- prepublish"); expect(workflow).toContain("- publish"); expect(workflow).toContain("default: metadata"); expect(workflow).not.toContain("inputs.channel"); expect(workflow).toContain("OPEN_DESIGN_RELEASE_DRY_RUN: ${{ inputs.dry_run == 'publish' && 'false' || inputs.dry_run }}"); expect(workflow).toContain("RELEASE_PUBLIC_ORIGIN: ${{ vars.CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN }}"); expect(workflow).toContain("run: bash .github/scripts/release/github/stable-notes.sh"); expect(workflow).toContain("dry_run: ${{ steps.stable.outputs.dry_run }}"); expect(workflow).toContain("dry_run_mode: ${{ steps.stable.outputs.dry_run_mode }}"); expect(workflow).toContain("if: ${{ needs.metadata.outputs.run_prepublish_jobs == 'true' }}"); expect(workflow).toContain("RELEASE_PUBLISH_SIDE_EFFECTS: ${{ needs.metadata.outputs.publish_side_effects_enabled }}"); expect(script).toContain("function parseStableDryRunMode"); expect(script).toContain("OPEN_DESIGN_RELEASE_DRY_RUN must be metadata, prepublish, true, or false"); expect(script).toContain('setOutput("dry_run", dryRun ? "true" : "false");'); expect(script).toContain('setOutput("dry_run_mode", stableDryRunMode);'); expect(script).toContain('setOutput("run_prepublish_jobs", runPrepublishJobs ? "true" : "false");'); expect(script).toContain('setOutput("publish_side_effects_enabled", publishSideEffectsEnabled ? "true" : "false");'); }); it("[P2] writes stable release notes from the release public origin variable", async () => { for (const [envName, origin] of [ ["RELEASE_PUBLIC_ORIGIN", "https://releases.open-design.ai/current/"], ["CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN", "https://releases.open-design.ai/legacy/"], ] as const) { const runnerTemp = await mkdtemp(join(tmpdir(), "od-stable-notes-")); const outputPath = join(runnerTemp, "github-output.txt"); try { await execFileAsync("bash", [releaseStableNotesScriptPath], { cwd: workspaceRoot, env: { ...process.env, BRANCH_NAME: "release/v0.13.0", CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN: envName === "CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN" ? origin : "", GITHUB_OUTPUT: outputPath, GITHUB_REPOSITORY: "nexu-io/open-design", GITHUB_SHA: "0123456789abcdef0123456789abcdef01234567", RELEASE_CHANNEL: "stable", RELEASE_PUBLIC_ORIGIN: envName === "RELEASE_PUBLIC_ORIGIN" ? origin : "", RELEASE_SIGNED: "true", RELEASE_VERSION: "0.13.0", RUNNER_TEMP: runnerTemp, VERSION_TAG: "open-design-v0.13.0", }, }); const outputs = parseGithubOutput(await readFile(outputPath, "utf8")); const notes = await readFile(outputs.notes_file ?? "", "utf8"); expect(notes).toContain(`R2 metadata: ${origin.replace(/\/+$/, "")}/stable/latest/metadata.json`); expect(notes).toContain(`E2E report: ${origin.replace(/\/+$/, "")}/stable/versions/0.13.0/report.zip`); } finally { await rm(runnerTemp, { force: true, recursive: true }); } } }); it("[P2] validates stable dry-run prerelease metadata from a release branch", async () => { const baseVersion = await readPackagedVersion(); const prereleaseVersion = `${baseVersion}-prerelease.12`; const objects: Record = {}; const fixture = await startStablePrereleaseMetadataServer(objects); objects[`prerelease/versions/${prereleaseVersion}/metadata.json`] = stablePrereleaseMetadataFixture( baseVersion, prereleaseVersion, fixture.origin, ); const runnerTemp = await mkdtemp(join(tmpdir(), "od-release-stable-dry-run-")); try { await mkdir(join(runnerTemp, "bin"), { recursive: true }); await writeFakeGhBin(join(runnerTemp, "bin"), []); const fakePath = `${join(runnerTemp, "bin")}${delimiter}${process.env.PATH ?? ""}`; const result = await execFileAsync(process.execPath, ["--experimental-strip-types", releaseStableScriptPath], { cwd: workspaceRoot, env: { ...process.env, GITHUB_REF_NAME: `release/v${baseVersion}`, GITHUB_REPOSITORY: "nexu-io/open-design", GITHUB_SHA: "0123456789abcdef0123456789abcdef01234567", NODE_TLS_REJECT_UNAUTHORIZED: "0", OPEN_DESIGN_RELEASE_CHANNEL: "stable", OPEN_DESIGN_RELEASE_DRY_RUN: "true", OPEN_DESIGN_RELEASES_PUBLIC_ORIGIN: fixture.origin, OPEN_DESIGN_GH_NODE_SCRIPT: join(runnerTemp, "bin", "gh"), OPEN_DESIGN_STABLE_PRERELEASE_VERSION: prereleaseVersion, Path: fakePath, PATH: fakePath, }, }); expect(result.stdout).toContain(`[release-stable] validated prerelease: ${prereleaseVersion}`); expect(result.stdout).toContain("[release-stable] channel: stable"); expect(result.stdout).toContain("[release-stable] dry run: true"); expect(result.stdout).toContain(`[release-stable] version tag: open-design-v${baseVersion}`); } finally { await fixture.close(); await rm(runnerTemp, { force: true, recursive: true }); } }); it("[P2] rejects invalid release dry-run values before remote checks", async () => { const output = await runReleaseStableForFailure({ GITHUB_REF_NAME: "release/v0.10.0", OPEN_DESIGN_RELEASE_DRY_RUN: "maybe", OPEN_DESIGN_STABLE_VERSION: "", }); expect(output).toContain("OPEN_DESIGN_RELEASE_DRY_RUN must be metadata, prepublish, true, or false"); }); it("keeps both beta release lanes on the shared payload-aware metadata surface", async () => { const [releaseBetaWorkflow, releaseBetaSelfHostedWorkflow, platformPublishScript, publishMetadataScript] = await Promise.all([ readFile(releaseBetaWorkflowPath, "utf8"), readFile(releaseBetaSelfHostedWorkflowPath, "utf8"), readFile(releaseBetaPlatformPublishScriptPath, "utf8"), readFile(releasePublishMetadataScriptPath, "utf8"), ]); for (const workflow of [releaseBetaWorkflow, releaseBetaSelfHostedWorkflow]) { expect(workflow).toContain("RELEASE_ARTIFACT_MODE: dmg-and-payload"); expect(workflow).toContain("tools-release publish-platform"); expect(workflow).toContain("tools-release publish-metadata"); expect(workflow).toContain("RELEASE_MANIFEST_DIR:"); } expect(releaseBetaWorkflow).toContain("RELEASE_ASSET_SUFFIX: ${{ needs.metadata.outputs.asset_version_suffix }}"); expect(releaseBetaSelfHostedWorkflow).toContain("RELEASE_ASSET_SUFFIX: auto"); expect(platformPublishScript).toContain("artifacts.payload"); expect(platformPublishScript).toContain("open-design-${releaseVersion}${assetSuffix}-mac-${arch}-payload.zip"); expect(platformPublishScript).toContain("open-design-${releaseVersion}${assetSuffix}-win-x64-payload.7z"); expect(publishMetadataScript).toContain("for (const [artifactName, artifact] of Object.entries(manifest.artifacts ?? {}))"); expect(publishMetadataScript).toContain("outputs[`${target}_${artifactName}_url`] = artifact.url"); }); it("publishes release-betas mac_x64 payloads while preserving the zip feed", async () => { const workflow = await readFile(releaseBetaWorkflowPath, "utf8"); const macX64Job = sectionBetween(workflow, " build_mac_x64:", " build_win_x64:"); const prepareStep = sectionBetween(macX64Job, " - name: Prepare mac_x64 assets", " - name: Publish mac_x64 platform"); const publishStep = sectionBetween(macX64Job, " - name: Publish mac_x64 platform", " - name: Upload mac_x64 publish manifest"); const artifactMode = "RELEASE_ARTIFACT_MODE: ${{ inputs.mac_x64_target == 'all' && 'all' || 'dmg-and-payload' }}"; expect(prepareStep).toContain(artifactMode); expect(publishStep).toContain(artifactMode); }); it("keeps the self-hosted beta lane metadata-driven with reusable platform publish scripts", async () => { const [workflow, posixBuildScript, windowsBuildScript, platformPublishScript, publishMetadataScript] = await Promise.all([ readFile(releaseBetaSelfHostedWorkflowPath, "utf8"), readFile(releaseBetaPosixBuildScriptPath, "utf8"), readFile(releaseBetaWindowsBuildScriptPath, "utf8"), readFile(releaseBetaPlatformPublishScriptPath, "utf8"), readFile(releasePublishMetadataScriptPath, "utf8"), ]); expect(workflow).toContain("enable_win_x64:"); expect(workflow).toContain("enable_mac_arm64:"); expect(workflow).toContain("enable_mac_x64:"); expect(workflow).toContain("enable_linux_x64:"); expect(workflow).toMatch(/enable_win_x64:[\s\S]*?default: true/); expect(workflow).toMatch(/enable_mac_arm64:[\s\S]*?default: true/); expect(workflow).toMatch(/publish:[\s\S]*?default: true/); expect(workflow).toMatch(/release_public_origin:[\s\S]*?default: "https:\/\/s3\.nexu\.space\/od-releases"/); expect(workflow).toContain("win_x64_smoke_mode:"); expect(workflow).toContain("win_x64_target:"); expect(workflow).toContain("win_x64_update_metadata_url:"); expect(workflow).toContain("win_x64_update_target_version:"); expect(workflow).toContain("mac_arm64_sign_mode:"); expect(workflow).toContain("mac_arm64_smoke_mode:"); expect(workflow).toMatch(/win_x64_smoke_mode:[\s\S]*?options:[\s\S]*?- skip[\s\S]*?- core[\s\S]*?- full[\s\S]*?default: core/); expect(workflow).toMatch(/mac_arm64_smoke_mode:[\s\S]*?options:[\s\S]*?- skip[\s\S]*?- core[\s\S]*?- full[\s\S]*?default: core/); expect(workflow).toMatch(/win_x64_sign_mode:[\s\S]*?options:[\s\S]*?- "off"[\s\S]*?- "on"[\s\S]*?default: "off"/); expect(workflow).toMatch(/mac_arm64_sign_mode:[\s\S]*?options:[\s\S]*?- "no"[\s\S]*?- "sign-only"[\s\S]*?- "notarize"[\s\S]*?default: "sign-only"/); expect(workflow).not.toContain("win_enable:"); expect(workflow).not.toContain("mac_enable:"); expect(workflow).not.toMatch(/^ enable_win:/m); expect(workflow).not.toMatch(/^ enable_mac:/m); expect(workflow).not.toMatch(/^ sign_mode:/m); expect(workflow).not.toMatch(/^ smoke_mode:/m); expect(workflow).not.toMatch(/^ update_metadata_url:/m); expect(workflow).not.toMatch(/^ update_target_version:/m); expect(workflow).toContain("name: Prepare betas metadata"); expect(workflow).toContain("OPEN_DESIGN_BETAS_METADATA_URL: ${{ inputs.release_public_origin }}/betas/latest/metadata.json"); expect(workflow).toContain("OPEN_DESIGN_STABLE_METADATA_URL: https://releases.open-design.ai/stable/latest/metadata.json"); expect(workflow).toContain('repo_dir="$PWD/_release-metadata"'); expect(workflow).toContain("--filter=blob:none --depth=1"); expect(workflow).toContain("for attempt in 1 2 3"); expect(workflow).toContain("working-directory: _release-metadata"); expect(workflow).toContain("Install metadata toolchain"); expect(workflow).toContain("pnpm install --frozen-lockfile --prefer-offline"); expect(workflow).toContain("tools-release prepare betas"); expect(workflow).not.toContain('git fetch --force --depth=1 origin "+refs/tags/open-design-v*:refs/tags/open-design-v*"'); expect(workflow).toContain("release-beta-s requires at least one target to be enabled"); expect(workflow).toContain("release_version: ${{ inputs.publish && steps.reserve.outputs.release_version || inputs.release_version != '' && inputs.release_version || steps.betas.outputs.release_version }}"); expect(workflow).toContain("if: ${{ inputs.publish }}"); expect(workflow).toContain("Reject unsupported self-hosted mac_x64"); expect(workflow).toContain("Reject unsupported self-hosted linux_x64"); expect(workflow).toContain("name: Probe Windows signing capability"); expect(workflow).toContain("probe-win-signing.ps1"); expect(workflow).toContain("needs: metadata"); expect(workflow).toContain('-ReleaseTarget win_x64'); expect(workflow).toContain('-ReleaseVersion "${{ needs.metadata.outputs.release_version }}"'); expect(workflow).toContain('OD_BETA_WINDOWS_SIGNING_ENABLED: ${{ steps.sign_probe.outputs.enabled }}'); expect(workflow).toContain('OD_BETA_WINDOWS_SIGNING_PROBED: ${{ steps.sign_probe.outputs.probed }}'); expect(workflow).toContain('OD_BETA_WINDOWS_SIGNTOOL_PATH: ${{ steps.sign_probe.outputs.signtool_path }}'); expect(workflow).toContain("OD_PACKAGED_E2E_WIN_UPDATE_METADATA_URL: ${{ inputs.win_x64_update_metadata_url }}"); expect(workflow).toContain("OD_PACKAGED_E2E_WIN_UPDATE_VERSION: ${{ inputs.win_x64_update_target_version }}"); expect(windowsBuildScript).toContain('"pnpm.cmd", "exec", "tools-pack", "win", "build"'); expect(windowsBuildScript).toContain('if ($SmokeMode -eq "full" -and -not $hasExternalUpdateMetadata -and -not $hasExternalUpdateArtifactPair)'); expect(windowsBuildScript).not.toContain("fnm"); expect(windowsBuildScript).not.toContain("RUNNER_TEMP"); expect(windowsBuildScript).not.toContain("GITHUB_OUTPUT"); expect(windowsBuildScript).not.toContain("GITHUB_STEP_SUMMARY"); expect(posixBuildScript).toContain("RELEASE_TARGET"); expect(posixBuildScript).toContain("REQUIRE_VELA_CLI"); expect(posixBuildScript).toContain('--cache-dir "$TOOLS_PACK_CACHE_DIR"'); expect(posixBuildScript).not.toContain("OPEN_DESIGN_RELEASE_PROFILE"); expect(posixBuildScript).not.toContain("corepack prepare"); expect(posixBuildScript).not.toContain("RUNNER_TEMP"); expect(workflow).toContain("Publish win_x64 platform"); expect(workflow).toContain("tools-release publish-platform"); expect(workflow).toContain("Write win_x64 release report"); expect(workflow).toContain("RELEASE_REPORT_DIR: C:\\.tmp\\runner\\od-beta\\win_x64\\release-report\\win_x64"); expect(posixBuildScript).toContain('OD_PACKAGED_E2E_MAC_SMOKE_PROFILE="$RELEASE_SMOKE_MODE"'); expect(workflow).toContain("runs-on: [self-hosted, macOS, ARM64, nexu-mac, release-beta]"); expect(workflow).toContain("path: _release-build"); expect(workflow).toContain("working-directory: _release-build"); expect(workflow).toContain("fnm exec --using=24 -- bash tools/release/scripts/build-platform.sh"); expect(workflow).toContain("MAC_TOOLS_PACK_CACHE_DIR: /Users/runner/.tmp/runner/od-beta/mac_arm64/tools-pack-cache"); expect(workflow).toContain("MAC_TOOLS_PACK_DIR: /Users/runner/.tmp/runner/od-beta/mac_arm64/tools-pack"); expect(workflow).toContain("TOOLS_PACK_CACHE_DIR: ${{ env.MAC_TOOLS_PACK_CACHE_DIR }}"); expect(workflow).toContain("TOOLS_PACK_DIR: ${{ env.MAC_TOOLS_PACK_DIR }}"); expect(workflow).toContain("Write mac_arm64 release report"); expect(workflow).toContain("fnm exec --using=24 -- pnpm exec tools-release write-report"); expect(workflow).toContain("fnm.exe\" exec --using=24 -- pnpm.cmd exec tools-release write-report"); expect(workflow).toContain("fnm.exe\" exec --using=24 -- pnpm.cmd exec tools-release publish-platform"); expect(workflow).toContain("Prepare mac_arm64 assets"); expect(workflow).toContain("RELEASE_TARGET: mac_arm64"); expect(workflow).toContain("RELEASE_SIGNED: ${{ (inputs.mac_arm64_delivery_mode == 'internal-updater' || inputs.mac_arm64_sign_mode != 'no') && 'true' || 'false' }}"); expect(workflow).toContain("RELEASE_REPORT_ZIP_PATH: ${{ runner.temp }}/release-report/mac_arm64-report.zip"); expect(workflow).toContain("name: Publish betas metadata to Nexu S3"); expect(workflow).toContain("Upload mac_arm64 publish manifest fallback"); expect(workflow).toContain("Upload win_x64 publish manifest fallback"); expect(workflow).toContain("Download mac_arm64 publish manifest fallback"); expect(workflow).toContain("Download win_x64 publish manifest fallback"); expect(workflow).toContain("continue-on-error: true"); expect(workflow).toContain("Download mac_arm64 platform manifest"); expect(workflow).toContain("Download win_x64 platform manifest"); expect(workflow).not.toContain('manifest_url="${RELEASE_PUBLIC_ORIGIN%/}/betas/versions/${RELEASE_VERSION}${RELEASE_ASSET_SUFFIX}/platforms/${RELEASE_TARGET}.json"'); expect(workflow).not.toContain('curl -fsSL "$manifest_url" -o "$RELEASE_MANIFEST_DIR/$RELEASE_TARGET.json"'); expect(workflow).not.toContain('fallback_manifest="$RELEASE_FALLBACK_MANIFEST_DIR/$RELEASE_TARGET.json"'); expect(workflow).toContain("tools-release download-platform-manifest"); expect(workflow).toContain("RELEASE_STORAGE_ENDPOINT: ${{ secrets.NEXU_S3_ENDPOINT }}"); expect(workflow).toContain("tools-release publish-metadata"); expect(workflow).toContain("RELEASE_ASSET_SUFFIX: auto"); expect(workflow).toContain("RELEASE_MANIFEST_DIR: ${{ runner.temp }}/release-platform-manifests"); expect(workflow).toContain("-IncludeZip $${{ inputs.win_x64_target == 'all' || inputs.win_x64_target == 'zip' }}"); expect(workflow).toContain("release-beta-s publish requires win_x64_target=nsis or all"); expect(workflow).toContain("open-design-betas-win-x64-publish-manifest"); expect(workflow).toContain("open-design-betas-mac-arm64-publish-manifest"); expect(workflow).toContain('STATE_SOURCE: ${{ needs.metadata.outputs.state_source }}'); expect(workflow).not.toContain("Verify betas metadata"); expect(workflow).not.toContain("tools-release verify-metadata"); expect(workflow).not.toContain("tools-release summary-metadata"); expect(workflow).toContain("release-beta-s publishes to an internal S3 namespace; public metadata fetch verification is intentionally skipped."); expect(publishMetadataScript).toContain("validateManifest"); expect(publishMetadataScript).toContain("manifest.releaseVersion !== releaseVersion"); expect(publishMetadataScript).toContain("manifest.github?.runId !== currentRunId"); expect(publishMetadataScript).not.toContain("manifest.github?.runAttempt !== currentRunAttempt"); expect(publishMetadataScript).toContain("manifest.github?.commit !== currentCommit"); expect(publishMetadataScript).toContain("manifest.platformKey !== target"); expect(publishMetadataScript).toContain("manifest.r2.versionPrefix.includes(`/versions/${releaseVersion}`)"); expect(publishMetadataScript).toContain('if (assetVersionSuffix === "auto")'); expect(publishMetadataScript).toContain('assetVersionSuffix = allReadyTargetsSigned ? ".signed" : ".unsigned";'); expect(publishMetadataScript).toContain("const feedVersionPrefix = manifest.r2?.versionPrefix;"); expect(publishMetadataScript).toContain("refusing stale ${def.target} platform manifest"); expect(publishMetadataScript).toContain("publishLatestPlatformObjects"); expect(platformPublishScript).not.toContain("await upload(join(releaseAssetsDir, name), `${latestPrefix}/${name}`"); expect(platformPublishScript).not.toContain("await upload(manifestPath, `${latestPrefix}/platforms/${target}.json`"); expect(platformPublishScript).toContain('const target = requiredTarget();'); expect(platformPublishScript).toContain("legacyPlatformKey"); expect(workflow).not.toContain("win_enable:"); expect(workflow).not.toContain("mac_enable:"); expect(workflow).not.toContain(".github/scripts/release/build-mac.sh"); expect(workflow).not.toContain(".github/scripts/release/r2/publish-platform.ts"); expect(workflow).not.toContain("publish-beta-metadata.ps1"); expect(workflow).not.toContain("probe-beta-public-read.ps1"); expect(workflow).not.toContain("publish-beta.ps1 -IndexPath"); }); it("rejects stale latest platform manifests from a previous beta version", async () => { const fixture = await startReleaseMetadataObjectStore({}); const runnerTemp = await mkdtemp(join(tmpdir(), "od-release-betas-metadata-")); const platformManifestRoot = join(runnerTemp, "release-platform-manifests"); try { await mkdir(platformManifestRoot, { recursive: true }); await writeFile( join(platformManifestRoot, "mac_arm64.json"), `${JSON.stringify( { artifacts: { dmg: { url: "https://releases.open-design.ai/betas/versions/1.2.3-beta.3.unsigned/Open Design Beta.dmg", }, }, channel: "beta", github: { commit: "current-sha", runAttempt: 2, runId: 222222222, }, legacyPlatformKey: "mac", platformKey: "mac_arm64", releaseTarget: "mac_arm64", r2: { versionPrefix: "beta/versions/1.2.3-beta.3.unsigned", }, releaseVersion: "1.2.3-beta.3", signed: false, status: "published", }, null, 2, )}\n`, ); const result = await execFileAsync( process.execPath, ["--experimental-strip-types", releasePublishMetadataScriptPath], { cwd: workspaceRoot, env: { ...process.env, BASE_VERSION: "1.2.3", ENABLE_LINUX_X64: "false", ENABLE_MAC_ARM64: "true", ENABLE_MAC_X64: "false", ENABLE_WIN_X64: "false", RELEASE_RUN_ATTEMPT: "2", RELEASE_RUN_ID: "222222222", RELEASE_COMMIT: "current-sha", MAC_ARM64_RESULT: "success", RELEASE_CHANNEL: "beta", RELEASE_MANIFEST_DIR: platformManifestRoot, RELEASE_METADATA_DIR: join(runnerTemp, "release-metadata"), RELEASE_OUTPUTS_PATH: join(runnerTemp, "release-metadata", "outputs.json"), RELEASE_PUBLIC_ORIGIN: "https://releases.open-design.ai", RELEASE_SIGNED: "false", RELEASE_STORAGE_ACCESS_KEY_ID: "test-access-key", RELEASE_STORAGE_BUCKET: fixture.bucket, RELEASE_STORAGE_ENDPOINT: fixture.endpointUrl, RELEASE_STORAGE_REGION: "auto", RELEASE_STORAGE_SECRET_ACCESS_KEY: "test-secret-key", RELEASE_VERSION: "1.2.3-beta.4", STATE_SOURCE: "test", }, maxBuffer: 1024 * 1024, }, ).then( (value) => ({ status: "fulfilled" as const, value }), (reason: unknown) => ({ reason, status: "rejected" as const }), ); expect(result.status).toBe("rejected"); expect(String(result.status === "rejected" ? result.reason : "")).toContain( "refusing stale mac_arm64 platform manifest for 1.2.3-beta.4: releaseVersion=1.2.3-beta.3", ); expect(fixture.uploadedObjectKeys()).toEqual([]); } finally { await fixture.close(); await rm(runnerTemp, { force: true, recursive: true }); } }); it("rejects stale latest platform manifests from a previous same-version beta workflow run", async () => { const fixture = await startReleaseMetadataObjectStore({}); const runnerTemp = await mkdtemp(join(tmpdir(), "od-release-betas-metadata-")); const platformManifestRoot = join(runnerTemp, "release-platform-manifests"); try { await mkdir(platformManifestRoot, { recursive: true }); await writeFile( join(platformManifestRoot, "mac_arm64.json"), `${JSON.stringify( { artifacts: { dmg: { url: "https://releases.open-design.ai/betas/versions/1.2.3-beta.4.unsigned/Open Design Beta.dmg", }, }, channel: "beta", github: { commit: "previous-sha", runAttempt: 1, runId: 111111111, }, legacyPlatformKey: "mac", platformKey: "mac_arm64", releaseTarget: "mac_arm64", r2: { versionPrefix: "beta/versions/1.2.3-beta.4.unsigned", }, releaseVersion: "1.2.3-beta.4", signed: false, status: "published", }, null, 2, )}\n`, ); const result = await execFileAsync( process.execPath, ["--experimental-strip-types", releasePublishMetadataScriptPath], { cwd: workspaceRoot, env: { ...process.env, BASE_VERSION: "1.2.3", ENABLE_LINUX_X64: "false", ENABLE_MAC_ARM64: "true", ENABLE_MAC_X64: "false", ENABLE_WIN_X64: "false", RELEASE_RUN_ATTEMPT: "2", RELEASE_RUN_ID: "222222222", RELEASE_COMMIT: "current-sha", MAC_ARM64_RESULT: "success", RELEASE_CHANNEL: "beta", RELEASE_MANIFEST_DIR: platformManifestRoot, RELEASE_METADATA_DIR: join(runnerTemp, "release-metadata"), RELEASE_OUTPUTS_PATH: join(runnerTemp, "release-metadata", "outputs.json"), RELEASE_PUBLIC_ORIGIN: "https://releases.open-design.ai", RELEASE_SIGNED: "false", RELEASE_STORAGE_ACCESS_KEY_ID: "test-access-key", RELEASE_STORAGE_BUCKET: fixture.bucket, RELEASE_STORAGE_ENDPOINT: fixture.endpointUrl, RELEASE_STORAGE_REGION: "auto", RELEASE_STORAGE_SECRET_ACCESS_KEY: "test-secret-key", RELEASE_VERSION: "1.2.3-beta.4", STATE_SOURCE: "test", }, maxBuffer: 1024 * 1024, }, ).then( (value) => ({ status: "fulfilled" as const, value }), (reason: unknown) => ({ reason, status: "rejected" as const }), ); expect(result.status).toBe("rejected"); expect(String(result.status === "rejected" ? result.reason : "")).toContain( "refusing stale mac_arm64 platform manifest for 1.2.3-beta.4: github.runId=111111111", ); expect(fixture.uploadedObjectKeys()).toEqual([]); } finally { await fixture.close(); await rm(runnerTemp, { force: true, recursive: true }); } }); it("accepts same-run latest platform manifests from an older workflow attempt", async () => { const fixture = await startReleaseMetadataObjectStore({}); const runnerTemp = await mkdtemp(join(tmpdir(), "od-release-betas-metadata-")); const platformManifestRoot = join(runnerTemp, "release-platform-manifests"); try { await mkdir(platformManifestRoot, { recursive: true }); await writeFile( join(platformManifestRoot, "mac_arm64.json"), `${JSON.stringify( { artifacts: { dmg: { url: "https://releases.open-design.ai/betas/versions/1.2.3-beta.4.unsigned/Open Design Beta.dmg", }, }, channel: "beta", github: { commit: "current-sha", runAttempt: 1, runId: 222222222, }, legacyPlatformKey: "mac", platformKey: "mac_arm64", releaseTarget: "mac_arm64", r2: { versionPrefix: "beta/versions/1.2.3-beta.4.unsigned", }, releaseVersion: "1.2.3-beta.4", signed: false, status: "published", }, null, 2, )}\n`, ); await execFileAsync(process.execPath, ["--experimental-strip-types", releasePublishMetadataScriptPath], { cwd: workspaceRoot, env: { ...process.env, BASE_VERSION: "1.2.3", ENABLE_LINUX_X64: "false", ENABLE_MAC_ARM64: "true", ENABLE_MAC_X64: "false", ENABLE_WIN_X64: "false", RELEASE_RUN_ATTEMPT: "2", RELEASE_RUN_ID: "222222222", RELEASE_COMMIT: "current-sha", MAC_ARM64_RESULT: "success", RELEASE_CHANNEL: "beta", RELEASE_MANIFEST_DIR: platformManifestRoot, RELEASE_METADATA_DIR: join(runnerTemp, "release-metadata"), RELEASE_OUTPUTS_PATH: join(runnerTemp, "release-metadata", "outputs.json"), RELEASE_PUBLIC_ORIGIN: "https://releases.open-design.ai", RELEASE_SIGNED: "false", RELEASE_STORAGE_ACCESS_KEY_ID: "test-access-key", RELEASE_STORAGE_BUCKET: fixture.bucket, RELEASE_STORAGE_ENDPOINT: fixture.endpointUrl, RELEASE_STORAGE_REGION: "auto", RELEASE_STORAGE_SECRET_ACCESS_KEY: "test-secret-key", RELEASE_VERSION: "1.2.3-beta.4", STATE_SOURCE: "test", }, maxBuffer: 1024 * 1024, }); expect(fixture.uploadedObjectKeys()).toEqual([ "beta/versions/1.2.3-beta.4/metadata.json", "beta/latest/metadata.json", "beta/latest/platforms/mac_arm64.json", ]); } finally { await fixture.close(); await rm(runnerTemp, { force: true, recursive: true }); } }); it("resolves auto asset suffix from target-first win_x64 platform manifests in beta metadata publish", async () => { const fixture = await startReleaseMetadataObjectStore({ "beta/versions/1.2.3-beta.4.unsigned/latest.yml": "versioned updater feed", }); const runnerTemp = await mkdtemp(join(tmpdir(), "od-release-betas-win-metadata-")); const platformManifestRoot = join(runnerTemp, "release-platform-manifests"); try { await mkdir(platformManifestRoot, { recursive: true }); await writeFile( join(platformManifestRoot, "win_x64.json"), `${JSON.stringify( { artifacts: { installer: { url: "https://releases.open-design.ai/betas/versions/1.2.3-beta.4.unsigned/open-design-1.2.3-beta.4.unsigned-win-x64-setup.exe", }, }, channel: "beta", github: { commit: "current-sha", runAttempt: 2, runId: 222222222, }, legacyPlatformKey: "win", feed: { name: "latest.yml", url: "https://releases.open-design.ai/betas/versions/1.2.3-beta.4.unsigned/latest.yml", }, platform: "win", platformKey: "win_x64", releaseTarget: "win_x64", releaseVersion: "1.2.3-beta.4", r2: { versionPrefix: "beta/versions/1.2.3-beta.4.unsigned", }, signed: false, status: "published", }, null, 2, )}\n`, ); await execFileAsync(process.execPath, ["--experimental-strip-types", releasePublishMetadataScriptPath], { cwd: workspaceRoot, env: { ...process.env, BASE_VERSION: "1.2.3", ENABLE_LINUX_X64: "false", ENABLE_MAC_ARM64: "false", ENABLE_MAC_X64: "false", ENABLE_WIN_X64: "true", RELEASE_RUN_ATTEMPT: "2", RELEASE_RUN_ID: "222222222", RELEASE_COMMIT: "current-sha", RELEASE_ASSET_SUFFIX: "auto", RELEASE_CHANNEL: "beta", RELEASE_MANIFEST_DIR: platformManifestRoot, RELEASE_METADATA_DIR: join(runnerTemp, "release-metadata"), RELEASE_OUTPUTS_PATH: join(runnerTemp, "release-metadata", "outputs.json"), RELEASE_PUBLIC_ORIGIN: "https://releases.open-design.ai", RELEASE_SIGNED: "false", RELEASE_STORAGE_ACCESS_KEY_ID: "test-access-key", RELEASE_STORAGE_BUCKET: fixture.bucket, RELEASE_STORAGE_ENDPOINT: fixture.endpointUrl, RELEASE_STORAGE_REGION: "auto", RELEASE_STORAGE_SECRET_ACCESS_KEY: "test-secret-key", RELEASE_VERSION: "1.2.3-beta.4", STATE_SOURCE: "test", WIN_X64_RESULT: "success", }, maxBuffer: 1024 * 1024, }); const metadata = JSON.parse(await readFile(join(runnerTemp, "release-metadata", "metadata.json"), "utf8")); expect(metadata.assetVersionSuffix).toBe(".unsigned"); expect(metadata.readyTargets).toEqual(["win_x64"]); expect(metadata.platforms.win.r2.versionPrefix).toBe("beta/versions/1.2.3-beta.4.unsigned"); expect(metadata.releaseTargets.win_x64.r2.versionPrefix).toBe("beta/versions/1.2.3-beta.4.unsigned"); expect(fixture.uploadedObjectKeys()).toEqual([ "beta/versions/1.2.3-beta.4.unsigned/metadata.json", "beta/latest/metadata.json", "beta/latest/platforms/win_x64.json", "beta/latest/latest.yml", ]); } finally { await fixture.close(); await rm(runnerTemp, { force: true, recursive: true }); } }); it("preserves launcher payload artifacts in beta latest metadata and action outputs", async () => { const fixture = await startReleaseMetadataObjectStore({ "beta/versions/1.2.3-beta.4.unsigned/latest.yml": "versioned updater feed", }); const runnerTemp = await mkdtemp(join(tmpdir(), "od-release-betas-payload-metadata-")); const platformManifestRoot = join(runnerTemp, "release-platform-manifests"); try { await mkdir(platformManifestRoot, { recursive: true }); await writeFile( join(platformManifestRoot, "mac_arm64.json"), `${JSON.stringify( { artifacts: { dmg: { url: "https://releases.open-design.ai/betas/versions/1.2.3-beta.4.unsigned/open-design-1.2.3-beta.4.unsigned-mac-arm64.dmg", }, payload: { sha256Url: "https://releases.open-design.ai/betas/versions/1.2.3-beta.4.unsigned/open-design-1.2.3-beta.4.unsigned-mac-arm64-payload.zip.sha256", url: "https://releases.open-design.ai/betas/versions/1.2.3-beta.4.unsigned/open-design-1.2.3-beta.4.unsigned-mac-arm64-payload.zip", }, }, channel: "beta", github: { commit: "current-sha", runAttempt: 2, runId: 222222222, }, legacyPlatformKey: "mac", platform: "mac", platformKey: "mac_arm64", releaseTarget: "mac_arm64", releaseVersion: "1.2.3-beta.4", r2: { versionPrefix: "beta/versions/1.2.3-beta.4.unsigned", }, signed: false, status: "published", }, null, 2, )}\n`, ); await writeFile( join(platformManifestRoot, "win_x64.json"), `${JSON.stringify( { artifacts: { installer: { url: "https://releases.open-design.ai/betas/versions/1.2.3-beta.4.unsigned/open-design-1.2.3-beta.4.unsigned-win-x64-setup.exe", }, payload: { sha256Url: "https://releases.open-design.ai/betas/versions/1.2.3-beta.4.unsigned/open-design-1.2.3-beta.4.unsigned-win-x64-payload.7z.sha256", url: "https://releases.open-design.ai/betas/versions/1.2.3-beta.4.unsigned/open-design-1.2.3-beta.4.unsigned-win-x64-payload.7z", }, }, channel: "beta", feed: { name: "latest.yml", url: "https://releases.open-design.ai/betas/versions/1.2.3-beta.4.unsigned/latest.yml", }, github: { commit: "current-sha", runAttempt: 2, runId: 222222222, }, legacyPlatformKey: "win", platform: "win", platformKey: "win_x64", releaseTarget: "win_x64", releaseVersion: "1.2.3-beta.4", r2: { versionPrefix: "beta/versions/1.2.3-beta.4.unsigned", }, signed: false, status: "published", }, null, 2, )}\n`, ); await execFileAsync(process.execPath, ["--experimental-strip-types", releasePublishMetadataScriptPath], { cwd: workspaceRoot, env: { ...process.env, BASE_VERSION: "1.2.3", ENABLE_LINUX_X64: "false", ENABLE_MAC_ARM64: "true", ENABLE_MAC_X64: "false", ENABLE_WIN_X64: "true", RELEASE_RUN_ATTEMPT: "2", RELEASE_RUN_ID: "222222222", RELEASE_COMMIT: "current-sha", RELEASE_ASSET_SUFFIX: "auto", RELEASE_CHANNEL: "beta", RELEASE_MANIFEST_DIR: platformManifestRoot, RELEASE_METADATA_DIR: join(runnerTemp, "release-metadata"), RELEASE_OUTPUTS_PATH: join(runnerTemp, "release-metadata", "outputs.json"), RELEASE_PUBLIC_ORIGIN: "https://releases.open-design.ai", RELEASE_SIGNED: "false", RELEASE_STORAGE_ACCESS_KEY_ID: "test-access-key", RELEASE_STORAGE_BUCKET: fixture.bucket, RELEASE_STORAGE_ENDPOINT: fixture.endpointUrl, RELEASE_STORAGE_REGION: "auto", RELEASE_STORAGE_SECRET_ACCESS_KEY: "test-secret-key", RELEASE_VERSION: "1.2.3-beta.4", STATE_SOURCE: "test", MAC_ARM64_RESULT: "success", WIN_X64_RESULT: "success", }, maxBuffer: 1024 * 1024, }); const metadata = JSON.parse(await readFile(join(runnerTemp, "release-metadata", "metadata.json"), "utf8")) as { platforms: { mac: { artifacts?: { payload?: { sha256Url?: string; url?: string } } }; win: { artifacts?: { payload?: { sha256Url?: string; url?: string } } }; }; releaseTargets: { mac_arm64: { artifacts?: { payload?: { sha256Url?: string; url?: string } } }; win_x64: { artifacts?: { payload?: { sha256Url?: string; url?: string } } }; }; }; const outputs = JSON.parse(await readFile(join(runnerTemp, "release-metadata", "outputs.json"), "utf8")) as Record; expect(metadata.platforms.mac.artifacts?.payload?.url).toContain("mac-arm64-payload.zip"); expect(metadata.platforms.mac.artifacts?.payload?.sha256Url).toContain("mac-arm64-payload.zip.sha256"); expect(metadata.platforms.win.artifacts?.payload?.url).toContain("win-x64-payload.7z"); expect(metadata.platforms.win.artifacts?.payload?.sha256Url).toContain("win-x64-payload.7z.sha256"); expect(metadata.releaseTargets.mac_arm64.artifacts?.payload?.url).toBe(metadata.platforms.mac.artifacts?.payload?.url); expect(metadata.releaseTargets.win_x64.artifacts?.payload?.url).toBe(metadata.platforms.win.artifacts?.payload?.url); expect(outputs.mac_arm64_payload_url).toBe(metadata.platforms.mac.artifacts?.payload?.url); expect(outputs.win_x64_payload_url).toBe(metadata.platforms.win.artifacts?.payload?.url); expect(fixture.uploadedObjectKeys()).toEqual([ "beta/versions/1.2.3-beta.4.unsigned/metadata.json", "beta/latest/metadata.json", "beta/latest/platforms/mac_arm64.json", "beta/latest/platforms/win_x64.json", "beta/latest/latest.yml", ]); } finally { await fixture.close(); await rm(runnerTemp, { force: true, recursive: true }); } }); it("keeps beta runner bootstrap in workflows instead of release scripts", async () => { const [workflow, posixBuildScript, winBuildScript] = await Promise.all([ readFile(releaseBetaSelfHostedWorkflowPath, "utf8"), readFile(releaseBetaPosixBuildScriptPath, "utf8"), readFile(releaseBetaWindowsBuildScriptPath, "utf8"), ]); expect(workflow).toContain("fnm exec --using=24 -- bash tools/release/scripts/build-platform.sh"); expect(workflow).toContain('& "C:\\Users\\runner\\.cargo\\bin\\fnm.exe" exec --using=24 -- pwsh -NoProfile -File tools\\release\\scripts\\build-platform.ps1'); expect(workflow).toContain("corepack prepare pnpm@10.33.2 --activate"); expect(workflow).toContain('pnpm.cmd install --frozen-lockfile --prefer-offline'); expect(workflow).toContain("sudo -n \"$OPEN_DESIGN_MAC_SIGNING_HELPER\" \"$cert_path\" \"$password_path\""); expect(workflow).not.toContain("PATH: /usr/local/libexec/open-design/wrappers:${{ env.PATH }}"); expect(posixBuildScript).not.toContain("fnm"); expect(posixBuildScript).not.toContain("corepack"); expect(posixBuildScript).not.toContain("pnpm install"); expect(winBuildScript).not.toContain("fnm"); expect(winBuildScript).not.toContain("corepack"); expect(winBuildScript).not.toContain("pnpm install"); }); }); function expectChannelWorkflowNamespaces( workflow: string, channel: "beta" | "preview" | "prerelease", options: { hasLinuxSmoke: boolean }, ): void { const namespace = `release-${channel}`; expect(workflow).toContain(`--namespace ${namespace}`); expect(workflow).toContain(`OD_PACKAGED_E2E_NAMESPACE: ${namespace}`); expect(workflow).toContain(`--namespace ${namespace}-intel`); expect(workflow).toContain(`"--namespace", "${namespace}-win",`); expect(workflow).toContain(`OD_PACKAGED_E2E_NAMESPACE: ${namespace}-win`); expect(workflow).toContain(`--namespace ${namespace}-linux`); if (options.hasLinuxSmoke) { expect(workflow).toContain(`OD_PACKAGED_E2E_NAMESPACE: ${namespace}-linux`); } } function expectWindowsUpdaterSmokeContract(workflow: string, channel: "beta" | "preview" | "prerelease" | "stable"): void { expect(workflow).toContain("win_x64_smoke_mode:"); expect(workflow).toContain("win_x64_update_metadata_url:"); expect(workflow).toContain("win_x64_update_target_version:"); expect(workflow).toMatch(/win_x64_smoke_mode:[\s\S]*?options:[\s\S]*?- skip[\s\S]*?- core[\s\S]*?- full[\s\S]*?default: core/); expect(workflow).toContain("OD_PACKAGED_E2E_WIN_SMOKE_PROFILE: ${{ inputs.win_x64_smoke_mode }}"); expect(workflow).toContain("OD_PACKAGED_E2E_WIN_UPDATE_FIXTURE: ${{ inputs.win_x64_smoke_mode == 'full' && inputs.win_x64_update_metadata_url == '' && inputs.win_x64_update_target_version == '' && 'tools-serve' || '' }}"); expect(workflow).toContain("OD_PACKAGED_E2E_WIN_UPDATE_METADATA_URL: ${{ inputs.win_x64_update_metadata_url }}"); expect(workflow).toContain("OD_PACKAGED_E2E_WIN_UPDATE_VERSION: ${{ inputs.win_x64_update_target_version }}"); if (channel === "stable") { expect(workflow).toContain("Build stable win_x64 update fixture"); expect(workflow).toContain('full Windows stable smoke requires stable version x.y.z'); expect(workflow).toContain('pnpm.cmd exec tools-pack win cleanup --dir $toolsPackDir --namespace "${{ needs.metadata.outputs.win_namespace }}" --json'); expect(workflow).toContain("--cache-dir $cacheDir `"); expect(workflow).toContain('pnpm.cmd exec tools-pack win validate-payload --namespace "${{ needs.metadata.outputs.win_namespace }}" --payload-path $build.payloadPath --expected-version "${{ needs.metadata.outputs.release_version }}" --json'); } else { expect(workflow).toContain(`Build ${channel} win_x64 update fixture`); expect(workflow).toContain(`full Windows smoke requires a counted ${channel} version`); } expect(workflow).not.toContain("OD_PACKAGED_E2E_WIN_SMOKE_PROFILE: core"); } function expectCountedReleaseWorkflowCallContract(workflow: string, channel: "preview" | "prerelease"): void { expect(workflow).toContain("workflow_dispatch:"); expect(workflow).toContain("workflow_call:"); expect(workflow).toContain("ref:"); expect(workflow).toContain("release_version:"); expect(workflow).toContain("description: \"Optional git ref to build."); expect(workflow).toContain("ref: ${{ inputs.ref != '' && inputs.ref || github.ref }}"); expect(workflow).toContain("Resolve built commit"); expect(workflow).toContain("GITHUB_SHA: ${{ env.BUILT_SHA }}"); expect(workflow).toContain("GITHUB_REF_NAME: ${{ inputs.ref != '' && inputs.ref || github.ref_name }}"); expect(workflow).toContain(`Capture previous ${channel} commit`); expect(workflow).toContain("previous_commit: ${{ steps.prev.outputs.previous_commit }}"); expect(workflow).toContain("version_metadata_url:"); expect(workflow).toContain("mac_arm64_url:"); expect(workflow).toContain("mac_intel_url:"); expect(workflow).toContain("win_url:"); expect(workflow).toContain("linux_url:"); expect(workflow).toContain("GITHUB_SHA: ${{ needs.metadata.outputs.commit }}"); expect(workflow).toContain("version_metadata_url: ${{ steps.outputs.outputs.version_metadata_url }}"); expect(workflow).toContain("mac_arm64_url: ${{ steps.outputs.outputs.mac_arm64_dmg_url }}"); expect(workflow).toContain("mac_intel_url: ${{ steps.outputs.outputs.mac_x64_dmg_url }}"); expect(workflow).toContain("win_url: ${{ steps.outputs.outputs.win_x64_installer_url }}"); expect(workflow).toContain("linux_url: ${{ steps.outputs.outputs.linux_x64_appImage_url }}"); } function expectReleaseLinuxBuildPreservesEvidence(workflow: string, stepName: string): void { const step = workflow.match(new RegExp(`- name: ${stepName}\\r?\\n(?:.+\\r?\\n)+?(?=\\r?\\n - name: Smoke .+ linux AppImage runtime)`, "m"))?.[0]; expect(step).toBeDefined(); expect(step).toContain('report_dir="$RUNNER_TEMP/release-report/linux"'); expect(step).toContain('mkdir -p "$report_dir"'); expect(step).toContain('build_json_path="$report_dir/tools-pack.json"'); expect(step).toContain('build_log_path="$report_dir/tools-pack.log"'); expect(step).toContain('printf \'%s\\n\' "$build_output" | tee "$build_json_path"'); } function expectReleaseLinuxSmokePreservesEvidenceBeforeApt(workflow: string, stepName: string): void { const step = workflow.match(new RegExp(`- name: ${stepName}\\r?\\n(?:.+\\r?\\n)+?(?=\\r?\\n - name: Upload linux e2e spec report)`, "m"))?.[0]; expect(step).toBeDefined(); const aptIndex = step?.indexOf("sudo apt-get update") ?? -1; const reportDirIndex = step?.indexOf('report_dir="$RUNNER_TEMP/release-report/linux"') ?? -1; expect(aptIndex).toBeGreaterThan(-1); expect(reportDirIndex).toBeGreaterThan(-1); expect(reportDirIndex).toBeLessThan(aptIndex); } async function startStablePrereleaseMetadataServer(objects: Record): Promise<{ close: () => Promise; origin: string; }> { const server = createHttpsServer( { cert: stablePrereleaseMetadataCert, key: stablePrereleaseMetadataKey, }, (request, response) => { const objectKey = decodeURIComponent(new URL(request.url ?? "/", "https://127.0.0.1").pathname.replace(/^\/+/, "")); if (request.method !== "GET" || !(objectKey in objects)) { response.statusCode = 404; response.end("not found"); return; } response.setHeader("content-type", "application/json; charset=utf-8"); response.end(JSON.stringify(objects[objectKey])); }, ); await new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", () => { server.off("error", reject); resolve(); }); }); const address = server.address(); if (address == null || typeof address === "string") { throw new Error("stable prerelease metadata server did not bind to a TCP port"); } return { close: () => new Promise((resolve, reject) => { server.close((error) => (error == null ? resolve() : reject(error))); }), origin: `https://127.0.0.1:${address.port}`, }; } function stablePrereleaseMetadataFixture(baseVersion: string, prereleaseVersion: string, publicOrigin: string): Record { const versionPrefix = `prerelease/versions/${prereleaseVersion}`; const versionUrl = `${publicOrigin}/${versionPrefix}`; const artifact = (name: string) => ({ sha256Url: `${versionUrl}/${name}.sha256`, url: `${versionUrl}/${name}`, }); return { baseVersion, channel: "prerelease", github: { branch: `release/v${baseVersion}`, commit: "0123456789abcdef0123456789abcdef01234567", repository: "nexu-io/open-design", workflow: "release-prerelease", }, prereleaseNumber: 12, prereleaseVersion, platforms: { mac: { arch: "arm64", artifacts: { dmg: artifact("Open Design.dmg"), zip: artifact("Open Design-mac-arm64.zip"), }, enabled: true, signed: true, }, macIntel: { arch: "x64", artifacts: { dmg: artifact("Open Design Intel.dmg"), zip: artifact("Open Design-mac-x64.zip"), }, enabled: true, signed: true, }, win: { arch: "x64", artifacts: { installer: artifact("Open Design Setup.exe"), }, enabled: true, }, }, r2: { report: { type: "zip", url: `${versionUrl}/report.zip`, }, reportZipUrl: `${versionUrl}/report.zip`, versionMetadataUrl: `${versionUrl}/metadata.json`, versionPrefix, }, releaseVersion: prereleaseVersion, signed: true, }; } async function startReleaseMetadataObjectStore(objects: Record): Promise<{ bucket: string; close: () => Promise; endpointUrl: string; uploadedObjectKeys: () => string[]; }> { const bucket = "release-bucket"; const uploadedObjectKeys: string[] = []; const server = createServer((request, response) => { void handleReleaseMetadataObjectStoreRequest(request, response, bucket, objects, uploadedObjectKeys); }); await new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", () => { server.off("error", reject); resolve(); }); }); const address = server.address(); if (address == null || typeof address === "string") { throw new Error("release metadata object store did not bind to a TCP port"); } return { bucket, close: () => new Promise((resolve, reject) => { server.close((error) => (error == null ? resolve() : reject(error))); }), endpointUrl: `http://127.0.0.1:${address.port}`, uploadedObjectKeys: () => [...uploadedObjectKeys], }; } async function handleReleaseMetadataObjectStoreRequest( request: IncomingMessage, response: ServerResponse, bucket: string, objects: Record, uploadedObjectKeys: string[], ): Promise { const path = new URL(request.url ?? "/", "http://127.0.0.1").pathname; const bucketPrefix = `/${bucket}/`; if (!path.startsWith(bucketPrefix)) { response.statusCode = 404; response.end("not found"); return; } const objectKey = decodeURIComponent(path.slice(bucketPrefix.length)); if (request.method === "GET") { if (!(objectKey in objects)) { response.statusCode = 404; response.end("not found"); return; } const body = JSON.stringify(objects[objectKey]); response.setHeader("content-type", "application/json; charset=utf-8"); response.end(body); return; } if (request.method === "PUT") { uploadedObjectKeys.push(objectKey); for await (const _chunk of request) { // Drain the request body so the client can complete cleanly. } response.statusCode = 200; response.end("ok"); return; } response.statusCode = 405; response.end("method not allowed"); } const stablePrereleaseMetadataKey = `-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC1hoV1GwxqTYdO Zs0pY5hnp8BtTwdF6dWsXoFWYw9IPpBTmyNeleRcLtrht/oc5oRS05tC97qmb5eL RigyXUmwrpt/VjJ7ursDa3qGnljkqVxqBkRAUdXBMCVPkMogKWvJy/S61Vthvf7K K5HhofwcuPPvRBdhdZgtw/7nZY49HYutd7wP/U7iqCYBMpWr0I29jSs1S2xY9fH8 ih/exDGe3PHm8yQao4pHUUFVXoAI5w6tYsmNep6b+5NYPHnHSaXd7h5gaF+nIJE4 78jgRQHKjQ2iNf/53/o/d5SAMb/9lZ7stNT8RIFOJUz1IP8Zsz3VKwAvXKXZDObr 0MS4JrPdAgMBAAECggEATcF0HD/8VvKjsU0ut3pud4QvVINEGcn6mY2XuFHRY4BN IUr0YRkyytvVLVe5vrRtXO9Ac/Sakp19XA6uvDgijxiUCfz5ve80GVhqEQz2BeiX 6eCKTsTfG5QMf2MFebZUcgm36Gno7VrNr3rvT6erzv/YmZZgr4IIMB5i62qgfYOY ABSg6b223RSVeZXNvWxovKycBUUa26lrzRu5jpuexjAccmgbiE86exhzW7FK2zjZ XH8rOxSDJ49+ipPOGsJ+rZMdtvHq6BO/QU4O9IkBLNuHAIbr/WcjBgnAPskQTrOM i3vWqPNVw3tPjBWCOtzy0UllG0L5Sxnx5cceFvL9HwKBgQDieIaM89In+VETI+x4 aUmQXxVcisZR0FWQytl+XbWe4T1zxEj4fFjd/phgv0M60599/mwCCGrImxKM8cnb mjxv2FX+or9+2IFpaSOi+Qj6/IxcTTWoMU0t4AQjOgbRf3iBpVz6JysnKKpqqukT GGOnzGWz0gFmDAqKm0zkGy7czwKBgQDNMb6hrSGobMRlCndgx//w/SdDq/IqAbIS QyAvYgNuOXV3J4sD2Z1TwYxZM2Oq5rhOPfZr8SnqM7d+LknLPiGMKV7z6vL/BOu8 ZB5+EmMZwqNmSOMaFZM+77OC/zxDCznqTm4N5vDdg+6SByCtuyCm+Jraj0PtHtkD krdWqBfHkwKBgDpTzluZJGQ1OyNR2kJ843xycL7/4uoJXTBIflGkcvVzj280e5K7 ++tY+gfY2sjY3jgGAe1YG6CFB/cTAukzRSONNUC6y9Uwj8wFTy9XMm/qAYB4RjyG Thllm8sy07S7Pt8tJtAqrFuOhq2oTRUk7+20n/D7Qm705PYj317UfXJTAoGABdYM XfzWoDu3ukf57T7DAM+ydjJFyPwTXIGcQLzA7DmmJaVyRsHBv8gZfdAAXbQCOfd5 MsjBMHAYH/ahEq7JtXrXwIhGMQqqycjvNRbAytLGYvpfuzYx4fBfYrJvvFhtZUSl zK9s2mAOQQkC3O4dl6IqhVzdybi+42Mg484UHxECgYEAht1ef0Gc6RKZpmqttlZJ 1G4lsR1Aws3dintACs8lza5aaufrY07gF8z3rkW6tPGEWfol3CYOT2U5UiUw+iKG F/Pa3L5wCxuRKKWx0ip0PFhDPrpWfVCm2CLlUlZLEjpmF2iUZgmkaScjYqG8R16a C8cywTs1ku5aYIaN8YcAigI= -----END PRIVATE KEY-----`; const stablePrereleaseMetadataCert = `-----BEGIN CERTIFICATE----- MIIDCTCCAfGgAwIBAgIUbNGmwcWmZP5tw6gm8s2RXzWJv+IwDQYJKoZIhvcNAQEL BQAwFDESMBAGA1UEAwwJMTI3LjAuMC4xMB4XDTI2MDYwODA0MDczNVoXDTI2MDYw OTA0MDczNVowFDESMBAGA1UEAwwJMTI3LjAuMC4xMIIBIjANBgkqhkiG9w0BAQEF AAOCAQ8AMIIBCgKCAQEAtYaFdRsMak2HTmbNKWOYZ6fAbU8HRenVrF6BVmMPSD6Q U5sjXpXkXC7a4bf6HOaEUtObQve6pm+Xi0YoMl1JsK6bf1Yye7q7A2t6hp5Y5Klc agZEQFHVwTAlT5DKIClrycv0utVbYb3+yiuR4aH8HLjz70QXYXWYLcP+52WOPR2L rXe8D/1O4qgmATKVq9CNvY0rNUtsWPXx/Iof3sQxntzx5vMkGqOKR1FBVV6ACOcO rWLJjXqem/uTWDx5x0ml3e4eYGhfpyCROO/I4EUByo0NojX/+d/6P3eUgDG//ZWe 7LTU/ESBTiVM9SD/GbM91SsAL1yl2Qzm69DEuCaz3QIDAQABo1MwUTAdBgNVHQ4E FgQU8Z0Oy/q8fAqp9005cn2sW4K6oB4wHwYDVR0jBBgwFoAU8Z0Oy/q8fAqp9005 cn2sW4K6oB4wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAlJTb 7zi4FKJqYuXZ9YWmV96Ri+vBcNfO2dwKBxFtJXm0Ai2Q4ruutuFPYwY6UYGTN5gC HJ0/WxuPK5ftAE6UU+Mghu0dJlH+gWmOq5cDyhYdnEi8R6z5AsPtPEYlkkIvhUO1 k1BtCP0h4Kh8fuaILGuXQNOaKizIWF2lEEHfCmvKhgOF6dKWs38zdetFQCLRIaHg ZyGlUhPCUbKdTiBJuCGaDKzeEAlC8dsar2zjg9CVue7w3CaamQpjnV0d2IHJiVAH QONQvdtLnZ6GeNPe06oBrq7R9SL5/tkqgSq8lCrDE6jFZnfXNMdDmZY3wTcFcdyG yW/DsIUs5ZzcHza5rw== -----END CERTIFICATE-----`;