From 2018093ee1fa2a3f5a4acee1aa9888b7a03e40c3 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sun, 14 Dec 2025 20:13:25 +1100 Subject: [PATCH 01/11] feat: GitHub changelog agent --- apps/web/.env.example | 10 +- .../components/page-settings/github-agent.tsx | 275 ++++++++++++ .../components/page-settings/integrations.tsx | 7 +- .../inngest/jobs/process-github-changelog.ts | 205 +++++++++ apps/web/package.json | 4 +- apps/web/pages/api/inngest.ts | 6 +- .../pages/api/integrations/github/callback.ts | 21 + .../api/integrations/github/installations.ts | 194 +++++++++ .../pages/api/integrations/github/repos.ts | 59 +++ .../pages/api/integrations/github/webhook.ts | 128 ++++++ apps/web/pages/integrations/github/setup.tsx | 406 ++++++++++++++++++ .../pages/[page_id]/settings/[activeTab].tsx | 1 + apps/web/utils/changelog-ai.ts | 59 +++ apps/web/utils/github.ts | 163 +++++++ .../supabase/migrations/21_github_agent.sql | 46 ++ packages/supabase/types/index.ts | 113 ++++- pnpm-lock.yaml | 245 +++++++++++ 17 files changed, 1925 insertions(+), 17 deletions(-) create mode 100644 apps/web/components/page-settings/github-agent.tsx create mode 100644 apps/web/inngest/jobs/process-github-changelog.ts create mode 100644 apps/web/pages/api/integrations/github/callback.ts create mode 100644 apps/web/pages/api/integrations/github/installations.ts create mode 100644 apps/web/pages/api/integrations/github/repos.ts create mode 100644 apps/web/pages/api/integrations/github/webhook.ts create mode 100644 apps/web/pages/integrations/github/setup.tsx create mode 100644 apps/web/utils/changelog-ai.ts create mode 100644 apps/web/utils/github.ts create mode 100644 packages/supabase/migrations/21_github_agent.sql diff --git a/apps/web/.env.example b/apps/web/.env.example index 7634391..958b6ba 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -1,5 +1,3 @@ -NEXT_PUBLIC_PAGES_DOMAIN=http://localhost:3000 - # Supabase details from https://app.supabase.io NEXT_PUBLIC_SUPABASE_URL= NEXT_PUBLIC_SUPABASE_ANON_KEY= @@ -34,4 +32,12 @@ NEXT_PUBLIC_SANITY_PROJECT_ID=jeixxcw8 # ManagePrompt MANAGEPROMPT_SECRET= MANAGEPROMPT_CHANGEGPT_WORKFLOW_ID= +MANAGEPROMPT_CHANGELOG_WORKFLOW_ID= + +# GitHub App +GITHUB_APP_ID= +GITHUB_APP_PRIVATE_KEY= +GITHUB_WEBHOOK_SECRET= +GITHUB_APP_SLUG=changespage +NEXT_PUBLIC_GITHUB_APP_URL=https://github.com/apps/changespage diff --git a/apps/web/components/page-settings/github-agent.tsx b/apps/web/components/page-settings/github-agent.tsx new file mode 100644 index 0000000..0c1155c --- /dev/null +++ b/apps/web/components/page-settings/github-agent.tsx @@ -0,0 +1,275 @@ +import { Spinner } from "@changes-page/ui"; +import { ExternalLinkIcon, TrashIcon } from "@heroicons/react/outline"; +import { useEffect, useState } from "react"; +import { notifyError, notifySuccess } from "../core/toast.component"; +import WarningDialog from "../dialogs/warning-dialog.component"; + +interface GitHubInstallation { + id: string; + page_id: string; + installation_id: number; + repository_owner: string; + repository_name: string; + enabled: boolean; + ai_instructions: string | null; + created_at: string; +} + +export default function GitHubAgentSettings({ pageId }: { pageId: string }) { + const [installations, setInstallations] = useState([]); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(null); + const [deleteTarget, setDeleteTarget] = useState(null); + const [deleting, setDeleting] = useState(false); + + useEffect(() => { + fetchInstallations(); + }, [pageId]); + + async function fetchInstallations() { + try { + const res = await fetch(`/api/integrations/github/installations?page_id=${pageId}`); + const data = await res.json(); + if (data.error) { + notifyError(data.error.message); + } else { + setInstallations(data); + } + } catch (err) { + notifyError("Failed to load GitHub installations"); + } finally { + setLoading(false); + } + } + + async function updateInstallation( + installation: GitHubInstallation, + updates: { enabled?: boolean; ai_instructions?: string | null } + ) { + setSaving(installation.id); + try { + const res = await fetch("/api/integrations/github/installations", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + id: installation.id, + page_id: pageId, + ...updates, + }), + }); + const data = await res.json(); + if (data.error) { + notifyError(data.error.message); + } else { + setInstallations((prev) => + prev.map((i) => (i.id === installation.id ? data : i)) + ); + notifySuccess("Settings saved"); + } + } catch (err) { + notifyError("Failed to update settings"); + } finally { + setSaving(null); + } + } + + async function deleteInstallation() { + if (!deleteTarget) return; + setDeleting(true); + try { + const res = await fetch( + `/api/integrations/github/installations?id=${deleteTarget.id}&page_id=${pageId}`, + { method: "DELETE" } + ); + const data = await res.json(); + if (data.error) { + notifyError(data.error.message); + } else { + setInstallations((prev) => prev.filter((i) => i.id !== deleteTarget.id)); + notifySuccess("Repository disconnected"); + } + } catch (err) { + notifyError("Failed to disconnect repository"); + } finally { + setDeleting(false); + setDeleteTarget(null); + } + } + + const githubAppUrl = process.env.NEXT_PUBLIC_GITHUB_APP_URL || "https://github.com/apps/changespage"; + + return ( + <> + !open && setDeleteTarget(null)} + confirmCallback={deleteInstallation} + processing={deleting} + /> + +
+
+
+
+

+ GitHub Changelog Agent +

+

+ Automatically generate changelog drafts from your PRs by mentioning @changespage. +

+
+
+
+
+
+ {loading ? ( +
+ +
+ ) : installations.length === 0 ? ( +
+ + + +

+ No repositories connected +

+

+ Connect a GitHub repository to start generating changelogs from PRs. +

+ +
+ ) : ( +
+ {installations.map((installation) => ( +
+
+
+ + + +
+

+ {installation.repository_owner}/{installation.repository_name} +

+

+ Connected {new Date(installation.created_at).toLocaleDateString()} +

+
+
+
+