Two releases published together (inbuxa-server's main and release/2026 on 2026-10-29) reached the forum 14 s apart; Discourse answered the second with 429 and the job failed. On 429 the script now waits the wait_seconds Discourse gives (15 s when it gives none, 60 s at most) and tries again, three times in all. A retry can't double-post: the external_id makes a repeat a 422, which is already treated as announced.
93 lines
4.4 KiB
Bash
Executable File
93 lines
4.4 KiB
Bash
Executable File
#!/bin/bash
|
|
# Called by action.yml. Everything user-written (release name, notes) comes in
|
|
# through the event JSON file and is only ever handled by jq -- never spliced
|
|
# into a command line or a JSON string by the shell.
|
|
set -euo pipefail
|
|
|
|
category_path=$(jq -r --arg r "$REPO" '.[$r] // empty' "$MAP")
|
|
if [ -z "$category_path" ]; then
|
|
echo "::error::$REPO is not in discourse-release/release-map.json"
|
|
exit 1
|
|
fi
|
|
|
|
if [ -n "${TAG:-}" ]; then
|
|
# Called with a tag (a release made by the job's own token): fetch it and
|
|
# treat it as the event, so everything below is the same either way.
|
|
base="${CI_SERVER_INTERNAL:-$GITHUB_SERVER_URL}"
|
|
EVENT_PATH=$(mktemp)
|
|
curl -fsS -H "Authorization: token $GITEA_TOKEN" "$base/api/v1/repos/$REPO/releases/tags/$TAG" |
|
|
jq '{release: .}' > "$EVENT_PATH"
|
|
fi
|
|
if [ "$(jq -r '.release.draft // false' "$EVENT_PATH")" = true ]; then
|
|
echo "release is a draft; not announcing"
|
|
exit 0
|
|
fi
|
|
|
|
tag=$(jq -r '.release.tag_name' "$EVENT_PATH")
|
|
# The category id, looked up anonymously (the category is public); the
|
|
# release-bot key is scoped to creating posts and nothing else.
|
|
category_id=$(curl -fsS "$FORUM/c/$category_path/find_by_slug.json" | jq -r '.category.id')
|
|
|
|
# One topic per repo+tag, ever: Discourse enforces external_id uniqueness.
|
|
external_id="rel-$(printf '%s@%s' "$REPO" "$tag" | sha1sum | cut -c1-40)"
|
|
|
|
payload=$(jq -n --arg repo "$REPO" --argjson category "$category_id" --arg ext "$external_id" \
|
|
--slurpfile ev "$EVENT_PATH" '
|
|
($ev[0].release) as $r
|
|
| ($repo | split("/") | last) as $name
|
|
# A pre-release is a release candidate: said in the title and the first
|
|
# line, so nobody takes it for what a plain install or `latest` gets.
|
|
| ($r.prerelease // false) as $rc
|
|
| {
|
|
# repo + tag, not the release name: names vary by project (some repeat
|
|
# the tag, some carry a product name in capitals), tags do not.
|
|
title: (if $rc then "\($name) \($r.tag_name) release candidate" else "\($name) \($r.tag_name) released" end),
|
|
category: $category,
|
|
external_id: $ext,
|
|
raw: (
|
|
(if $rc
|
|
then "**\($name) \($r.tag_name)** is a release candidate: a pre-release for testing, not yet the release. `latest` and a plain install stay where they are.\n\n"
|
|
else "**\($name) \($r.tag_name)** is out.\n\n" end)
|
|
+ (if ($r.body // "") == "" then "" else "\($r.body)\n\n" end)
|
|
+ "---\n[Release on Gitea](\($r.html_url))"
|
|
)
|
|
}')
|
|
|
|
# Two releases published together (a repo's main and its maintenance branch)
|
|
# hit the forum's rate limit: Discourse answers 429 with how long to wait.
|
|
# Wait that long (60 s at most) and try again, three times in all.
|
|
for attempt in 1 2 3; do
|
|
status=$(curl -sS -o /tmp/discourse-release.out -w '%{http_code}' -X POST "$FORUM/posts.json" \
|
|
-H "Api-Key: $API_KEY" -H "Api-Username: release-bot" \
|
|
-H "Content-Type: application/json" --data-binary "$payload")
|
|
body=$(cat /tmp/discourse-release.out)
|
|
[ "$status" = 429 ] && [ "$attempt" -lt 3 ] || break
|
|
delay=$(jq -r '(.extras.wait_seconds // 15) | if type == "number" then . else 15 end | if . > 60 then 60 else . end | ceil' <<<"$body" 2>/dev/null || echo 15)
|
|
echo "forum rate limit; trying again in $((delay + 1)) s"
|
|
sleep $((delay + 1))
|
|
done
|
|
case "$status" in
|
|
200)
|
|
url="$FORUM/t/$(jq -r '.topic_slug' <<<"$body")/$(jq -r '.topic_id' <<<"$body")"
|
|
echo "announced: $url" ;;
|
|
422)
|
|
# Discourse words this "External has already been taken" (older versions:
|
|
# "External ID ..."); a racing run can also hit the title check first.
|
|
if jq -e '.errors | tostring | test("External( ID| has already been taken)|title has already been used"; "i")' <<<"$body" >/dev/null; then
|
|
echo "already announced (external_id $external_id); nothing to do"
|
|
exit 0
|
|
fi
|
|
echo "::error::forum refused the post: $body"; exit 1 ;;
|
|
*)
|
|
echo "::error::forum returned HTTP $status: $body"; exit 1 ;;
|
|
esac
|
|
|
|
if [ -n "${DISCORD_WEBHOOK:-}" ]; then
|
|
jq -n --arg u "$url" --slurpfile ev "$EVENT_PATH" --arg repo "$REPO" '
|
|
($ev[0].release) as $r
|
|
| {content: ("**\($repo | split("/") | last) \($r.tag_name)** " + (if ($r.prerelease // false) then "release candidate" else "released" end) + "\n\($u)")[0:1900]}' |
|
|
curl -fsS -X POST "$DISCORD_WEBHOOK" -H "Content-Type: application/json" --data-binary @- >/dev/null \
|
|
&& echo "posted to Discord" \
|
|
|| echo "::warning::Discord webhook failed; the forum post stands"
|
|
fi
|