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 "
echo "Verifying public download URLs from manifest..."
python3 - " $PROJECT_ROOT /releases/manifest.json " <<'PY' | while read -r url size; do
import json
import sys
manifest = json.load( open( sys.argv[ 1] ) )
for component in manifest[ "components" ] :
print( component[ "download_url" ] , component[ "size_bytes" ] )
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 } ) "
done
echo " Release v ${ VERSION } published and verified on $REMOTE . "