From e77ccff0e10275f1e05c6e2008d46656f49120c8 Mon Sep 17 00:00:00 2001 From: archipelago Date: Wed, 8 Jul 2026 19:04:14 -0400 Subject: [PATCH] =?UTF-8?q?fix(release):=20close=20two=20publish-path=20ve?= =?UTF-8?q?rification=20gaps=20(=C2=A7B=20hardening)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - create-release.sh: assert the freshly built frontend dist actually embeds the bumped version before packaging — the ui-dist-version guard in tests/release/run.sh is behind --with-build, which the release path never passes, so a silently no-opped npm build could ship a stale dist with a valid sha256. Verified against the real dist (passes on current version, trips on a missing one). - publish-release-assets.sh: verify published assets by downloading and comparing sha256 against the manifest, not just Content-Length — a size-correct/content-wrong mirror asset now fails the publish gate. Verified live against the v1.7.99-alpha assets on the vps2 mirror. Co-Authored-By: Claude Fable 5 --- scripts/create-release.sh | 9 +++++++++ scripts/publish-release-assets.sh | 18 ++++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/scripts/create-release.sh b/scripts/create-release.sh index dad07e0b..3835033d 100755 --- a/scripts/create-release.sh +++ b/scripts/create-release.sh @@ -158,6 +158,15 @@ 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 "[5/8] Validating curated changelog..." CHANGELOG_FILE="$PROJECT_ROOT/CHANGELOG.md" diff --git a/scripts/publish-release-assets.sh b/scripts/publish-release-assets.sh index 1527ca9a..1650d0a9 100755 --- a/scripts/publish-release-assets.sh +++ b/scripts/publish-release-assets.sh @@ -98,18 +98,24 @@ upload_asset() { upload_asset "$BACKEND" "archipelago" upload_asset "$FRONTEND" "archipelago-frontend-${VERSION}.tar.gz" -echo "Verifying public download URLs from manifest..." -python3 - "$PROJECT_ROOT/releases/manifest.json" <<'PY' | while read -r url size; do +echo "Verifying public download URLs from manifest (size + sha256)..." +python3 - "$PROJECT_ROOT/releases/manifest.json" <<'PY' | while read -r url size sha; do import json import sys manifest = json.load(open(sys.argv[1])) for component in manifest["components"]: - print(component["download_url"], component["size_bytes"]) + print(component["download_url"], component["size_bytes"], component["sha256"]) PY - headers=$(curl -fsSI -L --max-time 60 "$url") || fail "download URL failed: $url" - actual_size=$(printf '%s\n' "$headers" | awk 'tolower($1)=="content-length:" { size=$2 } END { gsub("\r", "", size); print size }') - [ "$actual_size" = "$size" ] || fail "download size mismatch for $url (expected $size, got ${actual_size:-missing})" + # Full GET, not HEAD: a size-correct/content-wrong mirror asset must fail + # the publish gate, so compare the actual bytes against the manifest sha256. + tmp=$(mktemp) + curl -fsSL --max-time 900 -o "$tmp" "$url" || { rm -f "$tmp"; fail "download URL failed: $url"; } + actual_size=$(stat -c %s "$tmp") + actual_sha=$(sha256sum "$tmp" | awk '{print $1}') + rm -f "$tmp" + [ "$actual_size" = "$size" ] || fail "download size mismatch for $url (expected $size, got $actual_size)" + [ "$actual_sha" = "$sha" ] || fail "download sha256 mismatch for $url (expected $sha, got $actual_sha)" done echo "Release v${VERSION} published and verified on $REMOTE."