Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions .github/workflows/release-announce.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: Release Announcements

on:
release:
types: [published]
workflow_dispatch:
inputs:
dry_run:
description: 'Dry run (no actual posts)'
required: false
default: 'false'
type: boolean
skip_discord:
description: 'Skip Discord posting'
required: false
default: false
type: boolean
skip_twitter:
description: 'Skip Twitter posting'
required: false
default: false
type: boolean
skip_www:
description: 'Skip WWW update'
required: false
default: false
type: boolean

jobs:
announce:
name: Announce Release
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Run Release Announcements
env:
# Discord
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}

# Twitter/X
TWITTER_API_KEY: ${{ secrets.TWITTER_API_KEY }}
TWITTER_API_SECRET: ${{ secrets.TWITTER_API_SECRET }}
TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }}
TWITTER_ACCESS_SECRET: ${{ secrets.TWITTER_ACCESS_SECRET }}

# WWW Repository
WWW_REPO_TOKEN: ${{ secrets.WWW_REPO_TOKEN }}
WWW_REPO_OWNER: lane711
WWW_REPO_NAME: sonicjs-www

# Release Information (from GitHub release event)
RELEASE_TAG: ${{ github.event.release.tag_name }}
RELEASE_BODY: ${{ github.event.release.body }}
RELEASE_URL: ${{ github.event.release.html_url }}
RELEASE_PUBLISHED_AT: ${{ github.event.release.published_at }}

# Options
DRY_RUN: ${{ inputs.dry_run || 'false' }}
run: |
ARGS=""
if [ "${{ inputs.skip_discord }}" = "true" ]; then
ARGS="$ARGS --skip-discord"
fi
if [ "${{ inputs.skip_twitter }}" = "true" ]; then
ARGS="$ARGS --skip-twitter"
fi
if [ "${{ inputs.skip_www }}" = "true" ]; then
ARGS="$ARGS --skip-www"
fi
node scripts/release/index.js $ARGS

- name: Summary
if: always()
run: |
echo "## Release Announcement Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Version:** ${{ github.event.release.tag_name || 'Manual trigger' }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Platforms" >> $GITHUB_STEP_SUMMARY
echo "- Discord: ${{ inputs.skip_discord == 'true' && '⏭️ Skipped' || '✅ Attempted' }}" >> $GITHUB_STEP_SUMMARY
echo "- Twitter: ${{ inputs.skip_twitter == 'true' && '⏭️ Skipped' || '✅ Attempted' }}" >> $GITHUB_STEP_SUMMARY
echo "- WWW: ${{ inputs.skip_www == 'true' && '⏭️ Skipped' || '✅ Attempted' }}" >> $GITHUB_STEP_SUMMARY
if [ "${{ inputs.dry_run }}" = "true" ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "⚠️ **This was a dry run - no actual posts were made**" >> $GITHUB_STEP_SUMMARY
fi
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Thumbs.db
# Temporary files
*.tmp
*.temp
.release-content.json

# Vitest
coverage/
Expand Down
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "sonicjs-ai",
"version": "2.3.4",
"private": true,
"type": "module",
"workspaces": [
"packages/*",
"www",
Expand Down Expand Up @@ -31,9 +32,11 @@
"publish:core": "npm run build:core && npm publish --workspace=@sonicjs-cms/core",
"publish:create-app": "npm publish --workspace=create-sonicjs",
"publish:all": "npm run publish:core && npm run publish:create-app",
"release:patch": "npm run version:patch && npm run publish:all && node scripts/notify-discord.js",
"release:minor": "npm run version:minor && npm run publish:all && node scripts/notify-discord.js",
"release:major": "npm run version:major && npm run publish:all && node scripts/notify-discord.js",
"release:patch": "npm run version:patch && npm run publish:all && npm run release:announce",
"release:minor": "npm run version:minor && npm run publish:all && npm run release:announce",
"release:major": "npm run version:major && npm run publish:all && npm run release:announce",
"release:announce": "node scripts/release/index.js",
"release:announce:dry": "node scripts/release/index.js --dry-run",
"notify:discord": "node scripts/notify-discord.js",
"db:reset": "npm run setup:db --workspace=my-sonicjs-app"
},
Expand Down
10 changes: 8 additions & 2 deletions scripts/notify-discord.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
#!/usr/bin/env node

/**
* Discord release notification script
* Discord release notification script (Legacy)
*
* Posts a release notification to Discord when a new version is published.
* Posts a basic release notification to Discord when a new version is published.
* Requires DISCORD_WEBHOOK_URL environment variable to be set.
*
* NOTE: This script is kept for backwards compatibility.
* For AI-enhanced announcements (Discord + Twitter + WWW), use:
* npm run release:announce
*
* @deprecated Use `npm run release:announce` for full release announcements
*/

import fs from 'fs'
Expand Down
177 changes: 177 additions & 0 deletions scripts/release/generate-content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
#!/usr/bin/env node

/**
* Content Handling for Release Announcements
*
* Content is generated by Claude Code before running this script.
* This module handles reading content from environment variables or files.
*/

/**
* @typedef {Object} ReleaseInfo
* @property {string} version - Release version (e.g., "2.3.4")
* @property {string} body - Raw release notes markdown
* @property {string} tagName - Git tag name
* @property {string} url - GitHub release URL
* @property {string} publishedAt - ISO date string
*/

/**
* @typedef {Object} GeneratedContent
* @property {Object} discord - Discord-specific content
* @property {string} discord.title - Embed title
* @property {string} discord.description - Embed description
* @property {string[]} discord.highlights - Key highlights as bullet points
* @property {Object} twitter - Twitter-specific content
* @property {string} twitter.text - Tweet text (max 280 chars)
* @property {string[]} twitter.hashtags - Hashtags to append
* @property {Object} www - Website content
* @property {string} www.homeChangelog - Summary for home page
* @property {string} www.fullChangelog - Full changelog entry markdown
*/

/**
* Get content from environment variable (set by Claude Code)
* @param {ReleaseInfo} releaseInfo - Release information
* @param {Object} options - Options
* @param {boolean} options.dryRun - If true, return mock content
* @returns {Promise<GeneratedContent>}
*/
export async function generateContent(releaseInfo, options = {}) {
if (options.dryRun) {
console.log('🔵 [DRY RUN] Using mock content')
return getMockContent(releaseInfo)
}

// Try to read content from environment variable (JSON)
const contentJson = process.env.RELEASE_CONTENT
if (contentJson) {
try {
const content = JSON.parse(contentJson)
console.log('✅ Using provided release content')
return content
} catch (error) {
console.error('❌ Failed to parse RELEASE_CONTENT:', error.message)
}
}

// Try to read from file
const fs = await import('fs')
const path = await import('path')
const { fileURLToPath } = await import('url')

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const contentFilePath = path.join(__dirname, '../../.release-content.json')

if (fs.existsSync(contentFilePath)) {
try {
const content = JSON.parse(fs.readFileSync(contentFilePath, 'utf8'))
console.log('✅ Using content from .release-content.json')
// Clean up the file after reading
fs.unlinkSync(contentFilePath)
return content
} catch (error) {
console.error('❌ Failed to read .release-content.json:', error.message)
}
}

// Fall back to template content
console.log('ℹ️ No pre-generated content found, using template')
return getFallbackContent(releaseInfo)
}

/**
* Get mock content for dry runs
* @param {ReleaseInfo} releaseInfo
* @returns {GeneratedContent}
*/
function getMockContent(releaseInfo) {
return {
discord: {
title: `🚀 SonicJS v${releaseInfo.version} Released! [DRY RUN]`,
description: 'This is a dry run - no actual content was generated.',
highlights: [
'[DRY RUN] Highlight 1',
'[DRY RUN] Highlight 2',
'[DRY RUN] Highlight 3'
]
},
twitter: {
text: `[DRY RUN] SonicJS v${releaseInfo.version} is here! Check out the latest release.`,
hashtags: ['SonicJS', 'DryRun']
},
www: {
homeChangelog: `[DRY RUN] v${releaseInfo.version} - Test release`,
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`
}
}
}

/**
* Get fallback content when no pre-generated content is available
* @param {ReleaseInfo} releaseInfo
* @returns {GeneratedContent}
*/
function getFallbackContent(releaseInfo) {
const version = releaseInfo.version
const body = releaseInfo.body || 'Bug fixes and improvements.'

// Extract first few lines for highlights
const lines = body.split('\n').filter(line => line.trim().startsWith('-') || line.trim().startsWith('*'))
const highlights = lines.slice(0, 3).map(line => line.replace(/^[-*]\s*/, '').trim())

return {
discord: {
title: `🚀 SonicJS v${version} Released!`,
description: `A new version of SonicJS has been published with improvements and fixes.`,
highlights: highlights.length > 0 ? highlights : ['Various improvements and bug fixes']
},
twitter: {
text: `SonicJS v${version} is now available! The open-source headless CMS for Cloudflare Workers just got better.`,
hashtags: ['SonicJS', 'CloudflareWorkers', 'HeadlessCMS', 'OpenSource']
},
www: {
homeChangelog: `v${version} - ${new Date().toISOString().split('T')[0]}`,
fullChangelog: `## v${version} - ${new Date().toISOString().split('T')[0]}\n\n${body}`
}
}
}

/**
* Read release info from environment or package.json
* @returns {Promise<ReleaseInfo>}
*/
export async function getReleaseInfo() {
// Try to get from environment (GitHub Actions)
if (process.env.RELEASE_TAG && process.env.RELEASE_BODY) {
return {
version: process.env.RELEASE_TAG.replace(/^v/, ''),
tagName: process.env.RELEASE_TAG,
body: process.env.RELEASE_BODY,
url: process.env.RELEASE_URL || `https://github.com/lane711/sonicjs/releases/tag/${process.env.RELEASE_TAG}`,
publishedAt: process.env.RELEASE_PUBLISHED_AT || new Date().toISOString()
}
}

// Fall back to reading from package.json
const fs = await import('fs')
const path = await import('path')
const { fileURLToPath } = await import('url')

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const rootDir = path.join(__dirname, '../..')
const corePackageJsonPath = path.join(rootDir, 'packages/core/package.json')

const corePackageJson = JSON.parse(fs.readFileSync(corePackageJsonPath, 'utf8'))
const version = corePackageJson.version

return {
version,
tagName: `v${version}`,
body: process.env.RELEASE_BODY || '',
url: `https://github.com/lane711/sonicjs/releases/tag/v${version}`,
publishedAt: new Date().toISOString()
}
}
Loading
Loading