diff --git a/.github/workflows/discord-release-notify.yml b/.github/workflows/discord-release-notify.yml index c025175a..129b1bad 100644 --- a/.github/workflows/discord-release-notify.yml +++ b/.github/workflows/discord-release-notify.yml @@ -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}`);