From 99ff3987b3d8f6f3cb3c4421c047d799187b3166 Mon Sep 17 00:00:00 2001 From: Ryan B Date: Fri, 9 Oct 2026 00:22:57 -0400 Subject: [PATCH 1/4] Add opt-in portable unsigned packaging without changing release workflow --- README.md | 5 ++ package.json | 3 +- scripts/package-extension.mjs | 108 ++++++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 scripts/package-extension.mjs diff --git a/README.md b/README.md index 6916e6a..d8ef6e8 100644 --- a/README.md +++ b/README.md @@ -193,3 +193,8 @@ This optional test uses `web-ext` and a disposable headless profile, never your signed-in profile. It exercises the real compatibility layer against local HTTP fixtures and verifies lifecycle delivery and foreground-tab preservation. It does not exercise the signed-in Codex native transport. + +For an unsigned review build on macOS, Linux, or Windows, commit the working tree +and run `pnpm package:portable`. It uses Node and Git, writes ZIP/XPI and a review-source +archive under `dist/`, and preserves the release checksum filenames. The existing +PowerShell packaging command and release workflow are unchanged. diff --git a/package.json b/package.json index 6326b76..9d5f145 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "version:set": "node scripts/set-version.mjs", "test": "npm run check && node tests/test-amo-auth.mjs && node tests/test-companion-required.mjs && node tests/test-sidepanel-focus-compat.mjs && node tests/test-sidepanel-host-access.mjs && node tests/test-sidepanel-bootstrap.mjs && node tests/test-sidepanel-companion-setup.mjs && node tests/test-chatgpt-feature-parity.mjs && node tests/test-firefox-compat.mjs && node tests/test-editing-assets.mjs && node tests/test-native-host.mjs", "package": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/package-extension.ps1", - "test:live": "node tests/test-firefox-lifecycle-live.mjs" + "test:live": "node tests/test-firefox-lifecycle-live.mjs", + "package:portable": "node scripts/package-extension.mjs" } } diff --git a/scripts/package-extension.mjs b/scripts/package-extension.mjs new file mode 100644 index 0000000..92f2fc0 --- /dev/null +++ b/scripts/package-extension.mjs @@ -0,0 +1,108 @@ +#!/usr/bin/env node +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { execFileSync } from "node:child_process"; +import { deflateRawSync } from "node:zlib"; +import { createHash } from "node:crypto"; + +const root = path.dirname(path.dirname(fileURLToPath(import.meta.url))); +process.chdir(root); +for (const script of ["check-version.mjs", "verify-extension.mjs"]) { + execFileSync(process.execPath, [path.join("scripts", script)], { stdio: "inherit" }); +} +if (execFileSync("git", ["status", "--porcelain", "--untracked-files=normal"], { encoding: "utf8" }).trim()) { + throw new Error("Commit the source before packaging so the source archive matches the extension."); +} +const { version } = JSON.parse(fs.readFileSync("version.json", "utf8")); +const stem = `codex-computer-use-firefox-zen-${version}`; +const dist = path.join(root, "dist"); +fs.mkdirSync(dist, { recursive: true }); + +const crcTable = Array.from({ length: 256 }, (_, value) => { + for (let bit = 0; bit < 8; bit++) value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1; + return value >>> 0; +}); +function crc32(data) { + let crc = 0xffffffff; + for (const byte of data) crc = crcTable[(crc ^ byte) & 255] ^ (crc >>> 8); + return (crc ^ 0xffffffff) >>> 0; +} + +const files = []; +function walk(directory, prefix = "") { + const entries = fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0); + for (const entry of entries) { + const name = prefix + entry.name; + const full = path.join(directory, entry.name); + if (entry.isSymbolicLink()) throw new Error(`Refusing to package symlink ${name}`); + if (entry.isDirectory()) walk(full, `${name}/`); + else if (entry.isFile()) files.push({ name, data: fs.readFileSync(full) }); + } +} +walk(path.join(root, "extension")); +if (files.length >= 65535) throw new Error("ZIP64 is not supported by this packager."); + +const parts = []; +const central = []; +let offset = 0; +for (const { name, data } of files) { + const filename = Buffer.from(name); + const compressed = deflateRawSync(data, { level: 9 }); + const crc = crc32(data); + if (filename.length > 65535 || data.length > 0xffffffff || offset > 0xffffffff) { + throw new Error(`ZIP64 is required for ${name}.`); + } + // ZIP-standard paths, UTF-8 names, and a fixed 1980-01-01 timestamp. + const local = Buffer.alloc(30); + local.writeUInt32LE(0x04034b50); + local.writeUInt16LE(20, 4); + local.writeUInt16LE(0x800, 6); + local.writeUInt16LE(8, 8); + local.writeUInt16LE(0x21, 12); + local.writeUInt32LE(crc, 14); + local.writeUInt32LE(compressed.length, 18); + local.writeUInt32LE(data.length, 22); + local.writeUInt16LE(filename.length, 26); + const entry = Buffer.alloc(46); + entry.writeUInt32LE(0x02014b50); + entry.writeUInt16LE(20, 4); + entry.writeUInt16LE(20, 6); + entry.writeUInt16LE(0x800, 8); + entry.writeUInt16LE(8, 10); + entry.writeUInt16LE(0x21, 14); + entry.writeUInt32LE(crc, 16); + entry.writeUInt32LE(compressed.length, 20); + entry.writeUInt32LE(data.length, 24); + entry.writeUInt16LE(filename.length, 28); + entry.writeUInt32LE(offset, 42); + parts.push(local, filename, compressed); + central.push(entry, filename); + offset += local.length + filename.length + compressed.length; +} +const centralData = Buffer.concat(central); +if (offset + centralData.length > 0xffffffff) throw new Error("ZIP64 is required for this archive."); +const end = Buffer.alloc(22); +end.writeUInt32LE(0x06054b50); +end.writeUInt16LE(files.length, 8); +end.writeUInt16LE(files.length, 10); +end.writeUInt32LE(centralData.length, 12); +end.writeUInt32LE(offset, 16); +const archive = Buffer.concat([...parts, centralData, end]); +for (const suffix of ["zip", "xpi"]) fs.writeFileSync(path.join(dist, `${stem}.${suffix}`), archive); +execFileSync("git", ["archive", "--format=zip", `--output=${path.join(dist, `${stem}-source.zip`)}`, "HEAD"]); + +const artifacts = [`${stem}.zip`, `${stem}.xpi`, `${stem}-source.zip`]; +const checksums = artifacts.map((name) => { + const checksum = `${createHash("sha256").update(fs.readFileSync(path.join(dist, name))).digest("hex")} ${name}\n`; + // Preserve the existing release workflow's per-artifact checksum names. + fs.writeFileSync(path.join(dist, name.replace(/\.(?:zip|xpi)$/u, ".sha256")), checksum); + return checksum; +}); +fs.writeFileSync(path.join(dist, "SHA256SUMS"), checksums.join("")); +console.log(JSON.stringify({ + version, + commit: execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim(), + files: files.length, + artifacts: artifacts.map((name) => ({ path: path.join(dist, name), bytes: fs.statSync(path.join(dist, name)).size })), +}, null, 2)); From 890ba8d918e56fea84ace46c00c6f6197ce8f2f9 Mon Sep 17 00:00:00 2001 From: Ryan B Date: Fri, 9 Oct 2026 00:23:22 -0400 Subject: [PATCH 2/4] Keep ZIP and XPI checksum filename references distinct --- scripts/package-extension.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/package-extension.mjs b/scripts/package-extension.mjs index 92f2fc0..6530e8d 100644 --- a/scripts/package-extension.mjs +++ b/scripts/package-extension.mjs @@ -96,7 +96,7 @@ const artifacts = [`${stem}.zip`, `${stem}.xpi`, `${stem}-source.zip`]; const checksums = artifacts.map((name) => { const checksum = `${createHash("sha256").update(fs.readFileSync(path.join(dist, name))).digest("hex")} ${name}\n`; // Preserve the existing release workflow's per-artifact checksum names. - fs.writeFileSync(path.join(dist, name.replace(/\.(?:zip|xpi)$/u, ".sha256")), checksum); + fs.writeFileSync(path.join(dist, name.endsWith(".zip") ? name.slice(0, -4) + ".sha256" : name + ".sha256"), checksum); return checksum; }); fs.writeFileSync(path.join(dist, "SHA256SUMS"), checksums.join("")); From 3bb875f9c8dd994459b6c8fb1a6b7b6d1cc291b5 Mon Sep 17 00:00:00 2001 From: Ryan B Date: Fri, 9 Oct 2026 06:07:39 +0000 Subject: [PATCH 3/4] Package extension and review source from one committed snapshot --- package.json | 3 +- scripts/package-extension.mjs | 143 +++++++++++++++++------------- tests/test-portable-packaging.mjs | 64 +++++++++++++ 3 files changed, 149 insertions(+), 61 deletions(-) create mode 100644 tests/test-portable-packaging.mjs diff --git a/package.json b/package.json index 9d5f145..c0ba6fe 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,9 @@ "scripts": { "check": "node scripts/check-version.mjs && node scripts/verify-extension.mjs", "version:set": "node scripts/set-version.mjs", - "test": "npm run check && node tests/test-amo-auth.mjs && node tests/test-companion-required.mjs && node tests/test-sidepanel-focus-compat.mjs && node tests/test-sidepanel-host-access.mjs && node tests/test-sidepanel-bootstrap.mjs && node tests/test-sidepanel-companion-setup.mjs && node tests/test-chatgpt-feature-parity.mjs && node tests/test-firefox-compat.mjs && node tests/test-editing-assets.mjs && node tests/test-native-host.mjs", + "test": "npm run check && node tests/test-amo-auth.mjs && node tests/test-companion-required.mjs && node tests/test-sidepanel-focus-compat.mjs && node tests/test-sidepanel-host-access.mjs && node tests/test-sidepanel-bootstrap.mjs && node tests/test-sidepanel-companion-setup.mjs && node tests/test-chatgpt-feature-parity.mjs && node tests/test-firefox-compat.mjs && node tests/test-editing-assets.mjs && node tests/test-native-host.mjs && node tests/test-portable-packaging.mjs", "package": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/package-extension.ps1", + "test:package": "node tests/test-portable-packaging.mjs", "test:live": "node tests/test-firefox-lifecycle-live.mjs", "package:portable": "node scripts/package-extension.mjs" } diff --git a/scripts/package-extension.mjs b/scripts/package-extension.mjs index 6530e8d..7517b81 100644 --- a/scripts/package-extension.mjs +++ b/scripts/package-extension.mjs @@ -14,7 +14,36 @@ for (const script of ["check-version.mjs", "verify-extension.mjs"]) { if (execFileSync("git", ["status", "--porcelain", "--untracked-files=normal"], { encoding: "utf8" }).trim()) { throw new Error("Commit the source before packaging so the source archive matches the extension."); } -const { version } = JSON.parse(fs.readFileSync("version.json", "utf8")); +// Freeze one tree for both artifacts. Checkout filters (such as autocrlf) +// and ignored local files must never affect the reviewed package bytes. +const commit = execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim(); +const entries = execFileSync("git", ["ls-tree", "-rz", "--full-tree", commit], { + encoding: "utf8", maxBuffer: 16 * 1024 * 1024, +}).split("\0").filter(Boolean).map(entry => { + const match = /^(\d+) (\w+) ([a-f\d]+)\t([\s\S]+)$/u.exec(entry); + if (!match || match[2] !== "blob" || !["100644", "100755"].includes(match[1])) { + throw new Error(`Refusing to package non-regular committed entry ${entry}`); + } + return { name: match[4], object: match[3], mode: parseInt(match[1], 8) }; +}).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0); +const blobs = execFileSync("git", ["cat-file", "--batch"], { + input: entries.map(entry => entry.object).join("\n") + "\n", + maxBuffer: 256 * 1024 * 1024, +}); +let cursor = 0; +const sourceFiles = entries.map(entry => { + const end = blobs.indexOf(10, cursor); + const header = blobs.subarray(cursor, end).toString("ascii").split(" "); + const size = Number(header[2]); + if (end < 0 || header[0] !== entry.object || header[1] !== "blob" || !Number.isSafeInteger(size)) { + throw new Error(`Invalid committed blob for ${entry.name}`); + } + cursor = end + 1; + const data = blobs.subarray(cursor, cursor + size); + cursor += size + 1; + return { name: entry.name, data, mode: entry.mode }; +}); +const { version } = JSON.parse(sourceFiles.find(file => file.name === "version.json").data); const stem = `codex-computer-use-firefox-zen-${version}`; const dist = path.join(root, "dist"); fs.mkdirSync(dist, { recursive: true }); @@ -29,68 +58,62 @@ function crc32(data) { return (crc ^ 0xffffffff) >>> 0; } -const files = []; -function walk(directory, prefix = "") { - const entries = fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0); - for (const entry of entries) { - const name = prefix + entry.name; - const full = path.join(directory, entry.name); - if (entry.isSymbolicLink()) throw new Error(`Refusing to package symlink ${name}`); - if (entry.isDirectory()) walk(full, `${name}/`); - else if (entry.isFile()) files.push({ name, data: fs.readFileSync(full) }); - } -} -walk(path.join(root, "extension")); -if (files.length >= 65535) throw new Error("ZIP64 is not supported by this packager."); +const files = sourceFiles.filter(file => file.name.startsWith("extension/")) + .map(file => ({ name: file.name.slice("extension/".length), data: file.data, mode: file.mode })); -const parts = []; -const central = []; -let offset = 0; -for (const { name, data } of files) { - const filename = Buffer.from(name); - const compressed = deflateRawSync(data, { level: 9 }); - const crc = crc32(data); - if (filename.length > 65535 || data.length > 0xffffffff || offset > 0xffffffff) { - throw new Error(`ZIP64 is required for ${name}.`); +function zip(files) { + if (files.length >= 65535) throw new Error("ZIP64 is not supported by this packager."); + const parts = []; + const central = []; + let offset = 0; + for (const { name, data, mode } of files) { + const filename = Buffer.from(name); + const compressed = deflateRawSync(data, { level: 9 }); + const crc = crc32(data); + if (filename.length > 65535 || data.length > 0xffffffff || offset > 0xffffffff) { + throw new Error(`ZIP64 is required for ${name}.`); + } + // ZIP-standard paths, UTF-8 names, and a fixed 1980-01-01 timestamp. + const local = Buffer.alloc(30); + local.writeUInt32LE(0x04034b50); + local.writeUInt16LE(20, 4); + local.writeUInt16LE(0x800, 6); + local.writeUInt16LE(8, 8); + local.writeUInt16LE(0x21, 12); + local.writeUInt32LE(crc, 14); + local.writeUInt32LE(compressed.length, 18); + local.writeUInt32LE(data.length, 22); + local.writeUInt16LE(filename.length, 26); + const entry = Buffer.alloc(46); + entry.writeUInt32LE(0x02014b50); + entry.writeUInt16LE((3 << 8) | 20, 4); // Unix creator: preserve committed file modes. + entry.writeUInt16LE(20, 6); + entry.writeUInt16LE(0x800, 8); + entry.writeUInt16LE(8, 10); + entry.writeUInt16LE(0x21, 14); + entry.writeUInt32LE(crc, 16); + entry.writeUInt32LE(compressed.length, 20); + entry.writeUInt32LE(data.length, 24); + entry.writeUInt16LE(filename.length, 28); + entry.writeUInt32LE((mode << 16) >>> 0, 38); + entry.writeUInt32LE(offset, 42); + parts.push(local, filename, compressed); + central.push(entry, filename); + offset += local.length + filename.length + compressed.length; } - // ZIP-standard paths, UTF-8 names, and a fixed 1980-01-01 timestamp. - const local = Buffer.alloc(30); - local.writeUInt32LE(0x04034b50); - local.writeUInt16LE(20, 4); - local.writeUInt16LE(0x800, 6); - local.writeUInt16LE(8, 8); - local.writeUInt16LE(0x21, 12); - local.writeUInt32LE(crc, 14); - local.writeUInt32LE(compressed.length, 18); - local.writeUInt32LE(data.length, 22); - local.writeUInt16LE(filename.length, 26); - const entry = Buffer.alloc(46); - entry.writeUInt32LE(0x02014b50); - entry.writeUInt16LE(20, 4); - entry.writeUInt16LE(20, 6); - entry.writeUInt16LE(0x800, 8); - entry.writeUInt16LE(8, 10); - entry.writeUInt16LE(0x21, 14); - entry.writeUInt32LE(crc, 16); - entry.writeUInt32LE(compressed.length, 20); - entry.writeUInt32LE(data.length, 24); - entry.writeUInt16LE(filename.length, 28); - entry.writeUInt32LE(offset, 42); - parts.push(local, filename, compressed); - central.push(entry, filename); - offset += local.length + filename.length + compressed.length; + const centralData = Buffer.concat(central); + if (offset + centralData.length > 0xffffffff) throw new Error("ZIP64 is required for this archive."); + const end = Buffer.alloc(22); + end.writeUInt32LE(0x06054b50); + end.writeUInt16LE(files.length, 8); + end.writeUInt16LE(files.length, 10); + end.writeUInt32LE(centralData.length, 12); + end.writeUInt32LE(offset, 16); + return Buffer.concat([...parts, centralData, end]); } -const centralData = Buffer.concat(central); -if (offset + centralData.length > 0xffffffff) throw new Error("ZIP64 is required for this archive."); -const end = Buffer.alloc(22); -end.writeUInt32LE(0x06054b50); -end.writeUInt16LE(files.length, 8); -end.writeUInt16LE(files.length, 10); -end.writeUInt32LE(centralData.length, 12); -end.writeUInt32LE(offset, 16); -const archive = Buffer.concat([...parts, centralData, end]); +const archive = zip(files); for (const suffix of ["zip", "xpi"]) fs.writeFileSync(path.join(dist, `${stem}.${suffix}`), archive); -execFileSync("git", ["archive", "--format=zip", `--output=${path.join(dist, `${stem}-source.zip`)}`, "HEAD"]); +fs.writeFileSync(path.join(dist, `${stem}-source.zip`), zip(sourceFiles)); const artifacts = [`${stem}.zip`, `${stem}.xpi`, `${stem}-source.zip`]; const checksums = artifacts.map((name) => { @@ -102,7 +125,7 @@ const checksums = artifacts.map((name) => { fs.writeFileSync(path.join(dist, "SHA256SUMS"), checksums.join("")); console.log(JSON.stringify({ version, - commit: execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim(), + commit, files: files.length, artifacts: artifacts.map((name) => ({ path: path.join(dist, name), bytes: fs.statSync(path.join(dist, name)).size })), }, null, 2)); diff --git a/tests/test-portable-packaging.mjs b/tests/test-portable-packaging.mjs new file mode 100644 index 0000000..e454e3a --- /dev/null +++ b/tests/test-portable-packaging.mjs @@ -0,0 +1,64 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import { inflateRawSync } from "node:zlib"; +import { fileURLToPath } from "node:url"; + +const root = path.dirname(path.dirname(fileURLToPath(import.meta.url))); +const temp = fs.mkdtempSync(path.resolve(os.tmpdir(), "portable-packaging-")); +let checkout = path.join(temp, "checkout"); +const git = (...args) => execFileSync("git", args, { cwd: checkout, encoding: "utf8" }); +function zipEntries(buffer) { + const entries = new Map(); + let offset = 0; + while (buffer.readUInt32LE(offset) === 0x04034b50) { + const compressedSize = buffer.readUInt32LE(offset + 18); + const nameSize = buffer.readUInt16LE(offset + 26); + const extraSize = buffer.readUInt16LE(offset + 28); + const name = buffer.subarray(offset + 30, offset + 30 + nameSize).toString(); + const start = offset + 30 + nameSize + extraSize; + entries.set(name, inflateRawSync(buffer.subarray(start, start + compressedSize))); + offset = start + compressedSize; + } + return entries; +} +try { + execFileSync("git", ["clone", "--quiet", "--no-hardlinks", root, checkout]); + // Test the script being edited, even before its parent commit is published. + fs.copyFileSync(path.join(root, "scripts/package-extension.mjs"), path.join(checkout, "scripts/package-extension.mjs")); + git("add", "scripts/package-extension.mjs"); + git("-c", "user.name=Packaging regression", "-c", "user.email=packaging@localhost", + "commit", "--quiet", "--allow-empty", "-m", "Canonical packaging fixture"); + const { version } = JSON.parse(fs.readFileSync(path.join(checkout, "version.json"))); + const stem = `codex-computer-use-firefox-zen-${version}`; + const build = () => { + execFileSync(process.execPath, ["scripts/package-extension.mjs"], { cwd: checkout, stdio: "pipe" }); + return ["zip", "xpi", "source.zip"].map(suffix => fs.readFileSync( + path.join(checkout, "dist", suffix === "source.zip" ? `${stem}-source.zip` : `${stem}.${suffix}`), + )); + }; + const baseline = build(); + assert.deepEqual(baseline[0], baseline[1], "ZIP and XPI must carry identical bytes."); + const packaged = zipEntries(baseline[0]); + const source = zipEntries(baseline[2]); + assert.equal(packaged.size, [...source.keys()].filter(name => name.startsWith("extension/")).length); + for (const [name, data] of packaged) { + assert.deepEqual(data, source.get(`extension/${name}`), `Review source must contain the exact bytes for ${name}`); + } + fs.appendFileSync(path.join(checkout, ".git/info/exclude"), "\nextension/debug.log\n"); + fs.writeFileSync(path.join(checkout, "extension/debug.log"), "ignored local secret\n"); + assert.equal(git("status", "--porcelain").trim(), "", "The contaminating local file is ignored."); + assert.deepEqual(build(), baseline, "Ignored local files must not affect either archive."); + const crlfCheckout = path.join(temp, "crlf-checkout"); + execFileSync("git", ["clone", "--quiet", "--no-hardlinks", "--config", "core.autocrlf=true", checkout, crlfCheckout]); + checkout = crlfCheckout; + assert.equal(git("status", "--porcelain").trim(), "", "CRLF checkout remains clean."); + assert.ok(fs.readFileSync(path.join(checkout, "extension/manifest.json"), "utf8").includes("\r\n"), + "The fixture must actually exercise checkout byte conversion."); + assert.deepEqual(build(), baseline, "Checkout line-ending filters must not affect committed package bytes."); + console.log(JSON.stringify({ ok: true, exactReviewSource: true, ignoredFilesExcluded: true, checkoutFiltersIgnored: true })); +} finally { + fs.rmSync(temp, { recursive: true, force: true }); +} From cc6d86582f6a4751d82040b1477dbb4f21a31ebd Mon Sep 17 00:00:00 2001 From: Ryan B Date: Fri, 9 Oct 2026 06:33:30 +0000 Subject: [PATCH 4/4] Disable inherited signing for packaging test fixture commits --- tests/test-portable-packaging.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test-portable-packaging.mjs b/tests/test-portable-packaging.mjs index e454e3a..0e92c75 100644 --- a/tests/test-portable-packaging.mjs +++ b/tests/test-portable-packaging.mjs @@ -29,8 +29,9 @@ try { // Test the script being edited, even before its parent commit is published. fs.copyFileSync(path.join(root, "scripts/package-extension.mjs"), path.join(checkout, "scripts/package-extension.mjs")); git("add", "scripts/package-extension.mjs"); + // A disposable fixture must not depend on the contributor's signing key. git("-c", "user.name=Packaging regression", "-c", "user.email=packaging@localhost", - "commit", "--quiet", "--allow-empty", "-m", "Canonical packaging fixture"); + "-c", "commit.gpgsign=false", "commit", "--quiet", "--allow-empty", "-m", "Canonical packaging fixture"); const { version } = JSON.parse(fs.readFileSync(path.join(checkout, "version.json"))); const stem = `codex-computer-use-firefox-zen-${version}`; const build = () => {