Files
archy/scripts/create-release.sh
T
archipelagoandClaude Opus 5 8e814ca06a
Demo images / Build & push demo images (push) Failing after 2m22s
feat(registry): move image and OTA references to the public domain
Replaces the registry host across 86 files: 309 references, covering all 40
app manifests, the orchestrator and container crates, the release and catalog
scripts, both demo-images workflows, the ISO builder, demo-deploy, and the
frontend marketplace data.

Verified the domain actually serves the registry before rewriting anything,
rather than assuming the web host implies the registry:
- TLS verifies clean, HTTP/2 on the web root
- an anonymous token grants a manifest fetch (HTTP 200) with no credentials
- skopeo inspect --no-creds resolves an image and lists its tags

That last check is the one that matters: an outside developer with no account
can now pull, which was the functional blocker for publishing at all.

Plain-HTTP references become HTTPS in the same pass, so OTA downloads stop
crossing the network in the clear.

Deliberately NOT rewritten:
- The public FIPS anchor on port 8444. It is a functional network endpoint
  every node dials to bootstrap the mesh — closer to Bitcoin Core's hardcoded
  seeds than to leaked infrastructure. The domain does resolve to the same
  host, so it could become a hostname, but that adds a DNS dependency to the
  path used precisely when things are broken. Worth a deliberate decision,
  not a side effect of this change.
- The companion APK on port 2100. The domain returns 404 for that path, so
  rewriting it would swap a working URL for a broken one. The Releases page
  does serve (200), which is where the plan already wants those binaries.
- releases/app-catalog.json, releases/manifest.json and release-manifest.json.
  These carry `signature` and `signed_by`; editing their contents invalidates
  the signature and the fleet refuses artifacts that fail verification. They
  were rewritten in a first pass and reverted — they must be regenerated and
  re-signed through the signing ceremony instead, which needs the mnemonic.

So the catalog still advertises the old host until that ceremony runs. Nodes
resolve images through the signed catalog, not the on-disk manifests, so this
commit alone does not change what a node pulls.

Verified: archipelago-container 75/75; every manifest still parses with a
top-level app block; no signed artifact modified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 11:31:20 -04:00

297 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
# create-release.sh — Full release automation for Archipelago
#
# Bumps version in Cargo.toml and package.json, generates changelog from git log,
# creates release manifest, and creates git tag.
#
# Usage:
# ./scripts/create-release.sh 1.0.0 # Release v1.0.0
# ./scripts/create-release.sh 1.0.0 --dry-run # Preview without changes
#
# Releases are tarball-only. ISO builds are archived under
# image-recipe/_archived/. Nodes OTA-update from releases/manifest.json.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
DRY_RUN=false
VERSION=""
# Parse args
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
--help|-h)
echo "Usage: $0 VERSION [--dry-run]"
echo ""
echo "Steps performed:"
echo " 1. Validate version format (SemVer)"
echo " 2. Bump version in Cargo.toml and package.json"
echo " 3. Build backend"
echo " 4. Build frontend"
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:"
echo " --dry-run Show what would be done without making changes"
exit 0
;;
*)
if [ -z "$VERSION" ]; then
VERSION="$arg"
else
echo "Error: Unknown argument: $arg"
exit 1
fi
;;
esac
done
if [ -z "$VERSION" ]; then
echo "Error: VERSION argument required"
echo "Usage: $0 VERSION [--dry-run]"
exit 1
fi
# Validate SemVer format
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then
echo "Error: Version '$VERSION' is not valid SemVer (expected: X.Y.Z or X.Y.Z-suffix)"
exit 1
fi
# Check we're on main branch
BRANCH=$(git -C "$PROJECT_ROOT" branch --show-current)
if [ "$BRANCH" != "main" ]; then
echo "Error: Must be on 'main' branch (currently on '$BRANCH')"
exit 1
fi
# Check for uncommitted changes
if ! git -C "$PROJECT_ROOT" diff --quiet HEAD; then
echo "Error: Uncommitted changes detected. Commit or stash first."
exit 1
fi
# ── Pre-flight test gate ──────────────────────────────────────────────
# A release must not ship if the static/frontend/backend checks fail. This
# runs the release gate harness (cargo fmt/check, catalog drift, vitest, and
# the focused cargo suites — incl. the receive/port-drift/secret regressions).
# Skipped on --dry-run, or set SKIP_RELEASE_TESTS=1 to bypass in an emergency.
# The lifecycle bats harness (tests/lifecycle/run-gate.sh) still runs separately
# against live nodes — see tests/lifecycle/TESTING.md.
if ! $DRY_RUN; then
if [ "${SKIP_RELEASE_TESTS:-0}" = "1" ]; then
echo "WARNING: SKIP_RELEASE_TESTS=1 — bypassing the pre-flight test gate"
elif [ -x "$PROJECT_ROOT/tests/release/run.sh" ]; then
echo "[0/7] Running release gate (tests/release/run.sh)..."
if ! "$PROJECT_ROOT/tests/release/run.sh"; then
echo "Error: release gate failed — aborting release. Fix the failing"
echo " stage, or re-run with SKIP_RELEASE_TESTS=1 to override."
exit 1
fi
else
echo "WARNING: tests/release/run.sh not found/executable — skipping test gate"
fi
fi
# Check tag doesn't already exist
if git -C "$PROJECT_ROOT" tag -l "v$VERSION" | grep -q "v$VERSION"; then
echo "Error: Tag v$VERSION already exists"
exit 1
fi
# Get current version
CURRENT_CARGO_VERSION=$(grep '^version' "$PROJECT_ROOT/core/archipelago/Cargo.toml" | head -1 | sed 's/.*"\(.*\)".*/\1/')
CURRENT_NPM_VERSION=$(node -p "require('$PROJECT_ROOT/neode-ui/package.json').version")
echo "=== Archipelago Release v${VERSION} ==="
echo " Current Cargo version: ${CURRENT_CARGO_VERSION}"
echo " Current npm version: ${CURRENT_NPM_VERSION}"
echo " Target version: ${VERSION}"
echo " Dry run: ${DRY_RUN}"
echo ""
if $DRY_RUN; then
echo "[DRY RUN] Would perform the following:"
echo " 0. Run pre-flight test gate (tests/release/run.sh) — aborts on failure"
echo " 1. Update core/archipelago/Cargo.toml version to $VERSION"
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. 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, you would:"
echo " - Push: git push && git push --tags"
echo " - Build ISOs on server: ssh archipelago@192.0.2.10"
exit 0
fi
echo "[1/7] Bumping version in Cargo.toml..."
# Update archipelago Cargo.toml
sed -i.bak "s/^version = \".*\"/version = \"$VERSION\"/" "$PROJECT_ROOT/core/archipelago/Cargo.toml"
rm -f "$PROJECT_ROOT/core/archipelago/Cargo.toml.bak"
# Also update workspace Cargo.lock if it exists
if [ -f "$PROJECT_ROOT/core/Cargo.lock" ]; then
# Cargo will update the lock file on next build; touch the toml to trigger
true
fi
echo "[2/7] Bumping version in package.json..."
cd "$PROJECT_ROOT/neode-ui"
npm version "$VERSION" --no-git-tag-version --allow-same-version 2>/dev/null || true
cd "$PROJECT_ROOT"
echo "[3/8] Building backend..."
cd "$PROJECT_ROOT/core"
cargo build --release -p archipelago
cd "$PROJECT_ROOT"
echo "[4/8] Building frontend..."
cd "$PROJECT_ROOT/neode-ui"
npm run build 2>&1 | tail -3
cd "$PROJECT_ROOT"
# npm run build can silently no-op (vue-tsc EACCES burned us before) — a stale
# dist would ship with a perfectly valid sha256. Require the freshly built
# bundle to embed the version we just bumped to before it gets packaged.
if ! grep -rqo "${VERSION}" "$PROJECT_ROOT"/web/dist/neode-ui/assets/*.js; then
echo "Error: web/dist/neode-ui does not contain v${VERSION} — the frontend" >&2
echo " build no-opped or its output is stale. Aborting release." >&2
exit 1
fi
echo "[4b/8] Building packaged radio tools (archy-reticulum-daemon, archy-rnodeconf)..."
# These ride the frontend tarball's runtime payload (radio-tools/) and are
# promoted to /usr/local/bin by bootstrap.rs — the ONLY path that updates them
# on OTA-only nodes. Stale-dist releases re-broke fleet mesh once (v1.7.117),
# so always rebuild here; the manifest script hard-fails if they're missing.
(cd "$PROJECT_ROOT/reticulum-daemon" && ./build.sh) || {
echo "Error: reticulum-daemon/build.sh failed — radio tools are release-critical" >&2
exit 1
}
echo "[5/8] Validating curated changelog..."
CHANGELOG_FILE="$PROJECT_ROOT/CHANGELOG.md"
RELEASE_DATE=$(date +%Y-%m-%d)
if [ ! -f "$CHANGELOG_FILE" ] || ! grep -q "^## v${VERSION} (" "$CHANGELOG_FILE"; then
echo "Error: CHANGELOG.md must already contain curated notes for v${VERSION}."
echo "Add a section like:"
echo ""
echo "## v${VERSION} (${RELEASE_DATE})"
echo ""
echo "- User/operator-facing change ..."
echo "- Another concrete change ..."
echo "- Validation or operational note ..."
exit 1
fi
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
# hard-refuses to ship one. The mnemonic is read interactively (or from
# RELEASE_MASTER_MNEMONIC) — it must never land in files or shell history.
SIGNER="$PROJECT_ROOT/core/target/release/archipelago"
if [ ! -x "$SIGNER" ]; then
echo "Error: release binary not found at $SIGNER — cannot sign manifest" >&2
exit 1
fi
if [ -n "${RELEASE_MASTER_MNEMONIC:-}" ] || [ -t 0 ]; then
echo "[6b/8] Signing release manifest (paste the release master mnemonic when prompted)..."
"$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 — 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"
# §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":' "$PROJECT_ROOT/releases/manifest.json" \
|| ! grep -q "\"signed_by\": \"$EXPECTED_DID\"" "$PROJECT_ROOT/releases/manifest.json"; then
echo "" >&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" >&2
echo "" >&2
echo " (Signing needs a TTY for the mnemonic prompt, or RELEASE_MASTER_MNEMONIC set.)" >&2
exit 1
fi
"$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 version bump..."
git -C "$PROJECT_ROOT" add \
core/archipelago/Cargo.toml \
neode-ui/package.json \
neode-ui/package-lock.json \
CHANGELOG.md \
releases/manifest.json \
release-manifest.json \
2>/dev/null || true
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}"
echo ""
echo "=== Release v${VERSION} Ready ==="
echo ""
echo "Artifacts:"
echo " - Version bumped in Cargo.toml and package.json"
echo " - Changelog updated in CHANGELOG.md"
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 ""
echo "Next steps:"
echo " 1. Review: git log --oneline -5"
echo " 2. Publish commits, tag, artifacts, and verify download URLs:"
echo " scripts/publish-release-assets.sh ${VERSION} gitea-vps2"
echo " 3. Verify manifest is live on both mirrors:"
echo " curl -fsS http://localhost:3000/lfg2025/archy/raw/branch/main/releases/manifest.json"
echo " curl -fsS https://source.archipelago-foundation.org/lfg2025/archy/raw/branch/main/releases/manifest.json"