#!/usr/bin/env bash # create-release-manifest.sh — Build a release manifest for the Archipelago update system. # # Generates a JSON manifest with version info, changelog, and SHA256 hashes for # each component, matching the format expected by core/archipelago/src/update.rs. # # Usage: # ./scripts/create-release-manifest.sh --version 0.2.0 --date 2026-04-01 # # The script reads built artifacts from the build output directories and produces # a manifest.json file suitable for hosting at the UPDATE_MANIFEST_URL. set -euo pipefail # Defaults VERSION="" RELEASE_DATE="" OUTPUT_FILE="manifest.json" BACKEND_BINARY="" FRONTEND_ARCHIVE="" BASE_URL="https://github.com/archipelago-os/releases/releases/download" usage() { echo "Usage: $0 --version VERSION [--date DATE] [--output FILE]" echo "" echo "Options:" echo " --version VERSION Release version (e.g., 0.2.0) [required]" echo " --date DATE Release date (YYYY-MM-DD) [default: today]" echo " --output FILE Output manifest path [default: manifest.json]" echo " --backend PATH Path to backend binary [default: auto-detect]" echo " --frontend PATH Path to frontend archive [default: auto-detect]" echo " --base-url URL Base download URL [default: GitHub releases]" exit 1 } # Parse arguments while [[ $# -gt 0 ]]; do case "$1" in --version) VERSION="$2"; shift 2 ;; --date) RELEASE_DATE="$2"; shift 2 ;; --output) OUTPUT_FILE="$2"; shift 2 ;; --backend) BACKEND_BINARY="$2"; shift 2 ;; --frontend) FRONTEND_ARCHIVE="$2"; shift 2 ;; --base-url) BASE_URL="$2"; shift 2 ;; -h|--help) usage ;; *) echo "Unknown option: $1"; usage ;; esac done if [ -z "$VERSION" ]; then echo "Error: --version is required" usage fi if [ -z "$RELEASE_DATE" ]; then RELEASE_DATE=$(date +%Y-%m-%d) fi # Find project root SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" # Auto-detect backend binary if [ -z "$BACKEND_BINARY" ]; then BACKEND_BINARY="$PROJECT_ROOT/core/target/release/archipelago" fi # Auto-detect frontend archive if [ -z "$FRONTEND_ARCHIVE" ]; then FRONTEND_DIST="$PROJECT_ROOT/web/dist/neode-ui" if [ -d "$FRONTEND_DIST" ]; then FRONTEND_ARCHIVE="/tmp/archipelago-frontend-${VERSION}.tar.gz" echo "Creating frontend archive from $FRONTEND_DIST..." tar -czf "$FRONTEND_ARCHIVE" -C "$PROJECT_ROOT/web/dist" neode-ui fi fi # Compute SHA256 hash sha256_of() { if command -v sha256sum &>/dev/null; then sha256sum "$1" | awk '{print $1}' else shasum -a 256 "$1" | awk '{print $1}' fi } # File size in bytes size_of() { if [[ "$(uname)" == "Darwin" ]]; then stat -f%z "$1" else stat -c%s "$1" fi } # Get current version from Cargo.toml CURRENT_VERSION=$(grep '^version' "$PROJECT_ROOT/core/archipelago/Cargo.toml" | head -1 | sed 's/.*"\(.*\)".*/\1/') echo "Building release manifest v${VERSION}" echo " Current version: ${CURRENT_VERSION}" echo " Release date: ${RELEASE_DATE}" echo " Output: ${OUTPUT_FILE}" # Build components array COMPONENTS="[]" if [ -f "$BACKEND_BINARY" ]; then HASH=$(sha256_of "$BACKEND_BINARY") SIZE=$(size_of "$BACKEND_BINARY") echo " Backend binary: ${BACKEND_BINARY} (${SIZE} bytes, sha256: ${HASH})" COMPONENTS=$(echo "$COMPONENTS" | python3 -c " import sys, json c = json.load(sys.stdin) c.append({ 'name': 'archipelago', 'current_version': '$CURRENT_VERSION', 'new_version': '$VERSION', 'download_url': '$BASE_URL/v$VERSION/archipelago', 'sha256': '$HASH', 'size_bytes': $SIZE }) print(json.dumps(c)) ") else echo " Warning: Backend binary not found at $BACKEND_BINARY" fi if [ -n "$FRONTEND_ARCHIVE" ] && [ -f "$FRONTEND_ARCHIVE" ]; then HASH=$(sha256_of "$FRONTEND_ARCHIVE") SIZE=$(size_of "$FRONTEND_ARCHIVE") ARCHIVE_NAME=$(basename "$FRONTEND_ARCHIVE") echo " Frontend archive: ${FRONTEND_ARCHIVE} (${SIZE} bytes, sha256: ${HASH})" COMPONENTS=$(echo "$COMPONENTS" | python3 -c " import sys, json c = json.load(sys.stdin) c.append({ 'name': '$ARCHIVE_NAME', 'current_version': '$CURRENT_VERSION', 'new_version': '$VERSION', 'download_url': '$BASE_URL/v$VERSION/$ARCHIVE_NAME', 'sha256': '$HASH', 'size_bytes': $SIZE }) print(json.dumps(c)) ") else echo " Warning: Frontend archive not found" fi # Read changelog from CHANGELOG.md if available CHANGELOG="[]" CHANGELOG_FILE="$PROJECT_ROOT/CHANGELOG.md" if [ -f "$CHANGELOG_FILE" ]; then # Extract entries for this version (lines between ## vVERSION and next ##) ENTRIES=$(python3 -c " import re, sys 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).strip().split('\n')[:10]: line = line.strip() if line: print(line) " 2>/dev/null || echo "") if [ -n "$ENTRIES" ]; then CHANGELOG=$(echo "$ENTRIES" | python3 -c " import sys, json lines = [l.strip().lstrip('- ') for l in sys.stdin if l.strip()] print(json.dumps(lines)) ") fi fi # If no changelog entries found, add a default if [ "$CHANGELOG" = "[]" ]; then CHANGELOG="[\"Update to version ${VERSION}\"]" fi # Generate manifest python3 -c " import json manifest = { 'version': '$VERSION', 'release_date': '$RELEASE_DATE', 'changelog': $CHANGELOG, 'components': $COMPONENTS } print(json.dumps(manifest, indent=2)) " > "$OUTPUT_FILE" echo "" echo "Manifest written to: $OUTPUT_FILE" echo "" cat "$OUTPUT_FILE" echo "" echo "Next steps:" echo " 1. Review the manifest above" echo " 2. Upload artifacts to: $BASE_URL/v$VERSION/" echo " 3. Upload manifest.json to the releases repo main branch" echo " 4. Tag the release: git tag v$VERSION && git push --tags"