Files
security-game/tools/publish_release.sh
T
zwitschi afd086ba11
Full build / Import + headless tests (push) Successful in 35s
Full build / Full desktop build + optional release (push) Skipped
Fail loudly when release uploads do not land
Follow-up to the publishing fix: CI created the v0.2.11 release and logged each
upload, but the assets never arrived, so the job looked successful while the
release was empty. The host root filesystem is full (7.8G, 100%), and Gitea
answered HTTP 500 "Attachments.Save: no space left on device".

- publish_release.sh now checks the HTTP status of every asset upload and prints the
  response body on failure, then verifies the release reports as many assets as were
  sent. Empty releases can no longer pass silently.
- Deleted the empty v0.2.11 release so the "latest" release (v0.2.10) still has all
  five artifacts; the tag itself remains.
- TODO/MEMORY record the host disk blocker with concrete cleanup commands and note
  that v0.2.11 artifacts are built locally in build/ for upload once space exists.
2026-10-31 21:38:12 +01:00

130 lines
4.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# Publish a release to the Gitea/Forgejo host, uploading the built artifacts.
#
# Requires:
# - GITEA_TOKEN (token with write access) OR credentials usable for basic auth
# (e.g. the same ones stored in ~/.git-credentials by `git push`)
# - build artifacts from tools/build_releases.sh (+ optional installer/AppImage)
#
# Usage:
# GITEA_TOKEN=xxx ./tools/publish_release.sh [version]
# (version defaults to config/version from project.godot)
set -euo pipefail
REPO="${REPO:-zwitschi/security-game}"
HOST="${HOST:-https://git.allucanget.biz}"
VERSION="${1:-$(grep -oP 'config/version="\K[^"]+' project.godot)}"
TAG="v$VERSION"
# AUTH is a curl argument list. Populated by the first available method:
# 1. GITEA_TOKEN explicit token (repo secret in CI)
# 2. GITEA_AUTH_HEADER a ready-made "Authorization: ..." header
# 3. ~/.git-credentials basic auth for HOST
# 4. the checkout's git extraheader (Gitea Actions runners inject one for the
# job; this is what makes tag builds publish without any repo secret)
AUTH=()
if [[ -n "${GITEA_TOKEN:-}" ]]; then
AUTH=(-H "Authorization: token $GITEA_TOKEN")
elif [[ -n "${GITEA_AUTH_HEADER:-}" ]]; then
AUTH=(-H "$GITEA_AUTH_HEADER")
elif [[ -f "$HOME/.git-credentials" ]]; then
CRED="$(python3 -c "
import os
for l in open(os.path.expanduser('~/.git-credentials')):
if '$HOST'.split('//')[1] in l:
print(l.split('://',1)[1].split('@')[0]); break")"
if [[ -z "$CRED" ]]; then
echo "error: no credentials for $HOST in ~/.git-credentials" >&2
exit 1
fi
# curl basic auth. (Previously only CRED was set, so ${AUTH[@]} was unbound
# under `set -u` and every API call failed silently.)
AUTH=(-u "$CRED")
fi
if [[ ${#AUTH[@]} -eq 0 ]]; then
# Gitea Actions: actions/checkout configures a local extraheader with the
# runner's credentials. Reuse it for the API (the same server, so the header is
# valid for $HOST too).
GIT_HEADER="$(git config --local --get-regexp '^http\..*\.extraheader$' 2>/dev/null | head -n1 | cut -d' ' -f2- || true)"
if [[ -n "${GIT_HEADER:-}" ]]; then
AUTH=(-H "$GIT_HEADER")
fi
fi
if [[ ${#AUTH[@]} -eq 0 ]]; then
echo "error: no credentials. Set GITEA_TOKEN, GITEA_AUTH_HEADER, or provide ~/.git-credentials." >&2
exit 1
fi
# Artifacts that must exist (run tools/build_releases.sh first).
portables=(
"build/step-line-linux-$VERSION.x86_64"
"build/step-line-windows-$VERSION.exe"
"build/step-line-macos-$VERSION.zip"
)
extras=(
"build/step-line-setup-$VERSION.exe"
"build/step-line-$VERSION-x86_64.AppImage"
)
all_artifacts=("${portables[@]}")
for f in "${extras[@]}"; do
if [[ -f "$f" ]]; then
all_artifacts+=("$f")
else
echo "note: optional artifact missing (skipping): $f"
fi
done
API="$HOST/api/v1/repos/$REPO/releases"
echo "==> Creating release $TAG on $HOST/$REPO"
RELEASE_ID="$(curl -s -X POST "$API" "${AUTH[@]}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"body\":\"Step Line $VERSION — playable prototype. See README for controls.\",\"draft\":false,\"prerelease\":false}" \
| python3 -c "import sys,json;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)"
# If the release already exists (tag already pushed), reuse it.
if [[ -z "$RELEASE_ID" ]]; then
RELEASE_ID="$(curl -s "${AUTH[@]}" "$API/tags/$TAG" | python3 -c "import sys,json;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)"
fi
if [[ -z "$RELEASE_ID" ]]; then
echo "error: could not create or find release $TAG" >&2
exit 1
fi
echo "Release id: $RELEASE_ID"
for f in "${all_artifacts[@]}"; do
if [[ ! -f "$f" ]]; then
echo "error: missing artifact $f — run ./tools/build_releases.sh first" >&2
exit 1
fi
echo "==> Uploading $(basename "$f")"
# Fail loudly: a silent 4xx here used to leave a release with no artifacts while
# the job still reported success.
code="$(curl -sS -o /tmp/release-upload.json -w '%{http_code}' --retry 2 \
-X POST "$API/$RELEASE_ID/assets" "${AUTH[@]}" -F "attachment=@$f" || true)"
if [[ "$code" != "201" && "$code" != "200" ]]; then
echo "error: upload of $(basename "$f") failed with HTTP $code" >&2
cat /tmp/release-upload.json >&2 || true
echo "" >&2
exit 1
fi
done
# Verify the release actually carries the artifacts we just sent.
have="$(curl -sS "${AUTH[@]}" "$API/$RELEASE_ID" | python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
except Exception:
print(0); raise SystemExit
print(len(d.get('assets', [])))" 2>/dev/null || echo 0)"
if [[ "${have:-0}" -lt "${#all_artifacts[@]}" ]]; then
echo "error: release has $have assets, expected ${#all_artifacts[@]}" >&2
exit 1
fi
echo "Verified $have artifacts on the release"
echo ""
echo "Release live: $HOST/$REPO/releases/tag/$TAG"