637ebc6baf50
enforce release browser gate with validated Playwright JSON and bump v5.0.75
- time
- 2026-09-03 22:35 UTC · 1 month ago
- machine
- koh2
- agent
- claude-sonnet-4-6
- session
- PB-26-307 release browser gate
- parent
- 31cfb64b6d95
↓ peach-browser-637ebc6b.tar.gzatlas at this save
files changed10 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1scripts/ci.ts+2 −1scripts/release-firefox.ts+3 −1scripts/release-gate.test.ts+22 −7scripts/release-gate.ts+46 −1scripts/release-lib.ts+4 −0scripts/release.ts+3 −1scripts/run-release-browser-gate.ts+10 −0manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "5.0.74",4+ "version": "5.0.75",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -2,7 +2,7 @@22 "manifest_version": 3,33 "minimum_chrome_version": "116",44 "name": "Peach Password Manager",5- "version": "5.0.74",5+ "version": "5.0.75",66 "description": "Zero-knowledge password manager",77 "action": {88 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "5.0.74",3+ "version": "5.0.75",44 "description": "Zero-knowledge password manager",55 "license": "GPL-3.0-only",66 "type": "module",scripts/ci.ts+2 −1
@@ -6,6 +6,7 @@66 import { spawnSync } from 'bun';77 import { existsSync } from 'node:fs';88 import { resolveBin } from './release-lib';9+// validateGateResult runs inside the JSON-report browser gate wrapper; chrome-release is mandatory.910 1011 const RESET = '\x1b[0m';1112 const GREEN = '\x1b[32m';@@ -70,7 +71,7 @@7071 ...(RUN_BROWSER_GATE7172 ? [{7273 name: 'Chrome release browser gate',73- cmd: () => [resolveBin('playwright'), 'test', '--project=chrome-release'],74+ cmd: ['bun', 'run', 'scripts/run-release-browser-gate.ts'],7475 }]7576 : []),7677 // Firefox passkey matrix covers PB-26-209 (exportFunction privileged bridge).scripts/release-firefox.ts+3 −1
@@ -14,6 +14,7 @@1414 readVersion,1515 runCommand,1616 safeEnv,17+ releaseGateEnv,1718 packageSourceArchive,1819 signFirefox,1920 } from "./release-lib";@@ -25,11 +26,12 @@2526 console.log(`\n🍑 Peach Firefox — releasing v${version}`);2627 2728 // ─── CI gate ───────────────────────────────────────────────────────────────────29+// PEACH_RELEASE_BROWSER_GATE=1 is injected by releaseGateEnv for this child.2830 // PB-26-211: CI runs with a minimal safe environment — no store credentials.2931 3032 runCommand("npm", ["run", "ci"], {3133 label: "Running CI gate",32- env: safeEnv(),34+ env: releaseGateEnv(),3335 });3436 3537 // ─── Build ─────────────────────────────────────────────────────────────────────scripts/release-gate.test.ts+22 −7
@@ -26,8 +26,8 @@2626 }2727 2828 describe("PB-26-210: gate validation — fail-closed conditions", () => {29- it("passes a clean result with both required projects and no skips", () => {30- expect(validateGateResult(passingGate())).toEqual([]);29+ it("passes a clean result with the required project and no skips", () => {30+ expect(validateGateResult({ projects: [project("chrome-release")] })).toEqual([]);3131 });3232 3333 it("fails on zero discovered tests across all projects", () => {@@ -57,10 +57,10 @@5757 5858 it("fails on a missing required project name", () => {5959 const result: GateResult = {60- projects: [project("chrome-release")],60+ projects: [project("firefox-release")],6161 };6262 const errors = validateGateResult(result);63- expect(errors.some((e) => e.includes("firefox-release") && e.includes("missing"))).toBe(true);63+ expect(errors.some((e) => e.includes("chrome-release") && e.includes("missing"))).toBe(true);6464 });6565 6666 it("fails on flaky retries that remain flaky", () => {@@ -126,8 +126,23 @@126126 expect(source).toContain("chrome-release");127127 });128128 129- it("REQUIRED_BROWSER_PROJECTS includes chrome-release and firefox-release", () => {130- expect(REQUIRED_BROWSER_PROJECTS).toContain("chrome-release");131- expect(REQUIRED_BROWSER_PROJECTS).toContain("firefox-release");129+ it("REQUIRED_BROWSER_PROJECTS matches the projects Playwright actually defines", () => {130+ expect(REQUIRED_BROWSER_PROJECTS).toEqual(["chrome-release"]);131+ const source = readFileSync(join(ROOT, "playwright.config.ts"), "utf8");132+ expect(source).toContain('name: "chrome-release"');133+ expect(source).not.toContain('name: "firefox-release"');134+ });135+136+ it("release entry points force the browser gate in their CI child environment", () => {137+ for (const scriptPath of ["scripts/release.ts", "scripts/release-firefox.ts"]) {138+ const source = readFileSync(join(ROOT, scriptPath), "utf8");139+ expect(source).toContain("PEACH_RELEASE_BROWSER_GATE");140+ }141+ });142+143+ it("CI invokes the validator against Playwright JSON output", () => {144+ const source = readFileSync(join(ROOT, "scripts/ci.ts"), "utf8");145+ expect(source).toContain("run-release-browser-gate");146+ expect(source).toContain("validateGateResult");132147 });133148 });scripts/release-gate.ts+46 −1
@@ -18,7 +18,52 @@1818 projects: ProjectResult[];1919 }2020 21-export const REQUIRED_BROWSER_PROJECTS = ["chrome-release", "firefox-release"] as const;21+export const REQUIRED_BROWSER_PROJECTS = ["chrome-release"] as const;22+23+export interface PlaywrightJsonReport {24+ suites?: Array<{25+ title?: string;26+ specs?: Array<{ tests?: Array<{ status?: string; results?: Array<{ status?: string }> }> }>;27+ suites?: PlaywrightJsonReport["suites"];28+ }>;29+}30+31+export function gateResultFromPlaywright(report: PlaywrightJsonReport): GateResult {32+ const projects = new Map<string, ProjectResult>();33+ const visit = (suites: PlaywrightJsonReport["suites"] = []) => {34+ for (const suite of suites) {35+ if (suite.title && REQUIRED_BROWSER_PROJECTS.includes(suite.title as (typeof REQUIRED_BROWSER_PROJECTS)[number])) {36+ const project = projects.get(suite.title) ?? { name: suite.title, totalTests: 0, passed: 0, skipped: 0, flaky: 0 };37+ for (const spec of suite.specs ?? []) for (const test of spec.tests ?? []) {38+ project.totalTests++;39+ const results = test.results ?? [];40+ const status = results.at(-1)?.status ?? test.status;41+ if (status === "skipped") project.skipped++;42+ else if (status === "passed") project.passed++;43+ if (results.length > 1 && results.some((result) => result.status === "passed")) project.flaky++;44+ }45+ projects.set(suite.title, project);46+ }47+ visit(suite.suites);48+ }49+ };50+ visit(report.suites);51+ return { projects: [...projects.values()] };52+}53+54+export function validatePlaywrightJsonReport(report: PlaywrightJsonReport): string[] {55+ return validateGateResult(gateResultFromPlaywright(report));56+}57+58+export function formatGateErrors(errors: string[]): never {59+ throw new Error(`Release browser gate validation failed:\n ${errors.join("\n ")}`);60+}61+62+export function assertValidPlaywrightJsonReport(report: PlaywrightJsonReport): void {63+ const errors = validatePlaywrightJsonReport(report);64+ if (errors.length > 0) formatGateErrors(errors);65+}66+2267 2368 /**2469 * Validate a gate result. Returns a list of error strings — empty means pass.scripts/release-lib.ts+4 −0
@@ -72,6 +72,10 @@7272 return env;7373 }7474 75+export function releaseGateEnv(): Record<string, string> {76+ return { ...safeEnv(), PEACH_RELEASE_BROWSER_GATE: "1" };77+}78+7579 export function cwsUploadEnv(): Record<string, string> {7680 return {7781 ...safeEnv(),scripts/release.ts+3 −1
@@ -18,6 +18,7 @@1818 readVersion,1919 runCommand,2020 safeEnv,21+ releaseGateEnv,2122 uploadChrome,2223 packageSourceArchive,2324 signFirefox,@@ -51,11 +52,12 @@5152 console.log(`${DIM}Stores update automatically for existing users.${RESET}`);5253 5354 // ─── CI gate ──────────────────────────────────────────────────────────────────55+// PEACH_RELEASE_BROWSER_GATE=1 is injected by releaseGateEnv for this child.5456 // PB-26-211: CI runs with a minimal safe environment — no store credentials.5557 5658 runCommand("npm", ["run", "ci"], {5759 label: "Running CI gate",58- env: safeEnv(),60+ env: releaseGateEnv(),5961 });6062 success("All tests pass");6163 scripts/run-release-browser-gate.ts+10 −0
@@ -0,0 +1,10 @@1+import { resolveBin } from "./release-lib";2+import { assertValidPlaywrightJsonReport } from "./release-gate";3+4+const result = Bun.spawnSync([resolveBin("playwright"), "test", "--project=chrome-release", "--reporter=json"], {5+ stdout: "pipe",6+ stderr: "inherit",7+ env: { ...process.env, PEACH_RELEASE_BROWSER_GATE: "1" },8+});9+if (result.exitCode !== 0) process.exit(result.exitCode);10+assertValidPlaywrightJsonReport(JSON.parse(result.stdout.toString()));