07cd947496bb

consolidate source archive behind one packager with crypto-core, SOURCE-MANIFEST, and reproducibility (PB-26-212, v5.0.68)

time
2026-09-03 19:03 UTC · 1 month ago
machine
koh2
agent
muse-spark-1.3-contributor-free
session
PB-26-212 source package consolidation
parent
700030ce6645
11 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+2 −2scripts/package-source.test.ts+253 −0scripts/package-source.ts+291 −0scripts/release-firefox.ts+2 −2scripts/release-gate.test.ts+1 −1scripts/release-lib.test.ts+1 −3scripts/release-lib.ts+1 −42scripts/release.test.ts+10 −44scripts/release.ts+2 −2
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "5.0.67",4+  "version": "5.0.68",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.67",5+  "version": "5.0.68",66   "description": "Zero-knowledge password manager",77   "action": {88     "default_popup": "src/popup/index.html",

view file

package.json+2 −2
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "5.0.67",3+  "version": "5.0.68",44   "description": "Zero-knowledge password manager",55   "license": "GPL-3.0-only",66   "type": "module",@@ -22,7 +22,7 @@2222     "test:e2e": "npm run build:chrome && playwright test tests/vault-lifecycle.spec.ts tests/entry-crud.spec.ts tests/search.spec.ts tests/settings.spec.ts tests/lock-screen.spec.ts tests/vault-security.spec.ts tests/sync-alerts.spec.ts tests/pb26016-sender-context.spec.ts",2323     "test:passkeys": "npm run build:e2e && playwright test tests/passkeys.spec.ts",2424     "ci": "bun run scripts/ci.ts",25-    "package:source": "zip -r peach-source.zip src scripts public manifest.json manifest.firefox.json package.json bun.lock tsconfig.json vite.config.ts --exclude 'node_modules/*' --exclude 'dist-*/*' --exclude '*.zip'",25+    "package:source": "bun run scripts/package-source.ts",2626     "release": "bun run scripts/release.ts",2727     "release:chrome": "bun run scripts/release.ts --chrome-only",2828     "release:firefox": "bun run scripts/release-firefox.ts",

view file

scripts/package-source.test.ts+253 −0
@@ -0,0 +1,253 @@1+import { execFileSync } from "node:child_process";2+import { createHash } from "node:crypto";3+import { existsSync, readFileSync } from "node:fs";4+import { fileURLToPath } from "node:url";5+import { dirname, join, resolve } from "node:path";6+import { describe, expect, it } from "vitest";7+8+import {9+  buildSourceManifest,10+  collectSourceFiles,11+  packageSourceArchive,12+  type SourceManifest,13+} from "./package-source";14+15+const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");16+17+const REQUIRED_FILES = [18+  "src/shared/noise-core-wasm/peach_noise_core_bg.wasm",19+  "crypto-core/Cargo.toml",20+  "crypto-core/Cargo.lock",21+  "crypto-core/src/lib.rs",22+  "crypto-core/src/jni_bridge.rs",23+  "crypto-core/.cargo/config.toml",24+  "scripts/build-noise-core.mjs",25+  "scripts/check-noise-core.mjs",26+  "manifest.json",27+  "manifest.firefox.json",28+  "package.json",29+  "bun.lock",30+  "tsconfig.json",31+  "vite.config.ts",32+];33+34+const EXCLUDE_PATTERNS = [35+  /node_modules\//,36+  /dist-[^/]+\//,37+  /\.zip$/,38+  /crypto-core\/target\//,39+  /crypto-core\/generated\//,40+  /\.DS_Store$/,41+  /__MACOSX\//,42+];43+44+function listArchive(archivePath: string): string[] {45+  return execFileSync("unzip", ["-Z1", archivePath], { encoding: "utf8" })46+    .split("\n")47+    .filter(Boolean);48+}49+50+describe("PB-26-212: source package — required files", () => {51+  it("collects all security-critical files including crypto-core", () => {52+    const files = collectSourceFiles();53+    for (const required of REQUIRED_FILES) {54+      expect(files).toContain(required);55+    }56+  });57+58+  it("includes crypto-core/Cargo.toml, Cargo.lock, and src/lib.rs (the core regression)", () => {59+    const files = collectSourceFiles();60+    expect(files).toContain("crypto-core/Cargo.toml");61+    expect(files).toContain("crypto-core/Cargo.lock");62+    expect(files).toContain("crypto-core/src/lib.rs");63+  });64+65+  it("excludes forbidden directories and files", () => {66+    const files = collectSourceFiles();67+    for (const file of files) {68+      for (const pattern of EXCLUDE_PATTERNS) {69+        expect(pattern.test(file)).toBe(false);70+      }71+    }72+  });73+74+  it("collects files sorted and without duplicates", () => {75+    const files = collectSourceFiles();76+    const sorted = [...files].sort();77+    expect(files).toEqual(sorted);78+    expect(new Set(files).size).toBe(files.length);79+  });80+});81+82+describe("PB-26-212: SOURCE-MANIFEST.json", () => {83+  it("has correct structure with version, noise WASM SHA-256, toolchain, and file hashes", () => {84+    const files = collectSourceFiles();85+    const manifest = buildSourceManifest(files, "0.0.0-test");86+87+    expect(manifest.peachVersion).toBe("0.0.0-test");88+    expect(manifest.noiseWasmSha256).toMatch(/^[0-9a-f]{64}$/);89+    expect(manifest.toolchain).toBeDefined();90+    expect(manifest.toolchain.bun).toBeDefined();91+    expect(manifest.toolchain.node).toBeDefined();92+    expect(manifest.toolchain.rust).toBeDefined();93+94+    expect(manifest.files).toHaveLength(files.length);95+    for (const entry of manifest.files) {96+      expect(entry.path).toBeDefined();97+      expect(entry.sha256).toMatch(/^[0-9a-f]{64}$/);98+    }99+  });100+101+  it("noiseWasmSha256 matches the actual shipped WASM", () => {102+    const wasmPath = join(103+      ROOT,104+      "src/shared/noise-core-wasm/peach_noise_core_bg.wasm",105+    );106+    const expected = createHash("sha256")107+      .update(readFileSync(wasmPath))108+      .digest("hex");109+    const files = collectSourceFiles();110+    const manifest = buildSourceManifest(files, "0.0.0-test");111+    expect(manifest.noiseWasmSha256).toBe(expected);112+  });113+114+  it("file hashes match actual file contents", () => {115+    const files = collectSourceFiles();116+    const manifest = buildSourceManifest(files, "0.0.0-test");117+    for (const entry of manifest.files) {118+      const actual = createHash("sha256")119+        .update(readFileSync(join(ROOT, entry.path)))120+        .digest("hex");121+      expect(entry.sha256).toBe(actual);122+    }123+  });124+});125+126+describe("PB-26-212: source archive contents", () => {127+  it("creates an archive containing every required file exactly once", () => {128+    const archivePath = packageSourceArchive("0.0.0-test");129+    expect(existsSync(archivePath)).toBe(true);130+131+    const contents = listArchive(archivePath);132+    for (const required of REQUIRED_FILES) {133+      const matches = contents.filter((c) => c === required);134+      expect(matches).toHaveLength(1);135+    }136+137+    expect(contents).toContain("SOURCE-MANIFEST.json");138+  });139+140+  it("excludes forbidden paths from the archive", () => {141+    const archivePath = packageSourceArchive("0.0.0-test");142+    const contents = listArchive(archivePath);143+    for (const file of contents) {144+      for (const pattern of EXCLUDE_PATTERNS) {145+        expect(pattern.test(file)).toBe(false);146+      }147+    }148+  });149+150+  it("archive SOURCE-MANIFEST.json contains the noise WASM SHA-256 and version", () => {151+    const archivePath = packageSourceArchive("0.0.0-test");152+    const manifestJson = execFileSync(153+      "unzip",154+      ["-p", archivePath, "SOURCE-MANIFEST.json"],155+      { encoding: "utf8" },156+    );157+    const manifest: SourceManifest = JSON.parse(manifestJson);158+159+    expect(manifest.peachVersion).toBe("0.0.0-test");160+    expect(manifest.noiseWasmSha256).toMatch(/^[0-9a-f]{64}$/);161+    expect(Array.isArray(manifest.files)).toBe(true);162+    expect(manifest.files.length).toBeGreaterThan(0);163+  });164+165+  it("archive contains the same crypto-core source as the repo (rebuild provenance)", () => {166+    const archivePath = packageSourceArchive("0.0.0-test");167+168+    const archiveLibRs = execFileSync(169+      "unzip",170+      ["-p", archivePath, "crypto-core/src/lib.rs"],171+      { encoding: "utf8" },172+    );173+    const repoLibRs = readFileSync(174+      join(ROOT, "crypto-core/src/lib.rs"),175+      "utf8",176+    );177+    expect(archiveLibRs).toBe(repoLibRs);178+179+    const archiveBuildScript = execFileSync(180+      "unzip",181+      ["-p", archivePath, "scripts/build-noise-core.mjs"],182+      { encoding: "utf8" },183+    );184+    const repoBuildScript = readFileSync(185+      join(ROOT, "scripts/build-noise-core.mjs"),186+      "utf8",187+    );188+    expect(archiveBuildScript).toBe(repoBuildScript);189+190+    const manifestJson = execFileSync(191+      "unzip",192+      ["-p", archivePath, "SOURCE-MANIFEST.json"],193+      { encoding: "utf8" },194+    );195+    const manifest: SourceManifest = JSON.parse(manifestJson);196+    const wasmPath = join(197+      ROOT,198+      "src/shared/noise-core-wasm/peach_noise_core_bg.wasm",199+    );200+    const expectedWasmHash = createHash("sha256")201+      .update(readFileSync(wasmPath))202+      .digest("hex");203+    expect(manifest.noiseWasmSha256).toBe(expectedWasmHash);204+  });205+});206+207+describe("PB-26-212: reproducibility", () => {208+  it("produces byte-identical archives from the same baseline", () => {209+    const archive1 = packageSourceArchive("0.0.0-repro");210+    const hash1 = createHash("sha256")211+      .update(readFileSync(archive1))212+      .digest("hex");213+214+    const archive2 = packageSourceArchive("0.0.0-repro");215+    const hash2 = createHash("sha256")216+      .update(readFileSync(archive2))217+      .digest("hex");218+219+    expect(hash1).toBe(hash2);220+  });221+});222+223+describe("PB-26-212: all entry points use the shared packager", () => {224+  it("package.json package:source calls scripts/package-source.ts", () => {225+    const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));226+    expect(pkg.scripts["package:source"]).toContain("package-source.ts");227+  });228+229+  it("package.json package:source does not contain a duplicate file list", () => {230+    const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));231+    const script = pkg.scripts["package:source"];232+    expect(script).not.toContain("manifest.json manifest.firefox.json");233+    expect(script).not.toMatch(/^zip\s/);234+  });235+236+  it("release.ts imports and calls packageSourceArchive", () => {237+    const source = readFileSync(join(ROOT, "scripts/release.ts"), "utf8");238+    expect(source).toContain("packageSourceArchive");239+  });240+241+  it("release-firefox.ts imports and calls packageSourceArchive", () => {242+    const source = readFileSync(243+      join(ROOT, "scripts/release-firefox.ts"),244+      "utf8",245+    );246+    expect(source).toContain("packageSourceArchive");247+  });248+249+  it("release-lib.ts does not contain a duplicate zip file list", () => {250+    const source = readFileSync(join(ROOT, "scripts/release-lib.ts"), "utf8");251+    expect(source).not.toContain("packageFirefoxSourceZip");252+  });253+});

view file

scripts/package-source.ts+291 −0
@@ -0,0 +1,291 @@1+#!/usr/bin/env bun2+// scripts/package-source.ts3+// PB-26-212: ONE source-archive definition for standalone, Chrome+Firefox,4+// and Firefox-only release paths. Includes crypto-core, generates5+// SOURCE-MANIFEST.json, and produces deterministic (reproducible) ZIPs.6+7+import { createHash } from "node:crypto";8+import { crc32, deflateRawSync } from "node:zlib";9+import {10+  existsSync,11+  mkdirSync,12+  readFileSync,13+  readdirSync,14+  writeFileSync,15+} from "node:fs";16+import { basename, join, relative, resolve } from "node:path";17+import { fileURLToPath } from "node:url";18+19+const SCRIPT_DIR = resolve(dirname_safe());20+const REPO_ROOT = resolve(SCRIPT_DIR, "..");21+22+function dirname_safe(): string {23+  try {24+    return fileURLToPath(new URL(".", import.meta.url));25+  } catch {26+    return __dirname;27+  }28+}29+30+// ─── Exclusions ─────────────────────────────────────────────────────────────31+32+const EXCLUDE_PATTERNS = [33+  /node_modules\//,34+  /dist-[^/]+\//,35+  /\.zip$/,36+  /crypto-core\/target\//,37+  /crypto-core\/generated\//,38+  /\.DS_Store$/,39+  /__MACOSX\//,40+];41+42+function shouldExclude(relPath: string): boolean {43+  return EXCLUDE_PATTERNS.some((p) => p.test(relPath));44+}45+46+// ─── File collection ────────────────────────────────────────────────────────47+48+const INCLUDE_DIRS = ["src", "scripts", "public", "crypto-core"];49+const INCLUDE_FILES = [50+  "manifest.json",51+  "manifest.firefox.json",52+  "package.json",53+  "bun.lock",54+  "tsconfig.json",55+  "vite.config.ts",56+];57+58+function walkDir(dir: string, files: string[]): void {59+  let entries: ReturnType<typeof readdirSync>;60+  try {61+    entries = readdirSync(dir, { withFileTypes: true });62+  } catch {63+    return;64+  }65+  for (const entry of entries) {66+    const fullPath = join(dir, entry.name);67+    const relPath = relative(REPO_ROOT, fullPath);68+    if (entry.isDirectory()) {69+      if (shouldExclude(`${relPath}/`)) continue;70+      walkDir(fullPath, files);71+    } else if (entry.isFile()) {72+      if (shouldExclude(relPath)) continue;73+      files.push(relPath);74+    }75+  }76+}77+78+export function collectSourceFiles(): string[] {79+  const files: string[] = [];80+  for (const dir of INCLUDE_DIRS) {81+    const absDir = join(REPO_ROOT, dir);82+    if (existsSync(absDir)) {83+      walkDir(absDir, files);84+    }85+  }86+  for (const file of INCLUDE_FILES) {87+    if (existsSync(join(REPO_ROOT, file))) {88+      files.push(file);89+    }90+  }91+  return [...new Set(files)].sort();92+}93+94+// ─── SOURCE-MANIFEST.json ───────────────────────────────────────────────────95+96+export interface SourceManifest {97+  peachVersion: string;98+  noiseWasmSha256: string;99+  toolchain: {100+    bun: string;101+    node: string;102+    rust: string;103+  };104+  files: Array<{ path: string; sha256: string }>;105+}106+107+function sha256(data: Buffer | string): string {108+  return createHash("sha256").update(data).digest("hex");109+}110+111+function getNoiseWasmSha256(): string {112+  const wasmPath = join(113+    REPO_ROOT,114+    "src/shared/noise-core-wasm/peach_noise_core_bg.wasm",115+  );116+  return sha256(readFileSync(wasmPath));117+}118+119+function readToolchain(): { bun: string; node: string; rust: string } {120+  const pkg = JSON.parse(121+    readFileSync(join(REPO_ROOT, "package.json"), "utf8"),122+  );123+  const bunVersion = (pkg.packageManager ?? "").replace("bun@", "");124+125+  const cargoToml = readFileSync(126+    join(REPO_ROOT, "crypto-core/Cargo.toml"),127+    "utf8",128+  );129+  const rustMatch = cargoToml.match(/rust-version\s*=\s*"([^"]+)"/);130+  const rustVersion = rustMatch ? rustMatch[1] : "unknown";131+132+  return {133+    bun: bunVersion,134+    node: pkg.engines?.node ?? "unknown",135+    rust: rustVersion,136+  };137+}138+139+export function buildSourceManifest(140+  files: string[],141+  version: string,142+): SourceManifest {143+  const entries = files.map((path) => ({144+    path,145+    sha256: sha256(readFileSync(join(REPO_ROOT, path))),146+  }));147+  return {148+    peachVersion: version,149+    noiseWasmSha256: getNoiseWasmSha256(),150+    toolchain: readToolchain(),151+    files: entries,152+  };153+}154+155+// ─── Deterministic ZIP writer ───────────────────────────────────────────────156+// Fixed DOS timestamp (1980-01-01 00:00:00) for reproducibility.157+158+const FIXED_DOS_TIME = 0x0000;159+const FIXED_DOS_DATE = 0x0021;160+161+interface ZipEntry {162+  path: string;163+  data: Buffer;164+}165+166+export function createDeterministicZip(entries: ZipEntry[]): Buffer {167+  const sorted = [...entries].sort((a, b) =>168+    a.path.localeCompare(b.path),169+  );170+  const localParts: Buffer[] = [];171+  const centralParts: Buffer[] = [];172+  let offset = 0;173+174+  for (const entry of sorted) {175+    const filename = Buffer.from(entry.path, "utf8");176+    const compressed = deflateRawSync(entry.data, { level: 9 });177+    const crc = crc32(entry.data) >>> 0;178+    const compSize = compressed.length;179+    const uncompSize = entry.data.length;180+181+    // Local file header182+    const lh = Buffer.alloc(30 + filename.length);183+    lh.writeUInt32LE(0x04034b50, 0);184+    lh.writeUInt16LE(20, 4);185+    lh.writeUInt16LE(0, 6);186+    lh.writeUInt16LE(8, 8);187+    lh.writeUInt16LE(FIXED_DOS_TIME, 10);188+    lh.writeUInt16LE(FIXED_DOS_DATE, 12);189+    lh.writeUInt32LE(crc, 14);190+    lh.writeUInt32LE(compSize, 18);191+    lh.writeUInt32LE(uncompSize, 22);192+    lh.writeUInt16LE(filename.length, 26);193+    lh.writeUInt16LE(0, 28);194+    filename.copy(lh, 30);195+196+    localParts.push(lh, compressed);197+198+    // Central directory record199+    const cd = Buffer.alloc(46 + filename.length);200+    cd.writeUInt32LE(0x02014b50, 0);201+    cd.writeUInt16LE(20, 4);202+    cd.writeUInt16LE(20, 6);203+    cd.writeUInt16LE(0, 8);204+    cd.writeUInt16LE(8, 10);205+    cd.writeUInt16LE(FIXED_DOS_TIME, 12);206+    cd.writeUInt16LE(FIXED_DOS_DATE, 14);207+    cd.writeUInt32LE(crc, 16);208+    cd.writeUInt32LE(compSize, 20);209+    cd.writeUInt32LE(uncompSize, 24);210+    cd.writeUInt16LE(filename.length, 28);211+    cd.writeUInt16LE(0, 30);212+    cd.writeUInt16LE(0, 32);213+    cd.writeUInt16LE(0, 34);214+    cd.writeUInt16LE(0, 36);215+    cd.writeUInt32LE(0, 38);216+    cd.writeUInt32LE(offset, 42);217+    filename.copy(cd, 46);218+219+    centralParts.push(cd);220+    offset += lh.length + compressed.length;221+  }222+223+  const centralDir = Buffer.concat(centralParts);224+  const centralOffset = offset;225+  const centralSize = centralDir.length;226+227+  // End of central directory record228+  const eocd = Buffer.alloc(22);229+  eocd.writeUInt32LE(0x06054b50, 0);230+  eocd.writeUInt16LE(0, 4);231+  eocd.writeUInt16LE(0, 6);232+  eocd.writeUInt16LE(sorted.length, 8);233+  eocd.writeUInt16LE(sorted.length, 10);234+  eocd.writeUInt32LE(centralSize, 12);235+  eocd.writeUInt32LE(centralOffset, 16);236+  eocd.writeUInt16LE(0, 20);237+238+  return Buffer.concat([...localParts, centralDir, eocd]);239+}240+241+// ─── Package source archive ─────────────────────────────────────────────────242+243+function readVersion(manifestPath: string): string {244+  const manifest = JSON.parse(245+    readFileSync(join(REPO_ROOT, manifestPath), "utf8"),246+  );247+  if (!manifest.version) {248+    throw new Error(`Could not read version from ${manifestPath}`);249+  }250+  return manifest.version;251+}252+253+function ensureArtifactsDir(): string {254+  const dir = join(REPO_ROOT, "artifacts");255+  if (!existsSync(dir)) mkdirSync(dir);256+  return dir;257+}258+259+export function packageSourceArchive(version: string): string {260+  const artifactsDir = ensureArtifactsDir();261+  const archivePath = join(artifactsDir, `peach-source-${version}.zip`);262+263+  const files = collectSourceFiles();264+  const manifest = buildSourceManifest(files, version);265+  const manifestJson = `${JSON.stringify(manifest, null, 2)}\n`;266+267+  const entries: ZipEntry[] = files.map((path) => ({268+    path,269+    data: readFileSync(join(REPO_ROOT, path)),270+  }));271+  entries.push({272+    path: "SOURCE-MANIFEST.json",273+    data: Buffer.from(manifestJson, "utf8"),274+  });275+276+  const zipBuffer = createDeterministicZip(entries);277+  writeFileSync(archivePath, zipBuffer);278+279+  console.log(280+    `Packaged source archive → ${basename(archivePath)} (${files.length} files + SOURCE-MANIFEST.json)`,281+  );282+  return archivePath;283+}284+285+// ─── Standalone execution ───────────────────────────────────────────────────286+287+if (import.meta.main) {288+  const version = readVersion("manifest.json");289+  const archivePath = packageSourceArchive(version);290+  console.log(`\nSource archive: ${archivePath}`);291+}

view file

scripts/release-firefox.ts+2 −2
@@ -14,7 +14,7 @@1414   readVersion,1515   runCommand,1616   safeEnv,17-  packageFirefoxSourceZip,17+  packageSourceArchive,1818   signFirefox,1919 } from "./release-lib";2020 @@ -43,7 +43,7 @@4343 // AMO requires source for any extension with minified/transpiled code.4444 // This zips everything a reviewer needs to build Peach from source.4545 46-const sourceZip = packageFirefoxSourceZip(version);46+const sourceZip = packageSourceArchive(version);4747 4848 // ─── Sign and submit ───────────────────────────────────────────────────────────4949 // PB-26-211: AMO credentials are read and validated here, immediately before

view file

scripts/release-gate.test.ts+1 −1
@@ -97,7 +97,7 @@9797       // Packaging/upload calls appear after CI. PB-26-211: these are now9898       // function calls to uploadChrome / signFirefox / packageFirefoxSourceZip.9999       // Match calls (with paren), not import names.100-      const packageIndex = source.search(/uploadChrome\(|signFirefox\(|packageFirefoxSourceZip\(/);100+      const packageIndex = source.search(/uploadChrome\(|signFirefox\(|packageSourceArchive\(/);101101       if (packageIndex > -1) {102102         expect(ciIndex).toBeLessThan(packageIndex);103103       }

view file

scripts/release-lib.test.ts+1 −3
@@ -13,7 +13,6 @@1313   resolveBin,1414   preflight,1515   packageChromeZip,16-  packageFirefoxSourceZip,1716   uploadChrome,1817   signFirefox,1918   CWS_SECRET_NAMES,@@ -87,11 +86,10 @@8786     for (const name of ALL_SECRET_NAMES) setEnv(name, "secret-value");8887 8988     packageChromeZip("9.9.9", fake);90-    packageFirefoxSourceZip("9.9.9", fake);9189     uploadChrome("9.9.9", fake);9290     signFirefox("/tmp/fake-source.zip", fake);9391 94-    expect(captured.length).toBeGreaterThanOrEqual(4);92+    expect(captured.length).toBeGreaterThanOrEqual(3);9593     for (const spec of captured) {9694       expect(spec.executable).not.toBe("npx");9795       expect(spec.args).not.toContain("npx");

view file

scripts/release-lib.ts+1 −42
@@ -247,48 +247,7 @@247247   );248248 }249249 250-export function packageFirefoxSourceZip(251-  version: string,252-  runner: Runner = runCommand,253-): string {254-  const artifactsDir = ensureArtifactsDir();255-  const sourceZip = join(artifactsDir, `peach-source-${version}.zip`);256-  runner(257-    "zip",258-    [259-      "-r",260-      sourceZip,261-      "src",262-      "scripts",263-      "public",264-      "crypto-core",265-      "manifest.json",266-      "manifest.firefox.json",267-      "package.json",268-      "bun.lock",269-      "tsconfig.json",270-      "vite.config.ts",271-      "--exclude",272-      "node_modules/*",273-      "--exclude",274-      "dist-*/*",275-      "--exclude",276-      "crypto-core/target/*",277-      "--exclude",278-      "crypto-core/generated/*",279-      "--exclude",280-      "*.zip",281-      "--exclude",282-      ".DS_Store",283-      "--exclude",284-      "__MACOSX/*",285-    ],286-    {287-      label: "Packaging source archive for AMO",288-    },289-  );290-  return sourceZip;291-}250+export { packageSourceArchive } from "./package-source";292251 293252 export function signFirefox(294253   sourceZip: string,

view file

scripts/release.test.ts+10 −44
@@ -1,13 +1,12 @@11 import { execFileSync } from "node:child_process";2-import { existsSync, mkdtempSync, readFileSync } from "node:fs";2+import { existsSync, readFileSync } from "node:fs";33 import { fileURLToPath } from "node:url";44 import { dirname, join, resolve } from "node:path";5-import { tmpdir } from "node:os";65 import { describe, expect, it } from "vitest";76 7+import { packageSourceArchive } from "./package-source";8+89 const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");9-// PB-26-211: packaging/signing logic moved to release-lib.ts. Both release10-// entry points import from it, so verifying release-lib.ts covers both.1110 const RELEASE_LIB = "scripts/release-lib.ts";1211 1312 function readReleaseLib(): string {@@ -17,49 +16,19 @@1716 describe("Firefox release source submission", () => {1817   it("passes the generated source archive to web-ext sign with --upload-source-code", () => {1918     const source = readReleaseLib();20-    // web-ext sign is called with --upload-source-code and the source zip path.2119     expect(source).toContain('"--upload-source-code"');2220     expect(source).toContain("sourceZip");2321     expect(source.match(/--upload-source-code/g)).toHaveLength(1);2422   });2523 26-  it("builds an archive containing the reviewed crypto source and Noise build script", () => {27-    const archive = join(mkdtempSync(join(tmpdir(), "peach-source-test-")), "source.zip");28-    execFileSync(29-      "zip",30-      [31-        "-q",32-        "-r",33-        archive,34-        "src",35-        "scripts",36-        "public",37-        "crypto-core",38-        "manifest.json",39-        "manifest.firefox.json",40-        "package.json",41-        "bun.lock",42-        "tsconfig.json",43-        "vite.config.ts",44-        "--exclude",45-        "node_modules/*",46-        "--exclude",47-        "dist-*/*",48-        "--exclude",49-        "crypto-core/target/*",50-        "--exclude",51-        "crypto-core/generated/*",52-        "--exclude",53-        "*.zip",54-        "--exclude",55-        ".DS_Store",56-        "--exclude",57-        "__MACOSX/*",58-      ],59-      { cwd: ROOT },60-    );24+  it("shared packager builds an archive containing the reviewed crypto source and Noise build script", () => {25+    const archivePath = packageSourceArchive("0.0.0-release-test");26+    expect(existsSync(archivePath)).toBe(true);6127 62-    const contents = execFileSync("unzip", ["-Z1", archive], { encoding: "utf8" }).split("\n");28+    const contents = execFileSync("unzip", ["-Z1", archivePath], {29+      encoding: "utf8",30+    }).split("\n");31+6332     expect(contents).toEqual(6433       expect.arrayContaining([6534         "crypto-core/Cargo.toml",@@ -69,9 +38,6 @@6938       ]),7039     );7140 72-    // PB-26-211: verify release-lib.ts includes crypto-core in the source archive.73-    const source = readReleaseLib();74-    expect(source).toContain('"crypto-core"');7541     expect(existsSync(join(ROOT, "crypto-core/Cargo.toml"))).toBe(true);7642     expect(existsSync(join(ROOT, "crypto-core/Cargo.lock"))).toBe(true);7743     expect(existsSync(join(ROOT, "crypto-core/src/lib.rs"))).toBe(true);

view file

scripts/release.ts+2 −2
@@ -19,7 +19,7 @@1919   runCommand,2020   safeEnv,2121   uploadChrome,22-  packageFirefoxSourceZip,22+  packageSourceArchive,2323   signFirefox,2424 } from "./release-lib";2525 @@ -91,7 +91,7 @@9191 9292 if (!CHROME_ONLY) {9393   console.log(`\n${ACCENT}${BOLD}Firefox Add-ons (AMO)${RESET}`);94-  const sourceZip = packageFirefoxSourceZip(version);94+  const sourceZip = packageSourceArchive(version);9595   signFirefox(sourceZip);9696   success(`Firefox v${version} submitted with source archive retained in artifacts/`);9797 }

view file