2026-09-10 13:10:56 -04:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
# Create-or-update a Forgejo release and (re)upload its assets.
|
|
|
|
|
#
|
|
|
|
|
# Usage: publish-release.sh <tag> <release-name> <body-file> <asset> [<asset>...]
|
|
|
|
|
# Env: FORGEJO_TOKEN user PAT with repo write (required)
|
|
|
|
|
# REPO_API default https://forgejo.siriusdevops.com/api/v1/repos/sirius/onionwire
|
2026-09-10 13:58:46 -04:00
|
|
|
# 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.
|
2026-09-10 13:10:56 -04:00
|
|
|
#
|
|
|
|
|
# Idempotent: re-running for the same tag reuses the release and replaces
|
|
|
|
|
# same-named assets instead of failing with 409.
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
2026-09-10 14:08:39 -04:00
|
|
|
# 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 "$@"
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-10 13:10:56 -04:00
|
|
|
tag="${1:?usage: publish-release.sh <tag> <name> <body-file> <asset>...}"
|
|
|
|
|
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}"
|
2026-09-10 13:58:46 -04:00
|
|
|
target="${TARGET_COMMITISH:-}"
|
2026-09-10 13:10:56 -04:00
|
|
|
|
|
|
|
|
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])
|
2026-09-10 13:58:46 -04:00
|
|
|
payload = {
|
2026-09-10 13:10:56 -04:00
|
|
|
"tag_name": sys.argv[2],
|
|
|
|
|
"name": sys.argv[4],
|
|
|
|
|
"body": body,
|
|
|
|
|
"draft": False,
|
|
|
|
|
"prerelease": False,
|
2026-09-10 13:58:46 -04:00
|
|
|
}
|
|
|
|
|
# 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))
|
2026-09-10 13:10:56 -04:00
|
|
|
PY
|
|
|
|
|
|
2026-09-10 14:08:39 -04:00
|
|
|
code=$(api_curl -o /tmp/release-rel.json -w '%{http_code}' \
|
2026-09-10 13:10:56 -04:00
|
|
|
-H "Authorization: Bearer $FORGEJO_TOKEN" "$api/releases/tags/$tag")
|
|
|
|
|
if [ "$code" = "404" ]; then
|
|
|
|
|
echo "publish: creating release $tag"
|
2026-09-10 14:08:39 -04:00
|
|
|
api_curl -sf -X POST "$api/releases" \
|
2026-09-10 13:10:56 -04:00
|
|
|
-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)
|
2026-09-10 14:08:39 -04:00
|
|
|
api_curl -sf -X PATCH "$api/releases/$rid" \
|
2026-09-10 13:10:56 -04:00
|
|
|
-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; }
|
|
|
|
|
asset=$(basename "$f")
|
2026-09-10 14:08:39 -04:00
|
|
|
api_curl -s -H "Authorization: Bearer $FORGEJO_TOKEN" "$api/releases/$rid/assets" \
|
2026-09-10 13:10:56 -04:00
|
|
|
| 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)"
|
2026-09-10 14:08:39 -04:00
|
|
|
api_curl -sf -X DELETE -H "Authorization: Bearer $FORGEJO_TOKEN" \
|
2026-09-10 13:10:56 -04:00
|
|
|
"$api/releases/$rid/assets/$aid" > /dev/null
|
|
|
|
|
fi
|
|
|
|
|
done
|
2026-09-10 14:08:39 -04:00
|
|
|
api_curl -sf -X POST "$api/releases/$rid/assets?name=$asset" \
|
2026-09-10 13:10:56 -04:00
|
|
|
-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
|
|
|
|
|
|
2026-09-10 14:08:39 -04:00
|
|
|
api_curl -s -H "Authorization: Bearer $FORGEJO_TOKEN" "$api/releases/$rid/assets" \
|
2026-09-10 13:10:56 -04:00
|
|
|
| python3 -c "
|
|
|
|
|
import json,sys
|
|
|
|
|
for a in json.load(sys.stdin):
|
|
|
|
|
print('publish: asset ', a['name'], a['size'])"
|