Files
punktfunk/scripts/ci/discord-announce.sh
T
enricobuehlerandClaude Opus 4.8 15d09a8c10 feat(ci/release): author release notes in-repo + announce stable releases to Discord
Release notes now live in the repo at docs/releases/vX.Y.Z.md, authored during the
version bump (before the tag). ensure_release (gitea-release.sh + .ps1 twin) seeds the
Gitea release body from that file in the create POST, so the release is born WITH its
notes instead of being created empty and PATCHed afterward. canary/rc have no file ->
empty body, unchanged.

New manual `announce` workflow (workflow_dispatch, tag input) re-syncs the notes file
over the live release and posts a violet embed (title, notes lead-in, release-page link)
to the Discord #releases channel via the DISCORD_RELEASE_WEBHOOK secret. Stable-only:
a -rc tag is refused unless allow_prerelease=true. Pressing "go" is the quality gate, so
a half-built or failed release is never announced.

docs/releases/README.md documents the ritual; TEMPLATE.md is the skeleton.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 16:43:18 +02:00

115 lines
4.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# Announce a stable Punktfunk release to the Discord #releases channel.
#
# Model: release notes live in the repo at docs/releases/<tag>.md (authored during the version
# bump, BEFORE the tag is pushed — see docs/releases/README.md). CI seeds the Gitea release body
# from that file at creation, so the release is never noteless. This script is the deliberate
# "go" step (workflow_dispatch, .gitea/workflows/announce.yml): once every platform's CI is green
# it re-asserts the notes file over the live release, then posts a formatted embed to Discord.
#
# Usage: scripts/ci/discord-announce.sh <tag> # e.g. v0.18.0
# Env:
# GITHUB_SERVER_URL https://git.unom.io (Gitea Actions sets it)
# GITHUB_REPOSITORY unom/punktfunk (Gitea Actions sets it)
# GITEA_TOKEN PAT with repo write scope (from secrets.REGISTRY_TOKEN)
# DISCORD_RELEASE_WEBHOOK the #releases channel webhook URL (from secrets)
# ALLOW_PRERELEASE "1" to permit announcing a -rc/pre-release tag (default: refuse)
#
# Requires: curl + python3 (both present on the ubuntu-24.04 runner).
set -euo pipefail
TAG="${1:?usage: discord-announce.sh <tag>}"
: "${GITHUB_SERVER_URL:?}" "${GITHUB_REPOSITORY:?}" "${GITEA_TOKEN:?}" "${DISCORD_RELEASE_WEBHOOK:?}"
# Stable-only guard: a `-` in the tag marks a pre-release (v0.2.0-rc1). Refuse unless explicitly
# allowed, so an rc dispatched by accident never pings the community.
case "$TAG" in
*-*)
case "$(printf '%s' "${ALLOW_PRERELEASE:-0}" | tr '[:upper:]' '[:lower:]')" in
1|true|yes) : ;;
*) echo "discord-announce: '$TAG' looks like a pre-release; set ALLOW_PRERELEASE=1 to override" >&2
exit 1 ;;
esac ;;
esac
_here="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
API="$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY"
# Resolve the release object created by the build workflows (id, html_url, published_at).
release_json="$(curl -fsS "$API/releases/tags/$TAG" -H "Authorization: token $GITEA_TOKEN")"
RID="$(printf '%s' "$release_json" | python3 -c 'import json,sys;print(json.load(sys.stdin).get("id",""))')"
[ -n "$RID" ] || { echo "discord-announce: no release found for tag '$TAG' — has CI created it yet?" >&2; exit 1; }
# Re-assert the notes file over the live release (source of truth), covering an edit after the
# release object was first created. No-op if docs/releases/<tag>.md is absent.
# shellcheck source=scripts/ci/gitea-release.sh
. "$_here/gitea-release.sh"
apply_release_notes "$RID" "$TAG"
# Build and post the Discord embed. The description is the notes' lead-in (everything before the
# first `## ` section header), trimmed to a readable length, with a link to the full release page
# for the sections + downloads. Falls back to the release's own body if there is no notes file.
NOTES_PATH="$(_release_notes_path "$TAG" || true)"
payload_file="$(mktemp)"
trap 'rm -f "$payload_file"' EXIT
RELEASE_JSON="$release_json" NOTES_PATH="$NOTES_PATH" TAG="$TAG" python3 - "$payload_file" <<'PY'
import json, os, sys
rel = json.loads(os.environ["RELEASE_JSON"])
tag = os.environ["TAG"]
version = tag[1:] if tag.startswith("v") else tag
url = rel.get("html_url") or ""
published = rel.get("published_at") or rel.get("created_at") or ""
notes_path = os.environ.get("NOTES_PATH") or ""
body = ""
if notes_path and os.path.isfile(notes_path):
with open(notes_path, encoding="utf-8") as f:
body = f.read()
else:
body = rel.get("body") or ""
# Lead-in = text before the first `## ` section header (the intro paragraphs); fall back to the
# whole body when the notes open straight into a section.
lead = body
idx = body.find("\n## ")
if idx != -1:
lead = body[:idx]
lead = lead.strip()
if not lead:
lead = body.strip()
LIMIT = 1800
if len(lead) > LIMIT:
cut = lead.rfind("\n\n", 0, LIMIT)
if cut < LIMIT // 2:
cut = lead.rfind(" ", 0, LIMIT)
if cut <= 0:
cut = LIMIT
lead = lead[:cut].rstrip() + " …"
description = lead
if url:
description = (description + "\n\n" if description else "") + \
f"**[⬇️ Download & full release notes →]({url})**"
embed = {
"title": f"Punktfunk {version}",
"color": 0x6C5BF3, # --pf-brand deep violet
"description": description,
"footer": {"text": "Punktfunk • git.unom.io"},
}
if url:
embed["url"] = url
if published:
embed["timestamp"] = published
with open(sys.argv[1], "w", encoding="utf-8") as f:
json.dump({"embeds": [embed]}, f)
PY
curl -fsS -o /dev/null -X POST "$DISCORD_RELEASE_WEBHOOK" \
-H 'Content-Type: application/json' --data-binary @"$payload_file"
echo "discord-announce: posted Punktfunk $TAG to #releases (release $RID)"