fix(ci): rework discord-release-notify

- Reorder: send Discord FIRST; commit CHANGELOG + version bump ONLY on
  a successful announce, so a failed send never consumes a version.
- Raise max_tokens for the deepseek-v4-flash reasoning model (6000/4000)
  so reasoning+content fit and content is never empty.
- Guard against empty content (don't POST an empty Discord message).
- Add workflow_dispatch (manual re-fire) with an optional base ref input.
This commit is contained in:
David Montero 2026-06-11 19:12:52 +02:00
parent 603c6b861f
commit 3fe7de2d25
1 changed files with 110 additions and 101 deletions

View File

@ -6,14 +6,23 @@ on:
- closed
branches:
- release
# Manual re-fire (e.g. to retry a failed announce without a new merge).
# Uses THIS file from the dispatched ref; checks out `release` and announces
# whatever version release's frontend/package.json holds.
workflow_dispatch:
inputs:
base:
description: 'Base git ref/SHA to diff from (commits base..HEAD are summarised). Defaults to HEAD~50.'
required: false
default: ''
jobs:
notify:
# Solo cuando el PR fue efectivamente mergeado (no cerrado sin merge)
if: github.event.pull_request.merged == true
# Run on a merged PR into release, or on a manual dispatch.
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true
runs-on: ubuntu-latest
permissions:
contents: write # necesario para pushear CHANGELOG.md
contents: write # necesario para pushear CHANGELOG.md + version bump
steps:
- name: Checkout full history
@ -28,7 +37,7 @@ jobs:
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
- name: Announce on Discord, then commit CHANGELOG + version bump on success
uses: actions/github-script@v7
env:
DAVEAGENT_API_KEY: ${{ secrets.DAVEAGENT_API_KEY }}
@ -47,7 +56,11 @@ jobs:
} catch { return ''; }
}
async function callDeepSeek(prompt, maxTokens = 800, temperature = 0.3) {
// deepseek-v4-flash is a REASONING model: max_tokens caps the
// reasoning trace + the visible answer TOGETHER, so a stingy budget
// makes `content` come back empty (which once made us POST an empty
// Discord message). Give it generous budgets.
async function callDeepSeek(prompt, maxTokens, 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', {
@ -71,59 +84,55 @@ jobs:
return (data.choices?.[0]?.message?.content ?? '').trim();
}
// ── Version ──────────────────────────────────────────────────────
// The version to announce comes from frontend/package.json on the
// release branch. After announcing we bump the PATCH and commit it
// back to release, so EACH merge announces a fresh version
// (3.0.0 -> 3.0.1 -> 3.0.2 ...) instead of repeating the same one.
// To jump the major/minor, edit frontend/package.json on the
// release branch (e.g. set "version": "3.1.0") and the next merge
// continues from there.
// ── Version (read from release's package.json) ────────────────────
let version = '3.0.0';
try {
const pkg = JSON.parse(fs.readFileSync('frontend/package.json', 'utf8'));
version = pkg.version ?? '3.0.0';
} catch {}
function bumpPatch(v) {
const m = String(v).match(/^(\d+)\.(\d+)\.(\d+)/);
if (!m) return v; // leave non-semver strings untouched
if (!m) return v;
return `${m[1]}.${m[2]}.${Number(m[3]) + 1}`;
}
const nextVersion = bumpPatch(version);
console.log(`Version: v${version} -> next: v${nextVersion}`);
// ── Rango exacto del PR mergeado ──────────────────────────────────
// base.sha = tip de release ANTES del merge
// head.sha = tip de la rama fuente (master u otra)
// ── Commit range + metadata (pull_request OR manual dispatch) ─────
let baseSha, headSha, sourceBranch, prTitle, prBody, prNumber;
if (context.eventName === 'pull_request') {
const pr = context.payload.pull_request;
const baseSha = pr.base.sha;
const headSha = pr.head.sha;
const sourceBranch = pr.head.ref; // ej: "master"
const prTitle = pr.title ?? '';
const prBody = pr.body ?? '';
console.log(`PR: #${pr.number} "${prTitle}"`);
baseSha = pr.base.sha; // release tip BEFORE the merge
headSha = pr.head.sha; // source branch tip (master)
sourceBranch = pr.head.ref;
prTitle = pr.title ?? '';
prBody = pr.body ?? '';
prNumber = pr.number;
} else {
headSha = git('rev-parse HEAD');
const inputBase = (context.payload.inputs && context.payload.inputs.base || '').trim();
baseSha = inputBase || git('rev-parse HEAD~50') || headSha;
sourceBranch = 'release';
prTitle = `Release v${version}`;
prBody = '';
prNumber = 'manual';
}
console.log(`Event: ${context.eventName} PR: #${prNumber} "${prTitle}"`);
console.log(`Range: ${baseSha.slice(0,7)}..${headSha.slice(0,7)} (${sourceBranch} → release)`);
// Commits que entraron (con hash + subject + body + autor)
const commits = git(
`log ${baseSha}..${headSha} --pretty=format:"%h||%s||%b||%an|||" --no-merges`
) || git(`log -30 --pretty=format:"%h||%s||%b||%an|||" --no-merges`);
// Lineas simples para el anuncio Discord
) || git(`log -50 --pretty=format:"%h||%s||%b||%an|||" --no-merges`);
const commitLines = git(
`log ${baseSha}..${headSha} --pretty=format:"%h %s" --no-merges`
) || git(`log -20 --pretty=format:"%h %s" --no-merges`);
) || git(`log -50 --pretty=format:"%h %s" --no-merges`);
const changedFiles = git(`diff --name-only ${baseSha}..${headSha}`)
.split('\n').filter(Boolean);
console.log(`Commits found: ${commitLines.split('\n').filter(Boolean).length}`);
console.log(`Files changed: ${changedFiles.length}`);
// ──────────────────────────────────────────────────────────────────
// FASE 1 — Generar entrada de CHANGELOG con DeepSeek
// PHASE 1 — Generate the CHANGELOG entry (in memory only).
// ──────────────────────────────────────────────────────────────────
const changelogPrompt = `You are generating a CHANGELOG entry for version ${version} of Velxio.
@ -159,71 +168,14 @@ jobs:
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\n\n'
+ 'All notable changes to Velxio will be documented in this file.\n'
+ 'The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).\n\n'
+ changelogEntry + '\n\n'
+ `[${version}]: https://github.com/davidmonterocrespo24/velxio/releases/tag/v${version}\n`;
} 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');
// ── Bump frontend/package.json to the next patch ──────────────────
// This is the fix for the "same version announced every time" bug:
// nothing ever advanced the counter. Text-replace so the file's
// existing formatting is preserved.
try {
const pkgPath = 'frontend/package.json';
const pkgRaw = fs.readFileSync(pkgPath, 'utf8');
const bumped = pkgRaw.replace(
/("version"\s*:\s*")[^"]+(")/,
`$1${nextVersion}$2`
);
fs.writeFileSync(pkgPath, bumped, 'utf8');
console.log(`Bumped frontend/package.json: ${version} -> ${nextVersion}`);
} catch (e) {
console.log('package.json bump failed:', e.message);
}
// ── Commit y push del CHANGELOG + version bump ────────────────────
try {
execSync('git add CHANGELOG.md frontend/package.json', { stdio: 'inherit' });
execSync(
`git commit -m "chore: release v${version}, bump to v${nextVersion} [skip ci]"`,
{ stdio: 'inherit' }
);
execSync('git push origin release', { stdio: 'inherit' });
console.log(`CHANGELOG + version bump committed and pushed (next: v${nextVersion})`);
} catch (e) {
console.log('Nothing to commit or push failed:', e.message);
const changelogEntry = await callDeepSeek(changelogPrompt, 6000, 0.3);
if (!changelogEntry) {
core.setFailed('CHANGELOG generation returned empty content — aborting before any side effects');
return;
}
// ──────────────────────────────────────────────────────────────────
// FASE 2 — Generar anuncio Discord usando el CHANGELOG recien generado
// PHASE 2 — Generate the Discord announcement (in memory only).
// ──────────────────────────────────────────────────────────────────
const announcementPrompt = `You are writing a Discord announcement for Velxio v${version}.
@ -280,7 +232,7 @@ jobs:
- Write in English`;
console.log('Generating Discord announcement...');
let announcement = await callDeepSeek(announcementPrompt, 600, 0.7);
let announcement = await callDeepSeek(announcementPrompt, 4000, 0.7);
// Limpiar markdown residual
announcement = announcement
@ -289,7 +241,7 @@ jobs:
.replace(/__/g, '')
.trim();
// Truncar si excede el limite
// Truncar si excede el limite de Discord
if (announcement.length > 1900) {
const cut = announcement.lastIndexOf('\n', 1900);
announcement = announcement.slice(0, cut > 1200 ? cut : 1900).trim();
@ -301,8 +253,15 @@ jobs:
console.log(announcement);
console.log(`\nCharacters: ${announcement.length}/1900`);
// Guard: never POST an empty message (Discord 400) and never commit
// a version bump for an announcement that did not go out.
if (!announcement) {
core.setFailed('Announcement generation returned empty content — NOT posting, NOT committing');
return;
}
// ──────────────────────────────────────────────────────────────────
// FASE 3 — Enviar a Discord via Bot API
// PHASE 3 — Send to Discord FIRST. No side effects before this.
// ──────────────────────────────────────────────────────────────────
const botToken = process.env.DISCORD_BOT_TOKEN;
const channelId = process.env.DISCORD_CHANNEL_ID;
@ -328,7 +287,57 @@ jobs:
core.setFailed(`Discord ${discordRes.status}: ${err}`);
return;
}
console.log('='.repeat(70));
console.log(`Release v${version} announced on Discord`);
console.log(`PR #${pr.number} | Commits: ${commitLines.split('\n').filter(Boolean).length} | Files: ${changedFiles.length}`);
// ──────────────────────────────────────────────────────────────────
// PHASE 4 — Only NOW persist: write CHANGELOG + bump the version.
// A failed announce above returns early, so it never advances the
// version counter or writes a half-baked CHANGELOG.
// ──────────────────────────────────────────────────────────────────
const changelogPath = 'CHANGELOG.md';
let changelogContent;
if (!fs.existsSync(changelogPath)) {
changelogContent = '# Changelog\n\n'
+ 'All notable changes to Velxio will be documented in this file.\n'
+ 'The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).\n\n'
+ changelogEntry + '\n\n'
+ `[${version}]: https://github.com/davidmonterocrespo24/velxio/releases/tag/v${version}\n`;
} else {
const existing = fs.readFileSync(changelogPath, 'utf8');
const lines = existing.split('\n');
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');
// Bump the PATCH so the NEXT release announces a fresh version.
try {
const pkgPath = 'frontend/package.json';
const pkgRaw = fs.readFileSync(pkgPath, 'utf8');
const bumped = pkgRaw.replace(/("version"\s*:\s*")[^"]+(")/, `$1${nextVersion}$2`);
fs.writeFileSync(pkgPath, bumped, 'utf8');
console.log(`Bumped frontend/package.json: ${version} -> ${nextVersion}`);
} catch (e) {
console.log('package.json bump failed:', e.message);
}
try {
execSync('git add CHANGELOG.md frontend/package.json', { stdio: 'inherit' });
execSync(
`git commit -m "chore: release v${version}, bump to v${nextVersion} [skip ci]"`,
{ stdio: 'inherit' }
);
execSync('git push origin release', { stdio: 'inherit' });
console.log(`CHANGELOG + version bump committed and pushed (next: v${nextVersion})`);
} catch (e) {
console.log('Nothing to commit or push failed:', e.message);
}
console.log(`PR #${prNumber} | Commits: ${commitLines.split('\n').filter(Boolean).length} | Files: ${changedFiles.length}`);