feat: add Discord release merge notification workflow
This commit is contained in:
parent
f43c9d019d
commit
4b473ade6a
|
|
@ -0,0 +1,301 @@
|
|||
name: Discord — Release Merge Notification
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- release
|
||||
|
||||
# Evitar loop infinito: el bot hace push del CHANGELOG, lo que volveria a disparar este workflow.
|
||||
# Con este condicional saltamos si el pusher es el propio bot.
|
||||
jobs:
|
||||
notify:
|
||||
if: github.actor != 'github-actions[bot]'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # necesario para pushear CHANGELOG.md
|
||||
|
||||
steps:
|
||||
- name: Checkout full history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Configure git identity
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Generate CHANGELOG entry, commit, then send Discord announcement
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
DAVEAGENT_API_KEY: ${{ secrets.DAVEAGENT_API_KEY }}
|
||||
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }}
|
||||
DISCORD_CHANNEL_ID: ${{ secrets.DISCORD_CHANNEL_ID }}
|
||||
with:
|
||||
script: |
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────
|
||||
function git(cmd) {
|
||||
try {
|
||||
return execSync(`git ${cmd}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
||||
} catch { return ''; }
|
||||
}
|
||||
|
||||
async function callDeepSeek(prompt, maxTokens = 800, temperature = 0.3) {
|
||||
const apiKey = process.env.DAVEAGENT_API_KEY;
|
||||
if (!apiKey) throw new Error('DAVEAGENT_API_KEY not set');
|
||||
const res = await fetch('https://api.deepseek.com/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'deepseek-chat',
|
||||
max_tokens: maxTokens,
|
||||
temperature,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
throw new Error(`DeepSeek ${res.status}: ${err}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
return (data.choices?.[0]?.message?.content ?? '').trim();
|
||||
}
|
||||
|
||||
// ── Version ──────────────────────────────────────────────────────
|
||||
let version = '2.0.0';
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync('frontend/package.json', 'utf8'));
|
||||
version = pkg.version ?? '2.0.0';
|
||||
} catch {}
|
||||
console.log(`Version: v${version}`);
|
||||
|
||||
// ── Rango del merge ───────────────────────────────────────────────
|
||||
const sha = context.sha;
|
||||
const parents = git(`cat-file -p ${sha}`)
|
||||
.split('\n').filter(l => l.startsWith('parent'));
|
||||
const isMerge = parents.length >= 2;
|
||||
const baseRef = isMerge ? `${sha}^1` : `${sha}^`;
|
||||
const headRef = isMerge ? `${sha}^2` : sha;
|
||||
|
||||
// Commits que entraron (con hash + subject + body + autor)
|
||||
const rawLog = git(
|
||||
`log ${baseRef}..${headRef} --pretty=format:"%h||%s||%b||%an|||" --no-merges`
|
||||
);
|
||||
const commits = rawLog || git(
|
||||
`log -30 --pretty=format:"%h||%s||%b||%an|||" --no-merges`
|
||||
);
|
||||
|
||||
// Lineas simples para el anuncio Discord
|
||||
const commitLines = git(
|
||||
`log ${baseRef}..${headRef} --pretty=format:"%h %s" --no-merges`
|
||||
) || git(`log -20 --pretty=format:"%h %s" --no-merges`);
|
||||
|
||||
const changedFiles = git(`diff --name-only ${baseRef}..${headRef}`)
|
||||
.split('\n').filter(Boolean);
|
||||
const diffSummary = git(`diff --stat ${baseRef}..${headRef}`)
|
||||
.split('\n').filter(Boolean).pop() ?? '';
|
||||
|
||||
console.log(`Commits found: ${commitLines.split('\n').filter(Boolean).length}`);
|
||||
console.log(`Files changed: ${changedFiles.length}`);
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// FASE 1 — Generar entrada de CHANGELOG con DeepSeek
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
const changelogPrompt = `You are generating a CHANGELOG entry for version ${version} of Velxio.
|
||||
|
||||
Velxio is a fully local, open-source Arduino/RP2040 emulator and circuit simulator.
|
||||
|
||||
GIT COMMITS (hash||subject||body||author):
|
||||
${commits}
|
||||
|
||||
Generate a changelog entry using EXACTLY this format:
|
||||
|
||||
## [${version}] - ${today}
|
||||
|
||||
### Added
|
||||
- New user-facing features
|
||||
|
||||
### Changed
|
||||
- Changes in existing functionality
|
||||
|
||||
### Fixed
|
||||
- Bug fixes
|
||||
|
||||
RULES:
|
||||
1. Group commits by category (Added, Changed, Fixed, Performance, Removed, Security)
|
||||
2. Write in past tense, user-facing language
|
||||
3. Skip trivial commits (formatting, typos, version bumps)
|
||||
4. Combine related commits into single entries
|
||||
5. Remove commit hashes and author names
|
||||
6. Only include sections that have real content
|
||||
7. Output ONLY the changelog entry block, nothing else`;
|
||||
|
||||
console.log('Generating CHANGELOG entry...');
|
||||
const changelogEntry = await callDeepSeek(changelogPrompt, 800, 0.3);
|
||||
|
||||
// ── Actualizar CHANGELOG.md ───────────────────────────────────────
|
||||
const changelogPath = 'CHANGELOG.md';
|
||||
let changelogContent;
|
||||
|
||||
if (!fs.existsSync(changelogPath)) {
|
||||
changelogContent = `# Changelog
|
||||
|
||||
All notable changes to Velxio will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
${changelogEntry}
|
||||
|
||||
[${version}]: https://github.com/davidmonterocrespo24/velxio/releases/tag/v${version}
|
||||
`;
|
||||
} else {
|
||||
const existing = fs.readFileSync(changelogPath, 'utf8');
|
||||
const lines = existing.split('\n');
|
||||
|
||||
// Insertar antes del primer ## [ o despues del header
|
||||
let insertIdx = lines.findIndex(l => l.startsWith('## ['));
|
||||
if (insertIdx === -1) {
|
||||
insertIdx = lines.findIndex((l, i) => i > 3 && l.trim() === '') + 1;
|
||||
}
|
||||
|
||||
lines.splice(insertIdx, 0, changelogEntry, '');
|
||||
|
||||
const linkLine = `[${version}]: https://github.com/davidmonterocrespo24/velxio/releases/tag/v${version}`;
|
||||
if (!existing.includes(linkLine)) lines.push(linkLine);
|
||||
|
||||
changelogContent = lines.join('\n');
|
||||
}
|
||||
|
||||
fs.writeFileSync(changelogPath, changelogContent, 'utf8');
|
||||
console.log('CHANGELOG.md updated');
|
||||
|
||||
// ── Commit y push del CHANGELOG ───────────────────────────────────
|
||||
try {
|
||||
execSync('git add CHANGELOG.md', { stdio: 'inherit' });
|
||||
execSync(
|
||||
`git commit -m "docs: update CHANGELOG for v${version} [skip ci]"`,
|
||||
{ stdio: 'inherit' }
|
||||
);
|
||||
execSync('git push origin release', { stdio: 'inherit' });
|
||||
console.log('CHANGELOG.md committed and pushed');
|
||||
} catch (e) {
|
||||
console.log('Nothing to commit or push failed:', e.message);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// FASE 2 — Generar anuncio Discord usando el CHANGELOG recien generado
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
const announcementPrompt = `You are writing a Discord announcement for Velxio v${version}.
|
||||
|
||||
Velxio is a fully local, open-source Arduino/RP2040 emulator and circuit simulator that runs in the browser.
|
||||
Website: https://velxio.dev/
|
||||
GitHub: https://github.com/davidmonterocrespo24/velxio
|
||||
|
||||
CRITICAL: The announcement MUST be 1900 characters or less.
|
||||
|
||||
CHANGELOG for this version (use this as the source of truth):
|
||||
${changelogEntry}
|
||||
|
||||
RECENT COMMITS (for additional context):
|
||||
${commitLines}
|
||||
|
||||
STYLE EXAMPLES:
|
||||
|
||||
EXAMPLE 1:
|
||||
v2.0.0 @everyone
|
||||
|
||||
Velxio just got a major upgrade. Here is what is new.
|
||||
|
||||
GROUND CHECK FOR LEDS
|
||||
LEDs now require a proper cathode connection to GND. No more phantom lights without a complete circuit.
|
||||
|
||||
GENERIC OUTPUT COMPONENT PROTECTION
|
||||
Any output component connected without a ground wire stays off. The simulator now enforces real circuit behavior for all components.
|
||||
|
||||
SSD1306 SPI MODE
|
||||
The SSD1306 OLED display now supports both I2C and SPI. Switch protocols from the component property dialog.
|
||||
|
||||
Try it now: https://velxio.dev/
|
||||
Full release details: https://github.com/davidmonterocrespo24/velxio/releases/tag/v2.0.0
|
||||
|
||||
EXAMPLE 2:
|
||||
v2.0.1 @everyone
|
||||
|
||||
Quick fixes in this update:
|
||||
|
||||
Fixed LED staying on after simulation stops
|
||||
Fixed wire color not persisting on reload
|
||||
Fixed serial monitor scroll position resetting mid-output
|
||||
|
||||
Update your Docker image or open https://velxio.dev/
|
||||
|
||||
STYLE RULES:
|
||||
- Start with version number and @everyone on the first line
|
||||
- No emojis
|
||||
- No markdown (no **, no ###)
|
||||
- Use ALL CAPS for section headers when grouping multiple features
|
||||
- Focus on USER BENEFITS, not implementation details
|
||||
- Under 1900 characters
|
||||
- End with link to GitHub release
|
||||
- Write in English`;
|
||||
|
||||
console.log('Generating Discord announcement...');
|
||||
let announcement = await callDeepSeek(announcementPrompt, 600, 0.7);
|
||||
|
||||
// Limpiar markdown residual
|
||||
announcement = announcement
|
||||
.replace(/###/g, '')
|
||||
.replace(/\*\*/g, '')
|
||||
.replace(/__/g, '')
|
||||
.trim();
|
||||
|
||||
// Truncar si excede el limite
|
||||
if (announcement.length > 1900) {
|
||||
const cut = announcement.lastIndexOf('\n', 1900);
|
||||
announcement = announcement.slice(0, cut > 1200 ? cut : 1900).trim();
|
||||
}
|
||||
|
||||
console.log('='.repeat(70));
|
||||
console.log('DISCORD ANNOUNCEMENT:');
|
||||
console.log('='.repeat(70));
|
||||
console.log(announcement);
|
||||
console.log(`\nCharacters: ${announcement.length}/1900`);
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// FASE 3 — Enviar a Discord via Bot API
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
const botToken = process.env.DISCORD_BOT_TOKEN;
|
||||
const channelId = process.env.DISCORD_CHANNEL_ID;
|
||||
if (!botToken || !channelId) {
|
||||
core.setFailed('DISCORD_BOT_TOKEN or DISCORD_CHANNEL_ID secret not configured');
|
||||
return;
|
||||
}
|
||||
|
||||
const discordRes = await fetch(
|
||||
`https://discord.com/api/v10/channels/${channelId}/messages`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bot ${botToken}`,
|
||||
},
|
||||
body: JSON.stringify({ content: announcement }),
|
||||
}
|
||||
);
|
||||
|
||||
if (!discordRes.ok) {
|
||||
const err = await discordRes.text();
|
||||
core.setFailed(`Discord ${discordRes.status}: ${err}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('='.repeat(70));
|
||||
console.log(`Release v${version} announced on Discord`);
|
||||
console.log(`Commits: ${commitLines.split('\n').filter(Boolean).length} | Files: ${changedFiles.length}`);
|
||||
Loading…
Reference in New Issue