2026-05-19 12:10:42 -04:00
#!/usr/bin/env bash
# Publish an Archipelago OTA release to a Gitea remote and verify downloads.
set -euo pipefail
VERSION = " ${ 1 :- } "
REMOTE = " ${ 2 :- gitea -vps2 } "
if [ -z " $VERSION " ] ; then
echo "Usage: $0 VERSION [remote]"
exit 1
fi
SCRIPT_DIR = " $( cd " $( dirname " ${ BASH_SOURCE [0] } " ) " && pwd ) "
PROJECT_ROOT = " $( cd " $SCRIPT_DIR /.." && pwd ) "
VERSION_DIR = " $PROJECT_ROOT /releases/v ${ VERSION } "
BACKEND = " $VERSION_DIR /archipelago"
FRONTEND = " $VERSION_DIR /archipelago-frontend- ${ VERSION } .tar.gz"
fail() { echo "Error: $* " >& 2; exit 1; }
[ -f " $PROJECT_ROOT /releases/manifest.json" ] || fail "releases/manifest.json missing"
[ -f " $BACKEND " ] || fail "backend artifact missing: $BACKEND "
[ -f " $FRONTEND " ] || fail "frontend artifact missing: $FRONTEND "
" $SCRIPT_DIR /check-release-manifest.sh"
2026-07-02 12:33:01 -04:00
# §A supply-chain gate: never publish an unsigned OTA manifest. Fleet nodes
# with the pinned release-root anchor refuse to auto-apply unsigned manifests,
# and enforcement will tighten to hard-reject — an unsigned publish would
# strand them. Grep proves presence; ceremony verify proves the crypto.
EXPECTED_DID = "did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur"
grep -q '"signature":' " $PROJECT_ROOT /releases/manifest.json" \
&& grep -q "\"signed_by\": \" $EXPECTED_DID \"" " $PROJECT_ROOT /releases/manifest.json" \
|| fail "releases/manifest.json is not signed by the release root — run: bash scripts/sign-manifest.sh"
if [ -x " $PROJECT_ROOT /core/target/release/archipelago" ] ; then
" $PROJECT_ROOT /core/target/release/archipelago" ceremony verify " $PROJECT_ROOT /releases/manifest.json" \
|| fail "manifest signature failed cryptographic verification"
fi
2026-05-19 12:10:42 -04:00
remote_url = $( git -C " $PROJECT_ROOT " remote get-url " $REMOTE " )
case " $remote_url " in
http://*@*) ;;
*) fail " $REMOTE must be an authenticated http:// Gitea remote URL for API uploads" ;;
esac
auth = ${ remote_url #http:// }
auth = ${ auth %@* }
host_path = ${ remote_url #http:// $auth @ }
host = ${ host_path %%/* }
repo_path = ${ host_path #*/ }
repo_path = ${ repo_path %.git }
api = "http:// $host /api/v1/repos/ $repo_path "
release_url = " $api /releases/tags/v ${ VERSION } "
echo "Pushing main and v ${ VERSION } to $REMOTE ..."
git -C " $PROJECT_ROOT " push " $REMOTE " main "refs/tags/v ${ VERSION } "
release_json = $( curl -fsS -u " $auth " " $release_url " || true )
if [ -z " $release_json " ] ; then
echo "Creating Gitea release v ${ VERSION } ..."
release_body = $( python3 - " $VERSION " <<'PY'
import json
import sys
version = sys.argv[1]
print(json.dumps({
"tag_name": f"v{version}",
"target_commitish": "main",
"name": f"v{version}",
"body": f"Archipelago v{version} release artifacts for OTA updates.",
"draft": False,
"prerelease": True,
}))
PY
)
release_json = $( curl -fsS -u " $auth " -H 'Content-Type: application/json' -d " $release_body " " $api /releases" )
fi
release_id = $( printf '%s' " $release_json " | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' )
asset_names = $( curl -fsS -u " $auth " " $api /releases/ $release_id /assets" | python3 -c 'import json,sys; print("\n".join(a["name"] for a in json.load(sys.stdin)))' )
upload_asset() {
local path = " $1 "
local name = " $2 "
if printf '%s\n' " $asset_names " | grep -Fxq " $name " ; then
echo "Asset $name already exists; leaving it in place."
return
fi
echo "Uploading $name ..."
curl --fail --show-error --silent --http1.1 --connect-timeout 20 --max-time 900 \
-u " $auth " \
-F "attachment=@ $path " \
" $api /releases/ $release_id /assets?name= $name " >/dev/null
asset_names = $( printf '%s\n%s\n' " $asset_names " " $name " )
}
upload_asset " $BACKEND " "archipelago"
upload_asset " $FRONTEND " "archipelago-frontend- ${ VERSION } .tar.gz"
2026-07-08 19:04:14 -04:00
echo "Verifying public download URLs from manifest (size + sha256)..."
python3 - " $PROJECT_ROOT /releases/manifest.json" <<'PY' | while read -r url size sha; do
2026-05-19 12:10:42 -04:00
import json
import sys
manifest = json.load(open(sys.argv[1]))
for component in manifest["components"]:
2026-07-08 19:04:14 -04:00
print(component["download_url"], component["size_bytes"], component["sha256"])
2026-05-19 12:10:42 -04:00
PY
2026-07-08 19:04:14 -04:00
# 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 )"
2026-05-19 12:10:42 -04:00
done
echo "Release v ${ VERSION } published and verified on $REMOTE ."