#!/bin/bash # Validate releases/manifest.json: # - version matches core/archipelago/Cargo.toml # - changelog is non-empty (release notes are mandatory per product policy) # - 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 # # Exits non-zero on any mismatch so the release process fails loud. set -eo pipefail REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" MANIFEST="$REPO_ROOT/releases/manifest.json" if [ ! -f "$MANIFEST" ]; then echo "❌ releases/manifest.json missing" exit 1 fi fail() { echo "❌ $*"; exit 1; } 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/') if [ "$MANIFEST_VERSION" != "$CARGO_VERSION" ]; then fail "manifest version ($MANIFEST_VERSION) ≠ Cargo.toml ($CARGO_VERSION)" fi ok "version matches: $MANIFEST_VERSION" # Release notes mandatory — ships stuff nobody can read otherwise. CHANGELOG_COUNT=$(python3 -c "import json; print(len(json.load(open('$MANIFEST'))['changelog']))") if [ "$CHANGELOG_COUNT" -eq 0 ]; then fail "changelog is empty — every release MUST have release notes" fi ok "changelog has $CHANGELOG_COUNT lines" # Each component: the artifact on disk under releases/v/ must match # the declared sha256 and size_bytes. VERSION_DIR="$REPO_ROOT/releases/v${MANIFEST_VERSION}" if [ ! -d "$VERSION_DIR" ]; then fail "releases/v${MANIFEST_VERSION}/ missing — artifacts not staged" fi COMPONENT_COUNT=$(python3 -c "import json; print(len(json.load(open('$MANIFEST'))['components']))") for i in $(seq 0 $((COMPONENT_COUNT - 1))); do NAME=$(python3 -c "import json; print(json.load(open('$MANIFEST'))['components'][$i]['name'])") DECLARED_SHA=$(python3 -c "import json; print(json.load(open('$MANIFEST'))['components'][$i]['sha256'])") DECLARED_SIZE=$(python3 -c "import json; print(json.load(open('$MANIFEST'))['components'][$i]['size_bytes'])") # Component names other than exactly "archipelago" are the tarball's # filename; use as-is. The bare "archipelago" component maps to the # binary file literally named `archipelago`. FILE="$VERSION_DIR/$NAME" if [ "$NAME" = "archipelago" ]; then FILE="$VERSION_DIR/archipelago" fi if [ ! -f "$FILE" ]; then fail "component '$NAME' file missing at $FILE" fi ACTUAL_SHA=$(sha256sum "$FILE" | awk '{print $1}') ACTUAL_SIZE=$(stat -c%s "$FILE") if [ "$ACTUAL_SHA" != "$DECLARED_SHA" ]; then fail "component '$NAME' sha256 mismatch (declared=$DECLARED_SHA actual=$ACTUAL_SHA)" fi if [ "$ACTUAL_SIZE" != "$DECLARED_SIZE" ]; then fail "component '$NAME' size mismatch (declared=$DECLARED_SIZE actual=$ACTUAL_SIZE)" fi ok "component '$NAME': sha256 + size match on-disk artifact" done echo ok "releases/manifest.json passes all checks — safe to publish v${MANIFEST_VERSION}"