170 lines
9.2 KiB
TypeScript
170 lines
9.2 KiB
TypeScript
#!/usr/bin/env tsx
|
|
/**
|
|
* scripts/ipfs-selfseed-smoke.ts (v1.9.3, Ken)
|
|
*
|
|
* v1.9.3 hosts releases on our OWN nodes — no commercial pinning service. This
|
|
* pins the whole self-seed chain so no layer silently regresses:
|
|
* - release.yml computes the canonical CID with the PINNED Kubo (no upload), over
|
|
* the shared staging script, and the Kubo version + SHA-512 there MUST match
|
|
* ops/ipfs/morphit-ipfs-setup.sh (the drift risk of hardcoding it twice)
|
|
* - ops/ipfs/stage-release-dir.sh is the SINGLE deterministic staging path
|
|
* (no timestamp → CI's --only-hash CID == the seed box's add CID)
|
|
* - ops/ipfs/morphit-ipfs-seed.sh hosts it: stage → add → ASSERT CID==expected
|
|
* (fail loud) → announce
|
|
* - scripts/verify-cid-public.sh is the GUARD: resolves on >=1 public gateway or
|
|
* refuses the broadcast
|
|
* - NO commercial pinner (pinata / storacha / lighthouse) anywhere in the path
|
|
*
|
|
* Shell greps strip `#` comment lines first (a script's own comment necessarily
|
|
* names the anti-pattern it replaced — e.g. stage-release-dir.sh mentions the old
|
|
* `released_utc` in prose).
|
|
*/
|
|
import { readFileSync } from 'node:fs';
|
|
import { dirname, resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = resolve(HERE, '..');
|
|
let pass = 0,
|
|
fail = 0;
|
|
const ok = (m: string) => (pass++, console.log(` \u2713 ${m}`));
|
|
const bad = (m: string, d = '') => (fail++, console.log(` \u2717 ${m}${d ? `\n ${d}` : ''}`));
|
|
const read = (p: string) => readFileSync(resolve(ROOT, p), 'utf8');
|
|
// Drop full-line `#` comments so anti-pattern greps ignore a script's own prose.
|
|
const stripHash = (s: string) =>
|
|
s
|
|
.split('\n')
|
|
.filter((l) => !/^\s*#/.test(l))
|
|
.join('\n');
|
|
|
|
const yml = read('.forgejo/workflows/release.yml');
|
|
const setup = read('ops/ipfs/morphit-ipfs-setup.sh');
|
|
const stager = stripHash(read('ops/ipfs/stage-release-dir.sh'));
|
|
const seed = stripHash(read('ops/ipfs/morphit-ipfs-seed.sh'));
|
|
const guard = stripHash(read('scripts/verify-cid-public.sh'));
|
|
|
|
// ── 1. NO commercial pinner anywhere in the release/host path ────────
|
|
{
|
|
const PINNERS = /pinata|pinFileToIPFS|PINATA_JWT|pinByHash|lighthouse|storacha|fil\.one|up\.storacha/i;
|
|
const files: Array<[string, string]> = [
|
|
['release.yml', yml],
|
|
['stage-release-dir.sh', stager],
|
|
['morphit-ipfs-seed.sh', seed],
|
|
['verify-cid-public.sh', guard]
|
|
];
|
|
let clean = true;
|
|
for (const [name, body] of files) {
|
|
if (PINNERS.test(body)) {
|
|
clean = false;
|
|
bad(`no commercial pinner: ${name} still references one`);
|
|
}
|
|
}
|
|
clean && ok('no commercial pinner referenced anywhere (pinata / storacha / lighthouse)');
|
|
}
|
|
|
|
// ── 2. release.yml computes the CID with the pinned Kubo (no upload) ──
|
|
{
|
|
const checks: Array<[string, boolean]> = [
|
|
['installs a pinned Kubo (version + SHA-512)', /KUBO_VERSION/.test(yml) && /KUBO_SHA512/.test(yml)],
|
|
['verifies the Kubo checksum (sha512sum)', /sha512sum/.test(yml)],
|
|
['stages via the shared stage-release-dir.sh', /ops\/ipfs\/stage-release-dir\.sh/.test(yml)],
|
|
['uses the LOCAL just-built tarball (MORPHIT_STAGE_TARBALL)', /MORPHIT_STAGE_TARBALL/.test(yml)],
|
|
['computes the CID with ipfs add --only-hash (cidv1)', /add -rQ --cid-version 1 --only-hash/.test(yml)],
|
|
['records the CID to ipfs-cid.txt', /ipfs-cid\.txt/.test(yml)]
|
|
];
|
|
for (const [n, okp] of checks) okp ? ok(`release.yml: ${n}`) : bad(`release.yml: ${n}`);
|
|
}
|
|
|
|
// ── 3. Kubo version + SHA-512 MATCH between release.yml and the setup script ──
|
|
{
|
|
const ymlVer = (yml.match(/KUBO_VERSION:\s*(v[0-9][0-9.]*)/) || [])[1];
|
|
const ymlSha = (yml.match(/KUBO_SHA512:\s*([0-9a-fA-F]{128})/) || [])[1];
|
|
const setVer = (setup.match(/MORPHIT_KUBO_VERSION:-(v[0-9][0-9.]*)/) || [])[1];
|
|
const setSha = (setup.match(/MORPHIT_KUBO_SHA512:-([0-9a-fA-F]{128})/) || [])[1];
|
|
|
|
ymlVer ? ok(`release.yml Kubo version parsed (${ymlVer})`) : bad('release.yml Kubo version not found');
|
|
ymlSha ? ok('release.yml Kubo SHA-512 parsed') : bad('release.yml Kubo SHA-512 not found');
|
|
setVer ? ok(`setup script Kubo version parsed (${setVer})`) : bad('setup script Kubo version not found');
|
|
setSha ? ok('setup script Kubo SHA-512 parsed') : bad('setup script Kubo SHA-512 not found');
|
|
|
|
ymlVer && setVer && ymlVer === setVer
|
|
? ok(`Kubo VERSION in sync (release.yml == setup script == ${ymlVer})`)
|
|
: bad('Kubo version DRIFT between release.yml and morphit-ipfs-setup.sh', `${ymlVer} vs ${setVer}`);
|
|
ymlSha && setSha && ymlSha.toLowerCase() === setSha.toLowerCase()
|
|
? ok('Kubo SHA-512 in sync (release.yml == setup script)')
|
|
: bad('Kubo SHA-512 DRIFT between release.yml and morphit-ipfs-setup.sh');
|
|
}
|
|
|
|
// ── 4. stage-release-dir.sh — single deterministic staging path ──────
|
|
{
|
|
const checks: Array<[string, boolean]> = [
|
|
['supports LOCAL acquisition (MORPHIT_STAGE_TARBALL)', /MORPHIT_STAGE_TARBALL/.test(stager)],
|
|
['supports DOWNLOAD acquisition (release download base)', /MORPHIT_RELEASE_DOWNLOAD_BASE/.test(stager)],
|
|
['verifies the tarball sha256 (integrity)', /sha256sum/.test(stager) && /mismatch/i.test(stager)],
|
|
['stages a stable morphit-latest.tar.gz', /morphit-latest\.tar\.gz/.test(stager)],
|
|
['writes metadata.json with version + sha256', /"version":/.test(stager) && /"sha256":/.test(stager)],
|
|
['metadata is DETERMINISTIC (no released_utc timestamp)', !/released_utc/.test(stager)],
|
|
['notes come from the tarball via tar -O, not an external fetch (no curl of notes/.asc — v1.9.3 CID-divergence guard)', !/curl[^\n]*RELEASE-NOTES/i.test(stager) && !/curl[^\n]*\.asc/i.test(stager) && /tar -xzf[^\n]*-O/.test(stager)],
|
|
['discoverable dir — README.md + keyword-tagged metadata.json (v1.9.5)', /README\.md/.test(stager) && /"keywords":/.test(stager)]
|
|
];
|
|
for (const [n, okp] of checks) okp ? ok(`stager: ${n}`) : bad(`stager: ${n}`);
|
|
}
|
|
|
|
// ── 5. morphit-ipfs-seed.sh — origin host with the equality assertion ─
|
|
{
|
|
const checks: Array<[string, boolean]> = [
|
|
['stages via the shared stage-release-dir.sh', /stage-release-dir\.sh/.test(seed)],
|
|
['adds the directory (ipfs add --cid-version 1)', /add -rQ --cid-version 1/.test(seed)],
|
|
['ASSERTS the CID equals the expected/anchored one', /"\$CID" != "\$EXPECTED"/.test(seed) && /MISMATCH/i.test(seed)],
|
|
['fails loud (exit 1) on CID mismatch', /MISMATCH[\s\S]*exit 1/i.test(seed)],
|
|
['announces it (ipfs routing provide)', /routing provide/.test(seed)],
|
|
['rejects a non-vX.Y.Z tag', /v\[0-9\]/.test(seed)]
|
|
];
|
|
for (const [n, okp] of checks) okp ? ok(`seed: ${n}`) : bad(`seed: ${n}`);
|
|
}
|
|
|
|
// ── 6. verify-cid-public.sh — the broadcast guard ───────────────────
|
|
{
|
|
const checks: Array<[string, boolean]> = [
|
|
['polls independent public gateways', /MORPHIT_GUARD_GATEWAYS/.test(guard) && /ipfs\.io/.test(guard) && /dweb\.link/.test(guard)],
|
|
['fetches the CID metadata.json', /metadata\.json/.test(guard)],
|
|
['confirms the expected version (not just any 200)', /WANT_VER/.test(guard) && /grep -q/.test(guard)],
|
|
// cp591 — metadata.json (one tiny block) resolving is NOT proof the ~12MB
|
|
// tarball downloads; the guard must fetch morphit-latest.tar.gz IN FULL
|
|
// (curl -f → a truncated/partial transfer is rejected) before anchoring the
|
|
// CID, which also warms the gateway for the first visitors.
|
|
['also verifies the FULL tarball downloads (morphit-latest.tar.gz via curl -f)', /TARURL="[^"]*morphit-latest\.tar\.gz"/.test(guard) && /curl -fsSL[^\n]*\$TARURL/.test(guard)],
|
|
['passes on the FIRST gateway that serves (exit 0 in the loop)', /exit 0/.test(guard)],
|
|
['fails loud → no broadcast (exit 1)', /DO NOT BROADCAST[\s\S]*exit 1/i.test(guard)],
|
|
['backs off across rounds (retry cold content)', /ATTEMPTS/.test(guard) && /sleep/.test(guard)]
|
|
];
|
|
for (const [n, okp] of checks) okp ? ok(`guard: ${n}`) : bad(`guard: ${n}`);
|
|
}
|
|
|
|
// ── 7. morphit-ops wiring: the "Seed this release" action is offered ─
|
|
{
|
|
const harden = read('apps/ops-cli/src/commands/harden.ts');
|
|
/Seed this release to IPFS/.test(harden) && /morphit-ipfs-seed\.sh/.test(harden)
|
|
? ok('morphit-ops harden offers "Seed this release to IPFS" → morphit-ipfs-seed.sh')
|
|
: bad('morphit-ops seed action not wired');
|
|
}
|
|
|
|
// ── 8. morphit-ops UPGRADE auto-seeds the installed release (Block 3 fold) ──
|
|
{
|
|
const upgrade = read('apps/ops-cli/src/commands/upgrade.ts');
|
|
const checks: Array<[string, boolean]> = [
|
|
['upgrade seeds via morphit-ipfs-seed.sh', /morphit-ipfs-seed\.sh/.test(upgrade)],
|
|
['gated on IPFS hosting up (ipfs present + service active)', /is-active --quiet ipfs/.test(upgrade) && /command -v ipfs/.test(upgrade)],
|
|
['seeds the just-installed tag (latestTag)', /seedScript, latestTag/.test(upgrade)],
|
|
['non-fatal — never fails the upgrade over seeding', /never fail an upgrade over IPFS/.test(upgrade)]
|
|
];
|
|
for (const [n, okp] of checks) okp ? ok(`upgrade: ${n}`) : bad(`upgrade: ${n}`);
|
|
}
|
|
|
|
console.log('\n' + '\u2500'.repeat(56));
|
|
if (fail > 0) {
|
|
console.log(`\u2717 ipfs-selfseed smoke FAILED (${fail})`);
|
|
process.exit(1);
|
|
}
|
|
console.log('\u2713 self-seed IPFS is wired: no commercial pinner, pinned-Kubo CID compute, version/SHA in sync, deterministic staging, seed asserts CID equality, guard gates the broadcast');
|
|
console.log(`\u2713 all ${pass} ipfs-selfseed scenarios passed`);
|