Skip to content

Commit 6bfc250

Browse files
authored
Merge pull request #386 from lane711/ai-release-notes-issue
feat: add AI-powered release announcement system
2 parents a3f01cf + 875ef28 commit 6bfc250

8 files changed

Lines changed: 1095 additions & 5 deletions

File tree

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
name: Release Announcements
2+
3+
on:
4+
release:
5+
types: [published]
6+
workflow_dispatch:
7+
inputs:
8+
dry_run:
9+
description: 'Dry run (no actual posts)'
10+
required: false
11+
default: 'false'
12+
type: boolean
13+
skip_discord:
14+
description: 'Skip Discord posting'
15+
required: false
16+
default: false
17+
type: boolean
18+
skip_twitter:
19+
description: 'Skip Twitter posting'
20+
required: false
21+
default: false
22+
type: boolean
23+
skip_www:
24+
description: 'Skip WWW update'
25+
required: false
26+
default: false
27+
type: boolean
28+
29+
jobs:
30+
announce:
31+
name: Announce Release
32+
runs-on: ubuntu-latest
33+
34+
steps:
35+
- name: Checkout repository
36+
uses: actions/checkout@v4
37+
38+
- name: Setup Node.js
39+
uses: actions/setup-node@v4
40+
with:
41+
node-version: '20'
42+
cache: 'npm'
43+
44+
- name: Install dependencies
45+
run: npm ci
46+
47+
- name: Run Release Announcements
48+
env:
49+
# Discord
50+
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
51+
52+
# Twitter/X
53+
TWITTER_API_KEY: ${{ secrets.TWITTER_API_KEY }}
54+
TWITTER_API_SECRET: ${{ secrets.TWITTER_API_SECRET }}
55+
TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }}
56+
TWITTER_ACCESS_SECRET: ${{ secrets.TWITTER_ACCESS_SECRET }}
57+
58+
# WWW Repository
59+
WWW_REPO_TOKEN: ${{ secrets.WWW_REPO_TOKEN }}
60+
WWW_REPO_OWNER: lane711
61+
WWW_REPO_NAME: sonicjs-www
62+
63+
# Release Information (from GitHub release event)
64+
RELEASE_TAG: ${{ github.event.release.tag_name }}
65+
RELEASE_BODY: ${{ github.event.release.body }}
66+
RELEASE_URL: ${{ github.event.release.html_url }}
67+
RELEASE_PUBLISHED_AT: ${{ github.event.release.published_at }}
68+
69+
# Options
70+
DRY_RUN: ${{ inputs.dry_run || 'false' }}
71+
run: |
72+
ARGS=""
73+
if [ "${{ inputs.skip_discord }}" = "true" ]; then
74+
ARGS="$ARGS --skip-discord"
75+
fi
76+
if [ "${{ inputs.skip_twitter }}" = "true" ]; then
77+
ARGS="$ARGS --skip-twitter"
78+
fi
79+
if [ "${{ inputs.skip_www }}" = "true" ]; then
80+
ARGS="$ARGS --skip-www"
81+
fi
82+
node scripts/release/index.js $ARGS
83+
84+
- name: Summary
85+
if: always()
86+
run: |
87+
echo "## Release Announcement Summary" >> $GITHUB_STEP_SUMMARY
88+
echo "" >> $GITHUB_STEP_SUMMARY
89+
echo "**Version:** ${{ github.event.release.tag_name || 'Manual trigger' }}" >> $GITHUB_STEP_SUMMARY
90+
echo "" >> $GITHUB_STEP_SUMMARY
91+
echo "### Platforms" >> $GITHUB_STEP_SUMMARY
92+
echo "- Discord: ${{ inputs.skip_discord == 'true' && '⏭️ Skipped' || '✅ Attempted' }}" >> $GITHUB_STEP_SUMMARY
93+
echo "- Twitter: ${{ inputs.skip_twitter == 'true' && '⏭️ Skipped' || '✅ Attempted' }}" >> $GITHUB_STEP_SUMMARY
94+
echo "- WWW: ${{ inputs.skip_www == 'true' && '⏭️ Skipped' || '✅ Attempted' }}" >> $GITHUB_STEP_SUMMARY
95+
if [ "${{ inputs.dry_run }}" = "true" ]; then
96+
echo "" >> $GITHUB_STEP_SUMMARY
97+
echo "⚠️ **This was a dry run - no actual posts were made**" >> $GITHUB_STEP_SUMMARY
98+
fi

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ Thumbs.db
6060
# Temporary files
6161
*.tmp
6262
*.temp
63+
.release-content.json
6364

6465
# Vitest
6566
coverage/

package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"name": "sonicjs-ai",
33
"version": "2.3.4",
44
"private": true,
5+
"type": "module",
56
"workspaces": [
67
"packages/*",
78
"www",
@@ -32,9 +33,11 @@
3233
"publish:core": "npm run build:core && npm publish --workspace=@sonicjs-cms/core",
3334
"publish:create-app": "npm publish --workspace=create-sonicjs",
3435
"publish:all": "npm run publish:core && npm run publish:create-app",
35-
"release:patch": "npm run version:patch && npm run publish:all && node scripts/notify-discord.js",
36-
"release:minor": "npm run version:minor && npm run publish:all && node scripts/notify-discord.js",
37-
"release:major": "npm run version:major && npm run publish:all && node scripts/notify-discord.js",
36+
"release:patch": "npm run version:patch && npm run publish:all && npm run release:announce",
37+
"release:minor": "npm run version:minor && npm run publish:all && npm run release:announce",
38+
"release:major": "npm run version:major && npm run publish:all && npm run release:announce",
39+
"release:announce": "node scripts/release/index.js",
40+
"release:announce:dry": "node scripts/release/index.js --dry-run",
3841
"notify:discord": "node scripts/notify-discord.js",
3942
"db:reset": "npm run setup:db --workspace=my-sonicjs-app",
4043
"workspace": "npm install && npm run db:reset",

scripts/notify-discord.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
#!/usr/bin/env node
22

33
/**
4-
* Discord release notification script
4+
* Discord release notification script (Legacy)
55
*
6-
* Posts a release notification to Discord when a new version is published.
6+
* Posts a basic release notification to Discord when a new version is published.
77
* Requires DISCORD_WEBHOOK_URL environment variable to be set.
8+
*
9+
* NOTE: This script is kept for backwards compatibility.
10+
* For AI-enhanced announcements (Discord + Twitter + WWW), use:
11+
* npm run release:announce
12+
*
13+
* @deprecated Use `npm run release:announce` for full release announcements
814
*/
915

1016
import fs from 'fs'
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Content Handling for Release Announcements
5+
*
6+
* Content is generated by Claude Code before running this script.
7+
* This module handles reading content from environment variables or files.
8+
*/
9+
10+
/**
11+
* @typedef {Object} ReleaseInfo
12+
* @property {string} version - Release version (e.g., "2.3.4")
13+
* @property {string} body - Raw release notes markdown
14+
* @property {string} tagName - Git tag name
15+
* @property {string} url - GitHub release URL
16+
* @property {string} publishedAt - ISO date string
17+
*/
18+
19+
/**
20+
* @typedef {Object} GeneratedContent
21+
* @property {Object} discord - Discord-specific content
22+
* @property {string} discord.title - Embed title
23+
* @property {string} discord.description - Embed description
24+
* @property {string[]} discord.highlights - Key highlights as bullet points
25+
* @property {Object} twitter - Twitter-specific content
26+
* @property {string} twitter.text - Tweet text (max 280 chars)
27+
* @property {string[]} twitter.hashtags - Hashtags to append
28+
* @property {Object} www - Website content
29+
* @property {string} www.homeChangelog - Summary for home page
30+
* @property {string} www.fullChangelog - Full changelog entry markdown
31+
*/
32+
33+
/**
34+
* Get content from environment variable (set by Claude Code)
35+
* @param {ReleaseInfo} releaseInfo - Release information
36+
* @param {Object} options - Options
37+
* @param {boolean} options.dryRun - If true, return mock content
38+
* @returns {Promise<GeneratedContent>}
39+
*/
40+
export async function generateContent(releaseInfo, options = {}) {
41+
if (options.dryRun) {
42+
console.log('🔵 [DRY RUN] Using mock content')
43+
return getMockContent(releaseInfo)
44+
}
45+
46+
// Try to read content from environment variable (JSON)
47+
const contentJson = process.env.RELEASE_CONTENT
48+
if (contentJson) {
49+
try {
50+
const content = JSON.parse(contentJson)
51+
console.log('✅ Using provided release content')
52+
return content
53+
} catch (error) {
54+
console.error('❌ Failed to parse RELEASE_CONTENT:', error.message)
55+
}
56+
}
57+
58+
// Try to read from file
59+
const fs = await import('fs')
60+
const path = await import('path')
61+
const { fileURLToPath } = await import('url')
62+
63+
const __filename = fileURLToPath(import.meta.url)
64+
const __dirname = path.dirname(__filename)
65+
const contentFilePath = path.join(__dirname, '../../.release-content.json')
66+
67+
if (fs.existsSync(contentFilePath)) {
68+
try {
69+
const content = JSON.parse(fs.readFileSync(contentFilePath, 'utf8'))
70+
console.log('✅ Using content from .release-content.json')
71+
// Clean up the file after reading
72+
fs.unlinkSync(contentFilePath)
73+
return content
74+
} catch (error) {
75+
console.error('❌ Failed to read .release-content.json:', error.message)
76+
}
77+
}
78+
79+
// Fall back to template content
80+
console.log('ℹ️ No pre-generated content found, using template')
81+
return getFallbackContent(releaseInfo)
82+
}
83+
84+
/**
85+
* Get mock content for dry runs
86+
* @param {ReleaseInfo} releaseInfo
87+
* @returns {GeneratedContent}
88+
*/
89+
function getMockContent(releaseInfo) {
90+
return {
91+
discord: {
92+
title: `🚀 SonicJS v${releaseInfo.version} Released! [DRY RUN]`,
93+
description: 'This is a dry run - no actual content was generated.',
94+
highlights: [
95+
'[DRY RUN] Highlight 1',
96+
'[DRY RUN] Highlight 2',
97+
'[DRY RUN] Highlight 3'
98+
]
99+
},
100+
twitter: {
101+
text: `[DRY RUN] SonicJS v${releaseInfo.version} is here! Check out the latest release.`,
102+
hashtags: ['SonicJS', 'DryRun']
103+
},
104+
www: {
105+
homeChangelog: `[DRY RUN] v${releaseInfo.version} - Test release`,
106+
fullChangelog: `## v${releaseInfo.version} [DRY RUN]\n\n_This is a dry run test._\n\n### Changes\n- Test change 1\n- Test change 2`
107+
}
108+
}
109+
}
110+
111+
/**
112+
* Get fallback content when no pre-generated content is available
113+
* @param {ReleaseInfo} releaseInfo
114+
* @returns {GeneratedContent}
115+
*/
116+
function getFallbackContent(releaseInfo) {
117+
const version = releaseInfo.version
118+
const body = releaseInfo.body || 'Bug fixes and improvements.'
119+
120+
// Extract first few lines for highlights
121+
const lines = body.split('\n').filter(line => line.trim().startsWith('-') || line.trim().startsWith('*'))
122+
const highlights = lines.slice(0, 3).map(line => line.replace(/^[-*]\s*/, '').trim())
123+
124+
return {
125+
discord: {
126+
title: `🚀 SonicJS v${version} Released!`,
127+
description: `A new version of SonicJS has been published with improvements and fixes.`,
128+
highlights: highlights.length > 0 ? highlights : ['Various improvements and bug fixes']
129+
},
130+
twitter: {
131+
text: `SonicJS v${version} is now available! The open-source headless CMS for Cloudflare Workers just got better.`,
132+
hashtags: ['SonicJS', 'CloudflareWorkers', 'HeadlessCMS', 'OpenSource']
133+
},
134+
www: {
135+
homeChangelog: `v${version} - ${new Date().toISOString().split('T')[0]}`,
136+
fullChangelog: `## v${version} - ${new Date().toISOString().split('T')[0]}\n\n${body}`
137+
}
138+
}
139+
}
140+
141+
/**
142+
* Read release info from environment or package.json
143+
* @returns {Promise<ReleaseInfo>}
144+
*/
145+
export async function getReleaseInfo() {
146+
// Try to get from environment (GitHub Actions)
147+
if (process.env.RELEASE_TAG && process.env.RELEASE_BODY) {
148+
return {
149+
version: process.env.RELEASE_TAG.replace(/^v/, ''),
150+
tagName: process.env.RELEASE_TAG,
151+
body: process.env.RELEASE_BODY,
152+
url: process.env.RELEASE_URL || `https://github.com/lane711/sonicjs/releases/tag/${process.env.RELEASE_TAG}`,
153+
publishedAt: process.env.RELEASE_PUBLISHED_AT || new Date().toISOString()
154+
}
155+
}
156+
157+
// Fall back to reading from package.json
158+
const fs = await import('fs')
159+
const path = await import('path')
160+
const { fileURLToPath } = await import('url')
161+
162+
const __filename = fileURLToPath(import.meta.url)
163+
const __dirname = path.dirname(__filename)
164+
const rootDir = path.join(__dirname, '../..')
165+
const corePackageJsonPath = path.join(rootDir, 'packages/core/package.json')
166+
167+
const corePackageJson = JSON.parse(fs.readFileSync(corePackageJsonPath, 'utf8'))
168+
const version = corePackageJson.version
169+
170+
return {
171+
version,
172+
tagName: `v${version}`,
173+
body: process.env.RELEASE_BODY || '',
174+
url: `https://github.com/lane711/sonicjs/releases/tag/v${version}`,
175+
publishedAt: new Date().toISOString()
176+
}
177+
}

0 commit comments

Comments
 (0)