Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02ac4396d1 |
@@ -1,19 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## v1.8.7-alpha (2026-08-31)
|
||||
|
||||
- **What's New really does stop at v1.8.0 now.** The first correction removed old generated release blocks but missed six much older hand-written v1.2 sections at the bottom of the modal. Those sections are gone, and the release check now recognizes and rejects that legacy format too, so the history floor cannot falsely pass again.
|
||||
|
||||
- **The installer carries the same corrected release, Companion 0.5.28, and complete crash tooling.** Its artifact gate now checks the companion APK version, the v1.8.0 What's New floor, and every kdump package inside the finished ISO; `makedumpfile` is installed explicitly rather than accidentally relying on a recommended dependency that the minimal image deliberately omits.
|
||||
|
||||
## v1.8.6-alpha (2026-08-31)
|
||||
|
||||
- **Companion 0.5.28 is included in the node download this time, with the work that missed v1.8.5.** The companion hub can back up and restore its node list, act as a NIP-46 remote signer, and shows each paired node's FIPS mesh address with tap-to-copy. For Termux users, the included `fipssh` helper turns a durable node npub into its mesh address, so `fipssh user@npub1…` can reach SSH once that node has explicitly allowed port 22. The node-side “SSH over mesh” firewall toggle is not claimed here—it still needs implementation and remains off by default.
|
||||
|
||||
- **What's New now starts cleanly at v1.8.0 and is guaranteed to be newest-first.** Older alpha history no longer overwhelms the useful recent changes, the three stray v1.7 entries that appeared above current releases are gone, and the release check now fails if either the ordering or the v1.8.0 history floor drifts again.
|
||||
|
||||
- **A release can no longer advertise itself before its files exist.** New releases are prepared behind a pending manifest; the publisher uploads the backend and frontend, downloads both back and verifies their size and hash, and only then promotes the signed manifest to the path nodes read. The manifest generator also includes every curated What's New item instead of silently stopping after the first ten physical changelog lines.
|
||||
|
||||
## v1.8.5-alpha (2026-08-30)
|
||||
|
||||
- **Cuprate — an independent Monero node — is now an app.** Monero consensus validated by a second, unrelated codebase (Rust), the same layer of security-in-depth Bitcoin gets from Knots. Review caught two problems before anything shipped: the unrestricted RPC that can move funds stayed bound to the container's loopback (never published to the node, let alone the LAN — anything on the node could previously have reached it), and its restricted RPC moved off port 18089 to avoid colliding with Penpot. Honest caveat: upstream has cut no stable release yet, so the pin tracks an exact preview build (0.1.0-preview-18-g618ff14) and moves to their first tagged release when there is one.
|
||||
|
||||
Generated
+1
-1
@@ -104,7 +104,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "archipelago"
|
||||
version = "1.8.6-alpha"
|
||||
version = "1.8.5-alpha"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"archipelago-container",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "archipelago"
|
||||
version = "1.8.6-alpha"
|
||||
version = "1.8.5-alpha"
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
# SESSION — companion 0.5.28: shipped, published, playbook (2026-08-31)
|
||||
|
||||
**For: the companion agent (next session) + anyone shipping a companion
|
||||
release.** Session that closed the 2026-08-30 companion-agent queue (#61
|
||||
residual, #128, #139) and shipped 0.5.28 end-to-end.
|
||||
|
||||
## Release state at session end — ALL LIVE
|
||||
|
||||
Companion **0.5.28 / versionCode 48**, main @ PR #149 (`9f1a289d`), deploy
|
||||
handoff merged as PR #150 (`91374392`). Every public surface verified
|
||||
byte-identical (`shasum -a 256` = `fc786b46c704c5752f04fe603371365524c749734f17bd8858cf02fa2dbc34ca`):
|
||||
|
||||
| Surface | URL | State |
|
||||
|---|---|---|
|
||||
| Gitea raw-on-main | `http://146.59.87.168:3000/lfg2025/archy/raw/branch/main/neode-ui/public/packages/archipelago-companion.apk` | ✅ 0.5.28, v1+v2+v3 verified on download |
|
||||
| Foundation static `/packages/` (real-node QR URL) | `https://source.archipelago-foundation.org/packages/archipelago-companion.apk` | ✅ 0.5.28 |
|
||||
| Foundation Gitea-raw proxy | `…/lfg2025/archy/raw/branch/main/…` | ✅ 0.5.28 (6h cache — may lag after pushes) |
|
||||
| Demo `:2100` | `http://146.59.87.168:2100/packages/archipelago-companion.apk` | ✅ 0.5.28 (auto: CI + Portainer webhook) |
|
||||
|
||||
Only remaining live-surface step: **node web-bundle redeploys** so each
|
||||
node's own served copy is 0.5.28 — archi-dev-box's standard step, written up
|
||||
in `docs/HANDOFF-2026-08-31-companion-0.5.28-deploy.md` (its §1/§3 were
|
||||
already done by the time of this doc — only §2 outstanding).
|
||||
|
||||
Tracker: #128 and #139 closed with what-shipped comments; #61 (already
|
||||
closed) got a residual-fix follow-up. Signing cert unchanged (`d622e07e…`),
|
||||
so phones update in place.
|
||||
|
||||
## What shipped in 0.5.28 (map)
|
||||
|
||||
- **#61 residual (web)**: `isCompanionApp()` gates on `CompanionBanner.vue`
|
||||
render, `openCompanionIntro()` (useCompanionIntro.ts), and the overlay's
|
||||
manual-open watcher; overlay moved to the canonical helper. Vitest suite
|
||||
green (1013 tests).
|
||||
- **#128 Backup & Restore**: `Android/rust/archy-fips-core/src/backup.rs`
|
||||
(ADR-005 envelope, node-compatible), `BackupManager.kt`, hub sub-page
|
||||
`ui/components/BackupSection.kt`. Doc: `companion-backup-restore.md`.
|
||||
- **#139 Remote Signer**: `src/nostr.rs` (NIP-44 v2 + NIP-04 + BIP-340,
|
||||
official vectors), `nostr/BunkerManager.kt` + `NostrSignerPreferences.kt`,
|
||||
hub sub-page `ui/components/SignerSection.kt`, `nostrconnect://` deep link
|
||||
via `SignerLaunch`. Harness: `Android/tools/nip46-test-client.py`.
|
||||
Doc: `companion-nip46-remote-signer.md`.
|
||||
- **Hub modal redesign** (field feedback): both features are sub-pages like
|
||||
Nodes/FIPS; panel height cap 70%; scanner hosted by NESMenu outside the
|
||||
panel; back-arrow → hub.
|
||||
- **Extras**: node mesh ULA shown/copyable in the Nodes list (`MenuItem`
|
||||
subtitle); `Android/tools/fipssh` (npub→ULA is pure: `fd ‖ sha256(pubkey)[0..15]`,
|
||||
pinned by `npub_derives_the_same_mesh_ula_as_the_fips_identity` test).
|
||||
- **Node-side handoffs written**: `HANDOFF-2026-08-31-ssh-over-mesh.md`
|
||||
(SSH-over-mesh toggle) and the 0.5.28 deploy handoff.
|
||||
|
||||
## The deployment playbook (learned the hard way this session)
|
||||
|
||||
### Networking — everything goes through the Tor SOCKS proxy
|
||||
|
||||
Direct connections to `146.59.87.168` fail from this box ("Bad file
|
||||
descriptor"); git works because `~/.gitconfig` sets
|
||||
`proxy = socks5h://127.0.0.1:9050`. **For curl/Gitea API you must pass it
|
||||
explicitly:**
|
||||
|
||||
```bash
|
||||
curl -s --socks5-hostname 127.0.0.1:9050 ... # works
|
||||
curl -s ... # HTTP 000, "unreachable"
|
||||
```
|
||||
|
||||
This is why earlier sessions concluded "Gitea API unreachable" — wrong; it
|
||||
just needs the proxy flag.
|
||||
|
||||
### Gitea API + auth
|
||||
|
||||
- Base: `http://146.59.87.168:3000/api/v1` (v1.27.1), via the proxy.
|
||||
- The keychain git credential (`security find-internet-password -s
|
||||
146.59.87.168`, acct `v4v`) is a **`write:repository`-only token** — fine
|
||||
for git, CANNOT read/write issues.
|
||||
- Issue ops need `write:issue`. This session the user pasted a broad token
|
||||
(activitypub+misc+notification+organization+package+issue+repository) —
|
||||
**revocation still pending** (it's in chat scrollback). Ask the user for a
|
||||
scoped `write:issue` token next time.
|
||||
|
||||
### main is PROTECTED — ship via -ship branch + PR + API merge
|
||||
|
||||
`git push origin main` is rejected by pre-receive. The working sequence:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git checkout main && git reset --hard origin/main # local main is STALE (see below)
|
||||
git merge --no-ff companion/<ver> -m "Companion <ver> — …"
|
||||
./Android/ship-companion.sh # builds, signs v1+v2+v3, stages APK+meta, commits
|
||||
# its `git push` FAILS on protected main — expected. Push the branch instead:
|
||||
git push origin main:companion/<ver>-ship
|
||||
# then create + merge the PR via API:
|
||||
curl ... POST repos/lfg2025/archy/pulls {"head":"companion/<ver>-ship","base":"main",...}
|
||||
curl ... POST repos/lfg2025/archy/pulls/<n>/merge -d '{"Do":"merge"}'
|
||||
```
|
||||
|
||||
(Refinement for next time: run `ship-companion.sh` ON the `-ship` branch
|
||||
from the start — it pushes the current branch, which for a `-ship` branch
|
||||
succeeds directly.)
|
||||
|
||||
- **Local `main` is the pre-open-source-import lineage** (1115 stale
|
||||
commits, unrelated history). Always `reset --hard origin/main` before
|
||||
using it; never merge into it without the reset.
|
||||
- A **stale tag ref** (`v1.7.115-alpha`) can make `git fetch` fail
|
||||
("did not send all necessary objects") — `rm .git/refs/tags/v1.7.115-alpha`.
|
||||
- Last release's `-ship` branch for reference: `origin/companion/0.5.27-ship`.
|
||||
|
||||
### Build + verify (per release)
|
||||
|
||||
- Version lives in `Android/app/build.gradle.kts` (`versionCode` must
|
||||
strictly increase; meta json is auto-generated by the publish script from
|
||||
it). 0.5.28 → next is **0.5.29/vc49**.
|
||||
- APK package is `com.archipelago.app.debug` (the served artifact IS the
|
||||
debug build, committed repo keystore, cert SHA-256 `d622e07e…ec2664d`).
|
||||
Local `Android/app/debug.keystore` is untracked but produces that cert —
|
||||
verify per release: `apksigner verify --print-certs` on old vs new.
|
||||
- Build: `cd Android && JAVA_HOME=/opt/homebrew/opt/openjdk@17
|
||||
ANDROID_HOME=$HOME/Library/Android/sdk ./gradlew :app:assembleDebug`
|
||||
(builds the Rust via cargo-ndk, NDK under `~/Library/Android/sdk/ndk/`).
|
||||
Test build for the user: copy to `~/Desktop/archipelago-companion-<ver>.apk`.
|
||||
- Rust: `cd Android/rust/archy-fips-core && cargo test --lib` (24 tests at
|
||||
session end) + clippy. neode-ui: `npm ci` first (node_modules not kept),
|
||||
`npx vitest run`, `npm run type-check`.
|
||||
- Post-ship verify block: aapt2 badging, shasum vs Gitea raw, apksigner
|
||||
v1/v2/v3, the three public URLs' meta json (table above), foundation
|
||||
raw-proxy may serve up to 6h stale (cache-control: max-age=21600).
|
||||
|
||||
### Infrastructure facts
|
||||
|
||||
- `source.archipelago-foundation.org` = openresty on vps2 with **two
|
||||
surfaces**: static `/packages/` (manual mirror; the real-node QR URL) and
|
||||
a Gitea-raw proxy (6h cache, auto). Demo `:2100` redeploys automatically:
|
||||
`.gitea/workflows/demo-images.yml` fires on `main` pushes touching
|
||||
`neode-ui/**`, then calls the Portainer webhook.
|
||||
- **No SSH to vps2 from this box**: `archy_146_release` key declined for
|
||||
root/archipelago/dorian/lfg2025/deploy/git. Server-side work needs the
|
||||
archi-dev-box agent or the user.
|
||||
|
||||
## Open items for next session
|
||||
|
||||
1. **Confirm node web-bundle redeploys** happened (archi-dev-box; deploy
|
||||
handoff §2) — a paired node's own `/packages/` should serve vc48.
|
||||
2. **Token revocation** (user) + request a `write:issue`-scoped one.
|
||||
3. **Node-side roadmap** fed by this release: SSH-over-mesh toggle
|
||||
(`HANDOFF-2026-08-31-ssh-over-mesh.md`), node NIP-46 client (login flow B),
|
||||
node-side storage for companion backup envelopes.
|
||||
4. **On-device follow-ups**: the user's full 0.5.28 pass — signer e2e via
|
||||
the harness (`/tmp/nip46env/bin/python Android/tools/nip46-test-client.py`),
|
||||
backup round-trip on a wipe, and the zxing-cpp decision trigger
|
||||
(move-to-the-code; sketch is verified online:
|
||||
`io.github.zxing-cpp:android:3.1.1`, still NOT-actioned by design).
|
||||
5. Untracked on this box, deliberately left: `Android/app/debug.keystore`,
|
||||
`docs/1.8-alpha-improvements-tracker.md`,
|
||||
`docs/SESSION-1.8.0-OTA-PROGRESS.md`, `image-recipe/branding/source-logos/`
|
||||
(other workstreams' files).
|
||||
@@ -573,7 +573,7 @@ RUN mkdir -p /etc/polkit-1/rules.d && \
|
||||
# already-deployed nodes over OTA (idempotent no-op here once applied).
|
||||
RUN set -eu; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends kdump-tools kexec-tools makedumpfile rasdaemon; \
|
||||
apt-get install -y --no-install-recommends kdump-tools kexec-tools rasdaemon; \
|
||||
apt-get clean; rm -rf /var/lib/apt/lists/*; \
|
||||
CONF=/etc/default/kdump-tools; \
|
||||
sed -i 's|^#\?USE_KDUMP=.*|USE_KDUMP="1"|' "$CONF"; \
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "neode-ui",
|
||||
"version": "1.8.6-alpha",
|
||||
"version": "1.8.5-alpha",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "neode-ui",
|
||||
"version": "1.8.6-alpha",
|
||||
"version": "1.8.5-alpha",
|
||||
"dependencies": {
|
||||
"@scure/bip39": "^2.2.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "neode-ui",
|
||||
"private": true,
|
||||
"version": "1.8.6-alpha",
|
||||
"version": "1.8.5-alpha",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "./start-dev.sh",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+18
-16
@@ -1,29 +1,31 @@
|
||||
{
|
||||
"changelog": [
|
||||
"**Companion 0.5.28 is included in the node download this time, with the work that missed v1.8.5.** The companion hub can back up and restore its node list, act as a NIP-46 remote signer, and shows each paired node's FIPS mesh address with tap-to-copy. For Termux users, the included `fipssh` helper turns a durable node npub into its mesh address, so `fipssh user@npub1…` can reach SSH once that node has explicitly allowed port 22. The node-side “SSH over mesh” firewall toggle is not claimed here—it still needs implementation and remains off by default.",
|
||||
"**What's New now starts cleanly at v1.8.0 and is guaranteed to be newest-first.** Older alpha history no longer overwhelms the useful recent changes, the three stray v1.7 entries that appeared above current releases are gone, and the release check now fails if either the ordering or the v1.8.0 history floor drifts again.",
|
||||
"**A release can no longer advertise itself before its files exist.** New releases are prepared behind a pending manifest; the publisher uploads the backend and frontend, downloads both back and verifies their size and hash, and only then promotes the signed manifest to the path nodes read. The manifest generator also includes every curated What's New item instead of silently stopping after the first ten physical changelog lines."
|
||||
"**Cuprate — an independent Monero node — is now an app.** Monero consensus validated by a second, unrelated codebase (Rust), the same layer of security-in-depth Bitcoin gets from Knots. Review caught two problems before anything shipped: the unrestricted RPC that can move funds stayed bound to the container's loopback (never published to the node, let alone the LAN — anything on the node could previously have reached it), and its restricted RPC moved off port 18089 to avoid colliding with Penpot. Honest caveat: upstream has cut no stable release yet, so the pin tracks an exact preview build (0.1.0-preview-18-g618ff14) and moves to their first tagged release when there is one.",
|
||||
"**A frozen node now explains itself — and comes back on its own.** The host now captures a memory dump into /var/crash when the kernel panics *or* wedges (a hung kiosk used to sit dead until someone power-cycled it; now it dumps, reboots itself, and leaves the evidence behind), and records failing-memory signals (ECC errors) into a database as they happen. This is the first change delivered by a new host-update channel: the node's own updater now carries OS-level packages and settings to already-deployed machines — the crash-kernel's memory reservation is the one part that waits for a reboot, and the node says so rather than pretending.",
|
||||
"**Uninstalling an app can no longer report success when it failed.** The declarative path used to swallow every teardown error and report the app uninstalled, leaving the tile behind and the truth in the logs. A failed uninstall now stops and shows the real per-app errors, so \"still there\" is never presented as \"gone\".",
|
||||
"**Pictures to internet-only mesh contacts work now.** Sending an attachment inline always took the radio path and failed with \"Peer is federation-only (no radio twin)\" for contacts reachable only over the internet — and the size-adviser kept recommending a radio transfer those peers can't receive. Both fixed: inline sends route over the federation when that's the only way to reach the peer, and the advice no longer offers radio-only transfers to radio-unreachable contacts.",
|
||||
"**Disk cleanup finally has honest numbers.** Space \"free\" on a drive was counted including the slice the filesystem keeps reserved for root — roughly 5% of the disk, 92 GB on one dev box — so the automatic cleanup that's supposed to kick in at 90% never triggered and stale container images piled up unnoticed. Reserved space now counts as used, which is what the threshold was always meant to measure."
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"current_version": "1.8.6-alpha",
|
||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.6-alpha/archipelago",
|
||||
"current_version": "1.8.5-alpha",
|
||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.5-alpha/archipelago",
|
||||
"name": "archipelago",
|
||||
"new_version": "1.8.6-alpha",
|
||||
"sha256": "6171e1f46b0e2c2754a18e668f16810338a09b71a68dc365d65173ca1e4d400a",
|
||||
"size_bytes": 63930112
|
||||
"new_version": "1.8.5-alpha",
|
||||
"sha256": "54e91944c6395a53c8ac87ea97f61e8a7fc5ffd133f017180931ba4fa9566239",
|
||||
"size_bytes": 63934504
|
||||
},
|
||||
{
|
||||
"current_version": "1.8.6-alpha",
|
||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.6-alpha/archipelago-frontend-1.8.6-alpha.tar.gz",
|
||||
"name": "archipelago-frontend-1.8.6-alpha.tar.gz",
|
||||
"new_version": "1.8.6-alpha",
|
||||
"sha256": "1a5b54c516f91a4ea4491e08182f0f9521ee7e27935388814007f91695f06e0f",
|
||||
"size_bytes": 97769923
|
||||
"current_version": "1.8.5-alpha",
|
||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.5-alpha/archipelago-frontend-1.8.5-alpha.tar.gz",
|
||||
"name": "archipelago-frontend-1.8.5-alpha.tar.gz",
|
||||
"new_version": "1.8.5-alpha",
|
||||
"sha256": "a5d773e8225bfd8a34dfc74acf38bdc2c39bc138c62bf9f4137d0efd72cce3e9",
|
||||
"size_bytes": 97657151
|
||||
}
|
||||
],
|
||||
"release_date": "2026-08-31",
|
||||
"signature": "c21dc5cb5a6caa3253c08835b65d0417a312bc8c51acdf92df990ad2110ae762b77287c479a523a8521827ec7b4695114ff9c39924cf1d30d0d622abb53b9208",
|
||||
"signature": "f0bcec4935588ee428ebabfbb6e08e00dbd42202af92763073a7f98f4785925ca3565bf96fecbdd1695eff77fcf906783fd9053d82730b11c75441c6f5a60b05",
|
||||
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
||||
"version": "1.8.6-alpha"
|
||||
"version": "1.8.5-alpha"
|
||||
}
|
||||
|
||||
+18
-16
@@ -1,29 +1,31 @@
|
||||
{
|
||||
"changelog": [
|
||||
"**Companion 0.5.28 is included in the node download this time, with the work that missed v1.8.5.** The companion hub can back up and restore its node list, act as a NIP-46 remote signer, and shows each paired node's FIPS mesh address with tap-to-copy. For Termux users, the included `fipssh` helper turns a durable node npub into its mesh address, so `fipssh user@npub1…` can reach SSH once that node has explicitly allowed port 22. The node-side “SSH over mesh” firewall toggle is not claimed here—it still needs implementation and remains off by default.",
|
||||
"**What's New now starts cleanly at v1.8.0 and is guaranteed to be newest-first.** Older alpha history no longer overwhelms the useful recent changes, the three stray v1.7 entries that appeared above current releases are gone, and the release check now fails if either the ordering or the v1.8.0 history floor drifts again.",
|
||||
"**A release can no longer advertise itself before its files exist.** New releases are prepared behind a pending manifest; the publisher uploads the backend and frontend, downloads both back and verifies their size and hash, and only then promotes the signed manifest to the path nodes read. The manifest generator also includes every curated What's New item instead of silently stopping after the first ten physical changelog lines."
|
||||
"**Cuprate — an independent Monero node — is now an app.** Monero consensus validated by a second, unrelated codebase (Rust), the same layer of security-in-depth Bitcoin gets from Knots. Review caught two problems before anything shipped: the unrestricted RPC that can move funds stayed bound to the container's loopback (never published to the node, let alone the LAN — anything on the node could previously have reached it), and its restricted RPC moved off port 18089 to avoid colliding with Penpot. Honest caveat: upstream has cut no stable release yet, so the pin tracks an exact preview build (0.1.0-preview-18-g618ff14) and moves to their first tagged release when there is one.",
|
||||
"**A frozen node now explains itself — and comes back on its own.** The host now captures a memory dump into /var/crash when the kernel panics *or* wedges (a hung kiosk used to sit dead until someone power-cycled it; now it dumps, reboots itself, and leaves the evidence behind), and records failing-memory signals (ECC errors) into a database as they happen. This is the first change delivered by a new host-update channel: the node's own updater now carries OS-level packages and settings to already-deployed machines — the crash-kernel's memory reservation is the one part that waits for a reboot, and the node says so rather than pretending.",
|
||||
"**Uninstalling an app can no longer report success when it failed.** The declarative path used to swallow every teardown error and report the app uninstalled, leaving the tile behind and the truth in the logs. A failed uninstall now stops and shows the real per-app errors, so \"still there\" is never presented as \"gone\".",
|
||||
"**Pictures to internet-only mesh contacts work now.** Sending an attachment inline always took the radio path and failed with \"Peer is federation-only (no radio twin)\" for contacts reachable only over the internet — and the size-adviser kept recommending a radio transfer those peers can't receive. Both fixed: inline sends route over the federation when that's the only way to reach the peer, and the advice no longer offers radio-only transfers to radio-unreachable contacts.",
|
||||
"**Disk cleanup finally has honest numbers.** Space \"free\" on a drive was counted including the slice the filesystem keeps reserved for root — roughly 5% of the disk, 92 GB on one dev box — so the automatic cleanup that's supposed to kick in at 90% never triggered and stale container images piled up unnoticed. Reserved space now counts as used, which is what the threshold was always meant to measure."
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"current_version": "1.8.6-alpha",
|
||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.6-alpha/archipelago",
|
||||
"current_version": "1.8.5-alpha",
|
||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.5-alpha/archipelago",
|
||||
"name": "archipelago",
|
||||
"new_version": "1.8.6-alpha",
|
||||
"sha256": "6171e1f46b0e2c2754a18e668f16810338a09b71a68dc365d65173ca1e4d400a",
|
||||
"size_bytes": 63930112
|
||||
"new_version": "1.8.5-alpha",
|
||||
"sha256": "54e91944c6395a53c8ac87ea97f61e8a7fc5ffd133f017180931ba4fa9566239",
|
||||
"size_bytes": 63934504
|
||||
},
|
||||
{
|
||||
"current_version": "1.8.6-alpha",
|
||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.6-alpha/archipelago-frontend-1.8.6-alpha.tar.gz",
|
||||
"name": "archipelago-frontend-1.8.6-alpha.tar.gz",
|
||||
"new_version": "1.8.6-alpha",
|
||||
"sha256": "1a5b54c516f91a4ea4491e08182f0f9521ee7e27935388814007f91695f06e0f",
|
||||
"size_bytes": 97769923
|
||||
"current_version": "1.8.5-alpha",
|
||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.5-alpha/archipelago-frontend-1.8.5-alpha.tar.gz",
|
||||
"name": "archipelago-frontend-1.8.5-alpha.tar.gz",
|
||||
"new_version": "1.8.5-alpha",
|
||||
"sha256": "a5d773e8225bfd8a34dfc74acf38bdc2c39bc138c62bf9f4137d0efd72cce3e9",
|
||||
"size_bytes": 97657151
|
||||
}
|
||||
],
|
||||
"release_date": "2026-08-31",
|
||||
"signature": "c21dc5cb5a6caa3253c08835b65d0417a312bc8c51acdf92df990ad2110ae762b77287c479a523a8521827ec7b4695114ff9c39924cf1d30d0d622abb53b9208",
|
||||
"signature": "f0bcec4935588ee428ebabfbb6e08e00dbd42202af92763073a7f98f4785925ca3565bf96fecbdd1695eff77fcf906783fd9053d82730b11c75441c6f5a60b05",
|
||||
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
||||
"version": "1.8.6-alpha"
|
||||
"version": "1.8.5-alpha"
|
||||
}
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
#!/bin/bash
|
||||
# Validate the live or a pending release manifest:
|
||||
# Validate releases/manifest.json:
|
||||
# - version matches core/archipelago/Cargo.toml
|
||||
# - changelog contains curated release notes, not raw git log output
|
||||
# - every component's download_url exists on disk and matches sha256/size
|
||||
#
|
||||
# Run on every push from CI, and also locally before publishing a release:
|
||||
# scripts/check-release-manifest.sh [path/to/manifest.json]
|
||||
# scripts/check-release-manifest.sh
|
||||
#
|
||||
# Exits non-zero on any mismatch so the release process fails loud.
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
MANIFEST="${1:-$REPO_ROOT/releases/manifest.json}"
|
||||
[[ "$MANIFEST" = /* ]] || MANIFEST="$REPO_ROOT/$MANIFEST"
|
||||
MANIFEST="$REPO_ROOT/releases/manifest.json"
|
||||
|
||||
if [ ! -f "$MANIFEST" ]; then
|
||||
echo "❌ manifest missing: $MANIFEST"
|
||||
echo "❌ releases/manifest.json missing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -26,18 +25,6 @@ ok() { echo "✅ $*"; }
|
||||
MANIFEST_VERSION=$(python3 -c "import json; print(json.load(open('$MANIFEST'))['version'])")
|
||||
CARGO_VERSION=$(grep '^version' "$REPO_ROOT/core/archipelago/Cargo.toml" | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
|
||||
# A prepared release deliberately leaves the live manifest on the previous
|
||||
# version. Ordinary pushes are therefore harmless: only the publisher promotes
|
||||
# the pending manifest after its assets have been uploaded and downloaded back.
|
||||
if [ "$MANIFEST_VERSION" != "$CARGO_VERSION" ] && [ "$MANIFEST" = "$REPO_ROOT/releases/manifest.json" ]; then
|
||||
PENDING="$REPO_ROOT/releases/pending/v${CARGO_VERSION}/manifest.json"
|
||||
if [ -f "$PENDING" ]; then
|
||||
ok "live manifest remains v${MANIFEST_VERSION} while v${CARGO_VERSION} is pending"
|
||||
MANIFEST="$PENDING"
|
||||
MANIFEST_VERSION="$CARGO_VERSION"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$MANIFEST_VERSION" != "$CARGO_VERSION" ]; then
|
||||
fail "manifest version ($MANIFEST_VERSION) ≠ Cargo.toml ($CARGO_VERSION)"
|
||||
fi
|
||||
@@ -118,4 +105,4 @@ for i in $(seq 0 $((COMPONENT_COUNT - 1))); do
|
||||
done
|
||||
|
||||
echo
|
||||
ok "$MANIFEST passes all checks — safe to publish v${MANIFEST_VERSION}"
|
||||
ok "releases/manifest.json passes all checks — safe to publish v${MANIFEST_VERSION}"
|
||||
|
||||
@@ -261,19 +261,15 @@ content = open('$CHANGELOG_FILE').read()
|
||||
pattern = r'## .*?${VERSION}.*?\n(.*?)(?=\n## |\Z)'
|
||||
m = re.search(pattern, content, re.DOTALL)
|
||||
if m:
|
||||
for line in m.group(1).splitlines():
|
||||
for line in m.group(1).strip().split('\n')[:10]:
|
||||
line = line.strip()
|
||||
if not line.startswith('- '):
|
||||
continue
|
||||
text = line[2:].strip()
|
||||
if text.lower().startswith('validation '):
|
||||
continue
|
||||
print(text)
|
||||
if line:
|
||||
print(line)
|
||||
" 2>/dev/null || echo "")
|
||||
if [ -n "$ENTRIES" ]; then
|
||||
CHANGELOG=$(echo "$ENTRIES" | python3 -c "
|
||||
import sys, json
|
||||
lines = [l.strip() for l in sys.stdin if l.strip()]
|
||||
lines = [l.strip().lstrip('- ') for l in sys.stdin if l.strip()]
|
||||
print(json.dumps(lines))
|
||||
")
|
||||
fi
|
||||
@@ -302,7 +298,7 @@ echo ""
|
||||
cat "$OUTPUT_FILE"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Review and sign the manifest above"
|
||||
echo " 2. Keep it under releases/pending/v$VERSION/ — do NOT replace the live manifest"
|
||||
echo " 3. Run scripts/publish-release-assets.sh $VERSION gitea-vps2"
|
||||
echo " (it uploads + verifies assets before atomically promoting the manifest)"
|
||||
echo " 1. Review the manifest above"
|
||||
echo " 2. Upload artifacts to Gitea release v$VERSION"
|
||||
echo " 3. Commit manifest.json to releases/manifest.json on main"
|
||||
echo " 4. Tag the release: git tag v$VERSION && git push --tags"
|
||||
|
||||
+49
-39
@@ -2,8 +2,7 @@
|
||||
# create-release.sh — Full release automation for Archipelago
|
||||
#
|
||||
# Bumps version in Cargo.toml and package.json, generates changelog from git log,
|
||||
# creates a pending release manifest, and creates git tag. The live manifest is
|
||||
# promoted only by publish-release-assets.sh after the assets are verified.
|
||||
# creates release manifest, and creates git tag.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/create-release.sh 1.0.0 # Release v1.0.0
|
||||
@@ -31,9 +30,9 @@ for arg in "$@"; do
|
||||
echo " 2. Bump version in Cargo.toml and package.json"
|
||||
echo " 3. Build backend"
|
||||
echo " 4. Build frontend"
|
||||
echo " 5. Validate the curated changelog"
|
||||
echo " 6. Create pending release manifest"
|
||||
echo " 7. Commit release preparation"
|
||||
echo " 5. Generate changelog from git log"
|
||||
echo " 6. Create release manifest"
|
||||
echo " 7. Commit version bump"
|
||||
echo " 8. Create git tag v{VERSION}"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
@@ -122,13 +121,14 @@ if $DRY_RUN; then
|
||||
echo " 2. Update neode-ui/package.json version to $VERSION"
|
||||
echo " 3. Build backend (cargo build --release -p archipelago)"
|
||||
echo " 4. Build frontend (npm run build)"
|
||||
echo " 5. Validate the curated changelog"
|
||||
echo " 6. Create pending release manifest (the live manifest stays unchanged)"
|
||||
echo " 7. Commit: 'chore: prepare release v${VERSION}'"
|
||||
echo " 5. Generate changelog from git log since v${CURRENT_CARGO_VERSION}"
|
||||
echo " 6. Create release manifest"
|
||||
echo " 7. Commit: 'chore: release v${VERSION}'"
|
||||
echo " 8. Tag: v${VERSION}"
|
||||
echo ""
|
||||
echo "After this script, publish only with:"
|
||||
echo " scripts/publish-release-assets.sh ${VERSION} gitea-vps2"
|
||||
echo "After this script, you would:"
|
||||
echo " - Push: git push && git push --tags"
|
||||
echo " - Build ISOs on server: ssh archipelago@192.0.2.10"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -214,13 +214,9 @@ if [ ! -f "$CHANGELOG_FILE" ] || ! grep -q "^## v${VERSION} (" "$CHANGELOG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[6/8] Creating pending release manifest..."
|
||||
# Never write the fleet-visible path here. A normal `git push main` must not be
|
||||
# capable of advertising assets which have not been uploaded yet.
|
||||
PENDING_DIR="$PROJECT_ROOT/releases/pending/v${VERSION}"
|
||||
PENDING_MANIFEST="$PENDING_DIR/manifest.json"
|
||||
mkdir -p "$PENDING_DIR"
|
||||
"$SCRIPT_DIR/create-release-manifest.sh" --version "$VERSION" --date "$RELEASE_DATE" --output "$PENDING_MANIFEST" 2>&1 | grep -v "^$"
|
||||
echo "[6/8] Creating release manifest..."
|
||||
mkdir -p "$PROJECT_ROOT/releases"
|
||||
"$SCRIPT_DIR/create-release-manifest.sh" --version "$VERSION" --date "$RELEASE_DATE" --output "$PROJECT_ROOT/releases/manifest.json" 2>&1 | grep -v "^$"
|
||||
|
||||
# §A supply-chain: the OTA manifest must carry the release-root signature.
|
||||
# Nodes refuse to AUTO-apply unsigned manifests, and publish-release-assets.sh
|
||||
@@ -243,45 +239,60 @@ if [ -n "${RELEASE_MASTER_MNEMONIC:-}" ] || [ -t 0 ]; then
|
||||
echo " Enter by itself will NOT submit; pasting twice concatenates"
|
||||
echo " the phrases and fails on word count."
|
||||
echo "════════════════════════════════════════════════════════════════"
|
||||
"$SIGNER" ceremony sign "$PENDING_MANIFEST"
|
||||
"$SIGNER" ceremony verify "$PENDING_MANIFEST"
|
||||
"$SIGNER" ceremony sign "$PROJECT_ROOT/releases/manifest.json"
|
||||
"$SIGNER" ceremony verify "$PROJECT_ROOT/releases/manifest.json"
|
||||
else
|
||||
echo "⚠ WARNING: no TTY and RELEASE_MASTER_MNEMONIC unset — pending manifest left UNSIGNED."
|
||||
echo " This run will ABORT before committing (step 7 refuses an unsigned manifest)."
|
||||
echo " Sign it, then re-run: bash scripts/sign-manifest.sh $PENDING_MANIFEST"
|
||||
echo "⚠ WARNING: no TTY and RELEASE_MASTER_MNEMONIC unset — manifest left UNSIGNED."
|
||||
echo " This run will ABORT before committing (step 7 refuses an unsigned"
|
||||
echo " manifest), because nodes read releases/manifest.json from branch main"
|
||||
echo " and would refuse to auto-apply it."
|
||||
echo " Sign it, then re-run: bash scripts/sign-manifest.sh"
|
||||
fi
|
||||
cp "$PROJECT_ROOT/releases/manifest.json" "$PROJECT_ROOT/release-manifest.json"
|
||||
|
||||
echo "[6c/8] Staging release artifacts for validation..."
|
||||
VERSION_DIR="$PROJECT_ROOT/releases/v${VERSION}"
|
||||
FRONTEND_ARCHIVE="/tmp/archipelago-frontend-${VERSION}.tar.gz"
|
||||
mkdir -p "$VERSION_DIR"
|
||||
install -m 0755 "$PROJECT_ROOT/core/target/release/archipelago" "$VERSION_DIR/archipelago"
|
||||
install -m 0644 "$FRONTEND_ARCHIVE" "$VERSION_DIR/archipelago-frontend-${VERSION}.tar.gz"
|
||||
"$SCRIPT_DIR/check-release-manifest.sh" "$PENDING_MANIFEST"
|
||||
"$SCRIPT_DIR/check-release-manifest.sh"
|
||||
|
||||
# §A supply-chain gate, mirroring publish-release-assets.sh. The pending path
|
||||
# prevents an ordinary main push from exposing the release, but an unsigned
|
||||
# manifest is still unpublishable and must never be tagged as ready.
|
||||
# §A supply-chain gate, mirroring publish-release-assets.sh — but EARLIER,
|
||||
# because publishing is not the first way an unsigned manifest reaches the
|
||||
# fleet. Nodes fetch releases/manifest.json straight from branch `main`
|
||||
# (see the verification URLs printed below), so the COMMIT is what exposes
|
||||
# it, not the publish. publish-release-assets.sh refusing to ship is a
|
||||
# backstop that arrives one step too late: by then the unsigned manifest is
|
||||
# already on main and the fleet is already refusing to auto-apply.
|
||||
#
|
||||
# This is why every cycle needed a manual catch. The signing block above is
|
||||
# conditional — no TTY and no RELEASE_MASTER_MNEMONIC means it prints a
|
||||
# warning and falls through — and the commit then happened anyway. A release
|
||||
# commit carrying a manifest no node will accept has no valid use, so refuse
|
||||
# to create one rather than leave a tag that has to be re-cut.
|
||||
# Release root ROTATED 2026-08-05. v1.7.122-alpha was the last release signed
|
||||
# with the old root (z6Mkkid…q7ur) — it is the release that installed this
|
||||
# pin on every node. From v1.7.123 onward the new root signs, and nodes
|
||||
# running .122+ reject anything signed with the old key.
|
||||
EXPECTED_DID="did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT"
|
||||
if ! grep -q '"signature":' "$PENDING_MANIFEST" \
|
||||
|| ! grep -q "\"signed_by\": \"$EXPECTED_DID\"" "$PENDING_MANIFEST"; then
|
||||
if ! grep -q '"signature":' "$PROJECT_ROOT/releases/manifest.json" \
|
||||
|| ! grep -q "\"signed_by\": \"$EXPECTED_DID\"" "$PROJECT_ROOT/releases/manifest.json"; then
|
||||
echo "" >&2
|
||||
echo "Error: the pending manifest is NOT signed by the release root." >&2
|
||||
echo " Refusing to commit an unpublishable release." >&2
|
||||
echo "Error: releases/manifest.json is NOT signed by the release root." >&2
|
||||
echo " Refusing to commit — nodes read this file from branch main and will" >&2
|
||||
echo " refuse to auto-apply it, so the release would be dead on arrival." >&2
|
||||
echo "" >&2
|
||||
echo " Sign it, then re-run this script:" >&2
|
||||
echo " bash scripts/sign-manifest.sh $PENDING_MANIFEST" >&2
|
||||
echo " bash scripts/sign-manifest.sh" >&2
|
||||
echo "" >&2
|
||||
echo " (Signing needs a TTY for the mnemonic prompt, or RELEASE_MASTER_MNEMONIC set.)" >&2
|
||||
exit 1
|
||||
fi
|
||||
"$SIGNER" ceremony verify "$PENDING_MANIFEST" \
|
||||
"$SIGNER" ceremony verify "$PROJECT_ROOT/releases/manifest.json" \
|
||||
|| { echo "Error: manifest signature failed cryptographic verification — refusing to commit" >&2; exit 1; }
|
||||
|
||||
echo "[7/8] Committing release preparation..."
|
||||
echo "[7/8] Committing version bump..."
|
||||
git -C "$PROJECT_ROOT" add \
|
||||
core/archipelago/Cargo.toml \
|
||||
core/Cargo.lock \
|
||||
@@ -289,16 +300,15 @@ git -C "$PROJECT_ROOT" add \
|
||||
neode-ui/package-lock.json \
|
||||
neode-ui/public/catalog.json \
|
||||
CHANGELOG.md \
|
||||
releases/manifest.json \
|
||||
release-manifest.json \
|
||||
2>/dev/null || true
|
||||
# releases/** is ignored because binaries live in Gitea attachments; force-add
|
||||
# only this small signed pending manifest.
|
||||
git -C "$PROJECT_ROOT" add -f "releases/pending/v${VERSION}/manifest.json"
|
||||
# Cargo.lock (rewritten by the release build after the version bump) and
|
||||
# neode-ui/public/catalog.json (regenerated by the frontend build) belong in
|
||||
# THIS commit: leaving them dirty failed build-iso-release.sh's clean-tree
|
||||
# preflight on three consecutive releases (.127-.129, 2026-08-09/10).
|
||||
|
||||
git -C "$PROJECT_ROOT" commit -m "chore: prepare release v${VERSION}"
|
||||
git -C "$PROJECT_ROOT" commit -m "chore: release v${VERSION}"
|
||||
|
||||
echo "[8/8] Creating git tag..."
|
||||
git -C "$PROJECT_ROOT" tag -a "v${VERSION}" -m "Release v${VERSION}"
|
||||
@@ -309,8 +319,8 @@ echo ""
|
||||
echo "Artifacts:"
|
||||
echo " - Version bumped in Cargo.toml and package.json"
|
||||
echo " - Changelog updated in CHANGELOG.md"
|
||||
echo " - Pending manifest: releases/pending/v${VERSION}/manifest.json"
|
||||
echo " - Live manifest: unchanged until assets pass publication verification"
|
||||
echo " - Release manifest: releases/manifest.json"
|
||||
echo " - Release manifest copy: release-manifest.json"
|
||||
echo " - Staged artifacts: releases/v${VERSION}/"
|
||||
echo " - Git tag: v${VERSION}"
|
||||
echo ""
|
||||
|
||||
@@ -165,35 +165,16 @@ else
|
||||
sudo rm -rf "$TMPBIN"
|
||||
fi
|
||||
|
||||
# ── Frontend + companion payload ─────────────────────────────────────
|
||||
WEB_UI="$MNT/archipelago/web-ui"
|
||||
if [ -f "$WEB_UI/index.html" ]; then
|
||||
# ── Frontend payload present ─────────────────────────────────────────
|
||||
if [ -f "$MNT/archipelago/web-ui/index.html" ]; then
|
||||
ok "frontend payload (archipelago/web-ui/index.html)"
|
||||
if [ -f "$WEB_UI/aiui/index.html" ]; then
|
||||
if [ -f "$MNT/archipelago/web-ui/aiui/index.html" ]; then
|
||||
ok "AIUI included in frontend payload"
|
||||
else
|
||||
bad "AIUI missing from archipelago/web-ui"
|
||||
fi
|
||||
|
||||
COMPANION_META="$WEB_UI/packages/archipelago-companion.json"
|
||||
COMPANION_APK="$WEB_UI/packages/archipelago-companion.apk"
|
||||
if [ -s "$COMPANION_APK" ] && [ "$(jq -r '.versionName // empty' "$COMPANION_META" 2>/dev/null)" = "0.5.28" ] \
|
||||
&& [ "$(jq -r '.versionCode // empty' "$COMPANION_META" 2>/dev/null)" = "48" ]; then
|
||||
ok "Companion 0.5.28 (versionCode 48) APK included"
|
||||
else
|
||||
bad "Companion 0.5.28 APK/metadata missing or stale"
|
||||
fi
|
||||
|
||||
SETTINGS_BUNDLE="$(grep -rlF "v$EXPECTED_VERSION" "$WEB_UI/assets" 2>/dev/null | head -1)"
|
||||
if [ -n "$SETTINGS_BUNDLE" ] \
|
||||
&& grep -qF 'v1.8.0-alpha' "$SETTINGS_BUNDLE" \
|
||||
&& ! grep -qE 'v1\.[0-7]\.|v1\.2\.0-alpha' "$SETTINGS_BUNDLE"; then
|
||||
ok "What's New is v1.8.6-first with a v1.8.0 history floor"
|
||||
else
|
||||
bad "What's New payload is missing v1.8 history or still contains pre-v1.8 entries"
|
||||
warn "AIUI missing from archipelago/web-ui (verify rootfs copy before shipping)"
|
||||
fi
|
||||
else
|
||||
bad "no archipelago/web-ui payload on ISO"
|
||||
warn "no archipelago/web-ui payload on ISO (frontend may live in rootfs.tar only)"
|
||||
fi
|
||||
|
||||
echo
|
||||
|
||||
@@ -16,26 +16,14 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
VERSION_DIR="$PROJECT_ROOT/releases/v${VERSION}"
|
||||
BACKEND="$VERSION_DIR/archipelago"
|
||||
FRONTEND="$VERSION_DIR/archipelago-frontend-${VERSION}.tar.gz"
|
||||
PENDING_MANIFEST="$PROJECT_ROOT/releases/pending/v${VERSION}/manifest.json"
|
||||
LIVE_MANIFEST="$PROJECT_ROOT/releases/manifest.json"
|
||||
if [ -f "$PENDING_MANIFEST" ]; then
|
||||
MANIFEST="$PENDING_MANIFEST"
|
||||
PROMOTE_MANIFEST=1
|
||||
else
|
||||
# Backward compatibility for releases prepared before pending manifests.
|
||||
MANIFEST="$LIVE_MANIFEST"
|
||||
PROMOTE_MANIFEST=0
|
||||
fi
|
||||
|
||||
fail() { echo "Error: $*" >&2; exit 1; }
|
||||
|
||||
[ -f "$MANIFEST" ] || fail "release manifest missing: $MANIFEST"
|
||||
MANIFEST_VERSION=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "$MANIFEST")
|
||||
[ "$MANIFEST_VERSION" = "$VERSION" ] || fail "requested v$VERSION but $MANIFEST describes v$MANIFEST_VERSION"
|
||||
[ -f "$PROJECT_ROOT/releases/manifest.json" ] || fail "releases/manifest.json missing"
|
||||
[ -f "$BACKEND" ] || fail "backend artifact missing: $BACKEND"
|
||||
[ -f "$FRONTEND" ] || fail "frontend artifact missing: $FRONTEND"
|
||||
|
||||
"$SCRIPT_DIR/check-release-manifest.sh" "$MANIFEST"
|
||||
"$SCRIPT_DIR/check-release-manifest.sh"
|
||||
|
||||
# §A supply-chain gate: never publish an unsigned OTA manifest. Fleet nodes
|
||||
# with the pinned release-root anchor refuse to auto-apply unsigned manifests,
|
||||
@@ -44,11 +32,11 @@ MANIFEST_VERSION=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]
|
||||
# Release root ROTATED 2026-08-05; see create-release.sh. New root from
|
||||
# v1.7.123 onward.
|
||||
EXPECTED_DID="did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT"
|
||||
grep -q '"signature":' "$MANIFEST" \
|
||||
&& grep -q "\"signed_by\": \"$EXPECTED_DID\"" "$MANIFEST" \
|
||||
|| fail "$MANIFEST is not signed by the release root — run: bash scripts/sign-manifest.sh $MANIFEST"
|
||||
grep -q '"signature":' "$PROJECT_ROOT/releases/manifest.json" \
|
||||
&& grep -q "\"signed_by\": \"$EXPECTED_DID\"" "$PROJECT_ROOT/releases/manifest.json" \
|
||||
|| fail "releases/manifest.json is not signed by the release root — run: bash scripts/sign-manifest.sh"
|
||||
if [ -x "$PROJECT_ROOT/core/target/release/archipelago" ]; then
|
||||
"$PROJECT_ROOT/core/target/release/archipelago" ceremony verify "$MANIFEST" \
|
||||
"$PROJECT_ROOT/core/target/release/archipelago" ceremony verify "$PROJECT_ROOT/releases/manifest.json" \
|
||||
|| fail "manifest signature failed cryptographic verification"
|
||||
fi
|
||||
|
||||
@@ -142,36 +130,12 @@ echo "Verifying public download URLs (full GET + size + sha256)..."
|
||||
# hand during recovery. It fails hard on the first bad asset — the previous
|
||||
# inline `while read` ran in a pipe subshell, where a `fail` (exit) killed only
|
||||
# the subshell and let this script march on to "published and verified".
|
||||
"$PROJECT_ROOT/scripts/check-release-assets.sh" "$MANIFEST" \
|
||||
"$PROJECT_ROOT/scripts/check-release-assets.sh" "$PROJECT_ROOT/releases/manifest.json" \
|
||||
|| fail "asset verification failed — NOT pushing main. The manifest stays off the branch nodes read, so no node sees a version it cannot fetch. Repair the assets and re-run."
|
||||
|
||||
# Assets are proven fetchable — only now may the manifest become live. First
|
||||
# incorporate concurrent work, then promote in a dedicated commit. Until the
|
||||
# final push succeeds the remote still serves the previous manifest.
|
||||
echo "Assets verified. Synchronizing main before manifest promotion..."
|
||||
git -C "$PROJECT_ROOT" fetch "$REMOTE" main
|
||||
git -C "$PROJECT_ROOT" merge --no-edit "$REMOTE/main"
|
||||
|
||||
if [ "$PROMOTE_MANIFEST" = "1" ]; then
|
||||
cp "$MANIFEST" "$LIVE_MANIFEST"
|
||||
cp "$MANIFEST" "$PROJECT_ROOT/release-manifest.json"
|
||||
git -C "$PROJECT_ROOT" add releases/manifest.json release-manifest.json
|
||||
git -C "$PROJECT_ROOT" rm -f -- "releases/pending/v${VERSION}/manifest.json"
|
||||
git -C "$PROJECT_ROOT" commit -m "chore: publish release v${VERSION}"
|
||||
fi
|
||||
|
||||
echo "Publishing verified manifest to main (this makes v${VERSION} live)..."
|
||||
# A concurrent push can race the fetch above. Merge and retry without ever
|
||||
# force-pushing; the remote remains on its old, working manifest meanwhile.
|
||||
for attempt in 1 2 3; do
|
||||
if git -C "$PROJECT_ROOT" push "$REMOTE" HEAD:main; then
|
||||
break
|
||||
fi
|
||||
[ "$attempt" -lt 3 ] || fail "main advanced repeatedly; assets are safe but manifest was not promoted"
|
||||
echo "main advanced during publication; merging and retrying..."
|
||||
git -C "$PROJECT_ROOT" fetch "$REMOTE" main
|
||||
git -C "$PROJECT_ROOT" merge --no-edit "$REMOTE/main"
|
||||
done
|
||||
# Assets are proven fetchable — only now does the manifest become live.
|
||||
echo "Assets verified. Pushing main to $REMOTE (this makes v${VERSION} live)..."
|
||||
git -C "$PROJECT_ROOT" push "$REMOTE" main
|
||||
|
||||
echo "Release v${VERSION} published and verified on $REMOTE."
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
# One-step OTA-manifest signer (counterpart to sign-catalog.sh).
|
||||
#
|
||||
# Run: bash scripts/sign-manifest.sh [path/to/manifest.json]
|
||||
# Run: bash scripts/sign-manifest.sh
|
||||
# Then: paste your 24-word release master mnemonic, press Enter, then Ctrl-D.
|
||||
#
|
||||
# Signs the requested manifest (live by default) and cryptographically verifies the
|
||||
# Signs releases/manifest.json in place and cryptographically verifies the
|
||||
# result against the pinned release-root anchor. The mnemonic is read from the
|
||||
# terminal only (never stored, never in shell history, never passed to Claude).
|
||||
#
|
||||
@@ -18,9 +18,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
MANIFEST="${1:-$REPO/releases/manifest.json}"
|
||||
[[ "$MANIFEST" = /* ]] || MANIFEST="$REPO/$MANIFEST"
|
||||
[ -f "$MANIFEST" ] || { echo "Manifest not found: $MANIFEST" >&2; exit 1; }
|
||||
MANIFEST="$REPO/releases/manifest.json"
|
||||
|
||||
# Use ONLY a prebuilt signer — never compile here (compiling caused hangs in
|
||||
# the earlier catalog ceremony). Prefer the repo's release build.
|
||||
@@ -43,10 +41,9 @@ echo "════════════════════════
|
||||
|
||||
echo
|
||||
if "$BIN" ceremony verify "$MANIFEST"; then
|
||||
echo "✅ SUCCESS — manifest signed by the pinned release root: $MANIFEST"
|
||||
if [ "$MANIFEST" = "$REPO/releases/manifest.json" ]; then
|
||||
cp "$MANIFEST" "$REPO/release-manifest.json"
|
||||
fi
|
||||
echo "✅ SUCCESS — manifest signed by the pinned release root."
|
||||
echo " Commit + push releases/manifest.json (and release-manifest.json if present)."
|
||||
cp "$MANIFEST" "$REPO/release-manifest.json" 2>/dev/null || true
|
||||
else
|
||||
echo "❌ Signature did NOT verify against the pinned release-root anchor."
|
||||
echo " Do NOT commit. Check the mnemonic and re-run."
|
||||
|
||||
+28
-118
@@ -11,9 +11,8 @@ a block (from the curated CHANGELOG bullets) and inserts it newest-first.
|
||||
python3 scripts/sync-whats-new.py --check # exit 1 if anything is missing
|
||||
|
||||
Dev-process bullets ("Validation passed…/pending…") are dropped — the modal is
|
||||
user-facing. The visible history deliberately starts at v1.8.0-alpha; older
|
||||
blocks are removed so this remains a concise product history rather than an
|
||||
unbounded archive.
|
||||
user-facing. Only CHANGELOG versions are managed; older hand-written blocks
|
||||
(pre-CHANGELOG history) are never touched or removed.
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
@@ -26,7 +25,6 @@ MODAL = REPO / "neode-ui/src/views/settings/AccountInfoSection.vue"
|
||||
|
||||
MONTHS = ["", "January", "February", "March", "April", "May", "June", "July",
|
||||
"August", "September", "October", "November", "December"]
|
||||
MIN_VISIBLE_VERSION = (1, 8, 0)
|
||||
|
||||
HEADER_RE = re.compile(r"^## (v\d+\.\d+\.\d+\S*) \((\d{4})-(\d{2})-(\d{2})\)")
|
||||
|
||||
@@ -49,10 +47,7 @@ def parse_changelog():
|
||||
if m:
|
||||
ver, y, mo, d = m.groups()
|
||||
cur = {"ver": ver, "date": f"{MONTHS[int(mo)]} {int(d)}, {y}", "bullets": []}
|
||||
if version_key(ver) >= MIN_VISIBLE_VERSION:
|
||||
entries.append(cur)
|
||||
else:
|
||||
cur = None
|
||||
entries.append(cur)
|
||||
continue
|
||||
if cur is not None and line.startswith("- "):
|
||||
text = line[2:].strip()
|
||||
@@ -72,82 +67,14 @@ def undated_versions():
|
||||
return found
|
||||
|
||||
|
||||
def ordered_versions():
|
||||
"""Return generated modal versions in display order (top to bottom)."""
|
||||
return re.findall(r"<!-- (v\d+\.\d+\.\d+\S*) -->", MODAL.read_text())
|
||||
|
||||
|
||||
def legacy_blocks():
|
||||
"""Return old hand-written alpha blocks that predate generated markers."""
|
||||
return re.findall(r"<!-- (alpha\.[^ ]+) -->", MODAL.read_text())
|
||||
|
||||
|
||||
def version_key(version):
|
||||
match = re.match(r"v(\d+)\.(\d+)\.(\d+)", version)
|
||||
return tuple(map(int, match.groups()))
|
||||
|
||||
|
||||
def sort_modal_blocks(entries):
|
||||
"""Re-render current release-note blocks newest-first and remove old history."""
|
||||
lines = MODAL.read_text().splitlines(keepends=True)
|
||||
# Include the old hand-written `alpha.*` blocks in the replace range so
|
||||
# normalization can delete them. Previously the checker saw only generated
|
||||
# vX.Y.Z markers and falsely claimed the v1.8.0 history floor was enforced.
|
||||
marker = re.compile(r"^\s*<!-- ((?:v\d+\.\d+\.\d+\S*)|(?:alpha\.[^ ]+)) -->\s*$")
|
||||
blocks = []
|
||||
|
||||
for start, line in enumerate(lines):
|
||||
match = marker.match(line)
|
||||
if not match:
|
||||
continue
|
||||
depth = 0
|
||||
opened = False
|
||||
for index in range(start + 1, len(lines)):
|
||||
for tag in re.findall(r"</?div\b[^>]*>", lines[index]):
|
||||
if tag.startswith("</"):
|
||||
depth -= 1
|
||||
else:
|
||||
depth += 1
|
||||
opened = True
|
||||
if opened and depth == 0:
|
||||
blocks.append((start, index + 1, match.group(1), lines[start:index + 1]))
|
||||
break
|
||||
else:
|
||||
raise RuntimeError(f"unclosed What's New block for {match.group(1)}")
|
||||
|
||||
if not blocks:
|
||||
return False
|
||||
for previous, following in zip(blocks, blocks[1:]):
|
||||
gap = "".join(lines[previous[1]:following[0]])
|
||||
if gap.strip():
|
||||
raise RuntimeError("unexpected content between What's New release blocks")
|
||||
|
||||
by_version = {entry["ver"]: entry for entry in entries}
|
||||
retained = [
|
||||
b for b in blocks
|
||||
if b[2].startswith("v") and version_key(b[2]) >= MIN_VISIBLE_VERSION
|
||||
]
|
||||
sorted_segments = [
|
||||
render_block(by_version[b[2]]).splitlines(keepends=True)
|
||||
if b[2] in by_version else b[3]
|
||||
for b in sorted(retained, key=lambda b: version_key(b[2]), reverse=True)
|
||||
]
|
||||
output = lines[:blocks[0][0]]
|
||||
for segment in sorted_segments:
|
||||
output.extend(segment)
|
||||
output.extend(lines[blocks[-1][1]:])
|
||||
changed = output != lines
|
||||
if changed:
|
||||
MODAL.write_text("".join(output))
|
||||
return changed
|
||||
def existing_versions():
|
||||
text = MODAL.read_text()
|
||||
return set(re.findall(r"<!-- (v\d+\.\d+\.\d+\S*) -->", text))
|
||||
|
||||
|
||||
def to_html(text):
|
||||
text = text.replace("`", "")
|
||||
escaped = html.escape(text, quote=False) # & < > (Vue template-safe)
|
||||
escaped = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", escaped)
|
||||
escaped = re.sub(r"(?<!\*)\*([^*]+?)\*(?!\*)", r"<em>\1</em>", escaped)
|
||||
return escaped
|
||||
text = text.replace("`", "") # drop markdown code ticks (plain prose)
|
||||
return html.escape(text, quote=False) # & < > (Vue template-safe)
|
||||
|
||||
|
||||
def render_block(entry):
|
||||
@@ -186,53 +113,36 @@ def main():
|
||||
return 1
|
||||
|
||||
entries = parse_changelog()
|
||||
displayed = ordered_versions()
|
||||
have = set(displayed)
|
||||
have = existing_versions()
|
||||
missing = [e for e in entries if e["ver"] not in have]
|
||||
expected_order = sorted(displayed, key=version_key, reverse=True)
|
||||
out_of_order = displayed != expected_order
|
||||
too_old = [v for v in displayed if version_key(v) < MIN_VISIBLE_VERSION]
|
||||
too_old.extend(legacy_blocks())
|
||||
|
||||
if not missing and not out_of_order and not too_old:
|
||||
changed = False if check else sort_modal_blocks(entries)
|
||||
if changed:
|
||||
print("Re-rendered What's New blocks from the curated changelog.")
|
||||
else:
|
||||
print("What's New modal is in sync with CHANGELOG.md "
|
||||
f"({len(entries)} changelog versions, all present and newest-first).")
|
||||
if not missing:
|
||||
print("What's New modal is in sync with CHANGELOG.md "
|
||||
f"({len(entries)} changelog versions, all present).")
|
||||
return 0
|
||||
|
||||
names = ", ".join(e["ver"] for e in missing)
|
||||
if check:
|
||||
if missing:
|
||||
print("FAIL: these CHANGELOG versions have no block in the Settings "
|
||||
f"What's New modal: {names}", file=sys.stderr)
|
||||
if out_of_order:
|
||||
print("FAIL: What's New entries are not newest-first; the modal currently "
|
||||
f"opens at {displayed[0]} instead of {expected_order[0]}", file=sys.stderr)
|
||||
if too_old:
|
||||
print("FAIL: What's New contains entries older than the v1.8.0 history floor: "
|
||||
+ ", ".join(too_old), file=sys.stderr)
|
||||
print("FAIL: these CHANGELOG versions have no block in the Settings "
|
||||
f"What's New modal: {names}", file=sys.stderr)
|
||||
print("Run: python3 scripts/sync-whats-new.py", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if missing:
|
||||
# Insert before the first block; the full sort below makes this safe even
|
||||
# when a historical hand-written block was accidentally left at the top.
|
||||
lines = MODAL.read_text().splitlines(keepends=True)
|
||||
marker = re.compile(r"^\s*<!-- v\d+\.\d+\.\d+\S* -->\s*$")
|
||||
idx = next((i for i, ln in enumerate(lines) if marker.match(ln)), None)
|
||||
if idx is None:
|
||||
print("ERROR: could not find an existing version block marker in the modal.",
|
||||
file=sys.stderr)
|
||||
return 2
|
||||
lines.insert(idx, "".join(render_block(e) for e in missing))
|
||||
MODAL.write_text("".join(lines))
|
||||
print(f"Inserted {len(missing)} block(s): {names}")
|
||||
# Insert missing blocks newest-first, immediately before the newest existing
|
||||
# block marker (the first "<!-- v... -->" line in the file).
|
||||
lines = MODAL.read_text().splitlines(keepends=True)
|
||||
marker = re.compile(r"^\s*<!-- v\d+\.\d+\.\d+\S* -->\s*$")
|
||||
idx = next((i for i, ln in enumerate(lines) if marker.match(ln)), None)
|
||||
if idx is None:
|
||||
print("ERROR: could not find an existing version block marker in the modal.",
|
||||
file=sys.stderr)
|
||||
return 2
|
||||
|
||||
if sort_modal_blocks(entries):
|
||||
print("Normalized What's New blocks (v1.8.0+ only, newest-first).")
|
||||
# newest-first: sort missing by their order in `entries` (already newest-first)
|
||||
block_text = "".join(render_block(e) for e in missing)
|
||||
lines.insert(idx, block_text)
|
||||
MODAL.write_text("".join(lines))
|
||||
print(f"Inserted {len(missing)} block(s): {names}")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user