mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 20:14:34 +00:00
feat(ci): add semantic-release for dev branch with rich Discord notifications
- add .releaserc.cjs with branch-aware config (dev=prerelease, main=stable) - add scripts/send-discord-release.cjs that parses CHANGELOG.md - replace custom VERSION bumping with semantic-release - add Discord webhook URL validation - add release detection to prevent false notifications
This commit is contained in:
1 parent
338528777b
commit
0f590c80d6
9 files changed
+452
-232
No files matched your search
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Send Release Notification to Discord using Embeds
|
||||
*
|
||||
* Usage:
|
||||
* node send-discord-release.cjs <type> <webhook-url>
|
||||
*
|
||||
* Args:
|
||||
* type: 'production' or 'dev'
|
||||
* webhook-url: Discord webhook URL
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const https = require('https');
|
||||
const { URL } = require('url');
|
||||
|
||||
const releaseType = process.argv[2]; // 'production' or 'dev'
|
||||
const webhookUrl = process.argv[3];
|
||||
|
||||
if (!releaseType || !webhookUrl) {
|
||||
console.error('Usage: node send-discord-release.cjs <type> <webhook-url>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate webhook URL is Discord
|
||||
try {
|
||||
const parsed = new URL(webhookUrl);
|
||||
if (!parsed.hostname.endsWith('discord.com') || !parsed.pathname.startsWith('/api/webhooks/')) {
|
||||
console.error('[X] Invalid Discord webhook URL');
|
||||
process.exit(1);
|
||||
}
|
||||
} catch {
|
||||
console.error('[X] Invalid URL format');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract latest release from CHANGELOG.md
|
||||
*/
|
||||
function extractLatestRelease() {
|
||||
const changelogPath = 'CHANGELOG.md';
|
||||
|
||||
if (!fs.existsSync(changelogPath)) {
|
||||
return {
|
||||
version: 'Unknown',
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
sections: {},
|
||||
};
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(changelogPath, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
let version = 'Unknown';
|
||||
let date = new Date().toISOString().split('T')[0];
|
||||
let collecting = false;
|
||||
let currentSection = null;
|
||||
const sections = {};
|
||||
|
||||
for (const line of lines) {
|
||||
// Match: ## [1.0.0](url) (2025-01-01) or ## 1.0.0 (2025-01-01)
|
||||
const versionMatch = line.match(/^## \[?(\d+\.\d+\.\d+(?:-dev\.\d+)?)\]?.*?\((\d{4}-\d{2}-\d{2})\)/);
|
||||
if (versionMatch) {
|
||||
if (!collecting) {
|
||||
version = versionMatch[1];
|
||||
date = versionMatch[2];
|
||||
collecting = true;
|
||||
continue;
|
||||
} else {
|
||||
break; // Found next version, stop
|
||||
}
|
||||
}
|
||||
|
||||
if (!collecting) continue;
|
||||
|
||||
// Match section headers: ### Features, ### Bug Fixes
|
||||
const sectionMatch = line.match(/^### (.+)/);
|
||||
if (sectionMatch) {
|
||||
currentSection = sectionMatch[1];
|
||||
sections[currentSection] = [];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collect bullet points
|
||||
if (currentSection && line.trim().startsWith('*')) {
|
||||
const item = line.trim().substring(1).trim();
|
||||
if (item) {
|
||||
sections[currentSection].push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { version, date, sections };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Discord embed
|
||||
*/
|
||||
function createEmbed(release) {
|
||||
const isDev = releaseType === 'dev';
|
||||
const color = isDev ? 0xf59e0b : 0x10b981; // Orange for dev, Green for production
|
||||
const title = isDev ? `Dev Release ${release.version}` : `Release ${release.version}`;
|
||||
const url = `https://github.com/kaitranntt/ccs/releases/tag/v${release.version}`;
|
||||
|
||||
// Section name to indicator mapping (ASCII only per CLAUDE.md)
|
||||
const sectionIndicators = {
|
||||
Features: '[+]',
|
||||
'Bug Fixes': '[X]',
|
||||
Documentation: '[i]',
|
||||
Styles: '[~]',
|
||||
'Code Refactoring': '[~]',
|
||||
'Performance Improvements': '[!]',
|
||||
Tests: '[T]',
|
||||
'Build System': '[B]',
|
||||
CI: '[C]',
|
||||
};
|
||||
|
||||
const fields = [];
|
||||
|
||||
for (const [sectionName, items] of Object.entries(release.sections)) {
|
||||
if (items.length === 0) continue;
|
||||
|
||||
const indicator = sectionIndicators[sectionName] || '[*]';
|
||||
let fieldValue = items.map((item) => `• ${item}`).join('\n');
|
||||
|
||||
// Discord field value max is 1024 characters
|
||||
if (fieldValue.length > 1024) {
|
||||
const truncateAt = fieldValue.lastIndexOf('\n', 1000);
|
||||
fieldValue = fieldValue.substring(0, truncateAt > 0 ? truncateAt : 1000) + '\n... *(truncated)*';
|
||||
}
|
||||
|
||||
fields.push({
|
||||
name: `${indicator} ${sectionName}`,
|
||||
value: fieldValue,
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (fields.length === 0) {
|
||||
fields.push({
|
||||
name: '[i] Release Notes',
|
||||
value: 'Release completed. See changelog on GitHub.',
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
url,
|
||||
color,
|
||||
timestamp: new Date().toISOString(),
|
||||
footer: {
|
||||
text: isDev ? 'npm i @kaitranntt/ccs@dev' : 'npm i @kaitranntt/ccs@latest',
|
||||
},
|
||||
fields,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Send to Discord webhook
|
||||
*/
|
||||
function sendToDiscord(embed) {
|
||||
const payload = {
|
||||
username: releaseType === 'dev' ? 'CCS Dev Release' : 'CCS Release',
|
||||
embeds: [embed],
|
||||
};
|
||||
|
||||
const url = new URL(webhookUrl);
|
||||
const options = {
|
||||
hostname: url.hostname,
|
||||
path: url.pathname + url.search,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
console.log('[OK] Discord notification sent');
|
||||
} else {
|
||||
console.error(`[X] Discord webhook failed: ${res.statusCode}`);
|
||||
console.error(data);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
console.error('[X] Error sending Discord notification:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
req.write(JSON.stringify(payload));
|
||||
req.end();
|
||||
}
|
||||
|
||||
// Main
|
||||
try {
|
||||
const release = extractLatestRelease();
|
||||
console.log(`[i] Preparing ${releaseType} notification for v${release.version}`);
|
||||
|
||||
const embed = createEmbed(release);
|
||||
sendToDiscord(embed);
|
||||
} catch (error) {
|
||||
console.error('[X] Error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
/**
|
||||
* semantic-release plugin to sync VERSION file
|
||||
*
|
||||
* semantic-release updates package.json but not the VERSION file.
|
||||
* This plugin keeps VERSION in sync for shell scripts and installers.
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Called during the prepare step before git commit
|
||||
*/
|
||||
prepare(_pluginConfig, context) {
|
||||
const { nextRelease, logger } = context;
|
||||
const versionFile = path.join(process.cwd(), 'VERSION');
|
||||
|
||||
// Write version without 'v' prefix (e.g., "5.1.0" not "v5.1.0")
|
||||
const version = nextRelease.version;
|
||||
fs.writeFileSync(versionFile, version + '\n');
|
||||
logger.log('[sync-version-plugin] Updated VERSION file to %s', version);
|
||||
|
||||
// Also update installers for standalone installs
|
||||
const installSh = path.join(process.cwd(), 'installers', 'install.sh');
|
||||
const installPs1 = path.join(process.cwd(), 'installers', 'install.ps1');
|
||||
|
||||
if (fs.existsSync(installSh)) {
|
||||
let content = fs.readFileSync(installSh, 'utf8');
|
||||
content = content.replace(/^CCS_VERSION=".*"/m, `CCS_VERSION="${version}"`);
|
||||
fs.writeFileSync(installSh, content);
|
||||
logger.log('[sync-version-plugin] Updated installers/install.sh');
|
||||
}
|
||||
|
||||
if (fs.existsSync(installPs1)) {
|
||||
let content = fs.readFileSync(installPs1, 'utf8');
|
||||
content = content.replace(/^\$CcsVersion = ".*"/m, `$CcsVersion = "${version}"`);
|
||||
fs.writeFileSync(installPs1, content);
|
||||
logger.log('[sync-version-plugin] Updated installers/install.ps1');
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user