OverviewFilesCommitsIssuesDocsReleasesPeers

pearbrowser-desktop commits / 0008740

build: retire Pear v2 release fallback
00087408887f950010f225cd80613d46d8d49c27
by bigdestiny2 <33146784+bigdestiny2@users.noreply.github.com> · 2026-07-22T19:49:04.000Z

Changed files (5) +46 -200

README.md +8 -9
……@@ -157,20 +157,19 @@ mkdir -p pear-ecosystem/01-browser
157157git clone https://github.com/bigdestiny2/pearbrowser-desktop pear-ecosystem/01-browser/pearbrowser-desktop
158158cd pear-ecosystem/01-browser/pearbrowser-desktop
159159npm install
160pear run --dev .
160npm run start
161161```
162162
163163Source installs are standalone. The desktop packages default to npm `latest` for HiveRelay; the root package defaults to npm `latest` for `p2p-hiverelay`, `p2p-hiverelay-client`, and `p2p-hiverelay-verifier`, with the current dist-tag resolving to `0.20.2` in the lockfile, so a clone of just this repo resolves them from the registry. `npm install` runs `scripts/check-hiverelay-layout.mjs`, which exits quietly for the registry line and fails only if the HiveRelay dependency/lockfile line drifts or you opt into incomplete/mismatched `file:` workspace dependencies. A sibling `../../00-core/hiverelay` checkout is optional and only needed for HiveRelay co-development.
164164
165UI files use htm + React (no build step). Backend in `backend/` is CommonJS. See `package.json` `pear` field for runtime config, and `pear.json` for multisig signing config.
165UI files use htm + React (no build step). Backend in `backend/` is CommonJS. The source checkout retains its native-shell development wiring while the v3 embedded-runtime host is completed; it does not accept remote app links as worker input.
166166
167167## Release pipeline
168168
169Solo publisher, two steps:
169The checked-in command is a native-release **preflight**, not a publisher:
170170
171171```sh
172./scripts/release-prod.sh # pear stage --purge + pear release (deprecated path)
173node scripts/pin-self-on-hiverelay.js # re-pin the new length on relays
172./scripts/release-prod.sh # local v3 release evidence checks only
174173```
175174
176175Catalogue updates are versioned from [`catalog-source/pearbrowser-network.catalog.json`](./catalog-source/pearbrowser-network.catalog.json):
……@@ -181,9 +180,9 @@ node scripts/publish-catalog-bee.js catalog-source/pearbrowser-network.catalog.j
181180node scripts/verify-live-catalog.js --expect-app peercord --expect-app peerit --expect-app hiveworm
182181```
183182
184`pear release` is deprecated in Pear runtime `v2.4.0` but still works and we use it deliberately — the replacement (`pear provision` + `pear multisig` quorum-cosigning) is designed for multi-publisher releases. A solo 1-of-1 multisig is pure ceremony with no security gain.
185
186**When to migrate to multisig:** when we add a co-signer (genuine quorum security), or when Pear actually removes `pear release` (not just deprecates it). The link config + provision target are pre-staged in `pear.json` so the migration is just plumbing — see the `_comment` field there.
183The preflight never stages or publishes a release. Promotion requires a signed
184native package, an AppRelease v2 record, independent availability evidence, and
185human approval of clean-install, upgrade, rollback, and data-continuity proof.
187186
188187## Operator scripts
189188
……@@ -216,7 +215,7 @@ node scripts/verify-live-catalog.js --expect-app peercord --expect-app peerit --
216215| `npm run check:release-evidence` | Reads the operator evidence log and fails until required gates are marked `PASS` or documented `DEFER`, with a final announcement decision. |
217216| `scripts/verify-app-full.js --key <driveKey>` | Deeper fresh-peer blob sampling across a drive's file tree. |
218217| `scripts/verify-pear-bundle-contract.js --key <driveKey>` | Metadata-only Pear bundle contract check: reads `pear.json` and selected files from a fresh peer without executing third-party code. |
219| `scripts/release-prod.sh` | The two-step release pipeline above. |
218| `scripts/release-prod.sh` | Fail-closed v3 native-release preflight; it never publishes. |
220219
221220## Distribution
222221
pear.json +1 -1
……@@ -1,5 +1,5 @@
11{
2 "_comment": "Pear project links + (future) multisig config. Multisig is intentionally NOT set up: we're a solo publisher, the deprecated `pear release` still works, and a 1-of-1 quorum adds ceremony without security. When we get a co-signer (or Pear actually removes `pear release`), add the publicKeys + namespace + quorum block here and switch scripts/release-prod.sh to the provision+multisig path.",
2 "_comment": "Legacy v2 release identifiers retained only for migration lookup. They are not executable runtime inputs or a v3 publishing workflow.",
33 "links": {
44 "production": "pear://tco5k7h38uoxatedp1wongdbhjxow1x7jiwm3t1i9cujbebhsbty",
55 "provisionTarget": "pear://pdsd1p3m1xsz8aofyndbwux5hqs19h8uswnsrfee5cz8sj9pmj4o"
scripts/generate-native-install-snippet.mjs +3 -10
……@@ -433,17 +433,10 @@ function printInstallGuide (report) {
433433 console.log('```')
434434 if (fallback) {
435435 console.log()
436 console.log('## Recovery Fallback')
436 console.log('## Legacy Migration Record')
437437 console.log()
438 console.log('The stable Pear link remains available for testers and recovery while native packaging matures:')
439 console.log()
440 console.log('```sh')
441 console.log('npm i -g pear')
442 console.log('pear')
443 console.log(`pear run ${fallback}`)
444 console.log('```')
445 console.log()
446 console.log('This fallback is not the preferred public install path because `pear run` is deprecated in Pear runtime `v2.4.0`, but it is still useful when diagnosing a native package issue.')
438 console.log(`Legacy migration record: \`${fallback}\`.`)
439 console.log('Keep existing data intact while migrating. This record is not a v3 launch or install command; use the native package and app-specific migration guidance.')
447440 }
448441}
449442
scripts/release-prod.sh +26 -176
……@@ -1,190 +1,40 @@
11#!/usr/bin/env bash
2# Release PearBrowser desktop to the production pear:// channel.
2# PearBrowser v3 native-release preflight.
33#
4# Uses the deprecated `pear release` for now — still works in Pear
5# runtime v2.x. The new replacement is multi-publisher multisig
6# (`pear provision` + `pear multisig {request,sign,verify,commit}`),
7# which is overkill for a solo publisher. Switch to the multisig flow
8# if/when:
9# 1. `pear release` is actually removed (not just deprecated), OR
10# 2. We add co-signers (genuine quorum security)
11# Until then, simpler is better.
12#
13# Usage:
14# ./scripts/release-prod.sh # full release: stage → release → pin → verify
15# ./scripts/release-prod.sh --no-pin # skip pin + verify (for dry-run / hotfix tests)
16#
17# After publishing, the script automatically pins the new length on
18# HiveRelay, then confirms the release is actually reachable. Caveat:
19# verify-pin.js (a fresh-peer round-trip) CANNOT pass when run on the
20# publisher machine — this box's durable seeder announces the drive from
21# behind a NAT (`firewalled true`), and Hyperswarm can't holepunch a peer
22# to itself; the relays only DIAL the seeder for this heavy-history key,
23# they don't announce it as servers. So on the publisher box the script
24# confirms reachability via the durable seeder's own log instead (restarts
25# the seeder at the new length, checks it announced + has live remote
26# peers). verify-pin still runs first and short-circuits to success when
27# invoked from a remote host / CI (where it genuinely can pass).
4# This command intentionally has no publication authority. The v2 staged-drive
5# publisher was removed: a remote Pear link is neither a desktop installer nor
6# a valid update target for the embedded runtime. A release operator must build
7# a native package, create and sign its AppRelease v2 record, publish through
8# the approved release workflow, then attach independent availability evidence.
289
2910set -euo pipefail
3011cd "$(dirname "$0")/.."
3112
32SKIP_PIN=0
33for arg in "$@"; do
34 if [[ "$arg" == "--no-pin" ]]; then SKIP_PIN=1; fi
35done
36
37PROD_LINK=$(node -p "require('./pear.json').links.production")
38
39if [[ -z "$PROD_LINK" ]]; then
40 echo "✗ Missing production link in pear.json" >&2
41 exit 1
42fi
43
44strip_ansi() {
45 sed -E $'s/\x1B\\[[0-9;?]*[[:alpha:]]//g'
46}
47
48echo "▸ production link: $PROD_LINK"
49echo
50
51PREV_LEN=$(pear info "$PROD_LINK" 2>/dev/null | grep -E "^\s*release\s" | awk '{print $2}')
52echo "▸ previous released length: $PREV_LEN"
53echo
54
55# ── 1. stage ──────────────────────────────────────────────
56echo "============================================================"
57echo " 1/2 pear stage"
58echo "============================================================"
59# --purge is important: Pear drives are append-only, so adding a file to the
60# stage ignore list only prevents future adds. It does not remove a file that
61# was staged in an older release unless we explicitly purge ignored paths.
62STAGE_OUT=$(pear stage --purge "$PROD_LINK" . 2>&1)
63STAGE_CLEAN=$(printf '%s\n' "$STAGE_OUT" | strip_ansi)
64printf '%s\n' "$STAGE_CLEAN" | tail -8
65# Parse the staged length. Newer Pear prints "Latest length: N" (not the older
66# "Latest: N"), so accept both. A miss here is NOT fatal — the release step
67# below re-reads the authoritative released length — so fall back to pear info
68# and continue rather than aborting the whole release on a cosmetic parse change.
69NEW_LEN=$(printf '%s\n' "$STAGE_CLEAN" | awk '/Latest length:/ {print $3; exit} /Latest:/ {print $2; exit}')
70if [[ -z "$NEW_LEN" ]]; then
71 NEW_LEN=$(pear info "$PROD_LINK" 2>/dev/null | awk '/^[[:space:]]*length[[:space:]]/ {print $2; exit}')
72 echo "▸ (staged length not in stage output — read from pear info: ${NEW_LEN:-unknown})"
73fi
74echo
75echo "▸ new staged length: ${NEW_LEN:-unknown}"
76echo
77
78# ── 2. release ────────────────────────────────────────────
79echo "============================================================"
80echo " 2/2 pear release (deprecated path, still works)"
81echo "============================================================"
82RELEASE_OUT=$(pear release "$PROD_LINK" . 2>&1)
83RELEASE_CLEAN=$(printf '%s\n' "$RELEASE_OUT" | strip_ansi)
84printf '%s\n' "$RELEASE_CLEAN" | tail -5
85RELEASE_LEN=$(printf '%s\n' "$RELEASE_CLEAN" | awk '/Latest length:/ {print $3; exit}')
86if [[ -n "$RELEASE_LEN" ]]; then
87 NEW_LEN="$RELEASE_LEN"
88fi
89echo
90
91echo "✅ Released $PROD_LINK"
92echo " length $PREV_LEN → $NEW_LEN"
93echo
13if [[ "${1:-}" == "--help" ]]; then
14 cat <<'EOF'
15Usage: ./scripts/release-prod.sh
9416
95if [[ "$SKIP_PIN" == "1" ]]; then
96 echo "▸ --no-pin given, skipping pin + verification"
97 echo " manually: node scripts/pin-self-on-hiverelay.js"
98 echo " node scripts/verify-pin.js --expect $NEW_LEN --hiverelay"
17Runs local native-release preflight only. It never stages, publishes, seeds,
18or modifies a remote release. Use the human-gated v3 release workflow after
19this command reports success.
20EOF
9921 exit 0
10022fi
10123
102# ── 3. pin on HiveRelay backbone ──────────────────────────
103echo "============================================================"
104echo " 3/4 pin on HiveRelay (refresh 365-day TTL)"
105echo "============================================================"
106node scripts/pin-self-on-hiverelay.js
107echo
108
109# ── 4. verify the pin actually serves the new length ──────
110echo "============================================================"
111echo " 4/4 verify content is reachable from the network"
112echo "============================================================"
113
114SEED_LABEL="com.pearbrowser.seed"
115SEED_LOG="$HOME/Library/Logs/pearbrowser-seed.log"
24echo "PearBrowser v3 native-release preflight"
25echo "This command has no publication authority."
11626
117# Confirm reachability via the local durable seeder's log. Returns 0 if the
118# seeder re-announced this drive at the new length AND has ≥1 live remote
119# peer — the only confirmation that actually works on the publisher box,
120# where verify-pin can't holepunch to the firewalled local seeder.
121confirm_via_seed_log() {
122 launchctl print "gui/$(id -u)/$SEED_LABEL" >/dev/null 2>&1 || return 1
123 echo " ▸ this box runs the durable seeder — confirming via its log"
124 # rotate the log so we read a FRESH announce, then restart the seeder so
125 # it re-opens the drive at $NEW_LEN and re-announces on the swarm
126 [[ -f "$SEED_LOG" ]] && mv "$SEED_LOG" "$SEED_LOG.prev" 2>/dev/null
127 launchctl kickstart -k "gui/$(id -u)/$SEED_LABEL" >/dev/null 2>&1 || return 1
128 local i peers
129 for i in $(seq 1 12); do # up to ~60s for boot + announce + peers
130 sleep 5
131 [[ -f "$SEED_LOG" ]] || continue
132 grep -q '\^_\^ announced' "$SEED_LOG" 2>/dev/null || continue
133 peers=$(grep -Eo '[0-9]+ peers' "$SEED_LOG" 2>/dev/null | awk '{print $1}' | sort -n | tail -1)
134 if [[ -n "$peers" && "$peers" -ge 1 ]]; then
135 echo " ✓ seeder announced + $peers live remote peer(s) pulling from it"
136 return 0
137 fi
138 done
139 echo " ✗ seeder did not announce / no peers within 60s"
140 return 1
141}
27npm run check:release-evidence
28npm run check:native-signing
29npm run check:linux-appimage-metadata
14230
143# (a) Try the real fresh-peer round-trip once. Passes from a remote host /
144# CI (and short-circuits the whole step). On the publisher box it sees
145# "no peers" — expected, not a real outage — so we fall through to (b).
146echo " ▸ fresh-peer verify (verify-pin.js --expect $NEW_LEN --hiverelay)..."
147if node scripts/verify-pin.js --expect "$NEW_LEN" --hiverelay; then
148 echo
149 echo "🎉 Release complete and verified end-to-end (fresh remote peer)."
150 echo " Anyone running 'pear run $PROD_LINK' will get length $NEW_LEN."
151 exit 0
152fi
153
154# (b) Co-located false negative — confirm via the durable seeder instead.
155echo
156echo " ▸ no peers from here (expected on the publisher box: the seeder is"
157echo " firewalled, so a same-NAT verify can't holepunch to it)"
158if confirm_via_seed_log; then
159 echo
160 echo "🎉 Release complete — served live by the durable seeder."
161 echo " verify-pin can only PASS from a different machine/network; run"
162 echo " 'node scripts/verify-pin.js --expect $NEW_LEN --hiverelay' elsewhere to"
163 echo " double-check externally."
164 echo " Anyone running 'pear run $PROD_LINK' will get length $NEW_LEN."
165 exit 0
166fi
31cat <<'EOF'
16732
168# (c) Not the publisher box (no local seeder) — genuine propagation wait.
169# Relays may still be downloading the new blobs; retry patiently.
170echo " ▸ no local seeder to confirm against — retrying fresh-peer verify"
171echo " (up to 10 min, every 90s)"
172ATTEMPT=0
173MAX_ATTEMPTS=7 # 7 * 90s ≈ 10 min
174until node scripts/verify-pin.js --expect "$NEW_LEN" --hiverelay; do
175 ATTEMPT=$((ATTEMPT + 1))
176 if [[ $ATTEMPT -ge $MAX_ATTEMPTS ]]; then
177 echo
178 echo "✗ Release succeeded but blobs not yet reachable from a fresh peer."
179 echo " The drive IS published — re-run \`node scripts/verify-pin.js --expect $NEW_LEN --hiverelay\`"
180 echo " in 10–20 min. If it keeps failing, relays may need a higher maxStorage"
181 echo " cap (see scripts/pin-self-on-hiverelay.js SEED_OPTS)."
182 exit 2
183 fi
184 echo " ↻ retry $ATTEMPT/$MAX_ATTEMPTS in 90s..."
185 sleep 90
186done
33Preflight complete. Before promotion, an approved operator must provide:
34 1. a signed native package for the target platform;
35 2. a verified AppRelease v2 record and compatible local worker entrypoint;
36 3. independent HiveRelay availability evidence; and
37 4. clean-install, upgrade, rollback, and data-continuity evidence.
18738
188echo
189echo "🎉 Release complete and verified end-to-end."
190echo " Anyone running 'pear run $PROD_LINK' will get length $NEW_LEN."
39No release was staged or published.
40EOF
test/release-packaging.test.js +8 -4
……@@ -289,11 +289,15 @@ test('PearBrowser catalogue release row stays in sync with package and public ad
289289})
290290
291291test('release script purges ignored files from previous Pear stages', () => {
292 assert.match(releaseScript, /pear stage --purge/)
292 assert.match(releaseScript, /native-release preflight/)
293 assert.match(releaseScript, /no publication authority/)
294 assert.doesNotMatch(releaseScript, /pear stage/)
295 assert.doesNotMatch(releaseScript, /pear release/)
293296})
294297
295test('release verification asks HiveRelay for signed seed proof evidence', () => {
296 assert.match(releaseScript, /verify-pin\.js --expect \$NEW_LEN --hiverelay/)
298test('v3 release preflight leaves availability verification to the approved release workflow', () => {
299 assert.doesNotMatch(releaseScript, /verify-pin\.js/)
300 assert.match(releaseScript, /independent HiveRelay availability evidence/)
297301 assert.match(verifyPin, /HiveRelayClient/)
298302 assert.match(verifyPin, /proveSeeded/)
299303 assert.match(verifyPin, /verifySeededFallback/)
……@@ -1467,7 +1471,7 @@ test('native install snippet generator emits release-note packages for every des
14671471 assert.match(guide.stdout, /\[PearBrowser-0\.5\.0-macos-arm64\.dmg\]\(https:\/\/example\.invalid\/PearBrowser-0\.5\.0-macos-arm64\.dmg\)/)
14681472 assert.match(guide.stdout, /npm run -s generate:native-install-guide/)
14691473 assert.match(guide.stdout, /PowerShell/)
1470 assert.match(guide.stdout, /pear run pear:\/\/tco5k7h38uoxatedp1wongdbhjxow1x7jiwm3t1i9cujbebhsbty/)
1474 assert.match(guide.stdout, /legacy migration record/i)
14711475
14721476 const packageProofDir = join(fixture, 'package-proof')
14731477 mkdirSync(packageProofDir)