import { describe, expect, test } from "bun:test"; import { readFileSync } from "node:fs"; import { auditReleaseCommitCoverage, buildIssueBody, buildReleaseNotes, buildReleaseSummary, buildReleaseTitle, checkpointRunIds, draftRunResumeAction, nextVersion, parseReleaseCheckpoint, parseReleaseArgs, playDeliveryResumeAction, playDeliveryFailureStrategy, prepareReleaseCheckpoint, recordCheckpointRun, RELEASE_WORKFLOWS, RELEASE_VALIDATIONS, resolveReleaseVersion, reusedAssetMatches, selectPublishedDmg, signedWindowsUpdateAuditPassed, shouldCancelSupersededRun, waitForRun, } from "./release.mjs"; describe("release automation", () => { test("prepares and audits the official Docker image with every formal release", () => { expect(RELEASE_WORKFLOWS.docker).toBe("docker-image.yml"); }); test("dispatches a unified post-release endpoint timing report", () => { expect(RELEASE_WORKFLOWS.timings).toBe("release-timings.yml"); const releaseSource = readFileSync(new URL("./release.mjs", import.meta.url), "utf8"); expect(releaseSource).toContain("desktop_run_id=\${desktopRunId}"); expect(releaseSource).toContain("mobile_run_id=\${mobileRunId}"); expect(releaseSource).toContain("docker_run_id=\${dockerRunId}"); expect(releaseSource).toContain("store_run_id=\${timingStoreRunId}"); expect(releaseSource).toContain("attempt_run_ids=\${timingAttemptRunIds}"); expect(releaseSource).toContain("endpoint timing report continues in background"); }); test("blocks publication until the Draft Android APK passes the Play signature gate", () => { const releaseSource = readFileSync(new URL("./release.mjs", import.meta.url), "utf8"); const signatureGate = releaseSource.indexOf('label: "Draft Android Play signature gate"'); const publication = releaseSource.indexOf('"--draft=false"'); expect(RELEASE_WORKFLOWS.androidPlaySignature).toBe("android-play-signature-audit.yml"); expect(signatureGate).toBeGreaterThanOrEqual(0); expect(publication).toBeGreaterThan(signatureGate); }); test("blocks publication until the offline-signed Windows update passes an independent audit", () => { const releaseSource = readFileSync(new URL("./release.mjs", import.meta.url), "utf8"); const signing = releaseSource.indexOf("signDraftWindowsUpdate({"); const audit = releaseSource.indexOf('label: "Draft signed Windows update audit"'); const publication = releaseSource.indexOf('"--draft=false"'); expect(signing).toBeGreaterThanOrEqual(0); expect(audit).toBeGreaterThan(signing); expect(publication).toBeGreaterThan(audit); }); test("requires the signed Windows audit job instead of accepting a rebuild-only workflow", () => { expect(signedWindowsUpdateAuditPassed({ jobs: [{ name: "Audit signed Windows update", conclusion: "success" }], })).toBe(true); expect(signedWindowsUpdateAuditPassed({ jobs: [ { name: "Audit signed Windows update", conclusion: "skipped" }, { name: "Finalize desktop release assets", conclusion: "success" }, ], })).toBe(false); }); test("checks the offline Windows key before creating release state", () => { const releaseSource = readFileSync(new URL("./release.mjs", import.meta.url), "utf8"); const releaseMain = releaseSource.indexOf("const releaseMain = async"); const keyCheck = releaseSource.indexOf("assertWindowsUpdateSigningKey({", releaseMain); const issueCreation = releaseSource.indexOf('"issue",\n "create"', releaseMain); expect(keyCheck).toBeGreaterThan(releaseMain); expect(issueCreation).toBeGreaterThan(keyCheck); }); test("starts Play delivery as soon as Android preparation finishes", () => { const releaseSource = readFileSync(new URL("./release.mjs", import.meta.url), "utf8"); const androidReady = releaseSource.lastIndexOf("const androidReleaseReady"); const mobileWait = releaseSource.indexOf('label: "Draft Android assets"', androidReady); const playDelivery = releaseSource.indexOf("await ensurePlayDelivery", androidReady); const allDraftGates = releaseSource.indexOf("await Promise.all", androidReady); expect(RELEASE_WORKFLOWS.storeDelivery).toBe("store-delivery.yml"); expect(mobileWait).toBeGreaterThan(androidReady); expect(playDelivery).toBeGreaterThan(mobileWait); expect(playDelivery).toBeLessThan(allDraftGates); }); test("restores an exact Draft checkpoint without exposing it in Issue text", () => { const checkpoint = { releaseSha: "abc", desktopRunId: 123 }; const body = ``; expect(parseReleaseCheckpoint(body, "v1.42.0")).toEqual(checkpoint); expect(parseReleaseCheckpoint(body, "v1.42.1")).toBeNull(); expect(parseReleaseCheckpoint("malformed", "v1.42.0")).toBeNull(); }); test("preserves Play delivery and Run history when a Draft target advances", () => { const storedState = { releaseSha: "old", desktopRunId: 11, storeRunId: 12, playDelivery: { tag: "v1.42.0", releaseSha: "old", storeRunId: 12 }, }; const checkpoint = prepareReleaseCheckpoint({ storedState, releaseSha: "new" }); expect(checkpoint).toMatchObject({ releaseSha: "new", playDelivery: storedState.playDelivery, runHistory: [ { field: "desktopRunId", runId: 11, releaseSha: "old" }, { field: "storeRunId", runId: 12, releaseSha: "old" }, ], }); expect(checkpoint.desktopRunId).toBeUndefined(); recordCheckpointRun(checkpoint, "mobileRunId", 13); recordCheckpointRun(checkpoint, "mobileRunId", 13); expect(checkpointRunIds(checkpoint)).toEqual([11, 12, 13]); }); test("verifies an immutable Play delivery without uploading across mobile-compatible fixes", () => { const playDelivery = { tag: "v1.42.0", releaseSha: "old" }; expect(playDeliveryResumeAction({ playDelivery, tag: "v1.42.0", headSha: "old", })).toBe("verify"); expect(playDeliveryResumeAction({ playDelivery, tag: "v1.42.0", headSha: "new", mobileInputsChanged: false, })).toBe("verify"); expect(playDeliveryResumeAction({ playDelivery, tag: "v1.42.0", headSha: "new", mobileInputsChanged: true, })).toBe("block"); expect(playDeliveryResumeAction({ playDelivery, tag: "v1.43.0", headSha: "new", })).toBe("upload"); }); test("cancels only active Runs from superseded release targets", () => { expect(shouldCancelSupersededRun({ runView: { headSha: "old", status: "in_progress" }, headSha: "new", })).toBe(true); expect(shouldCancelSupersededRun({ runView: { headSha: "old", status: "completed" }, headSha: "new", })).toBe(false); expect(shouldCancelSupersededRun({ runView: { headSha: "new", status: "in_progress" }, headSha: "new", })).toBe(false); }); test("reuses successful Draft runs and reruns only failed ones", () => { const headSha = "abc"; expect(draftRunResumeAction({ runId: 1, runView: { headSha, status: "completed", conclusion: "success" }, headSha, })).toBe("reuse"); expect(draftRunResumeAction({ runId: 2, runView: { headSha, status: "completed", conclusion: "failure" }, headSha, })).toBe("rerun"); expect(draftRunResumeAction({ runId: 3, runView: { headSha: "different", status: "completed", conclusion: "success" }, headSha, })).toBe("dispatch"); }); test("only retries a Play delivery when upload definitely did not start", () => { const run = (conclusion) => ({ jobs: [{ name: "Deliver Google Play", steps: [{ name: "Upload bundle to Google Play", conclusion }], }], }); expect(playDeliveryFailureStrategy(run("skipped"))).toBe("rerun"); expect(playDeliveryFailureStrategy(run("success"))).toBe("recover"); expect(playDeliveryFailureStrategy(run("failure"))).toBe("recover"); }); test("runs the complete project regression suite before release", () => { expect(RELEASE_VALIDATIONS).toContainEqual({ label: "Project regression tests", args: ["run", "test"], }); }); test("retries transient GitHub status failures while waiting for a workflow", async () => { let attempts = 0; let polls = 0; const result = await waitForRun({ repository: "tianma-if/edgeever", runId: 123, label: "Draft assets", viewRun: () => { attempts += 1; if (attempts < 3) throw new Error("transient EOF"); return { status: "completed", conclusion: "success", url: "https://example.test/run" }; }, waitForNextPoll: async () => { polls += 1; }, }); expect(result.conclusion).toBe("success"); expect(attempts).toBe(3); expect(polls).toBe(2); }); test("parses paired bilingual changes and labels", () => { expect( parseReleaseArgs([ "--issue-title", "Improve release flow", "--bump", "minor", "--label", "enhancement", "--change-en", "Run checks in parallel.", "--change-zh", "并行运行检查。", "--change-locale", "ja-JP:チェックを並列実行します。", "--change-commit", "abc1234", ]), ).toMatchObject({ issueTitle: "Improve release flow", bump: "minor", labels: ["enhancement"], changesEn: ["Run checks in parallel."], changesZh: ["并行运行检查。"], localizedChanges: { "ja-JP": ["チェックを並列実行します。"] }, changeCommits: ["abc1234"], }); }); test("keeps post-release desktop installation explicitly opt-in", () => { expect(parseReleaseArgs([ "--issue-title", "Release", "--bump", "patch", "--label", "maintenance", "--change-en", "Update the release flow.", "--change-zh", "更新发布流程。", "--change-commit", "abc1234", ])).toMatchObject({ installDesktop: false }); expect(parseReleaseArgs([ "--issue-title", "Release", "--bump", "patch", "--label", "maintenance", "--change-en", "Update the release flow.", "--change-zh", "更新发布流程。", "--change-commit", "abc1234", "--install-desktop", ])).toMatchObject({ installDesktop: true }); }); test("rejects mismatched bilingual changes", () => { expect(() => parseReleaseArgs([ "--issue-title", "Broken input", "--bump", "patch", "--label", "bug", "--change-en", "Only English.", ]) ).toThrow("--change-en and --change-zh"); }); test("increments stable semantic versions", () => { expect(nextVersion("1.6.50", "patch")).toBe("1.6.51"); expect(nextVersion("1.6.50", "minor")).toBe("1.7.0"); expect(nextVersion("1.6.50", "major")).toBe("2.0.0"); expect(() => nextVersion("1.6", "patch")).toThrow("stable X.Y.Z"); expect(() => nextVersion("1.6.50", "automatic")).toThrow("patch, minor, or major"); }); test("resumes a Draft only when it matches the requested version and HEAD", () => { const draftCandidate = { tagName: "v1.17.1", isDraft: true, isPrerelease: false, targetCommitish: "current-head", }; expect(resolveReleaseVersion({ previousVersion: "1.17.0", packageVersion: "1.17.1", bump: "patch", headSha: "current-head", draftCandidate, })).toMatchObject({ releaseVersion: "1.17.1", releaseBaseTag: "v1.17.0", resumedDraft: draftCandidate, withdrawnDraft: null, }); }); test("reserves a withdrawn Draft version and keeps the published audit baseline", () => { const draftCandidate = { tagName: "v1.17.1", isDraft: true, isPrerelease: false, targetCommitish: "withdrawn-release", }; expect(resolveReleaseVersion({ previousVersion: "1.17.0", packageVersion: "1.17.1", bump: "patch", headSha: "current-head", draftCandidate, draftTargetIsAncestor: true, })).toMatchObject({ releaseVersion: "1.17.2", releaseBaseTag: "v1.17.0", resumedDraft: null, withdrawnDraft: draftCandidate, }); }); test("rejects a Draft from unrelated history", () => { expect(() => resolveReleaseVersion({ previousVersion: "1.17.0", packageVersion: "1.17.1", bump: "patch", headSha: "current-head", draftCandidate: { tagName: "v1.17.1", isDraft: true, isPrerelease: false, targetCommitish: "unrelated-head", }, draftTargetIsAncestor: false, })).toThrow("current HEAD or its history"); }); test("uses the stable tag as the GitHub Release title", () => { expect(buildReleaseTitle("v1.6.55")).toBe("v1.6.55"); expect(() => buildReleaseTitle("1.6.55")).toThrow("stable vX.Y.Z tag"); }); test("requires an explicit version bump", () => { expect(() => parseReleaseArgs([ "--issue-title", "Missing bump", "--label", "bug", "--change-en", "Fix a bug.", "--change-zh", "修复问题。", "--change-commit", "abc1234", ]) ).toThrow("--bump must be patch, minor, or major"); }); test("builds concise user-facing bilingual release notes", () => { const notes = buildReleaseNotes({ changesEn: ["Improve the release flow."], changesZh: ["优化发布流程。"], issueNumber: 126, }); expect(notes).toContain("## Key Changes"); expect(notes).toContain("Related Issue: #126"); expect(notes).toContain("## 🇨🇳 中文说明 / Chinese Changelog"); expect(notes).toContain("关联 Issue:#126"); expect(notes.indexOf("## 🇨🇳 中文说明 / Chinese Changelog")) .toBeLessThan(notes.indexOf("## Key Changes")); expect(notes.indexOf("优化发布流程。")) .toBeLessThan(notes.indexOf("Improve the release flow.")); expect(notes).not.toContain("## Verification"); expect(notes).not.toContain("## 验证"); expect(notes).not.toContain("bun run"); expect(notes).not.toContain("Version bump"); expect(notes).not.toContain("release plan"); expect(notes).not.toContain("\\n"); }); test("builds the in-app summary from the same bilingual release changes", () => { expect(buildReleaseSummary({ version: "1.6.55", changesEn: ["Improve the release flow."], changesZh: ["优化发布流程。"], localizedChanges: { "ja-JP": ["リリースフローを改善します。"] }, })).toEqual({ version: "1.6.55", changes: { "en-US": ["Improve the release flow."], "zh-CN": ["优化发布流程。"], "ja-JP": ["リリースフローを改善します。"], }, }); }); test("persists localized in-app release changes when preparing versions", () => { const releaseSource = readFileSync(new URL("./release.mjs", import.meta.url), "utf8"); const updateCall = releaseSource.indexOf("const versionPaths = updateReleaseVersions({"); const localizedChanges = releaseSource.indexOf( "localizedChanges: options.localizedChanges", updateCall, ); const updateCallEnd = releaseSource.indexOf("});", updateCall); expect(updateCall).toBeGreaterThanOrEqual(0); expect(localizedChanges).toBeGreaterThan(updateCall); expect(localizedChanges).toBeLessThan(updateCallEnd); }); test("builds a bilingual umbrella Issue", () => { const body = buildIssueBody({ changesEn: ["Parallel checks."], changesZh: ["并行检查。"], commitCoverageAudit: { mappings: [{ changeIndex: 0, commits: [{ sha: "aaaaaaaa11111111111111111111111111111111" }], }], ignored: [{ commit: { sha: "bbbbbbbb22222222222222222222222222222222" }, reason: "test-only coverage", }], }, }); expect(body).toContain("## Summary"); expect(body).toContain("- Parallel checks."); expect(body).toContain("## 中文说明"); expect(body).toContain("- 并行检查。"); expect(body).toContain("## Commit coverage audit"); expect(body).toContain("Play-signed Android arm64 APK"); expect(body).toContain("unsigned Windows x64 Preview"); expect(body).toContain("Linux x64 AppImage Preview"); expect(body).toContain("- Change 1: `aaaaaaaa`"); expect(body).toContain("- Excluded `bbbbbbbb`: test-only coverage"); }); test("requires every bilingual change to map to commits", () => { expect(() => parseReleaseArgs([ "--issue-title", "Missing commit mapping", "--bump", "patch", "--label", "bug", "--change-en", "Fix a bug.", "--change-zh", "修复问题。", ]) ).toThrow("--change-commit"); }); test("audits complete commit coverage across changes and explicit ignores", () => { const commits = [ { sha: "aaaaaaaa11111111111111111111111111111111", subject: "feat: add resource actions" }, { sha: "bbbbbbbb22222222222222222222222222222222", subject: "test: cover resource actions" }, { sha: "cccccccc33333333333333333333333333333333", subject: "fix: stabilize editor" }, ]; const audit = auditReleaseCommitCoverage({ commits, changeCommits: ["aaaaaaaa,cccccccc"], ignoredCommits: ["bbbbbbbb:test-only coverage"], }); expect(audit.mappings[0].commits.map((commit) => commit.sha)).toEqual([ commits[0].sha, commits[2].sha, ]); expect(audit.ignored).toEqual([{ commit: commits[1], reason: "test-only coverage" }]); }); test("blocks a release when a commit is missing from the notes audit", () => { const commits = [ { sha: "aaaaaaaa11111111111111111111111111111111", subject: "feat: documented" }, { sha: "bbbbbbbb22222222222222222222222222222222", subject: "fix: accidentally omitted" }, ]; expect(() => auditReleaseCommitCoverage({ commits, changeCommits: ["aaaaaaaa"], ignoredCommits: [], })).toThrow("bbbbbbbb fix: accidentally omitted"); }); test("requires valid ignore reasons and rejects covered commits as ignored", () => { const commits = [ { sha: "aaaaaaaa11111111111111111111111111111111", subject: "feat: documented" }, ]; expect(() => auditReleaseCommitCoverage({ commits, changeCommits: ["aaaaaaaa"], ignoredCommits: ["aaaaaaaa:no public impact"], })).toThrow("both covered and ignored"); expect(() => auditReleaseCommitCoverage({ commits, changeCommits: [], ignoredCommits: ["aaaaaaaa"], })).toThrow(":"); }); test("automatically accounts for a resumable release version commit", () => { const releaseCommit = { sha: "dddddddd44444444444444444444444444444444", subject: "chore: release v1.10.3 [skip ci]", }; const audit = auditReleaseCommitCoverage({ commits: [releaseCommit], changeCommits: [], ignoredCommits: [], }); expect(audit.ignored).toEqual([{ commit: releaseCommit, reason: "release automation commit" }]); }); test("requires reused assets to keep name, size, and digest", () => { const previous = [{ name: "app.apk", size: 10, digest: "sha256:abc" }]; expect(reusedAssetMatches(previous, [...previous], "app.apk")).toBe(true); expect( reusedAssetMatches(previous, [{ ...previous[0], digest: "sha256:def" }], "app.apk"), ).toBe(false); }); test("selects the DMG matching the current Mac architecture", () => { const assets = [ { name: "EdgeEver-1.6.51-mac-arm64.dmg", size: 10, digest: "sha256:abc", }, { name: "EdgeEver-1.6.51-mac-x64.dmg", size: 11, digest: "sha256:def", }, ]; expect( selectPublishedDmg(assets, "arm64"), ).toMatchObject({ asset: { name: "EdgeEver-1.6.51-mac-arm64.dmg" }, version: "1.6.51", }); expect(selectPublishedDmg(assets, "x64")).toMatchObject({ asset: { name: "EdgeEver-1.6.51-mac-x64.dmg" }, version: "1.6.51", }); }); });