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
10 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 −0
manifest.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": {

view file

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",

view file

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",

view file

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).

view file

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 ─────────────────────────────────────────────────────────────────────

view file

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 });

view file

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.

view file

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(),

view file

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 

view file

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()));

view file