Files
qqq-cli/scripts/update-packaging.mjs
james.maesandClaude Fable 5.1 4f41f41a05 fix(release): update-packaging refuses to write missing sums; real 1.0.0 sums
scripts/update-packaging.mjs could not be shown to write zeros: run against the real
cli-v1.0.0 SHA256SUMS from the all-zero placeholders at 1bc4ddd it fills correct values
(reproduced in a clean worktree), and branch head 8fc630d already carried them. The old
script was fragile, though: it read lines with split/reverse (a `*file` binary-mode name
or a stray column would silently mis-key), never checked that a sum was 64 hex, accepted
all-zero sums, did not check that any placeholder had actually matched, and wrote the
formula before it knew the manifest would succeed.

Now it parses `shasum -a 256` lines strictly, requires every target to be 64 lowercase
hex and not all zeros, requires at least one url/sha256 pair and no unfilled sha256 left in
the formula, computes both files first and writes only if everything passed, otherwise
exits 1 with "nothing was written". Tested in scripts/update-packaging.test.mjs (fixture
SHA256SUMS; run by `npm test` via `test:scripts`). Docs (packaging README, release runbook
step 7, tap README) say to compare the diff with SHA256SUMS and that tap/bucket CI catches
a wrong or zero sum only when its PR is opened and read.

Ran against /Volumes/HD-2/Git.Local/QRun-IO/.worktrees/qqq-cli-release/dist/SHA256SUMS;
the packaging files are unchanged from 8fc630d, and each sum was checked by hand against it:
  darwin-arm64  5017a32ac5031d9146d541381099a959e5c19bbf04c48f725045d89d6007b211
  darwin-x64    eb9b1d43d24b639118d94b175c343080a39ae9ba64c24645c1792ff3b16a37f1
  linux-arm64   25f513bdda13bbe40cf536844eebc1e3a7e21cf7804024f25ebe7c42793d7ebc
  linux-x64     804d06cb7c0012c2f185ae5d94c6bb8f9e2482a440bb3c165361f1a14c941e41
  windows-x64   cf418d7842a227cee412171878228004f9bc5098a272c64b89512d1e2d06a176
(linux musl sums are in SHA256SUMS but not used by the formula or manifest.)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-01 12:57:49 -06:00

89 lines
4.0 KiB
JavaScript

#!/usr/bin/env node
/*
* Copyright 2026 QRun.IO, Inc.
* Licensed under the Apache License, Version 2.0 (see LICENSE).
*/
/**
* Fills the version and sha256 values of the packaging files (docs/packaging/homebrew-tap/Formula/qqq.rb for Homebrew,
* docs/packaging/qqq.json for Scoop) from packages/cli/package.json and a release's SHA256SUMS.
* Run it on the binaries that were uploaded to the GitHub release, not on a local rebuild: the
* commit is stamped into each binary, so every build has different checksums.
*
* Refuses to write anything (exit 1, both files untouched) when a sum is missing, is not 64 lowercase hex digits,
* is all zeros, or when a placeholder in the files is left unfilled.
*
* Usage: node scripts/update-packaging.mjs [dist/SHA256SUMS]
*/
import { readFileSync, realpathSync, writeFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
const HEX64 = /^[0-9a-f]{64}$/
const root = resolve(import.meta.dirname, '..')
const FORMULA_SUMS = /(url "[^"]*\/(qqq-[a-z0-9-]+)"\n\s*sha256 ")([0-9a-f]{64})"/g
/** Parses `shasum -a 256` output (`<hash> <file>`, `*` before the name tolerated) into a Map of file to hash. */
export function parseSums(text) {
const sums = new Map()
for (const line of text.split('\n')) {
if (line.trim() === '') continue
const match = line.match(/^(\S+)\s+\*?(\S+)$/)
if (!match) throw new Error(`unparseable SHA256SUMS line: ${JSON.stringify(line)}`)
sums.set(match[2], match[1].toLowerCase())
}
return sums
}
function sumFor(sums, file) {
const hash = sums.get(file)
if (hash === undefined) throw new Error(`SHA256SUMS has no ${file}`)
if (!HEX64.test(hash)) throw new Error(`SHA256SUMS has a malformed sum for ${file}: ${JSON.stringify(hash)}`)
if (/^0+$/.test(hash)) throw new Error(`SHA256SUMS has an all-zero sum for ${file}`)
return hash
}
/** Returns the new text of both packaging files; throws, leaving the caller nothing to write, on any problem. */
export function updatePackaging({ version, sums, formula, manifest }) {
let formulaCount = 0
const newFormula = formula
.replace(/^(\s*version ")[^"]+"/m, `$1${version}"`)
.replace(FORMULA_SUMS, (_, head, file) => {
formulaCount++
return `${head}${sumFor(sums, file)}"`
})
if (formulaCount === 0) throw new Error('the formula has no url/sha256 pairs to fill')
const left = newFormula.match(/sha256 "(?![0-9a-f]{64}")[^"]*"/g)
if (left || /sha256 "0{64}"/.test(newFormula)) throw new Error(`the formula still has unfilled sha256 values: ${left ?? 'zeros'}`)
const parsed = JSON.parse(manifest)
parsed.version = version
parsed.architecture['64bit'].url = `https://github.com/QRun-IO/qqq-cli/releases/download/cli-v${version}/qqq-windows-x64.exe`
parsed.architecture['64bit'].hash = sumFor(sums, 'qqq-windows-x64.exe')
return { formula: newFormula, manifest: JSON.stringify(parsed, null, 4) + '\n', formulaCount }
}
function main() {
const sumsPath = resolve(process.argv[2] ?? join(root, 'dist/SHA256SUMS'))
const version = JSON.parse(readFileSync(join(root, 'packages/cli/package.json'), 'utf8')).version
const formulaPath = join(root, 'docs/packaging/homebrew-tap/Formula/qqq.rb')
const manifestPath = join(root, 'docs/packaging/qqq.json')
try {
const out = updatePackaging({
version,
sums: parseSums(readFileSync(sumsPath, 'utf8')),
formula: readFileSync(formulaPath, 'utf8'),
manifest: readFileSync(manifestPath, 'utf8'),
})
writeFileSync(formulaPath, out.formula)
writeFileSync(manifestPath, out.manifest)
console.log(`docs/packaging/homebrew-tap/Formula/qqq.rb (${out.formulaCount} sums) and docs/packaging/qqq.json now name ${version} with the sums from ${sumsPath}`)
} catch (error) {
console.error(`update-packaging: ${error.message}; nothing was written`)
process.exit(1)
}
}
// Run only as a script, not when the tests import it (realpath: argv[1] may go through a symlink such as macOS /var).
if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(import.meta.filename)) main()