-
Notifications
You must be signed in to change notification settings - Fork 9
216 lines (198 loc) · 9.49 KB
/
discord-release.yml
File metadata and controls
216 lines (198 loc) · 9.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
name: 'Discord: release announcement'
# ---------------------------------------------------------------------------
# Why this workflow exists (and what it deliberately does NOT do)
#
# - Listens to GitHub Release events and posts a rich embed announcement to
# the dedicated #releases Discord channel.
# - Decoupled from npm publish: publishing is a manual `bun publish` from a
# maintainer's laptop (with --otp), and creating a GitHub Release is the
# human gate that triggers the announcement. This keeps NPM_TOKEN out of
# CI entirely.
# - Sibling workflow: .github/workflows/discord-activity.yml posts per-push
# commit summaries to a different (commits) channel via DISCORD_WEBHOOK.
# The two workflows never overlap: push events vs release events are
# independent, and they target different webhooks / channels / audiences.
#
# Tag conventions supported:
# <pkg-name>@<semver> monorepo per-package release (preferred)
# v<semver> repo-wide release (fallback)
# anything else treated as a generic release; the package name
# field renders as the GitHub repo full name
# ---------------------------------------------------------------------------
on:
release:
# `published` fires on stable, `prereleased` fires on pre-release. We
# listen to both and branch on github.event.release.prerelease for the
# visual differentiation (orange border + @next install hint).
types: [published, prereleased]
# If a release is edited and re-published quickly, only the latest event
# wins. Prevents duplicate announcements when a maintainer fixes a typo
# in the release notes seconds after publishing.
concurrency:
group: discord-release-${{ github.event.release.id }}
cancel-in-progress: true
jobs:
announce:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
# ----------------------------------------------------------------------
# Parse the release event into the variables the embed needs.
#
# We do all jq / regex / truncation in one bash step (instead of
# multiple GHA expressions) because:
# - bash regex with BASH_REMATCH is cleaner than templating extraction
# into the embed at JSON-build time
# - keeps the second step purely about embed assembly + curl
# ----------------------------------------------------------------------
- name: Parse release metadata
id: meta
env:
RELEASE_JSON: ${{ toJSON(github.event.release) }}
run: |
set -euo pipefail
TAG=$(jq -r '.tag_name' <<< "$RELEASE_JSON")
NAME=$(jq -r '.name // .tag_name' <<< "$RELEASE_JSON")
URL=$(jq -r '.html_url' <<< "$RELEASE_JSON")
AUTHOR=$(jq -r '.author.login' <<< "$RELEASE_JSON")
AUTHOR_URL=$(jq -r '.author.html_url' <<< "$RELEASE_JSON")
IS_PRE=$(jq -r '.prerelease' <<< "$RELEASE_JSON")
PUBLISHED_AT=$(jq -r '.published_at' <<< "$RELEASE_JSON")
# Tag → (package, version) extraction.
# Regex notes:
# - The leading @? + slash optional pair handles BOTH unscoped
# (`mypkg@1.0.0`) and scoped (`@docs.plus/extension-hyperlink@4.3.0`)
# names without needing two branches.
# - We allow . and _ in the version segment to admit pre-release
# ids (4.3.0-rc.1, 4.3.0+build.5, 4.3.0-canary_2026-04-19).
if [[ "$TAG" =~ ^(@?[A-Za-z0-9._-]+(/[A-Za-z0-9._-]+)?)@(.+)$ ]]; then
PKG="${BASH_REMATCH[1]}"
VERSION="${BASH_REMATCH[3]}"
elif [[ "$TAG" =~ ^v(.+)$ ]]; then
PKG="${{ github.repository }}"
VERSION="${BASH_REMATCH[1]}"
else
PKG="${{ github.repository }}"
VERSION="$TAG"
fi
# Body trimming. Discord embed.description caps at 4096 chars.
# CHANGELOG slices for major versions can run long; truncate with
# a clear "...full notes" link to the GitHub Release page so no
# information is silently lost.
BODY=$(jq -r '.body // ""' <<< "$RELEASE_JSON")
if [ ${#BODY} -gt 3800 ]; then
BODY="${BODY:0:3800}
... [full release notes]($URL)"
fi
# Color: 0x22c55e (green-500) for stable, 0xf97316 (orange-500)
# for pre-release. Discord embed.color is a base-10 integer, so
# we encode the hex pre-converted.
# Color: 24-bit RGB encoded as decimal (Discord embed.color is int).
# Verified: $((16#22c55e)) == 2278750 ; $((16#f97316)) == 16347926.
if [ "$IS_PRE" = "true" ]; then
COLOR=16347926 # #f97316 — Tailwind orange-500
BADGE="Pre-release"
else
COLOR=2278750 # #22c55e — Tailwind green-500
BADGE="Release"
fi
# Stash for the next step. Scalars via GITHUB_OUTPUT, multi-line
# body via tempfile to side-step the EOF-delimiter quoting trap.
{
echo "pkg=$PKG"
echo "version=$VERSION"
echo "url=$URL"
echo "author=$AUTHOR"
echo "author_url=$AUTHOR_URL"
echo "is_pre=$IS_PRE"
echo "published_at=$PUBLISHED_AT"
echo "color=$COLOR"
echo "badge=$BADGE"
echo "name=$NAME"
} >> "$GITHUB_OUTPUT"
printf '%s' "$BODY" > "$RUNNER_TEMP/release_body.md"
# ----------------------------------------------------------------------
# Assemble the Discord embed and POST it.
#
# The embed is constructed entirely via `jq -n` with --arg / --argjson
# so every user-controlled field (release name, body, author login)
# is JSON-escaped at the boundary. This is the only safe way; building
# JSON via string interpolation would crumble on any release body
# containing a literal " or \ or newline (which is most of them).
# ----------------------------------------------------------------------
- name: Post embed to Discord
env:
# NOTE: deliberately a different secret from the push-activity
# workflow's DISCORD_WEBHOOK. Releases go to #releases (low-noise,
# user-facing); pushes go to the existing push channel (high-noise,
# team-facing). Naming convention: DISCORD_<qualifier>_WEBHOOK.
WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
PKG: ${{ steps.meta.outputs.pkg }}
VERSION: ${{ steps.meta.outputs.version }}
NAME: ${{ steps.meta.outputs.name }}
URL: ${{ steps.meta.outputs.url }}
AUTHOR: ${{ steps.meta.outputs.author }}
AUTHOR_URL: ${{ steps.meta.outputs.author_url }}
IS_PRE: ${{ steps.meta.outputs.is_pre }}
PUBLISHED_AT: ${{ steps.meta.outputs.published_at }}
COLOR: ${{ steps.meta.outputs.color }}
BADGE: ${{ steps.meta.outputs.badge }}
run: |
set -euo pipefail
if [ -z "${WEBHOOK:-}" ]; then
echo "::error::DISCORD_RELEASE_WEBHOOK secret is not set. See AGENTS.md → Releasing for setup."
exit 1
fi
# Install hint. For pre-releases we surface the @next dist-tag as
# the primary form so consumers don't accidentally pin a non-stable
# as their default. The pinned-version form remains as a comment.
if [ "$IS_PRE" = "true" ]; then
INSTALL=$(printf '```bash\nbun add %s@next\n# or: npm install %s@%s\n```' "$PKG" "$PKG" "$VERSION")
else
INSTALL=$(printf '```bash\nbun add %s@%s\n```' "$PKG" "$VERSION")
fi
BODY=$(cat "$RUNNER_TEMP/release_body.md")
PAYLOAD=$(jq -n \
--arg title "$NAME" \
--arg url "$URL" \
--arg desc "$BODY" \
--argjson color "$COLOR" \
--arg badge "$BADGE" \
--arg pkg "$PKG" \
--arg version "$VERSION" \
--arg install "$INSTALL" \
--arg author "$AUTHOR" \
--arg authorUrl "$AUTHOR_URL" \
--arg published "$PUBLISHED_AT" \
'{
embeds: [{
title: $title,
url: $url,
description: $desc,
color: $color,
author: { name: $author, url: $authorUrl },
fields: [
{ name: "Package", value: ("`" + $pkg + "`"), inline: true },
{ name: "Version", value: ("`" + $version + "`"), inline: true },
{ name: "Type", value: $badge, inline: true },
{ name: "Install", value: $install, inline: false }
],
footer: { text: "GitHub Releases" },
timestamp: $published
}]
}')
# Capture HTTP status so a Discord-side rejection (404 invalid
# webhook, 400 bad embed, 429 rate limit) fails the workflow loudly
# instead of silently no-op-ing the announcement.
HTTP_STATUS=$(curl -sS -o /tmp/discord_response.txt -w "%{http_code}" \
-X POST \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
"$WEBHOOK")
echo "Discord HTTP status: $HTTP_STATUS"
if [ "$HTTP_STATUS" -lt 200 ] || [ "$HTTP_STATUS" -ge 300 ]; then
echo "::error::Discord webhook rejected the request (HTTP $HTTP_STATUS)"
cat /tmp/discord_response.txt
exit 1
fi