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.
130 lines
4.7 KiB
Bash
Executable File
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"
|