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 at1bc4dddit fills correct values (reproduced in a clean worktree), and branch head8fc630dalready 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 from8fc630d, 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>
89 lines
4.0 KiB
JavaScript
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()
|