Skip to content

Commit f9f740f

Browse files
author
Auto-Co
committed
fix: webhook signature verification, theme provider lint, add deployment configs
1 parent 6cb737a commit f9f740f

10 files changed

Lines changed: 148 additions & 40 deletions

File tree

README.md

Lines changed: 53 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,67 @@
1-
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
1+
# DocuCraft — AI Documentation from Your GitHub Repos
22

3-
## Getting Started
3+
DocuCraft automatically generates PR descriptions, changelogs, and documentation from your GitHub repositories.
4+
5+
## Phase 0: PR Pilot
6+
7+
The initial release ("PR Pilot") focuses on two core features:
8+
- **Auto PR Descriptions** — Every pull request gets a clear, well-structured description generated from code changes
9+
- **Changelog Generation** — Generate release notes from merged PRs with one click
10+
11+
## Tech Stack
12+
13+
- **Frontend:** Next.js 16, React 19, Tailwind CSS v4, shadcn/ui
14+
- **Backend:** Next.js API routes (serverless)
15+
- **Database:** Supabase (Postgres)
16+
- **AI:** OpenAI (GPT-4o-mini)
17+
- **Auth:** GitHub App OAuth
18+
- **Deployment:** Vercel (frontend + API), Railway (background tasks), Supabase (DB)
419

5-
First, run the development server:
20+
## Getting Started
621

722
```bash
8-
npm run dev
9-
# or
10-
yarn dev
11-
# or
12-
pnpm dev
13-
# or
14-
bun dev
15-
```
23+
# Copy environment variables
24+
cp .env.example .env.local
1625

17-
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
26+
# Fill in your credentials:
27+
# - Supabase project URL and keys
28+
# - GitHub App credentials
29+
# - OpenAI API key
1830

19-
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
31+
# Run development server
32+
npm run dev
33+
```
2034

21-
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
35+
## Environment Variables
2236

23-
## Learn More
37+
| Variable | Description |
38+
|----------|-------------|
39+
| `NEXT_PUBLIC_SUPABASE_URL` | Supabase project URL |
40+
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Supabase anonymous key |
41+
| `SUPABASE_SERVICE_ROLE_KEY` | Supabase service role key |
42+
| `GITHUB_APP_ID` | GitHub App ID |
43+
| `GITHUB_APP_CLIENT_ID` | GitHub App client ID |
44+
| `GITHUB_APP_CLIENT_SECRET` | GitHub App client secret |
45+
| `GITHUB_APP_PRIVATE_KEY` | GitHub App private key |
46+
| `GITHUB_APP_WEBHOOK_SECRET` | GitHub App webhook secret |
47+
| `OPENAI_API_KEY` | OpenAI API key |
48+
| `OPENAI_MODEL` | OpenAI model (default: gpt-4o-mini) |
49+
| `NEXT_PUBLIC_APP_URL` | App URL (http://localhost:3000 for dev) |
2450

25-
To learn more about Next.js, take a look at the following resources:
51+
## Architecture
2652

27-
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28-
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
53+
1. **GitHub App** receives `pull_request` webhook events
54+
2. **Webhook handler** fetches PR diff, generates AI description via OpenAI
55+
3. **AI description** posted as PR comment and stored in Supabase
56+
4. **Dashboard** displays repos, PRs, and generated changelogs
57+
5. **Changelog generation** groups merged PRs and generates release notes
2958

30-
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
59+
## Pricing
3160

32-
## Deploy on Vercel
61+
- **Free** — Open source repos
62+
- **$29/mo Pro** — Individual developers, private repos
63+
- **$79/mo Team** — Small teams, multiple installations
3364

34-
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
65+
## License
3566

36-
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
67+
MIT

package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"react": "19.2.4",
2222
"react-dom": "19.2.4",
2323
"shadcn": "^4.11.0",
24+
"sonner": "^2.0.7",
2425
"tailwind-merge": "^3.6.0",
2526
"tw-animate-css": "^1.4.0"
2627
},

railway.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"$schema": "https://railway.app/railway.schema.json",
3+
"build": {
4+
"builder": "nixpacks",
5+
"buildCommand": "npm run build",
6+
"watchPatterns": ["src/**"]
7+
},
8+
"deploy": {
9+
"numReplicas": 1,
10+
"restartPolicyType": "on-failure",
11+
"restartPolicyMaxRetries": 3,
12+
"healthcheckPath": "/",
13+
"healthcheckTimeout": 60
14+
}
15+
}

src/app/api/github/webhook/route.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,27 @@ import { createAdminClient } from "@/lib/supabase/admin";
33
import { getPRDiff, getPRFiles, postPRComment } from "@/lib/github/api";
44
import { getInstallationToken } from "@/lib/github/token";
55
import { generatePRDescription } from "@/lib/ai/generate";
6+
import { verifyWebhookSignature } from "@/lib/github/webhook";
67

78
export async function POST(request: NextRequest) {
8-
const payload = await request.json();
9+
const payloadText = await request.text();
10+
const payload = JSON.parse(payloadText);
911

1012
const event = request.headers.get("x-github-event");
1113
const deliveryId = request.headers.get("x-github-delivery");
14+
const signature = request.headers.get("x-hub-signature-256");
1215

1316
if (!event || !deliveryId) {
1417
return NextResponse.json({ error: "Missing headers" }, { status: 400 });
1518
}
1619

20+
const webhookSecret = process.env.GITHUB_APP_WEBHOOK_SECRET;
21+
if (webhookSecret && signature) {
22+
if (!verifyWebhookSignature(payloadText, signature, webhookSecret)) {
23+
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
24+
}
25+
}
26+
1727
if (event !== "pull_request") {
1828
return NextResponse.json({ ok: true, message: "Ignored event type" });
1929
}

src/app/dashboard/page.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,18 @@ export default function Dashboard() {
3939
}, []);
4040

4141
useEffect(() => {
42-
if (!selectedRepo) return;
42+
const repoId = selectedRepo;
43+
if (!repoId) return;
4344

4445
async function loadPRs() {
4546
try {
46-
const res = await fetch(`/api/prs?repo_id=${selectedRepo}`);
47+
const res = await fetch(`/api/prs?repo_id=${repoId}`);
4748
const data = await res.json();
48-
setPrs((prev) => ({ ...prev, [selectedRepo]: data.prs || [] }));
49+
setPrs((prev) => {
50+
const next = { ...prev };
51+
next[repoId as number] = data.prs || [];
52+
return next;
53+
});
4954
} catch (error) {
5055
console.error("Failed to load PRs:", error);
5156
}

src/components/theme-provider.tsx

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,21 @@ const ThemeProviderContext = React.createContext<ThemeProviderState | undefined>
1919
undefined
2020
);
2121

22+
function getInitialTheme(storageKey: string, defaultTheme: Theme): Theme {
23+
if (typeof window === "undefined") return defaultTheme;
24+
const stored = localStorage.getItem(storageKey) as Theme | null;
25+
return stored || defaultTheme;
26+
}
27+
2228
export function ThemeProvider({
2329
children,
2430
defaultTheme = "system",
2531
storageKey = "docucraft-theme",
2632
...props
2733
}: ThemeProviderProps) {
28-
const [theme, setTheme] = React.useState<Theme>(defaultTheme);
29-
30-
React.useEffect(() => {
31-
const stored = localStorage.getItem(storageKey) as Theme | null;
32-
if (stored) {
33-
setTheme(stored);
34-
}
35-
}, [storageKey]);
34+
const [theme, setTheme] = React.useState<Theme>(
35+
getInitialTheme(storageKey, defaultTheme)
36+
);
3637

3738
React.useEffect(() => {
3839
const root = document.documentElement;
@@ -49,6 +50,19 @@ export function ThemeProvider({
4950
}
5051
}, [theme]);
5152

53+
React.useEffect(() => {
54+
const onSystemChange = (e: MediaQueryListEvent) => {
55+
if (theme === "system") {
56+
root.classList.remove("light", "dark");
57+
root.classList.add(e.matches ? "dark" : "light");
58+
}
59+
};
60+
const root = document.documentElement;
61+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
62+
mq.addEventListener("change", onSystemChange);
63+
return () => mq.removeEventListener("change", onSystemChange);
64+
}, [theme]);
65+
5266
const value = React.useMemo(
5367
() => ({
5468
theme,

src/components/ui/sonner.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client"
22

3-
import { useTheme } from "next-themes"
3+
import { useTheme } from "@/components/theme-provider"
44
import { Toaster as Sonner, type ToasterProps } from "sonner"
55
import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
66

src/lib/ai/generate.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import OpenAI from "openai";
22

3-
const openai = new OpenAI({
4-
apiKey: process.env.OPENAI_API_KEY,
5-
});
3+
function getOpenAI() {
4+
return new OpenAI({
5+
apiKey: process.env.OPENAI_API_KEY,
6+
});
7+
}
68

79
export async function generatePRDescription(params: {
810
title: string;
@@ -32,7 +34,7 @@ Generate a PR description with:
3234
3335
Keep it professional and concise. Use markdown.`;
3436

35-
const response = await openai.chat.completions.create({
37+
const response = await getOpenAI().chat.completions.create({
3638
model: process.env.OPENAI_MODEL || "gpt-4o-mini",
3739
messages: [{ role: "user", content: prompt }],
3840
temperature: 0.3,
@@ -64,7 +66,7 @@ Generate a changelog with:
6466
6567
Keep it professional and user-focused. Use markdown.`;
6668

67-
const response = await openai.chat.completions.create({
69+
const response = await getOpenAI().chat.completions.create({
6870
model: process.env.OPENAI_MODEL || "gpt-4o-mini",
6971
messages: [{ role: "user", content: prompt }],
7072
temperature: 0.3,

supabase/config.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[project]
2+
name = "docucraft"
3+
plan = "free"
4+
5+
[auth]
6+
enabled = true
7+
sites_url = "http://localhost:3000"
8+
additional_redirect_urls = ["https://docucraft.dev/**"]
9+
10+
[api]
11+
enabled = true
12+
port = 54321
13+
schemas = ["public", "graphql_public"]
14+
15+
[db]
16+
migration_dir = "supabase/migrations"
17+
18+
[storage]
19+
enabled = false

0 commit comments

Comments
 (0)