Skip to content

Commit 119705c

Browse files
committed
👷 Add release draft and Discord announcement workflows
- Tag pushes build, test, and create a draft release with the plugin files - Publishing a release announces it on Discord with version-based role pings - Drafts post a no-ping preview to a test channel; previews can be re-run by hand - Long notes are trimmed to fit Discord's 4000-character limit - Release notes rendering moved out of the packager so the announcer can share it - Templates now link the right release, escape URLs once, and drop the stale PR note
1 parent dde57b3 commit 119705c

8 files changed

Lines changed: 513 additions & 49 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Announce Release
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
# Re-run a preview (or the live post) by hand, e.g. after fixing the notes.
8+
workflow_dispatch:
9+
inputs:
10+
tag:
11+
description: Release tag (e.g. v1.11.0)
12+
required: true
13+
target:
14+
description: Where to post
15+
type: choice
16+
options: [test, live]
17+
default: test
18+
19+
jobs:
20+
announce:
21+
runs-on: ubuntu-latest
22+
23+
permissions:
24+
contents: read
25+
26+
env:
27+
TAG: ${{ inputs.tag || github.event.release.tag_name }}
28+
LIVE: ${{ github.event_name == 'release' || inputs.target == 'live' }}
29+
30+
steps:
31+
- uses: actions/checkout@v5
32+
with:
33+
ref: ${{ env.TAG }}
34+
- uses: oven-sh/setup-bun@v2
35+
36+
- name: Post announcement
37+
run: bun .scripts/release/announce.ts "${TAG#v}" $([ "$LIVE" = true ] && echo --live)
38+
env:
39+
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }}
40+
DISCORD_CHANNEL_ID: ${{ env.LIVE == 'true' && vars.DISCORD_CHANNEL_ID || vars.DISCORD_TEST_CHANNEL_ID }}
41+
DISCORD_ROLE_PRERELEASE: ${{ vars.DISCORD_ROLE_PRERELEASE }}
42+
DISCORD_ROLE_MINOR: ${{ vars.DISCORD_ROLE_MINOR }}
43+
DISCORD_ROLE_PATCH: ${{ vars.DISCORD_ROLE_PATCH }}
44+
DISCORD_ROLE_BREAKING: ${{ vars.DISCORD_ROLE_BREAKING }}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: Draft Release
2+
3+
on:
4+
push:
5+
tags: ['v*']
6+
7+
jobs:
8+
draft:
9+
runs-on: ubuntu-latest
10+
11+
permissions:
12+
contents: write
13+
14+
env:
15+
GH_TOKEN: ${{ github.token }}
16+
TAG: ${{ github.ref_name }}
17+
18+
steps:
19+
- uses: actions/checkout@v5
20+
- uses: oven-sh/setup-bun@v2
21+
- uses: actions/setup-node@v4
22+
with:
23+
node-version: 24
24+
25+
- name: Check tag matches package.json
26+
run: |
27+
expected="v$(jq -r .version package.json)"
28+
if [ "$TAG" != "$expected" ]; then
29+
echo "::error::Tag $TAG doesn't match package.json version ($expected)"
30+
exit 1
31+
fi
32+
if gh release view "$TAG" > /dev/null 2>&1; then
33+
echo "::error::A release for $TAG already exists"
34+
exit 1
35+
fi
36+
37+
- name: Install dependencies
38+
run: bun install --frozen-lockfile
39+
40+
- name: Run unit tests
41+
run: bun run test:unit
42+
43+
- name: Build
44+
run: bun run prod
45+
46+
# See build.yml
47+
- name: Cache Blockbench test environment
48+
uses: actions/cache@v4
49+
with:
50+
path: ~/.envbench
51+
key: envbench-${{ runner.os }}-animated-java-tests
52+
53+
- name: Install xvfb
54+
run: sudo apt-get update && sudo apt-get install -y xvfb
55+
56+
- name: Run tests
57+
run: bun run test:only
58+
59+
- name: Create draft release
60+
run: |
61+
# The packager skips the notes when changelog.json has no entry for this version.
62+
if [ ! -f dist/github_release_notes_template ]; then
63+
echo "::error::No release notes were generated. Is there a changelog.json entry for $TAG?"
64+
exit 1
65+
fi
66+
prerelease=$([[ "$TAG" == *-* ]] && echo --prerelease || true)
67+
gh release create "$TAG" --draft --verify-tag $prerelease \
68+
--title "$TAG" \
69+
--notes-file dist/github_release_notes_template \
70+
dist/pluginPackage/animated_java.js \
71+
dist/pluginPackage/about.md \
72+
dist/pluginPackage/changelog.json \
73+
dist/pluginPackage/icon.svg
74+
75+
- name: Preview announcement in test channel
76+
run: bun .scripts/release/announce.ts "${TAG#v}"
77+
env:
78+
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }}
79+
DISCORD_CHANNEL_ID: ${{ vars.DISCORD_TEST_CHANNEL_ID }}
80+
DISCORD_ROLE_PRERELEASE: ${{ vars.DISCORD_ROLE_PRERELEASE }}
81+
DISCORD_ROLE_MINOR: ${{ vars.DISCORD_ROLE_MINOR }}
82+
DISCORD_ROLE_PATCH: ${{ vars.DISCORD_ROLE_PATCH }}
83+
DISCORD_ROLE_BREAKING: ${{ vars.DISCORD_ROLE_BREAKING }}

‎.scripts/plugins/packagerPlugin.ts‎

Lines changed: 24 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import { basename, join } from 'node:path'
55
import { sveltePreprocess } from 'svelte-preprocess'
66
// @ts-expect-error - Types are broken in nodenext for this package, but it works fine.
77
import { typescript } from 'svelte-preprocess-esbuild'
8+
import {
9+
type Changelog,
10+
type ChangelogEntry,
11+
getChangelogEntry,
12+
renderReleaseNotes,
13+
} from '../release/releaseNotes'
814
import type { CompileOptions, CompileResult } from 'svelte/compiler'
915
import { compile, preprocess } from 'svelte/compiler'
1016
import { render } from 'svelte/server'
@@ -19,12 +25,6 @@ const DIST_PATH = './dist/'
1925
const DIST_PACKAGE_PATH = './dist/pluginPackage/'
2026
const CHANGELOG_PATH = './src/pluginPackage/changelog.json'
2127
const RELEASE_NOTES_TEMPLATES = './.scripts/plugins/releaseNoteTemplates/'
22-
const URL_REGEX =
23-
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9]{1,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/gm
24-
25-
function replaceTemplateVars(str: string, items: Record<string, string>) {
26-
return str.replace(/\{(.+?)\}/g, str => items[str.replace(/[\{\}]/g, '')] ?? str)
27-
}
2828

2929
/**
3030
* Convert a warning or error emitted from the svelte compiler for esbuild.
@@ -143,41 +143,25 @@ function plugin(): Plugin {
143143

144144
if (process.env.NODE_ENV === 'production') {
145145
console.log('📝 Creating changelogs...')
146-
const rawChangelog = fs.readFileSync(CHANGELOG_PATH, 'utf-8')
147-
const changelog = JSON.parse(rawChangelog)
146+
const changelog = JSON.parse(
147+
fs.readFileSync(CHANGELOG_PATH, 'utf-8')
148+
) as Changelog
149+
let entry: ChangelogEntry
150+
try {
151+
entry = getChangelogEntry(changelog, PACKAGE.version)
152+
} catch (e: any) {
153+
console.warn(`⚠️ ${e.message} in ${CHANGELOG_PATH}`)
154+
return
155+
}
148156
for (const file of fs.readdirSync(RELEASE_NOTES_TEMPLATES)) {
149-
let content = fs.readFileSync(join(RELEASE_NOTES_TEMPLATES, file), 'utf-8')
150-
151-
const versionChangelog = changelog[PACKAGE.version]
152-
if (!versionChangelog) {
153-
console.warn(
154-
`⚠️ No changelog found for version ${PACKAGE.version} in ${CHANGELOG_PATH}`
155-
)
156-
return
157-
}
158-
159-
let categories = ''
160-
for (const category of versionChangelog.categories) {
161-
categories +=
162-
`\n\n### ${category.title}\n\n` +
163-
category.list
164-
.map((v: string) => '- ' + v)
165-
.join('\n')
166-
.replaceAll('[BREAKING]', '⚠️ **BREAKING** —')
167-
}
168-
169-
content = replaceTemplateVars(content, {
170-
version: PACKAGE.version,
171-
categories: categories.trim(),
172-
})
173-
174-
if (content.includes('[[ESCAPE_URLS]]')) {
175-
content = content
176-
.replace('[[ESCAPE_URLS]]', '')
177-
.replaceAll(URL_REGEX, (match: string) => '<' + match + '>')
178-
}
179-
180-
fs.writeFileSync(join(DIST_PATH, file), content)
157+
const template = fs.readFileSync(
158+
join(RELEASE_NOTES_TEMPLATES, file),
159+
'utf-8'
160+
)
161+
fs.writeFileSync(
162+
join(DIST_PATH, file),
163+
renderReleaseNotes(template, PACKAGE.version, entry)
164+
)
181165
}
182166

183167
// if (fs.existsSync(PLUGIN_REPO_PATH)) {
Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
[[ESCAPE_URLS]]
22

3-
# :AnimatedJava: Animated Java Release v{version}
3+
# <:AnimatedJava:1349340042379661392> Animated Java Release v{version}
44

55
{categories}
66

7-
## How to Install
7+
## [How to Install](https://animated-java.dev/docs/getting-started/installing)
88

9-
Download `animated_java.js` from the [release page](<https://github.com/Animated-Java/animated-java/releases/tag/v1.10.1>), then [Follow this tutorial](<https://animated-java.dev/docs/getting-started/installing#advanced-installation-url>)
10-
11-
Once [this PR](<https://github.com/JannisX11/blockbench-plugins/pull/930>) has been merged, you can install the latest release of AJ directly from the Blockbench plugin list.
9+
It can take a while for new releases to appear in the Blockbench plugin list, so to get this one right away, either download `animated_java.js` from the [release page]({release_url}) or [install it via URL](https://animated-java.dev/docs/getting-started/installing#advanced-installation-url).
1210

1311
-# {pings}

‎.scripts/plugins/releaseNoteTemplates/github_release_notes_template‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
{categories}
44

5-
## How to Install
5+
## [How to Install](https://animated-java.dev/docs/getting-started/installing)
66

7-
Download `animated_java.js` from the [release page](https://github.com/Animated-Java/animated-java/releases/tag/v1.10.2), then [Follow this tutorial](https://animated-java.dev/docs/getting-started/installing#advanced-installation-url)
8-
9-
Once [this PR](https://github.com/JannisX11/blockbench-plugins/pull/930) has been merged, you can install the latest release of AJ directly from the Blockbench plugin list.
7+
It can take a while for new releases to appear in the Blockbench plugin list, so to get this one right away, either download `animated_java.js` below or [install it via URL](https://animated-java.dev/docs/getting-started/installing#advanced-installation-url).

‎.scripts/release/announce.ts‎

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* Posts a release announcement to a Discord channel.
3+
*
4+
* Usage: `bun .scripts/release/announce.ts <version> [--live]`
5+
*
6+
* Without `--live`, role mentions are shown but don't ping anyone, and the message isn't
7+
* crossposted. Used for previews in a private test channel.
8+
*
9+
* Env: `DISCORD_BOT_TOKEN`, `DISCORD_CHANNEL_ID`, and `DISCORD_ROLE_PRERELEASE`,
10+
* `DISCORD_ROLE_MINOR`, `DISCORD_ROLE_PATCH`, `DISCORD_ROLE_BREAKING`.
11+
*/
12+
import { readFileSync } from 'node:fs'
13+
import {
14+
type Changelog,
15+
type ReleasePing,
16+
getChangelogEntry,
17+
getReleasePings,
18+
getReleaseUrl,
19+
renderReleaseNotes,
20+
} from './releaseNotes'
21+
22+
const DISCORD_API = 'https://discord.com/api/v10'
23+
const USER_AGENT = 'DiscordBot (https://github.com/Animated-Java/animated-java, 1.0)'
24+
const TEMPLATE_PATH = './.scripts/plugins/releaseNoteTemplates/discord_release_notes_template'
25+
const CHANGELOG_PATH = './src/pluginPackage/changelog.json'
26+
const IS_COMPONENTS_V2 = 1 << 15
27+
// Shared by every text display in a Components V2 message.
28+
const MAX_TEXT_LENGTH = 4000
29+
const ACCENT_COLOR = 0x00aced
30+
const BREAKING_LABEL =
31+
'<:BreakingEmoji0:1432852678903463976><:BreakingEmoji1:1432852680404893818><:BreakingEmoji2:1432852682091270144><:BreakingEmoji3:1432852683534110790>'
32+
const ROLE_ENV_VARS: Record<ReleasePing, string> = {
33+
prerelease: 'DISCORD_ROLE_PRERELEASE',
34+
minor: 'DISCORD_ROLE_MINOR',
35+
patch: 'DISCORD_ROLE_PATCH',
36+
breaking: 'DISCORD_ROLE_BREAKING',
37+
}
38+
39+
function requireEnv(name: string) {
40+
const value = process.env[name]
41+
if (!value) throw new Error(`Missing environment variable ${name}`)
42+
return value
43+
}
44+
45+
async function discordPost(path: string, body?: unknown) {
46+
const response = await fetch(DISCORD_API + path, {
47+
method: 'POST',
48+
headers: {
49+
Authorization: `Bot ${requireEnv('DISCORD_BOT_TOKEN')}`,
50+
'Content-Type': 'application/json',
51+
'User-Agent': USER_AGENT,
52+
},
53+
body: body === undefined ? undefined : JSON.stringify(body),
54+
})
55+
if (!response.ok) {
56+
throw new Error(`POST ${path} failed: ${response.status} ${await response.text()}`)
57+
}
58+
return (await response.json()) as { id: string }
59+
}
60+
61+
async function main() {
62+
const [version, flag] = process.argv.slice(2)
63+
if (!version || (flag !== undefined && flag !== '--live')) {
64+
throw new Error('Usage: bun .scripts/release/announce.ts <version> [--live]')
65+
}
66+
const live = flag === '--live'
67+
const channelId = requireEnv('DISCORD_CHANNEL_ID')
68+
69+
const changelog = JSON.parse(readFileSync(CHANGELOG_PATH, 'utf-8')) as Changelog
70+
const entry = getChangelogEntry(changelog, version)
71+
const roleIds = getReleasePings(version, entry).map(ping => requireEnv(ROLE_ENV_VARS[ping]))
72+
const notes = renderReleaseNotes(readFileSync(TEMPLATE_PATH, 'utf-8'), version, entry, {
73+
vars: { pings: roleIds.map(id => `<@&${id}>`).join(' ') },
74+
maxLength: MAX_TEXT_LENGTH,
75+
breakingLabel: BREAKING_LABEL,
76+
})
77+
78+
const message = await discordPost(`/channels/${channelId}/messages`, {
79+
flags: IS_COMPONENTS_V2,
80+
allowed_mentions: { parse: [], roles: live ? roleIds : [] },
81+
components: [
82+
{
83+
type: 17, // Container
84+
accent_color: ACCENT_COLOR,
85+
components: [
86+
{ type: 10, content: notes }, // Text Display
87+
{
88+
type: 1, // Action Row
89+
components: [
90+
{
91+
type: 2,
92+
style: 5,
93+
label: 'View on GitHub',
94+
url: getReleaseUrl(version),
95+
},
96+
],
97+
},
98+
],
99+
},
100+
],
101+
})
102+
console.log(`Posted v${version} announcement (message ${message.id}) to channel ${channelId}`)
103+
104+
if (live) {
105+
await discordPost(`/channels/${channelId}/messages/${message.id}/crosspost`)
106+
console.log('Crossposted to following servers')
107+
}
108+
}
109+
110+
await main()

0 commit comments

Comments
 (0)