#!/usr/bin/env bash # Create-or-update a Forgejo release and (re)upload its assets. # # Usage: publish-release.sh [...] # Env: FORGEJO_TOKEN user PAT with repo write (required) # REPO_API default https://forgejo.siriusdevops.com/api/v1/repos/sirius/onionwire # TARGET_COMMITISH optional; set it only when the release may have to # create the tag (e.g. a push event's commit sha). # Leave empty to never move an existing tag. # # Idempotent: re-running for the same tag reuses the release and replaces # same-named assets instead of failing with 409. set -euo pipefail # Every API call goes through here. Publishing runs from CI containers that # reach Forgejo through Cloudflare, where a bare HTTP/2 request intermittently # dies with curl exit 92 (stream error) — retries plus forcing HTTP/1.1 make # that a non-event. api_curl() { curl -sS --http1.1 --retry 5 --retry-all-errors --retry-delay 3 \ --connect-timeout 20 --max-time 300 "$@" } tag="${1:?usage: publish-release.sh ...}" name="${2:?missing release name}" body_file="${3:?missing body file}" shift 3 : "${FORGEJO_TOKEN:?FORGEJO_TOKEN is not set}" api="${REPO_API:-https://forgejo.siriusdevops.com/api/v1/repos/sirius/onionwire}" target="${TARGET_COMMITISH:-}" jqp() { python3 -c "import json,sys; d=json.load(sys.stdin); print($1)"; } # Body with @TAG@ substituted, JSON-encoded by python (handles newlines/quotes). python3 - "$body_file" "$tag" "$target" "$name" > /tmp/release-body.json <<'PY' import json, sys body = open(sys.argv[1]).read().replace("@TAG@", sys.argv[2]) payload = { "tag_name": sys.argv[2], "name": sys.argv[4], "body": body, "draft": False, "prerelease": False, } # Only send target_commitish when asked: on an existing tag it is a request to # move the tag, which is never what a re-publish wants. if sys.argv[3]: payload["target_commitish"] = sys.argv[3] print(json.dumps(payload)) PY code=$(api_curl -o /tmp/release-rel.json -w '%{http_code}' \ -H "Authorization: Bearer $FORGEJO_TOKEN" "$api/releases/tags/$tag") if [ "$code" = "404" ]; then echo "publish: creating release $tag" api_curl -sf -X POST "$api/releases" \ -H "Authorization: Bearer $FORGEJO_TOKEN" \ -H 'Content-Type: application/json' \ --data @/tmp/release-body.json -o /tmp/release-rel.json elif [ "$code" = "200" ]; then echo "publish: release $tag exists, updating" rid=$(jqp "d['id']" < /tmp/release-rel.json) api_curl -sf -X PATCH "$api/releases/$rid" \ -H "Authorization: Bearer $FORGEJO_TOKEN" \ -H 'Content-Type: application/json' \ --data @/tmp/release-body.json -o /tmp/release-rel.json else echo "publish: unexpected status $code from GET releases/tags/$tag" >&2 cat /tmp/release-rel.json >&2; exit 1 fi rid=$(jqp "d['id']" < /tmp/release-rel.json) echo "publish: release id $rid ($tag)" # Replace same-named assets so re-runs are clean. for f in "$@"; do [ -f "$f" ] || { echo "publish: missing asset $f" >&2; exit 1; } # Never publish an empty asset: a 0-byte binary/checksum looks like a # successful release and silently breaks everyone who downloads it. bytes=$(wc -c < "$f") if [ "$bytes" -eq 0 ]; then echo "publish: refusing to upload empty asset $f (0 bytes, cwd=$PWD)" >&2 exit 1 fi asset=$(basename "$f") echo "publish: asset $asset is $bytes bytes on disk" api_curl -s -H "Authorization: Bearer $FORGEJO_TOKEN" "$api/releases/$rid/assets" \ | python3 -c " import json,sys for a in json.load(sys.stdin): print(a['id'], a.get('name','')) " | while read -r aid aname; do if [ "$aname" = "$asset" ]; then echo "publish: deleting stale asset $aname ($aid)" api_curl -sf -X DELETE -H "Authorization: Bearer $FORGEJO_TOKEN" \ "$api/releases/$rid/assets/$aid" > /dev/null fi done api_curl -sf -X POST "$api/releases/$rid/assets?name=$asset" \ -H "Authorization: Bearer $FORGEJO_TOKEN" \ -F "attachment=@$f" -o /tmp/release-asset.json python3 -c " import json;d=json.load(open('/tmp/release-asset.json')) print('publish: uploaded', d['name'], d['size'], 'bytes')" done api_curl -s -H "Authorization: Bearer $FORGEJO_TOKEN" "$api/releases/$rid/assets" \ | python3 -c " import json,sys for a in json.load(sys.stdin): print('publish: asset ', a['name'], a['size'])"