#!/usr/bin/env node /** * Default pretest gate: when `docs/audit/holepunch-drift-repos.json` lists repos, ensure * local clones under `BARE_OS_HOLEPUNCH_CLONES_ROOT` are not behind `origin/main`. * Disable with `BARE_OS_HOLEPUNCH_DRIFT_CHECK=0` (offline / no clones). * * Optional maintainer gate: `BARE_OS_HOLEPUNCH_DRIFT_TIER1=1` checks **`tier1Repos[]`** * from the same JSON (ignores **`repos[]`**), for a small Holepunch spine set. * * Prereq: `git fetch origin main` in each clone so origin/main is meaningful. * Config: docs/audit/holepunch-drift-repos.json → { repos: ["name", ...] } * Clones root: BARE_OS_HOLEPUNCH_CLONES_ROOT (same default as sync-holepunch-clones.mjs) */ import fs from 'node:fs' import path from 'node:path' import process from 'node:process' import { fileURLToPath } from 'node:url' import { execSync } from 'node:child_process' const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') const configPath = path.join(root, 'docs/audit/holepunch-drift-repos.json') const defaultClones = path.join( process.env.HOME || '', 'dev/pearcli/holepunch-repos/holepunchto_repos' ) const clonesRoot = String( process.env.BARE_OS_HOLEPUNCH_CLONES_ROOT || defaultClones ).trim() function gitOk(cmd, cwd) { try { execSync(cmd, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }) return true } catch { return false } } /** @param {string} dir */ function gitCommitsBehindOriginMain(dir) { try { const o = execSync('git rev-list --count HEAD..origin/main', { cwd: dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() const n = Number.parseInt(o, 10) return Number.isFinite(n) ? n : -1 } catch { return -1 } } function main() { const off = process.env.BARE_OS_HOLEPUNCH_DRIFT_CHECK === '0' || process.env.BARE_OS_HOLEPUNCH_DRIFT_CHECK === 'false' if (off) { console.log( 'verify-holepunch-clone-drift: skip (BARE_OS_HOLEPUNCH_DRIFT_CHECK=0)' ) return } if (!fs.existsSync(configPath)) { console.error('verify-holepunch-clone-drift: missing', configPath) process.exit(1) } let cfg try { cfg = JSON.parse(fs.readFileSync(configPath, 'utf8')) } catch (e) { console.error('verify-holepunch-clone-drift: parse config failed', e) process.exit(1) } const tier1 = process.env.BARE_OS_HOLEPUNCH_DRIFT_TIER1 === '1' || process.env.BARE_OS_HOLEPUNCH_DRIFT_TIER1 === 'true' let repos = Array.isArray(cfg.repos) ? cfg.repos.map((r) => String(r).trim()).filter(Boolean) : [] if (tier1) { repos = Array.isArray(cfg.tier1Repos) ? cfg.tier1Repos.map((r) => String(r).trim()).filter(Boolean) : [] if (!repos.length) { console.log( 'verify-holepunch-clone-drift: tier1 mode but tier1Repos[] empty — nothing to check' ) return } console.log( 'verify-holepunch-clone-drift: tier1 mode — checking', repos.length, 'repo(s)' ) } else if (!repos.length) { console.log('verify-holepunch-clone-drift: repos[] empty — nothing to check') return } let failed = false /** @type {{ name: string, commitsBehind: number }[]} */ const behindReport = [] for (const name of repos) { const dir = path.join(clonesRoot, name) if (!fs.existsSync(dir)) { console.warn( 'verify-holepunch-clone-drift: skip missing clone', path.relative(root, dir) ) continue } if (!gitOk('git rev-parse --git-dir', dir)) { console.warn('verify-holepunch-clone-drift: skip non-git', name) continue } const hasOriginMain = gitOk('git rev-parse --verify origin/main', dir) if (!hasOriginMain) { console.warn( 'verify-holepunch-clone-drift: no origin/main in', name, '(run git fetch origin main)' ) continue } const upToDate = gitOk( 'git merge-base --is-ancestor origin/main HEAD', dir ) if (!upToDate) { const commitsBehind = gitCommitsBehindOriginMain(dir) behindReport.push({ name, commitsBehind }) console.error( 'verify-holepunch-clone-drift: clone is behind origin/main:', name, commitsBehind >= 0 ? `(~${commitsBehind} commits behind)` : '' ) failed = true } } if (failed) { behindReport.sort((a, b) => b.commitsBehind - a.commitsBehind) const top = behindReport.slice(0, 12) console.error( 'verify-holepunch-clone-drift: repos most behind origin/main (actionable PR order):' ) for (const r of top) { console.error( ` - ${r.name}: ~${r.commitsBehind >= 0 ? r.commitsBehind : '?'} commits` ) } console.error( 'verify-holepunch-clone-drift: cross-check lockfile vs clones: node scripts/report-holepunch-lockfile-drift.mjs' ) console.error( 'verify-holepunch-clone-drift: FAIL — pull or fetch listed clones, or trim docs/audit/holepunch-drift-repos.json (see handbook ch.7 / scripts/release-checklist.mjs)' ) process.exit(1) } const writeReport = process.env.BARE_OS_HOLEPUNCH_DRIFT_WRITE_REPORT === '1' || process.env.BARE_OS_HOLEPUNCH_DRIFT_WRITE_REPORT === 'true' if (writeReport) { const outMd = path.join(root, 'docs/audit/holepunch-drift-last-run.md') const lines = [ '# Holepunch clone drift (last verify)', '', `Generated: ${new Date().toISOString()}`, '', 'All listed clones were at or ahead of `origin/main` when this file was written.', '', `Mode: ${tier1 ? 'tier1Repos[]' : 'repos[]'}`, '', 'Repos checked:', ...repos.map((n) => `- ${n}`), '', 'Re-run: `BARE_OS_HOLEPUNCH_DRIFT_WRITE_REPORT=1 npm run pretest` (or run this script directly after `git fetch`).', '' ] try { fs.mkdirSync(path.dirname(outMd), { recursive: true }) fs.writeFileSync(outMd, lines.join('\n'), 'utf8') } catch (e) { console.warn('verify-holepunch-clone-drift: could not write report', e) } } console.log('verify-holepunch-clone-drift: OK') } main()