Release-Ankündigungen + öffentliche Changelog-Seite
- Gitea-Release-Webhook: Releases werden archiviert (releases-Tabelle) und neue 'published'-Releases als oranges 🚀-Embed mit Buttons angekündigt - Release-Kanal auf der Setup-Seite wählbar (leer = Feature aus) + Test-Button - /changelog: öffentliche Seite mit Tag-Chips, Pre-Release-Badge, Release-Notes - /api/releases öffentlich; Updates/Redeliveries posten nicht doppelt Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
|
|||||||
import { Routes, Route, NavLink, Navigate } from 'react-router-dom';
|
import { Routes, Route, NavLink, Navigate } from 'react-router-dom';
|
||||||
import { apiGet } from './api.js';
|
import { apiGet } from './api.js';
|
||||||
import Devlogs from './pages/Devlogs.jsx';
|
import Devlogs from './pages/Devlogs.jsx';
|
||||||
|
import Changelog from './pages/Changelog.jsx';
|
||||||
import Commits from './pages/Commits.jsx';
|
import Commits from './pages/Commits.jsx';
|
||||||
import Settings from './pages/Settings.jsx';
|
import Settings from './pages/Settings.jsx';
|
||||||
|
|
||||||
@@ -69,6 +70,7 @@ export default function App() {
|
|||||||
|
|
||||||
<nav className="nav-links">
|
<nav className="nav-links">
|
||||||
<NavLink to="/devlogs">Devlog</NavLink>
|
<NavLink to="/devlogs">Devlog</NavLink>
|
||||||
|
<NavLink to="/changelog">Changelog</NavLink>
|
||||||
{me.admin && <NavLink to="/commits">Commits</NavLink>}
|
{me.admin && <NavLink to="/commits">Commits</NavLink>}
|
||||||
{me.admin && <NavLink to="/settings">Setup</NavLink>}
|
{me.admin && <NavLink to="/settings">Setup</NavLink>}
|
||||||
</nav>
|
</nav>
|
||||||
@@ -92,6 +94,7 @@ export default function App() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Navigate to="/devlogs" replace />} />
|
<Route path="/" element={<Navigate to="/devlogs" replace />} />
|
||||||
<Route path="/devlogs" element={<Devlogs me={me} />} />
|
<Route path="/devlogs" element={<Devlogs me={me} />} />
|
||||||
|
<Route path="/changelog" element={<Changelog />} />
|
||||||
<Route path="/commits" element={<Commits me={me} />} />
|
<Route path="/commits" element={<Commits me={me} />} />
|
||||||
<Route path="/settings" element={<Settings me={me} />} />
|
<Route path="/settings" element={<Settings me={me} />} />
|
||||||
<Route path="*" element={<Navigate to="/devlogs" replace />} />
|
<Route path="*" element={<Navigate to="/devlogs" replace />} />
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { apiGet } from '../api.js';
|
||||||
|
import { Markdown } from '../markdown.jsx';
|
||||||
|
|
||||||
|
const dateFmt = new Intl.DateTimeFormat('de-DE', {
|
||||||
|
day: '2-digit', month: 'long', year: 'numeric',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function Changelog() {
|
||||||
|
const [data, setData] = useState(null);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [error, setError] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setError(false);
|
||||||
|
apiGet(`/api/releases?page=${page}`).then(setData).catch(() => setError(true));
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}, [page]);
|
||||||
|
|
||||||
|
const pages = data ? Math.ceil(data.total / data.pageSize) : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className="page-header">
|
||||||
|
<div className="page-bg-text">CHANGELOG</div>
|
||||||
|
<div className="page-header-grid" aria-hidden="true" />
|
||||||
|
<div className="page-header-content">
|
||||||
|
<div className="page-tag">// Releases</div>
|
||||||
|
<h1 className="page-title">Changelog</h1>
|
||||||
|
<p className="page-subtitle">Alle Versionen und Release-Notes.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="content">
|
||||||
|
{error && <p className="notice">Changelog konnte nicht geladen werden.</p>}
|
||||||
|
{!error && !data && <p className="notice">Lade …</p>}
|
||||||
|
{data?.total === 0 && <p className="notice">Noch keine Releases veröffentlicht.</p>}
|
||||||
|
|
||||||
|
{data?.total > 0 &&
|
||||||
|
data.items.map((r) => (
|
||||||
|
<article className="card release-card" key={`${r.repo}/${r.tag}`}>
|
||||||
|
<div className="card-meta">
|
||||||
|
<span className="release-tag">{r.tag}</span>
|
||||||
|
{r.prerelease ? <span className="chip chip-branch">Pre-Release</span> : null}
|
||||||
|
<span className="chip chip-repo">{r.repo.split('/').pop()}</span>
|
||||||
|
<span className="card-rel">{dateFmt.format(new Date(r.published_at))}</span>
|
||||||
|
</div>
|
||||||
|
{r.name && r.name !== r.tag && <h3 className="release-name">{r.name}</h3>}
|
||||||
|
<div className="card-body">
|
||||||
|
<Markdown text={r.body || '*Keine Release-Notes.*'} />
|
||||||
|
</div>
|
||||||
|
{r.url && (
|
||||||
|
<div className="card-footer">
|
||||||
|
<a href={r.url} target="_blank" rel="noreferrer">Release in Gitea ansehen →</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{pages > 1 && (
|
||||||
|
<div className="pager">
|
||||||
|
<button className="btn" disabled={page <= 1} onClick={() => setPage(page - 1)}>
|
||||||
|
← Neuere
|
||||||
|
</button>
|
||||||
|
<span className="pager-info">
|
||||||
|
{String(page).padStart(2, '0')} / {String(pages).padStart(2, '0')}
|
||||||
|
</span>
|
||||||
|
<button className="btn" disabled={page >= pages} onClick={() => setPage(page + 1)}>
|
||||||
|
Ältere →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -136,6 +136,20 @@ export default function Settings({ me }) {
|
|||||||
<button className="btn" onClick={() => sendTest('commit')}>Test senden</button>
|
<button className="btn" onClick={() => sendTest('commit')}>Test senden</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="field">
|
||||||
|
<label>Release-Kanal <span className="field-hint">Ankündigungen bei neuen Gitea-Releases — leer = aus</span></label>
|
||||||
|
<div className="field-row">
|
||||||
|
<select
|
||||||
|
value={form.release_channel_id ?? ''}
|
||||||
|
onChange={(e) => setForm({ ...form, release_channel_id: e.target.value })}
|
||||||
|
>
|
||||||
|
<option value="">— deaktiviert —</option>
|
||||||
|
{channelOptions}
|
||||||
|
</select>
|
||||||
|
<button className="btn" onClick={() => sendTest('release')}>Test senden</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Commit-Feed */}
|
{/* Commit-Feed */}
|
||||||
|
|||||||
+18
-1
@@ -238,7 +238,7 @@ body::after {
|
|||||||
transition: border-color .25s, background .25s;
|
transition: border-color .25s, background .25s;
|
||||||
}
|
}
|
||||||
.card:hover { border-color: var(--border); background: var(--bg3); }
|
.card:hover { border-color: var(--border); background: var(--bg3); }
|
||||||
.card::before {
|
.timeline .card::before {
|
||||||
/* Marker auf der Timeline */
|
/* Marker auf der Timeline */
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute; left: -1.72rem; top: 1.55rem;
|
position: absolute; left: -1.72rem; top: 1.55rem;
|
||||||
@@ -337,6 +337,23 @@ body::after {
|
|||||||
.card-footer a { color: var(--muted); border-bottom: 1px solid transparent; }
|
.card-footer a { color: var(--muted); border-bottom: 1px solid transparent; }
|
||||||
.card-footer a:hover { color: var(--neon); }
|
.card-footer a:hover { color: var(--neon); }
|
||||||
|
|
||||||
|
/* ── CHANGELOG ─────────────────────────────────────── */
|
||||||
|
.release-card { margin-bottom: 1.5rem; }
|
||||||
|
.release-tag {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: .85rem; letter-spacing: .1em;
|
||||||
|
color: var(--neon);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: rgba(245, 197, 24, .06);
|
||||||
|
padding: .12rem .6rem;
|
||||||
|
}
|
||||||
|
.release-name {
|
||||||
|
font-family: var(--display);
|
||||||
|
font-size: 1.35rem; letter-spacing: .03em;
|
||||||
|
margin: .3rem 0 .2rem;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── COMMIT-LISTE ──────────────────────────────────── */
|
/* ── COMMIT-LISTE ──────────────────────────────────── */
|
||||||
.commit-list { border: 1px solid rgba(255, 255, 255, .06); background: var(--bg2); }
|
.commit-list { border: 1px solid rgba(255, 255, 255, .06); background: var(--bg2); }
|
||||||
.commit-row {
|
.commit-row {
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// Release-Ankündigungen: großes Embed in den konfigurierten Kanal (Setup-Seite)
|
||||||
|
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js';
|
||||||
|
import { releaseChannelId } from '../runtime-settings.js';
|
||||||
|
import { config } from '../config.js';
|
||||||
|
|
||||||
|
const BRAND_ORANGE = 0xff4d00;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release ankündigen (falls ein Kanal konfiguriert ist).
|
||||||
|
* @param {import('discord.js').Client} client
|
||||||
|
* @param {{ repo: string, tag: string, name: string|null, body: string|null,
|
||||||
|
* url: string|null, prerelease: boolean }} release
|
||||||
|
* @returns {Promise<boolean>} true, wenn gepostet
|
||||||
|
*/
|
||||||
|
export async function postReleaseEmbed(client, release) {
|
||||||
|
const channelId = releaseChannelId();
|
||||||
|
if (!channelId) return false;
|
||||||
|
|
||||||
|
const channel = await client.channels.fetch(channelId);
|
||||||
|
if (!channel?.isTextBased()) {
|
||||||
|
throw new Error(`Release-Kanal ${channelId} nicht gefunden oder kein Textkanal`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = (release.body ?? '').trim();
|
||||||
|
if (body.length > 3500) body = `${body.slice(0, 3500)}\n… (gekürzt)`;
|
||||||
|
|
||||||
|
const title = release.name?.trim() || release.tag;
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(BRAND_ORANGE)
|
||||||
|
.setTitle(`🚀 ${title}${release.prerelease ? ' (Pre-Release)' : ''}`)
|
||||||
|
.setDescription(body || '*Keine Release-Notes.*')
|
||||||
|
.addFields({ name: 'Version', value: `\`${release.tag}\``, inline: true },
|
||||||
|
{ name: 'Projekt', value: release.repo, inline: true })
|
||||||
|
.setFooter({ text: 'D4RKST3R // RELEASE' })
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
const buttons = new ActionRowBuilder().addComponents(
|
||||||
|
new ButtonBuilder().setStyle(ButtonStyle.Link).setLabel('Changelog').setEmoji('📋')
|
||||||
|
.setURL(`${config.publicUrl}/changelog`)
|
||||||
|
);
|
||||||
|
if (release.url) {
|
||||||
|
buttons.addComponents(
|
||||||
|
new ButtonBuilder().setStyle(ButtonStyle.Link).setLabel('Release ansehen').setEmoji('🔗')
|
||||||
|
.setURL(release.url)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await channel.send({ embeds: [embed], components: [buttons] });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -41,6 +41,20 @@ if (!devlogCols.some((c) => c.name === 'images')) {
|
|||||||
db.exec(`ALTER TABLE devlogs ADD COLUMN images TEXT NOT NULL DEFAULT '[]'`);
|
db.exec(`ALTER TABLE devlogs ADD COLUMN images TEXT NOT NULL DEFAULT '[]'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Releases (Gitea-Release-Webhook) — für Ankündigungen + Changelog-Seite
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS releases (
|
||||||
|
repo TEXT NOT NULL,
|
||||||
|
tag TEXT NOT NULL,
|
||||||
|
name TEXT,
|
||||||
|
body TEXT,
|
||||||
|
url TEXT,
|
||||||
|
prerelease INTEGER NOT NULL DEFAULT 0,
|
||||||
|
published_at TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (repo, tag)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
// Volltextsuche (FTS5) über Devlogs — Trigger halten den Index synchron
|
// Volltextsuche (FTS5) über Devlogs — Trigger halten den Index synchron
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE VIRTUAL TABLE IF NOT EXISTS devlogs_fts USING fts5(content, message_id UNINDEXED);
|
CREATE VIRTUAL TABLE IF NOT EXISTS devlogs_fts USING fts5(content, message_id UNINDEXED);
|
||||||
@@ -170,6 +184,33 @@ export function listCommits(limit, offset) {
|
|||||||
return { items: selectCommits.all(limit, offset), total: countCommitsStmt.get().n };
|
return { items: selectCommits.all(limit, offset), total: countCommitsStmt.get().n };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const upsertRelease = db.prepare(`
|
||||||
|
INSERT INTO releases (repo, tag, name, body, url, prerelease, published_at)
|
||||||
|
VALUES (@repo, @tag, @name, @body, @url, @prerelease, @published_at)
|
||||||
|
ON CONFLICT(repo, tag) DO UPDATE SET
|
||||||
|
name = excluded.name, body = excluded.body, url = excluded.url,
|
||||||
|
prerelease = excluded.prerelease
|
||||||
|
`);
|
||||||
|
|
||||||
|
/** Release speichern/aktualisieren. Gibt true zurück, wenn neu (→ ankündigen). */
|
||||||
|
export function saveRelease(release) {
|
||||||
|
const existed = db
|
||||||
|
.prepare('SELECT 1 FROM releases WHERE repo = ? AND tag = ?')
|
||||||
|
.get(release.repo, release.tag);
|
||||||
|
upsertRelease.run(release);
|
||||||
|
return !existed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectReleases = db.prepare(`
|
||||||
|
SELECT repo, tag, name, body, url, prerelease, published_at
|
||||||
|
FROM releases ORDER BY published_at DESC LIMIT ? OFFSET ?
|
||||||
|
`);
|
||||||
|
const countReleasesStmt = db.prepare('SELECT count(*) AS n FROM releases');
|
||||||
|
|
||||||
|
export function listReleases(limit, offset) {
|
||||||
|
return { items: selectReleases.all(limit, offset), total: countReleasesStmt.get().n };
|
||||||
|
}
|
||||||
|
|
||||||
/** Statistiken fürs Status-Panel der Settings-Seite */
|
/** Statistiken fürs Status-Panel der Settings-Seite */
|
||||||
export function archiveStats() {
|
export function archiveStats() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ export function devlogChannelId() {
|
|||||||
return getSetting('devlog_channel_id') || config.devlogChannelId;
|
return getSetting('devlog_channel_id') || config.devlogChannelId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Release-Ankündigungs-Kanal — nur per Settings-Seite, leer = Feature aus */
|
||||||
|
export function releaseChannelId() {
|
||||||
|
return getSetting('release_channel_id') || null;
|
||||||
|
}
|
||||||
|
|
||||||
/** Commit-Feed global an/aus (Default: an). Aus = weiter archivieren, nur nicht posten. */
|
/** Commit-Feed global an/aus (Default: an). Aus = weiter archivieren, nur nicht posten. */
|
||||||
export function commitFeedEnabled() {
|
export function commitFeedEnabled() {
|
||||||
return getSetting('commit_feed_enabled') !== '0';
|
return getSetting('commit_feed_enabled') !== '0';
|
||||||
|
|||||||
+28
-12
@@ -1,9 +1,9 @@
|
|||||||
// REST-API fürs Webinterface: Devlogs öffentlich, Commits + Settings nur für den Admin
|
// REST-API fürs Webinterface: Devlogs öffentlich, Commits + Settings nur für den Admin
|
||||||
import { EmbedBuilder } from 'discord.js';
|
import { EmbedBuilder } from 'discord.js';
|
||||||
import { listDevlogs, searchDevlogs, listCommits, archiveStats, getSetting, setSetting } from '../db.js';
|
import { listDevlogs, searchDevlogs, listCommits, listReleases, archiveStats, getSetting, setSetting } from '../db.js';
|
||||||
import { config } from '../config.js';
|
import { config } from '../config.js';
|
||||||
import { removeDevlog } from '../bot/devlog-archive.js';
|
import { removeDevlog } from '../bot/devlog-archive.js';
|
||||||
import { commitChannelId, devlogChannelId } from '../runtime-settings.js';
|
import { commitChannelId, devlogChannelId, releaseChannelId } from '../runtime-settings.js';
|
||||||
import { getSessionUser, isAdmin } from './auth.js';
|
import { getSessionUser, isAdmin } from './auth.js';
|
||||||
|
|
||||||
const PAGE_SIZE = 20;
|
const PAGE_SIZE = 20;
|
||||||
@@ -42,6 +42,13 @@ export function registerApiRoutes(app, client) {
|
|||||||
return { items: mapped, total, page, pageSize: PAGE_SIZE };
|
return { items: mapped, total, page, pageSize: PAGE_SIZE };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Changelog — öffentlich (Releases aller Repos)
|
||||||
|
app.get('/api/releases', async (request) => {
|
||||||
|
const { limit, offset, page } = paging(request);
|
||||||
|
const { items, total } = listReleases(limit, offset);
|
||||||
|
return { items, total, page, pageSize: PAGE_SIZE };
|
||||||
|
});
|
||||||
|
|
||||||
// RSS-Feed der Devlogs — öffentlich abonnierbar
|
// RSS-Feed der Devlogs — öffentlich abonnierbar
|
||||||
app.get('/feed.xml', async (request, reply) => {
|
app.get('/feed.xml', async (request, reply) => {
|
||||||
const { items } = listDevlogs(20, 0);
|
const { items } = listDevlogs(20, 0);
|
||||||
@@ -113,6 +120,7 @@ ${rssItems}
|
|||||||
return {
|
return {
|
||||||
commit_channel_id: commitChannelId(),
|
commit_channel_id: commitChannelId(),
|
||||||
devlog_channel_id: devlogChannelId(),
|
devlog_channel_id: devlogChannelId(),
|
||||||
|
release_channel_id: releaseChannelId() ?? '',
|
||||||
commit_feed_enabled: getSetting('commit_feed_enabled') !== '0',
|
commit_feed_enabled: getSetting('commit_feed_enabled') !== '0',
|
||||||
commit_branch_filter: getSetting('commit_branch_filter') ?? '',
|
commit_branch_filter: getSetting('commit_branch_filter') ?? '',
|
||||||
ignored_repos: getSetting('ignored_repos') ?? '',
|
ignored_repos: getSetting('ignored_repos') ?? '',
|
||||||
@@ -140,15 +148,19 @@ ${rssItems}
|
|||||||
|
|
||||||
const body = request.body ?? {};
|
const body = request.body ?? {};
|
||||||
|
|
||||||
// Kanäle: müssen existierende Textkanäle sein
|
// Kanäle: müssen existierende Textkanäle sein (release darf leer sein = deaktiviert)
|
||||||
for (const key of ['commit_channel_id', 'devlog_channel_id']) {
|
for (const key of ['commit_channel_id', 'devlog_channel_id', 'release_channel_id']) {
|
||||||
if (body[key] !== undefined) {
|
if (body[key] === undefined) continue;
|
||||||
const ch = client.channels.cache.get(String(body[key]));
|
const value = String(body[key]);
|
||||||
if (!ch?.isTextBased?.()) {
|
if (value === '' && key === 'release_channel_id') {
|
||||||
return reply.code(400).send({ error: `${key}: Kanal nicht gefunden` });
|
setSetting(key, '');
|
||||||
}
|
continue;
|
||||||
setSetting(key, String(body[key]));
|
|
||||||
}
|
}
|
||||||
|
const ch = client.channels.cache.get(value);
|
||||||
|
if (!ch?.isTextBased?.()) {
|
||||||
|
return reply.code(400).send({ error: `${key}: Kanal nicht gefunden` });
|
||||||
|
}
|
||||||
|
setSetting(key, value);
|
||||||
}
|
}
|
||||||
if (body.commit_feed_enabled !== undefined) {
|
if (body.commit_feed_enabled !== undefined) {
|
||||||
setSetting('commit_feed_enabled', body.commit_feed_enabled ? '1' : '0');
|
setSetting('commit_feed_enabled', body.commit_feed_enabled ? '1' : '0');
|
||||||
@@ -168,7 +180,11 @@ ${rssItems}
|
|||||||
if (requireAdmin(request, reply)) return;
|
if (requireAdmin(request, reply)) return;
|
||||||
|
|
||||||
const target = request.params.target;
|
const target = request.params.target;
|
||||||
const channelId = target === 'commit' ? commitChannelId() : target === 'devlog' ? devlogChannelId() : null;
|
const channelId =
|
||||||
|
target === 'commit' ? commitChannelId()
|
||||||
|
: target === 'devlog' ? devlogChannelId()
|
||||||
|
: target === 'release' ? releaseChannelId()
|
||||||
|
: null;
|
||||||
if (!channelId) {
|
if (!channelId) {
|
||||||
return reply.code(400).send({ error: 'Kein Kanal konfiguriert' });
|
return reply.code(400).send({ error: 'Kein Kanal konfiguriert' });
|
||||||
}
|
}
|
||||||
@@ -180,7 +196,7 @@ ${rssItems}
|
|||||||
.setColor(0xf5c518)
|
.setColor(0xf5c518)
|
||||||
.setTitle('🔧 Test')
|
.setTitle('🔧 Test')
|
||||||
.setDescription(
|
.setDescription(
|
||||||
`Test-Nachricht für den **${target === 'commit' ? 'Commit' : 'Devlog'}-Kanal** — von der Settings-Seite ausgelöst.`
|
`Test-Nachricht für den **${{ commit: 'Commit', devlog: 'Devlog', release: 'Release' }[target]}-Kanal** — von der Settings-Seite ausgelöst.`
|
||||||
)
|
)
|
||||||
.setFooter({ text: 'D4RKST3R // SETUP' }),
|
.setFooter({ text: 'D4RKST3R // SETUP' }),
|
||||||
],
|
],
|
||||||
|
|||||||
+38
-2
@@ -8,9 +8,10 @@ import { existsSync } from 'node:fs';
|
|||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { dirname, join } from 'node:path';
|
import { dirname, join } from 'node:path';
|
||||||
import { config } from '../config.js';
|
import { config } from '../config.js';
|
||||||
import { saveCommits } from '../db.js';
|
import { saveCommits, saveRelease } from '../db.js';
|
||||||
import { commitFeedEnabled, branchAllowed, repoIgnored } from '../runtime-settings.js';
|
import { commitFeedEnabled, branchAllowed, repoIgnored } from '../runtime-settings.js';
|
||||||
import { postPushEmbed } from '../bot/commit-feed.js';
|
import { postPushEmbed } from '../bot/commit-feed.js';
|
||||||
|
import { postReleaseEmbed } from '../bot/release-feed.js';
|
||||||
import { registerAuthRoutes } from './auth.js';
|
import { registerAuthRoutes } from './auth.js';
|
||||||
import { registerApiRoutes } from './api.js';
|
import { registerApiRoutes } from './api.js';
|
||||||
import { registerDevlogEndpoint } from './devlog-endpoint.js';
|
import { registerDevlogEndpoint } from './devlog-endpoint.js';
|
||||||
@@ -103,8 +104,43 @@ export async function startWebServer(client) {
|
|||||||
return reply.code(401).send({ error: 'invalid signature' });
|
return reply.code(401).send({ error: 'invalid signature' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Andere Events (Tag, Issue, …) freundlich ignorieren
|
|
||||||
const event = request.headers['x-gitea-event'];
|
const event = request.headers['x-gitea-event'];
|
||||||
|
|
||||||
|
// Release-Events → Ankündigung + Changelog-Archiv
|
||||||
|
if (event === 'release') {
|
||||||
|
const payload = request.body;
|
||||||
|
const repo = payload.repository?.full_name ?? 'unbekannt';
|
||||||
|
if (repoIgnored(repo) || !payload.release) {
|
||||||
|
return { ok: true, ignored: `release ${repo}` };
|
||||||
|
}
|
||||||
|
const release = {
|
||||||
|
repo,
|
||||||
|
tag: payload.release.tag_name ?? 'unbekannt',
|
||||||
|
name: payload.release.name ?? null,
|
||||||
|
body: payload.release.body ?? null,
|
||||||
|
url: payload.release.html_url ?? null,
|
||||||
|
prerelease: payload.release.prerelease ? 1 : 0,
|
||||||
|
published_at: payload.release.published_at ?? new Date().toISOString(),
|
||||||
|
};
|
||||||
|
const isNew = saveRelease(release);
|
||||||
|
request.log.info(`Release ${release.tag} in ${repo} (${payload.action}, neu: ${isNew})`);
|
||||||
|
|
||||||
|
// Nur neue Releases ankündigen — Updates/Löschungen nur still archivieren
|
||||||
|
let posted = false;
|
||||||
|
if (isNew && payload.action === 'published') {
|
||||||
|
try {
|
||||||
|
posted = await postReleaseEmbed(client, {
|
||||||
|
...release,
|
||||||
|
prerelease: Boolean(release.prerelease),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
request.log.error({ err: error }, 'Release-Embed konnte nicht gepostet werden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ok: true, release: release.tag, posted };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Andere Events (Tag, Issue, …) freundlich ignorieren
|
||||||
if (event !== 'push') {
|
if (event !== 'push') {
|
||||||
return { ok: true, ignored: event };
|
return { ok: true, ignored: event };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user