Publishing v1.7.121-alpha failed on auth after the manifest had already passed every check. The script required an `http://user:token@` remote, which left only `gitea-vps2` — whose token is dead — and rejected `gitea-ai`, the https remote whose credential actually works for git push. Same Gitea instance (146.59.87.168, v1.27.1) either way, so the restriction bought nothing and blocked the one usable path. Accepts http and https, and carries the scheme through to the API URL instead of hardcoding it. Note for diagnosis next time: `/api/v1/repos/.../releases` is publicly readable, so a 200 there does NOT prove the credential works. Use `/api/v1/user`, which requires real auth. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
129 lines
5.0 KiB
Bash
Executable File
129 lines
5.0 KiB
Bash
Executable File
#!/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"
|
|
|
|
# §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
|
|
|
|
remote_url=$(git -C "$PROJECT_ROOT" remote get-url "$REMOTE")
|
|
# https is accepted as well as http. Requiring http:// meant the only remote
|
|
# whose credential actually works for git push (the https one) was rejected,
|
|
# while the http remote it forced you to use had a dead token — so publishing
|
|
# failed on auth after the manifest had already passed every check
|
|
# (v1.7.121-alpha, 2026-08-04). The scheme is carried through to the API URL
|
|
# rather than assumed.
|
|
case "$remote_url" in
|
|
http://*@*|https://*@*) ;;
|
|
*) fail "$REMOTE must be an authenticated http(s):// Gitea remote URL for API uploads" ;;
|
|
esac
|
|
|
|
scheme=${remote_url%%://*}
|
|
rest=${remote_url#*://}
|
|
auth=${rest%%@*}
|
|
host_path=${rest#*@}
|
|
host=${host_path%%/*}
|
|
repo_path=${host_path#*/}
|
|
repo_path=${repo_path%.git}
|
|
api="$scheme://$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 (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"], component["sha256"])
|
|
PY
|
|
# 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."
|