From 055885142769dc755b3be98470eaec06dab20e83 Mon Sep 17 00:00:00 2001 From: lashman Date: Sat, 21 Mar 2026 22:38:46 +0200 Subject: [PATCH] gitea plugin now imports releases as changelog entries --- plugins/gitea-sync/index.js | 68 +++++++++++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 7 deletions(-) diff --git a/plugins/gitea-sync/index.js b/plugins/gitea-sync/index.js index 3dbc4d1..0dcfbe9 100644 --- a/plugins/gitea-sync/index.js +++ b/plugins/gitea-sync/index.js @@ -49,6 +49,7 @@ async function fetchPublicRepos(url, user, token) { if (!repo.private && !repo.fork) { repos.push({ name: repo.name, + fullName: repo.full_name, description: repo.description || null, htmlUrl: repo.html_url, updatedAt: repo.updated_at, @@ -63,6 +64,29 @@ async function fetchPublicRepos(url, user, token) { return repos; } +async function fetchReleases(url, repoFullName, token) { + const headers = {}; + if (token) headers["Authorization"] = `token ${token}`; + + const res = await fetch( + `${url}/api/v1/repos/${repoFullName}/releases?limit=50`, + { headers } + ); + if (!res.ok) return []; + + const releases = await res.json(); + return (releases || []) + .filter((r) => !r.draft) + .map((r) => ({ + id: r.id, + tag: r.tag_name, + name: r.name || r.tag_name, + body: r.body || "", + publishedAt: r.published_at || r.created_at, + htmlUrl: r.html_url, + })); +} + function parseProfileUrl(raw) { const clean = String(raw).replace(/\/+$/, ""); const parts = clean.split("/"); @@ -84,18 +108,24 @@ async function syncRepos(ctx) { return { synced: 0, error: "invalid profile URL - expected https://instance/username" }; } - const repos = await fetchPublicRepos(url, user, token ? String(token) : null); + const tkn = token ? String(token) : null; + const repos = await fetchPublicRepos(url, user, tkn); let created = 0; let updated = 0; + let changelogsCreated = 0; + + // track which releases we've already imported + const importedReleases = await ctx.store.get("importedReleases"); + const imported = new Set(Array.isArray(importedReleases) ? importedReleases : []); for (const repo of repos) { const slug = repo.name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-"); - const existing = await ctx.prisma.board.findUnique({ where: { slug } }); + let board = await ctx.prisma.board.findUnique({ where: { slug } }); - if (existing) { + if (board) { await ctx.prisma.board.update({ where: { slug }, - data: { externalUrl: repo.htmlUrl, description: repo.description || existing.description }, + data: { externalUrl: repo.htmlUrl, description: repo.description || board.description }, }); updated++; } else { @@ -103,7 +133,7 @@ async function syncRepos(ctx) { const iconName = pickRandom(ICONS, repo.name); const iconColor = pickRandom(COLORS, repo.name + "color"); - await ctx.prisma.board.create({ + board = await ctx.prisma.board.create({ data: { slug, name, @@ -115,14 +145,38 @@ async function syncRepos(ctx) { }); created++; } + + // fetch releases and create changelog entries + const releases = await fetchReleases(url, repo.fullName, tkn); + for (const release of releases) { + const releaseKey = `${repo.fullName}:${release.id}`; + if (imported.has(releaseKey)) continue; + + const body = release.body + ? `${release.body}\n\n[View release](${release.htmlUrl})` + : `[View release](${release.htmlUrl})`; + + await ctx.prisma.changelogEntry.create({ + data: { + title: `${titleCase(repo.name)} ${release.name}`, + body, + boardId: board.id, + publishedAt: new Date(release.publishedAt), + }, + }); + + imported.add(releaseKey); + changelogsCreated++; + } } + await ctx.store.set("importedReleases", [...imported]); const lastSync = new Date().toISOString(); await ctx.store.set("lastSync", lastSync); await ctx.store.set("repoCount", repos.length); - ctx.logger.info({ created, updated, total: repos.length }, "gitea-sync completed"); + ctx.logger.info({ created, updated, changelogsCreated, total: repos.length }, "gitea-sync completed"); - return { synced: repos.length, created, updated }; + return { synced: repos.length, created, updated, changelogsCreated }; } export default {