Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
913743923c | ||
|
|
017505c431 | ||
|
|
7a39d8fbd1 | ||
|
|
e3275353b9 |
@@ -1,154 +0,0 @@
|
|||||||
# 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).
|
|
||||||
@@ -362,42 +362,6 @@ init()
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
|
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
|
||||||
<!-- v1.7.107-alpha -->
|
|
||||||
<div>
|
|
||||||
<div class="flex items-center gap-2 mb-3">
|
|
||||||
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.107-alpha</span>
|
|
||||||
<span class="text-xs text-white/40">July 20, 2026</span>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
|
||||||
<p>Wi-Fi setup now heals itself on older nodes. Some nodes set up before a mid-year fix couldn't connect to a Wi-Fi network from the screen — it failed with a permissions error — because the piece that lets the node manage networking on your behalf was missing. Nodes now put that piece in place automatically on startup, so "scan, pick a network, type the password, connect" works without reinstalling.</p>
|
|
||||||
<p>Your node rejoins the mesh faster after an update. Applying this update briefly restarts the mesh service, and previously a node could sit disconnected from other nodes for up to five minutes before it retried. It now notices the restart and reconnects within seconds.</p>
|
|
||||||
<p>Behind the scenes: fixed the installer image build so it no longer stops on a component that was removed from the product, and so it correctly includes the private relay it was meant to bundle — two separate faults that had been failing the build.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- v1.7.51-alpha -->
|
|
||||||
<div>
|
|
||||||
<div class="flex items-center gap-2 mb-3">
|
|
||||||
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.51-alpha</span>
|
|
||||||
<span class="text-xs text-white/40">April 30, 2026</span>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
|
||||||
<p>Stack installs now adopt containers that already exist instead of failing on them — a repair or reinstall over leftover containers completes, and the adopted container's readiness is waited on like any fresh start.</p>
|
|
||||||
<p>Failed installs come with evidence: the install path waits for its containers, and when one doesn't become healthy it captures that container's logs, so the error on screen names the real culprit instead of a bare timeout.</p>
|
|
||||||
<p>Bitcoin RPC bindings are ensured as part of install, and the startup self-heal path gained additional ground for already-deployed nodes.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- v1.7.50-alpha -->
|
|
||||||
<div>
|
|
||||||
<div class="flex items-center gap-2 mb-3">
|
|
||||||
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.50-alpha</span>
|
|
||||||
<span class="text-xs text-white/40">April 30, 2026</span>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
|
||||||
<p>The OTA bridge older nodes needed: deployed binaries only knew how to apply two artifacts (the backend binary and the frontend archive), so the scripts, app specs and docker assets newer releases carry never reached them. This release packs those payloads inside the frontend tarball — the one channel old binaries do apply — and the new backend promotes them into /opt once it starts.</p>
|
|
||||||
<p>Runtime payloads are staged into timestamped directories and promoted atomically; a failed extraction cleans up its staging area instead of leaving half-written state for the next update to trip over.</p>
|
|
||||||
<p>This is the release that un-sticks the fleet's update pipeline: from here on, an OTA can carry more than the two artifacts, and app installs on updated nodes use the specs that match their backend.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- v1.8.5-alpha -->
|
<!-- v1.8.5-alpha -->
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-2 mb-3">
|
<div class="flex items-center gap-2 mb-3">
|
||||||
@@ -550,7 +514,6 @@ init()
|
|||||||
<p>Known gaps, unchanged from the last release: three voice-assistant ports remain open without authentication. Non-browser clients — phone apps for Vaultwarden, Home Assistant or Jellyfin, and git over the web — meet the login page and need an access token. The 5x real-node lifecycle gate was not run for this release.</p>
|
<p>Known gaps, unchanged from the last release: three voice-assistant ports remain open without authentication. Non-browser clients — phone apps for Vaultwarden, Home Assistant or Jellyfin, and git over the web — meet the login page and need an access token. The 5x real-node lifecycle gate was not run for this release.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- v1.7.125-alpha -->
|
<!-- v1.7.125-alpha -->
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-2 mb-3">
|
<div class="flex items-center gap-2 mb-3">
|
||||||
@@ -598,6 +561,7 @@ init()
|
|||||||
<p>Known gaps, disclosed rather than buried: non-browser clients — phone apps for Vaultwarden, Home Assistant or Jellyfin, and git over the web — meet the login page and need an access token. Three voice-assistant ports remain open without authentication; the correct fix puts them on a private network with the assistant. The 5x real-node lifecycle gate was not run for this release.</p>
|
<p>Known gaps, disclosed rather than buried: non-browser clients — phone apps for Vaultwarden, Home Assistant or Jellyfin, and git over the web — meet the login page and need an access token. Three voice-assistant ports remain open without authentication; the correct fix puts them on a private network with the assistant. The 5x real-node lifecycle gate was not run for this release.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- v1.7.122-alpha -->
|
<!-- v1.7.122-alpha -->
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-2 mb-3">
|
<div class="flex items-center gap-2 mb-3">
|
||||||
@@ -862,6 +826,18 @@ init()
|
|||||||
<p>Behind the scenes: fixed the installer image build so it no longer stops on a component that was removed from the product, and so it correctly includes the private relay it was meant to bundle.</p>
|
<p>Behind the scenes: fixed the installer image build so it no longer stops on a component that was removed from the product, and so it correctly includes the private relay it was meant to bundle.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- v1.7.107-alpha -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.107-alpha</span>
|
||||||
|
<span class="text-xs text-white/40">July 20, 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||||
|
<p>Wi-Fi setup now heals itself on older nodes. Some nodes set up before a mid-year fix couldn't connect to a Wi-Fi network from the screen — it failed with a permissions error — because the piece that lets the node manage networking on your behalf was missing. Nodes now put that piece in place automatically on startup, so "scan, pick a network, type the password, connect" works without reinstalling.</p>
|
||||||
|
<p>Your node rejoins the mesh faster after an update. Applying this update briefly restarts the mesh service, and previously a node could sit disconnected from other nodes for up to five minutes before it retried. It now notices the restart and reconnects within seconds.</p>
|
||||||
|
<p>Behind the scenes: fixed the installer image build so it no longer stops on a component that was removed from the product, and so it correctly includes the private relay it was meant to bundle — two separate faults that had been failing the build.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- v1.7.106-alpha -->
|
<!-- v1.7.106-alpha -->
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-2 mb-3">
|
<div class="flex items-center gap-2 mb-3">
|
||||||
@@ -1581,6 +1557,30 @@ init()
|
|||||||
<p>Grafana lifecycle actions repair missing rootless host listeners on port 3000, and Debian 13 install paths pull security updates from trixie-security during image/install creation.</p>
|
<p>Grafana lifecycle actions repair missing rootless host listeners on port 3000, and Debian 13 install paths pull security updates from trixie-security during image/install creation.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- v1.7.51-alpha -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.51-alpha</span>
|
||||||
|
<span class="text-xs text-white/40">April 30, 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||||
|
<p>Stack installs now adopt containers that already exist instead of failing on them — a repair or reinstall over leftover containers completes, and the adopted container's readiness is waited on like any fresh start.</p>
|
||||||
|
<p>Failed installs come with evidence: the install path waits for its containers, and when one doesn't become healthy it captures that container's logs, so the error on screen names the real culprit instead of a bare timeout.</p>
|
||||||
|
<p>Bitcoin RPC bindings are ensured as part of install, and the startup self-heal path gained additional ground for already-deployed nodes.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- v1.7.50-alpha -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.50-alpha</span>
|
||||||
|
<span class="text-xs text-white/40">April 30, 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||||
|
<p>The OTA bridge older nodes needed: deployed binaries only knew how to apply two artifacts (the backend binary and the frontend archive), so the scripts, app specs and docker assets newer releases carry never reached them. This release packs those payloads inside the frontend tarball — the one channel old binaries do apply — and the new backend promotes them into /opt once it starts.</p>
|
||||||
|
<p>Runtime payloads are staged into timestamped directories and promoted atomically; a failed extraction cleans up its staging area instead of leaving half-written state for the next update to trip over.</p>
|
||||||
|
<p>This is the release that un-sticks the fleet's update pipeline: from here on, an OTA can carry more than the two artifacts, and app installs on updated nodes use the specs that match their backend.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- v1.7.49-alpha -->
|
<!-- v1.7.49-alpha -->
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-2 mb-3">
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Validate releases/manifest.json:
|
# Validate the live or a pending release manifest:
|
||||||
# - version matches core/archipelago/Cargo.toml
|
# - version matches core/archipelago/Cargo.toml
|
||||||
# - changelog contains curated release notes, not raw git log output
|
# - changelog contains curated release notes, not raw git log output
|
||||||
# - every component's download_url exists on disk and matches sha256/size
|
# - 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:
|
# Run on every push from CI, and also locally before publishing a release:
|
||||||
# scripts/check-release-manifest.sh
|
# scripts/check-release-manifest.sh [path/to/manifest.json]
|
||||||
#
|
#
|
||||||
# Exits non-zero on any mismatch so the release process fails loud.
|
# Exits non-zero on any mismatch so the release process fails loud.
|
||||||
|
|
||||||
set -eo pipefail
|
set -eo pipefail
|
||||||
|
|
||||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
MANIFEST="$REPO_ROOT/releases/manifest.json"
|
MANIFEST="${1:-$REPO_ROOT/releases/manifest.json}"
|
||||||
|
[[ "$MANIFEST" = /* ]] || MANIFEST="$REPO_ROOT/$MANIFEST"
|
||||||
|
|
||||||
if [ ! -f "$MANIFEST" ]; then
|
if [ ! -f "$MANIFEST" ]; then
|
||||||
echo "❌ releases/manifest.json missing"
|
echo "❌ manifest missing: $MANIFEST"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -25,6 +26,18 @@ ok() { echo "✅ $*"; }
|
|||||||
MANIFEST_VERSION=$(python3 -c "import json; print(json.load(open('$MANIFEST'))['version'])")
|
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/')
|
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
|
if [ "$MANIFEST_VERSION" != "$CARGO_VERSION" ]; then
|
||||||
fail "manifest version ($MANIFEST_VERSION) ≠ Cargo.toml ($CARGO_VERSION)"
|
fail "manifest version ($MANIFEST_VERSION) ≠ Cargo.toml ($CARGO_VERSION)"
|
||||||
fi
|
fi
|
||||||
@@ -105,4 +118,4 @@ for i in $(seq 0 $((COMPONENT_COUNT - 1))); do
|
|||||||
done
|
done
|
||||||
|
|
||||||
echo
|
echo
|
||||||
ok "releases/manifest.json passes all checks — safe to publish v${MANIFEST_VERSION}"
|
ok "$MANIFEST passes all checks — safe to publish v${MANIFEST_VERSION}"
|
||||||
|
|||||||
@@ -261,15 +261,19 @@ content = open('$CHANGELOG_FILE').read()
|
|||||||
pattern = r'## .*?${VERSION}.*?\n(.*?)(?=\n## |\Z)'
|
pattern = r'## .*?${VERSION}.*?\n(.*?)(?=\n## |\Z)'
|
||||||
m = re.search(pattern, content, re.DOTALL)
|
m = re.search(pattern, content, re.DOTALL)
|
||||||
if m:
|
if m:
|
||||||
for line in m.group(1).strip().split('\n')[:10]:
|
for line in m.group(1).splitlines():
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if line:
|
if not line.startswith('- '):
|
||||||
print(line)
|
continue
|
||||||
|
text = line[2:].strip()
|
||||||
|
if text.lower().startswith('validation '):
|
||||||
|
continue
|
||||||
|
print(text)
|
||||||
" 2>/dev/null || echo "")
|
" 2>/dev/null || echo "")
|
||||||
if [ -n "$ENTRIES" ]; then
|
if [ -n "$ENTRIES" ]; then
|
||||||
CHANGELOG=$(echo "$ENTRIES" | python3 -c "
|
CHANGELOG=$(echo "$ENTRIES" | python3 -c "
|
||||||
import sys, json
|
import sys, json
|
||||||
lines = [l.strip().lstrip('- ') for l in sys.stdin if l.strip()]
|
lines = [l.strip() for l in sys.stdin if l.strip()]
|
||||||
print(json.dumps(lines))
|
print(json.dumps(lines))
|
||||||
")
|
")
|
||||||
fi
|
fi
|
||||||
@@ -298,7 +302,7 @@ echo ""
|
|||||||
cat "$OUTPUT_FILE"
|
cat "$OUTPUT_FILE"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Next steps:"
|
echo "Next steps:"
|
||||||
echo " 1. Review the manifest above"
|
echo " 1. Review and sign the manifest above"
|
||||||
echo " 2. Upload artifacts to Gitea release v$VERSION"
|
echo " 2. Keep it under releases/pending/v$VERSION/ — do NOT replace the live manifest"
|
||||||
echo " 3. Commit manifest.json to releases/manifest.json on main"
|
echo " 3. Run scripts/publish-release-assets.sh $VERSION gitea-vps2"
|
||||||
echo " 4. Tag the release: git tag v$VERSION && git push --tags"
|
echo " (it uploads + verifies assets before atomically promoting the manifest)"
|
||||||
|
|||||||
+39
-49
@@ -2,7 +2,8 @@
|
|||||||
# create-release.sh — Full release automation for Archipelago
|
# create-release.sh — Full release automation for Archipelago
|
||||||
#
|
#
|
||||||
# Bumps version in Cargo.toml and package.json, generates changelog from git log,
|
# Bumps version in Cargo.toml and package.json, generates changelog from git log,
|
||||||
# creates release manifest, and creates git tag.
|
# 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.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./scripts/create-release.sh 1.0.0 # Release v1.0.0
|
# ./scripts/create-release.sh 1.0.0 # Release v1.0.0
|
||||||
@@ -30,9 +31,9 @@ for arg in "$@"; do
|
|||||||
echo " 2. Bump version in Cargo.toml and package.json"
|
echo " 2. Bump version in Cargo.toml and package.json"
|
||||||
echo " 3. Build backend"
|
echo " 3. Build backend"
|
||||||
echo " 4. Build frontend"
|
echo " 4. Build frontend"
|
||||||
echo " 5. Generate changelog from git log"
|
echo " 5. Validate the curated changelog"
|
||||||
echo " 6. Create release manifest"
|
echo " 6. Create pending release manifest"
|
||||||
echo " 7. Commit version bump"
|
echo " 7. Commit release preparation"
|
||||||
echo " 8. Create git tag v{VERSION}"
|
echo " 8. Create git tag v{VERSION}"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Options:"
|
echo "Options:"
|
||||||
@@ -121,14 +122,13 @@ if $DRY_RUN; then
|
|||||||
echo " 2. Update neode-ui/package.json version to $VERSION"
|
echo " 2. Update neode-ui/package.json version to $VERSION"
|
||||||
echo " 3. Build backend (cargo build --release -p archipelago)"
|
echo " 3. Build backend (cargo build --release -p archipelago)"
|
||||||
echo " 4. Build frontend (npm run build)"
|
echo " 4. Build frontend (npm run build)"
|
||||||
echo " 5. Generate changelog from git log since v${CURRENT_CARGO_VERSION}"
|
echo " 5. Validate the curated changelog"
|
||||||
echo " 6. Create release manifest"
|
echo " 6. Create pending release manifest (the live manifest stays unchanged)"
|
||||||
echo " 7. Commit: 'chore: release v${VERSION}'"
|
echo " 7. Commit: 'chore: prepare release v${VERSION}'"
|
||||||
echo " 8. Tag: v${VERSION}"
|
echo " 8. Tag: v${VERSION}"
|
||||||
echo ""
|
echo ""
|
||||||
echo "After this script, you would:"
|
echo "After this script, publish only with:"
|
||||||
echo " - Push: git push && git push --tags"
|
echo " scripts/publish-release-assets.sh ${VERSION} gitea-vps2"
|
||||||
echo " - Build ISOs on server: ssh archipelago@192.0.2.10"
|
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -214,9 +214,13 @@ if [ ! -f "$CHANGELOG_FILE" ] || ! grep -q "^## v${VERSION} (" "$CHANGELOG_FILE"
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "[6/8] Creating release manifest..."
|
echo "[6/8] Creating pending release manifest..."
|
||||||
mkdir -p "$PROJECT_ROOT/releases"
|
# Never write the fleet-visible path here. A normal `git push main` must not be
|
||||||
"$SCRIPT_DIR/create-release-manifest.sh" --version "$VERSION" --date "$RELEASE_DATE" --output "$PROJECT_ROOT/releases/manifest.json" 2>&1 | grep -v "^$"
|
# 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 "^$"
|
||||||
|
|
||||||
# §A supply-chain: the OTA manifest must carry the release-root signature.
|
# §A supply-chain: the OTA manifest must carry the release-root signature.
|
||||||
# Nodes refuse to AUTO-apply unsigned manifests, and publish-release-assets.sh
|
# Nodes refuse to AUTO-apply unsigned manifests, and publish-release-assets.sh
|
||||||
@@ -239,60 +243,45 @@ if [ -n "${RELEASE_MASTER_MNEMONIC:-}" ] || [ -t 0 ]; then
|
|||||||
echo " Enter by itself will NOT submit; pasting twice concatenates"
|
echo " Enter by itself will NOT submit; pasting twice concatenates"
|
||||||
echo " the phrases and fails on word count."
|
echo " the phrases and fails on word count."
|
||||||
echo "════════════════════════════════════════════════════════════════"
|
echo "════════════════════════════════════════════════════════════════"
|
||||||
"$SIGNER" ceremony sign "$PROJECT_ROOT/releases/manifest.json"
|
"$SIGNER" ceremony sign "$PENDING_MANIFEST"
|
||||||
"$SIGNER" ceremony verify "$PROJECT_ROOT/releases/manifest.json"
|
"$SIGNER" ceremony verify "$PENDING_MANIFEST"
|
||||||
else
|
else
|
||||||
echo "⚠ WARNING: no TTY and RELEASE_MASTER_MNEMONIC unset — manifest left UNSIGNED."
|
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"
|
echo " This run will ABORT before committing (step 7 refuses an unsigned manifest)."
|
||||||
echo " manifest), because nodes read releases/manifest.json from branch main"
|
echo " Sign it, then re-run: bash scripts/sign-manifest.sh $PENDING_MANIFEST"
|
||||||
echo " and would refuse to auto-apply it."
|
|
||||||
echo " Sign it, then re-run: bash scripts/sign-manifest.sh"
|
|
||||||
fi
|
fi
|
||||||
cp "$PROJECT_ROOT/releases/manifest.json" "$PROJECT_ROOT/release-manifest.json"
|
|
||||||
|
|
||||||
echo "[6c/8] Staging release artifacts for validation..."
|
echo "[6c/8] Staging release artifacts for validation..."
|
||||||
VERSION_DIR="$PROJECT_ROOT/releases/v${VERSION}"
|
VERSION_DIR="$PROJECT_ROOT/releases/v${VERSION}"
|
||||||
FRONTEND_ARCHIVE="/tmp/archipelago-frontend-${VERSION}.tar.gz"
|
FRONTEND_ARCHIVE="/tmp/archipelago-frontend-${VERSION}.tar.gz"
|
||||||
mkdir -p "$VERSION_DIR"
|
mkdir -p "$VERSION_DIR"
|
||||||
install -m 0755 "$PROJECT_ROOT/core/target/release/archipelago" "$VERSION_DIR/archipelago"
|
install -m 0755 "$PROJECT_ROOT/core/target/release/archipelago" "$VERSION_DIR/archipelago"
|
||||||
install -m 0644 "$FRONTEND_ARCHIVE" "$VERSION_DIR/archipelago-frontend-${VERSION}.tar.gz"
|
install -m 0644 "$FRONTEND_ARCHIVE" "$VERSION_DIR/archipelago-frontend-${VERSION}.tar.gz"
|
||||||
"$SCRIPT_DIR/check-release-manifest.sh"
|
"$SCRIPT_DIR/check-release-manifest.sh" "$PENDING_MANIFEST"
|
||||||
|
|
||||||
# §A supply-chain gate, mirroring publish-release-assets.sh — but EARLIER,
|
# §A supply-chain gate, mirroring publish-release-assets.sh. The pending path
|
||||||
# because publishing is not the first way an unsigned manifest reaches the
|
# prevents an ordinary main push from exposing the release, but an unsigned
|
||||||
# fleet. Nodes fetch releases/manifest.json straight from branch `main`
|
# manifest is still unpublishable and must never be tagged as ready.
|
||||||
# (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
|
# 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
|
# 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
|
# pin on every node. From v1.7.123 onward the new root signs, and nodes
|
||||||
# running .122+ reject anything signed with the old key.
|
# running .122+ reject anything signed with the old key.
|
||||||
EXPECTED_DID="did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT"
|
EXPECTED_DID="did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT"
|
||||||
if ! grep -q '"signature":' "$PROJECT_ROOT/releases/manifest.json" \
|
if ! grep -q '"signature":' "$PENDING_MANIFEST" \
|
||||||
|| ! grep -q "\"signed_by\": \"$EXPECTED_DID\"" "$PROJECT_ROOT/releases/manifest.json"; then
|
|| ! grep -q "\"signed_by\": \"$EXPECTED_DID\"" "$PENDING_MANIFEST"; then
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
echo "Error: releases/manifest.json is NOT signed by the release root." >&2
|
echo "Error: the pending manifest is NOT signed by the release root." >&2
|
||||||
echo " Refusing to commit — nodes read this file from branch main and will" >&2
|
echo " Refusing to commit an unpublishable release." >&2
|
||||||
echo " refuse to auto-apply it, so the release would be dead on arrival." >&2
|
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
echo " Sign it, then re-run this script:" >&2
|
echo " Sign it, then re-run this script:" >&2
|
||||||
echo " bash scripts/sign-manifest.sh" >&2
|
echo " bash scripts/sign-manifest.sh $PENDING_MANIFEST" >&2
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
echo " (Signing needs a TTY for the mnemonic prompt, or RELEASE_MASTER_MNEMONIC set.)" >&2
|
echo " (Signing needs a TTY for the mnemonic prompt, or RELEASE_MASTER_MNEMONIC set.)" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
"$SIGNER" ceremony verify "$PROJECT_ROOT/releases/manifest.json" \
|
"$SIGNER" ceremony verify "$PENDING_MANIFEST" \
|
||||||
|| { echo "Error: manifest signature failed cryptographic verification — refusing to commit" >&2; exit 1; }
|
|| { echo "Error: manifest signature failed cryptographic verification — refusing to commit" >&2; exit 1; }
|
||||||
|
|
||||||
echo "[7/8] Committing version bump..."
|
echo "[7/8] Committing release preparation..."
|
||||||
git -C "$PROJECT_ROOT" add \
|
git -C "$PROJECT_ROOT" add \
|
||||||
core/archipelago/Cargo.toml \
|
core/archipelago/Cargo.toml \
|
||||||
core/Cargo.lock \
|
core/Cargo.lock \
|
||||||
@@ -300,15 +289,16 @@ git -C "$PROJECT_ROOT" add \
|
|||||||
neode-ui/package-lock.json \
|
neode-ui/package-lock.json \
|
||||||
neode-ui/public/catalog.json \
|
neode-ui/public/catalog.json \
|
||||||
CHANGELOG.md \
|
CHANGELOG.md \
|
||||||
releases/manifest.json \
|
|
||||||
release-manifest.json \
|
|
||||||
2>/dev/null || true
|
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
|
# Cargo.lock (rewritten by the release build after the version bump) and
|
||||||
# neode-ui/public/catalog.json (regenerated by the frontend build) belong in
|
# 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
|
# THIS commit: leaving them dirty failed build-iso-release.sh's clean-tree
|
||||||
# preflight on three consecutive releases (.127-.129, 2026-08-09/10).
|
# preflight on three consecutive releases (.127-.129, 2026-08-09/10).
|
||||||
|
|
||||||
git -C "$PROJECT_ROOT" commit -m "chore: release v${VERSION}"
|
git -C "$PROJECT_ROOT" commit -m "chore: prepare release v${VERSION}"
|
||||||
|
|
||||||
echo "[8/8] Creating git tag..."
|
echo "[8/8] Creating git tag..."
|
||||||
git -C "$PROJECT_ROOT" tag -a "v${VERSION}" -m "Release v${VERSION}"
|
git -C "$PROJECT_ROOT" tag -a "v${VERSION}" -m "Release v${VERSION}"
|
||||||
@@ -319,8 +309,8 @@ echo ""
|
|||||||
echo "Artifacts:"
|
echo "Artifacts:"
|
||||||
echo " - Version bumped in Cargo.toml and package.json"
|
echo " - Version bumped in Cargo.toml and package.json"
|
||||||
echo " - Changelog updated in CHANGELOG.md"
|
echo " - Changelog updated in CHANGELOG.md"
|
||||||
echo " - Release manifest: releases/manifest.json"
|
echo " - Pending manifest: releases/pending/v${VERSION}/manifest.json"
|
||||||
echo " - Release manifest copy: release-manifest.json"
|
echo " - Live manifest: unchanged until assets pass publication verification"
|
||||||
echo " - Staged artifacts: releases/v${VERSION}/"
|
echo " - Staged artifacts: releases/v${VERSION}/"
|
||||||
echo " - Git tag: v${VERSION}"
|
echo " - Git tag: v${VERSION}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -16,14 +16,26 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|||||||
VERSION_DIR="$PROJECT_ROOT/releases/v${VERSION}"
|
VERSION_DIR="$PROJECT_ROOT/releases/v${VERSION}"
|
||||||
BACKEND="$VERSION_DIR/archipelago"
|
BACKEND="$VERSION_DIR/archipelago"
|
||||||
FRONTEND="$VERSION_DIR/archipelago-frontend-${VERSION}.tar.gz"
|
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; }
|
fail() { echo "Error: $*" >&2; exit 1; }
|
||||||
|
|
||||||
[ -f "$PROJECT_ROOT/releases/manifest.json" ] || fail "releases/manifest.json missing"
|
[ -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 "$BACKEND" ] || fail "backend artifact missing: $BACKEND"
|
[ -f "$BACKEND" ] || fail "backend artifact missing: $BACKEND"
|
||||||
[ -f "$FRONTEND" ] || fail "frontend artifact missing: $FRONTEND"
|
[ -f "$FRONTEND" ] || fail "frontend artifact missing: $FRONTEND"
|
||||||
|
|
||||||
"$SCRIPT_DIR/check-release-manifest.sh"
|
"$SCRIPT_DIR/check-release-manifest.sh" "$MANIFEST"
|
||||||
|
|
||||||
# §A supply-chain gate: never publish an unsigned OTA manifest. Fleet nodes
|
# §A supply-chain gate: never publish an unsigned OTA manifest. Fleet nodes
|
||||||
# with the pinned release-root anchor refuse to auto-apply unsigned manifests,
|
# with the pinned release-root anchor refuse to auto-apply unsigned manifests,
|
||||||
@@ -32,11 +44,11 @@ fail() { echo "Error: $*" >&2; exit 1; }
|
|||||||
# Release root ROTATED 2026-08-05; see create-release.sh. New root from
|
# Release root ROTATED 2026-08-05; see create-release.sh. New root from
|
||||||
# v1.7.123 onward.
|
# v1.7.123 onward.
|
||||||
EXPECTED_DID="did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT"
|
EXPECTED_DID="did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT"
|
||||||
grep -q '"signature":' "$PROJECT_ROOT/releases/manifest.json" \
|
grep -q '"signature":' "$MANIFEST" \
|
||||||
&& grep -q "\"signed_by\": \"$EXPECTED_DID\"" "$PROJECT_ROOT/releases/manifest.json" \
|
&& grep -q "\"signed_by\": \"$EXPECTED_DID\"" "$MANIFEST" \
|
||||||
|| fail "releases/manifest.json is not signed by the release root — run: bash scripts/sign-manifest.sh"
|
|| fail "$MANIFEST is not signed by the release root — run: bash scripts/sign-manifest.sh $MANIFEST"
|
||||||
if [ -x "$PROJECT_ROOT/core/target/release/archipelago" ]; then
|
if [ -x "$PROJECT_ROOT/core/target/release/archipelago" ]; then
|
||||||
"$PROJECT_ROOT/core/target/release/archipelago" ceremony verify "$PROJECT_ROOT/releases/manifest.json" \
|
"$PROJECT_ROOT/core/target/release/archipelago" ceremony verify "$MANIFEST" \
|
||||||
|| fail "manifest signature failed cryptographic verification"
|
|| fail "manifest signature failed cryptographic verification"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -130,12 +142,36 @@ echo "Verifying public download URLs (full GET + size + sha256)..."
|
|||||||
# hand during recovery. It fails hard on the first bad asset — the previous
|
# 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
|
# 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".
|
# the subshell and let this script march on to "published and verified".
|
||||||
"$PROJECT_ROOT/scripts/check-release-assets.sh" "$PROJECT_ROOT/releases/manifest.json" \
|
"$PROJECT_ROOT/scripts/check-release-assets.sh" "$MANIFEST" \
|
||||||
|| 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."
|
|| 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 does the manifest become live.
|
# Assets are proven fetchable — only now may the manifest become live. First
|
||||||
echo "Assets verified. Pushing main to $REMOTE (this makes v${VERSION} live)..."
|
# incorporate concurrent work, then promote in a dedicated commit. Until the
|
||||||
git -C "$PROJECT_ROOT" push "$REMOTE" main
|
# 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
|
||||||
|
|
||||||
echo "Release v${VERSION} published and verified on $REMOTE."
|
echo "Release v${VERSION} published and verified on $REMOTE."
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# One-step OTA-manifest signer (counterpart to sign-catalog.sh).
|
# One-step OTA-manifest signer (counterpart to sign-catalog.sh).
|
||||||
#
|
#
|
||||||
# Run: bash scripts/sign-manifest.sh
|
# Run: bash scripts/sign-manifest.sh [path/to/manifest.json]
|
||||||
# Then: paste your 24-word release master mnemonic, press Enter, then Ctrl-D.
|
# Then: paste your 24-word release master mnemonic, press Enter, then Ctrl-D.
|
||||||
#
|
#
|
||||||
# Signs releases/manifest.json in place and cryptographically verifies the
|
# Signs the requested manifest (live by default) and cryptographically verifies the
|
||||||
# result against the pinned release-root anchor. The mnemonic is read from 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).
|
# terminal only (never stored, never in shell history, never passed to Claude).
|
||||||
#
|
#
|
||||||
@@ -18,7 +18,9 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
MANIFEST="$REPO/releases/manifest.json"
|
MANIFEST="${1:-$REPO/releases/manifest.json}"
|
||||||
|
[[ "$MANIFEST" = /* ]] || MANIFEST="$REPO/$MANIFEST"
|
||||||
|
[ -f "$MANIFEST" ] || { echo "Manifest not found: $MANIFEST" >&2; exit 1; }
|
||||||
|
|
||||||
# Use ONLY a prebuilt signer — never compile here (compiling caused hangs in
|
# Use ONLY a prebuilt signer — never compile here (compiling caused hangs in
|
||||||
# the earlier catalog ceremony). Prefer the repo's release build.
|
# the earlier catalog ceremony). Prefer the repo's release build.
|
||||||
@@ -41,9 +43,10 @@ echo "════════════════════════
|
|||||||
|
|
||||||
echo
|
echo
|
||||||
if "$BIN" ceremony verify "$MANIFEST"; then
|
if "$BIN" ceremony verify "$MANIFEST"; then
|
||||||
echo "✅ SUCCESS — manifest signed by the pinned release root."
|
echo "✅ SUCCESS — manifest signed by the pinned release root: $MANIFEST"
|
||||||
echo " Commit + push releases/manifest.json (and release-manifest.json if present)."
|
if [ "$MANIFEST" = "$REPO/releases/manifest.json" ]; then
|
||||||
cp "$MANIFEST" "$REPO/release-manifest.json" 2>/dev/null || true
|
cp "$MANIFEST" "$REPO/release-manifest.json"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "❌ Signature did NOT verify against the pinned release-root anchor."
|
echo "❌ Signature did NOT verify against the pinned release-root anchor."
|
||||||
echo " Do NOT commit. Check the mnemonic and re-run."
|
echo " Do NOT commit. Check the mnemonic and re-run."
|
||||||
|
|||||||
+74
-22
@@ -67,9 +67,53 @@ def undated_versions():
|
|||||||
return found
|
return found
|
||||||
|
|
||||||
|
|
||||||
def existing_versions():
|
def ordered_versions():
|
||||||
text = MODAL.read_text()
|
"""Return modal versions in display order (top to bottom)."""
|
||||||
return set(re.findall(r"<!-- (v\d+\.\d+\.\d+\S*) -->", text))
|
return re.findall(r"<!-- (v\d+\.\d+\.\d+\S*) -->", 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():
|
||||||
|
"""Sort complete release-note blocks newest-first, preserving gaps."""
|
||||||
|
lines = MODAL.read_text().splitlines(keepends=True)
|
||||||
|
marker = re.compile(r"^\s*<!-- (v\d+\.\d+\.\d+\S*) -->\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)}")
|
||||||
|
|
||||||
|
sorted_segments = [b[3] for b in sorted(blocks, key=lambda b: version_key(b[2]), reverse=True)]
|
||||||
|
output = []
|
||||||
|
cursor = 0
|
||||||
|
for (start, end, _version, _segment), replacement in zip(blocks, sorted_segments):
|
||||||
|
output.extend(lines[cursor:start])
|
||||||
|
output.extend(replacement)
|
||||||
|
cursor = end
|
||||||
|
output.extend(lines[cursor:])
|
||||||
|
changed = output != lines
|
||||||
|
if changed:
|
||||||
|
MODAL.write_text("".join(output))
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
def to_html(text):
|
def to_html(text):
|
||||||
@@ -113,36 +157,44 @@ def main():
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
entries = parse_changelog()
|
entries = parse_changelog()
|
||||||
have = existing_versions()
|
displayed = ordered_versions()
|
||||||
|
have = set(displayed)
|
||||||
missing = [e for e in entries if e["ver"] not in have]
|
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
|
||||||
|
|
||||||
if not missing:
|
if not missing and not out_of_order:
|
||||||
print("What's New modal is in sync with CHANGELOG.md "
|
print("What's New modal is in sync with CHANGELOG.md "
|
||||||
f"({len(entries)} changelog versions, all present).")
|
f"({len(entries)} changelog versions, all present and newest-first).")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
names = ", ".join(e["ver"] for e in missing)
|
names = ", ".join(e["ver"] for e in missing)
|
||||||
if check:
|
if check:
|
||||||
print("FAIL: these CHANGELOG versions have no block in the Settings "
|
if missing:
|
||||||
f"What's New modal: {names}", file=sys.stderr)
|
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)
|
||||||
print("Run: python3 scripts/sync-whats-new.py", file=sys.stderr)
|
print("Run: python3 scripts/sync-whats-new.py", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
# Insert missing blocks newest-first, immediately before the newest existing
|
if missing:
|
||||||
# block marker (the first "<!-- v... -->" line in the file).
|
# Insert before the first block; the full sort below makes this safe even
|
||||||
lines = MODAL.read_text().splitlines(keepends=True)
|
# when a historical hand-written block was accidentally left at the top.
|
||||||
marker = re.compile(r"^\s*<!-- v\d+\.\d+\.\d+\S* -->\s*$")
|
lines = MODAL.read_text().splitlines(keepends=True)
|
||||||
idx = next((i for i, ln in enumerate(lines) if marker.match(ln)), None)
|
marker = re.compile(r"^\s*<!-- v\d+\.\d+\.\d+\S* -->\s*$")
|
||||||
if idx is None:
|
idx = next((i for i, ln in enumerate(lines) if marker.match(ln)), None)
|
||||||
print("ERROR: could not find an existing version block marker in the modal.",
|
if idx is None:
|
||||||
file=sys.stderr)
|
print("ERROR: could not find an existing version block marker in the modal.",
|
||||||
return 2
|
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}")
|
||||||
|
|
||||||
# newest-first: sort missing by their order in `entries` (already newest-first)
|
if sort_modal_blocks():
|
||||||
block_text = "".join(render_block(e) for e in missing)
|
print("Sorted What's New blocks newest-first.")
|
||||||
lines.insert(idx, block_text)
|
|
||||||
MODAL.write_text("".join(lines))
|
|
||||||
print(f"Inserted {len(missing)} block(s): {names}")
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user