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:
parent
603c6b861f
commit
3fe7de2d25
|
|
@ -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)
|
||||
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}"`);
|
||||
// ── 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;
|
||||
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}`);
|
||||
|
|
|
|||
Loading…
Reference in New Issue