212 lines
7.8 KiB
Bash
Executable File
212 lines
7.8 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/gig-line-linux-$VERSION.x86_64"
|
|
"build/gig-line-windows-$VERSION.exe"
|
|
"build/gig-line-macos-$VERSION.zip"
|
|
)
|
|
extras=(
|
|
"build/gig-line-setup-$VERSION.exe"
|
|
"build/gig-line-$VERSION-x86_64.AppImage"
|
|
"build/gig-line-web-$VERSION.zip"
|
|
"build/gig-line-desktop-hq-$VERSION.zip"
|
|
)
|
|
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
|
|
|
|
# Preflight before creating anything on the host: no empty files, and enough
|
|
# local disk to assemble/checksum the release. Upload failures are rolled back
|
|
# below so a full or interrupted host never keeps a partial release.
|
|
total_bytes=0
|
|
for f in "${all_artifacts[@]}"; do
|
|
if [[ ! -s "$f" ]]; then
|
|
echo "error: missing or empty artifact $f — run ./tools/build_releases.sh first" >&2
|
|
exit 1
|
|
fi
|
|
total_bytes=$((total_bytes + $(stat -c '%s' "$f")))
|
|
done
|
|
free_kb="$(df -Pk build | awk 'NR==2 {print $4}')"
|
|
need_kb=$(((total_bytes + 1023) / 1024))
|
|
reserve_kb=262144
|
|
if [[ -z "${free_kb:-}" || "$free_kb" -lt $((need_kb + reserve_kb)) ]]; then
|
|
echo "error: not enough free disk in build/ for ${need_kb} KiB of artifacts plus a 256 MiB reserve (free: ${free_kb:-unknown} KiB)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Local checksums are useful evidence even though the release host does not need
|
|
# a duplicate checksum asset; CI keeps the manifest with the build workspace.
|
|
checksum_file="build/SHA256SUMS-$VERSION.txt"
|
|
: > "$checksum_file"
|
|
for f in "${all_artifacts[@]}"; do
|
|
sha256sum "$f" >> "$checksum_file"
|
|
done
|
|
echo "==> Checksums written to $checksum_file"
|
|
cat "$checksum_file"
|
|
if [[ "${PUBLISH_DRY_RUN:-0}" == "1" ]]; then
|
|
echo "DRY RUN: preflight and checksums passed; no release or upload was created."
|
|
exit 0
|
|
fi
|
|
|
|
API="$HOST/api/v1/repos/$REPO/releases"
|
|
created_release=0
|
|
uploaded_ids=()
|
|
|
|
rollback_release() {
|
|
echo "error: rolling back the partial release upload" >&2
|
|
for id in ${uploaded_ids[@]+"${uploaded_ids[@]}"}; do
|
|
curl -sS -X DELETE "${AUTH[@]}" "$API/$RELEASE_ID/assets/$id" >/dev/null 2>&1 || true
|
|
done
|
|
if [[ "$created_release" == "1" ]]; then
|
|
curl -sS -X DELETE "${AUTH[@]}" "$API/$RELEASE_ID" >/dev/null 2>&1 || true
|
|
echo "removed the new release $TAG" >&2
|
|
else
|
|
echo "removed only the ${#uploaded_ids[@]} newly uploaded asset(s) from existing release $TAG" >&2
|
|
fi
|
|
}
|
|
|
|
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\":\"Gig 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 [[ -n "$RELEASE_ID" ]]; then
|
|
created_release=1
|
|
fi
|
|
|
|
# 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
|
|
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
|
|
rollback_release
|
|
exit 1
|
|
fi
|
|
asset_id="$(python3 -c "import sys,json;print(json.load(open('/tmp/release-upload.json')).get('id',''))" 2>/dev/null || true)"
|
|
if [[ -z "$asset_id" ]]; then
|
|
echo "error: upload of $(basename "$f") returned no asset id" >&2
|
|
rollback_release
|
|
exit 1
|
|
fi
|
|
uploaded_ids+=("$asset_id")
|
|
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
|
|
rollback_release
|
|
exit 1
|
|
fi
|
|
echo "Verified $have artifacts on the release"
|
|
|
|
# Optional retention: set RETAIN_RELEASES=7 to keep the newest seven releases.
|
|
# The default 0 disables pruning; never delete old releases implicitly.
|
|
RETAIN_RELEASES="${RETAIN_RELEASES:-0}"
|
|
if [[ "$RETAIN_RELEASES" =~ ^[0-9]+$ ]] && (( RETAIN_RELEASES > 0 )); then
|
|
echo "==> Pruning releases older than the newest $RETAIN_RELEASES"
|
|
releases_json="$(curl -sS "${AUTH[@]}" "$API?limit=50")"
|
|
while IFS= read -r old_id; do
|
|
[[ -z "$old_id" ]] && continue
|
|
echo " removing release id $old_id"
|
|
curl -sS -X DELETE "${AUTH[@]}" "$API/$old_id" >/dev/null 2>&1 || true
|
|
done < <(RELEASES_JSON="$releases_json" RETAIN="$RETAIN_RELEASES" python3 - <<'PYPRUNE'
|
|
import json, os
|
|
try:
|
|
data = json.loads(os.environ["RELEASES_JSON"])
|
|
except Exception:
|
|
raise SystemExit
|
|
keep = int(os.environ["RETAIN"])
|
|
for release in sorted(data, key=lambda r: r.get("created_at", ""), reverse=True)[keep:]:
|
|
print(release.get("id", ""))
|
|
PYPRUNE
|
|
)
|
|
fi
|
|
|
|
echo ""
|
|
echo "Release live: $HOST/$REPO/releases/tag/$TAG"
|