363 lines
17 KiB
YAML
363 lines
17 KiB
YAML
name: Discord — Release Merge Notification
|
|
|
|
on:
|
|
pull_request:
|
|
types:
|
|
- 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:
|
|
# 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 + version bump
|
|
|
|
steps:
|
|
- name: Checkout full history
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
token: ${{ secrets.GITHUB_TOKEN }}
|
|
ref: release
|
|
|
|
- name: Configure git identity
|
|
run: |
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
|
|
- name: Announce on Discord, then commit CHANGELOG + version bump on success
|
|
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 ''; }
|
|
}
|
|
|
|
// 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', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${apiKey}`,
|
|
},
|
|
body: JSON.stringify({
|
|
model: 'deepseek-v4-flash',
|
|
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 (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;
|
|
return `${m[1]}.${m[2]}.${Number(m[3]) + 1}`;
|
|
}
|
|
const nextVersion = bumpPatch(version);
|
|
console.log(`Version: v${version} -> next: v${nextVersion}`);
|
|
|
|
// ── 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)`);
|
|
|
|
const commits = git(
|
|
`log ${baseSha}..${headSha} --pretty=format:"%h||%s||%b||%an|||" --no-merges`
|
|
) || 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 -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}`);
|
|
|
|
// ──────────────────────────────────────────────────────────────────
|
|
// PHASE 1 — Generate the CHANGELOG entry (in memory only).
|
|
// ──────────────────────────────────────────────────────────────────
|
|
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.
|
|
|
|
PR TITLE: ${prTitle}
|
|
PR DESCRIPTION: ${prBody}
|
|
SOURCE BRANCH: ${sourceBranch}
|
|
|
|
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, 6000, 0.3);
|
|
if (!changelogEntry) {
|
|
core.setFailed('CHANGELOG generation returned empty content — aborting before any side effects');
|
|
return;
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────
|
|
// PHASE 2 — Generate the Discord announcement (in memory only).
|
|
// ──────────────────────────────────────────────────────────────────
|
|
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, 4000, 0.7);
|
|
|
|
// Limpiar markdown residual
|
|
announcement = announcement
|
|
.replace(/###/g, '')
|
|
.replace(/\*\*/g, '')
|
|
.replace(/__/g, '')
|
|
.trim();
|
|
|
|
// 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();
|
|
}
|
|
|
|
console.log('='.repeat(70));
|
|
console.log('DISCORD ANNOUNCEMENT:');
|
|
console.log('='.repeat(70));
|
|
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;
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────
|
|
// 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;
|
|
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`);
|
|
|
|
// Create the GitHub Release + tag so the /releases/tag/v${version}
|
|
// link in the announcement actually resolves (otherwise it 404s).
|
|
// Tag the released commit; tolerate an already-existing tag.
|
|
try {
|
|
await github.rest.repos.createRelease({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
tag_name: `v${version}`,
|
|
target_commitish: headSha,
|
|
name: `v${version}`,
|
|
body: changelogEntry,
|
|
draft: false,
|
|
prerelease: false,
|
|
});
|
|
console.log(`GitHub Release v${version} created (tag at ${headSha.slice(0,7)})`);
|
|
} catch (e) {
|
|
console.log(`GitHub Release create skipped/failed (non-fatal): ${e.message}`);
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────
|
|
// 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}`);
|