tall the OpenAI SDK, Octokit for GitHub interactions, Zod for validation, and utility libraries for UI composition.
npx create-next-app@latest release-engine --typescript --tailwind --app --src-dir
cd release-engine
npm install openai @octokit/rest zod clsx tailwind-merge
2. Environment Configuration
Create a .env.local file. Never commit secrets. We recommend using a typed environment variable loader in production to fail fast on missing configs.
GITHUB_TOKEN=ghp_your_secure_token_here
OPENAI_API_KEY=sk-proj-your_key_here
NEXT_PUBLIC_APP_URL=http://localhost:3000
3. GitHub History Service
This service abstracts the GitHub API. It parses repository URLs, fetches commits, and normalizes the data structure. We filter out merge commits to reduce noise, as they rarely contain meaningful user-facing changes.
// src/services/git-history.service.ts
import { Octokit } from '@octokit/rest';
import { env } from '@/lib/env';
export interface CommitRecord {
id: string;
summary: string;
contributor: string;
timestamp: Date;
}
export class GitHistoryService {
private client: Octokit;
constructor() {
this.client = new Octokit({ auth: env.GITHUB_TOKEN });
}
async retrieveRecentChanges(
repoUrl: string,
limit: number = 50
): Promise<CommitRecord[]> {
const { owner, repo } = this.extractRepoDetails(repoUrl);
const response = await this.client.repos.listCommits({
owner,
repo,
per_page: limit,
sha: 'main', // Default branch; configurable in production
});
return response.data
.filter((commit) => !commit.commit.message.startsWith('Merge'))
.map((commit) => ({
id: commit.sha,
summary: commit.commit.message.split('\n')[0],
contributor: commit.commit.author?.name ?? 'Anonymous',
timestamp: new Date(commit.commit.author?.date ?? ''),
}));
}
private extractRepoDetails(url: string): { owner: string; repo: string } {
const match = url.match(/github\.com\/([^/]+)\/([^/]+)/);
if (!match) throw new Error('Invalid GitHub repository URL');
return { owner: match[1], repo: match[2].replace('.git', '') };
}
}
4. Release Note Synthesis Service
This service interacts with OpenAI. We use gpt-4o for its balance of speed and reasoning capability. The prompt instructs the model to categorize commits and output JSON. We enforce response_format: { type: 'json_object' } to ensure structural integrity.
// src/services/release-synthesis.service.ts
import OpenAI from 'openai';
import { CommitRecord } from './git-history.service';
import { env } from '@/lib/env';
export interface ReleaseOutput {
version: string;
categories: {
features: string[];
fixes: string[];
improvements: string[];
breaking_changes: string[];
};
summary: string;
}
export class ReleaseSynthesisService {
private ai: OpenAI;
constructor() {
this.ai = new OpenAI({ apiKey: env.OPENAI_API_KEY });
}
async generateReleaseNotes(
commits: CommitRecord[],
version: string
): Promise<ReleaseOutput> {
const commitContext = commits
.map((c) => `- ${c.summary} (${c.contributor})`)
.join('\n');
const systemPrompt = `
You are a technical release manager. Analyze the provided git commits and generate structured release notes.
Categorize changes into features, fixes, improvements, and breaking changes.
Write concise, user-friendly descriptions. Avoid technical jargon unless necessary.
Output must be valid JSON matching the requested schema.
`;
const completion = await this.ai.chat.completions.create({
model: 'gpt-4o',
messages: [
{ role: 'system', content: systemPrompt },
{
role: 'user',
content: `Version: ${version}\n\nCommits:\n${commitContext}`,
},
],
response_format: { type: 'json_object' },
temperature: 0.2, // Low temperature for consistency
});
const rawContent = completion.choices[0]?.message?.content;
if (!rawContent) throw new Error('LLM returned empty response');
return JSON.parse(rawContent) as ReleaseOutput;
}
}
5. Input Validation Schema
Zod schemas protect the application from malformed inputs. This schema validates the repository URL format and ensures the version tag is present.
// src/schemas/release.schema.ts
import { z } from 'zod';
export const ReleaseRequestSchema = z.object({
repositoryUrl: z
.string()
.url()
.regex(/github\.com\/[\w-]+\/[\w.-]+/, 'Must be a valid GitHub repository URL'),
versionTag: z
.string()
.min(1, 'Version tag is required')
.regex(/^v?\d+\.\d+\.\d+/, 'Version must follow semantic versioning (e.g., v1.0.0)'),
});
export type ReleaseRequest = z.infer<typeof ReleaseRequestSchema>;
6. Server Action Orchestration
Next.js 14 Server Actions provide a clean interface for client-server interaction. This action validates input, calls the services, and returns the result. Error handling is centralized to prevent stack traces from leaking.
// src/actions/generate-release.action.ts
'use server';
import { GitHistoryService } from '@/services/git-history.service';
import { ReleaseSynthesisService } from '@/services/release-synthesis.service';
import { ReleaseRequestSchema } from '@/schemas/release.schema';
import { revalidatePath } from 'next/cache';
export async function createReleaseNotes(formData: FormData) {
const validated = ReleaseRequestSchema.safeParse({
repositoryUrl: formData.get('repositoryUrl'),
versionTag: formData.get('versionTag'),
});
if (!validated.success) {
return { error: 'Invalid input parameters', details: validated.error.flatten() };
}
try {
const gitService = new GitHistoryService();
const synthesisService = new ReleaseSynthesisService();
const commits = await gitService.retrieveRecentChanges(validated.data.repositoryUrl);
const notes = await synthesisService.generateReleaseNotes(
commits,
validated.data.versionTag
);
revalidatePath('/releases');
return { data: notes };
} catch (err) {
console.error('Release generation failed:', err);
return { error: 'Failed to generate release notes. Check repository access or API limits.' };
}
}
Pitfall Guide
1. Context Window Overflow
- Explanation: Large repositories with hundreds of commits can exceed the LLM's context window, causing truncation or API errors.
- Fix: Implement chunking logic. If commits exceed a threshold, summarize them in batches and aggregate the results, or sample the most recent N commits based on a heuristic (e.g., excluding chore commits).
2. Merge Commit Pollution
- Explanation: Merge commits often contain generic messages like "Merge branch 'main'" or duplicate the messages of merged commits, cluttering the output.
- Fix: Filter commits where the message starts with "Merge" or use the GitHub API parameter
--no-merges equivalent. The GitHistoryService above includes this filter.
3. Hallucination of Features
- Explanation: The LLM may invent features or exaggerate the impact of minor commits if the prompt is too permissive.
- Fix: Use a low temperature setting (e.g., 0.2) and include explicit instructions in the system prompt to only categorize information present in the commits. Add a constraint: "Do not invent features. If a commit is unclear, omit it."
4. JSON Parsing Failures
- Explanation: Even with
json_object mode, models sometimes wrap output in markdown code blocks or add trailing text, breaking JSON.parse.
- Fix: Implement a robust parser that strips markdown fences before parsing. Alternatively, use OpenAI's function calling or structured outputs feature if available in your SDK version.
5. Token Leakage to Client
- Explanation: Accidentally exposing
GITHUB_TOKEN or OPENAI_API_KEY in client-side bundles allows unauthorized usage and billing theft.
- Fix: Never import environment variables in client components. Use the
server directive for actions and ensure secrets are only accessed in server-side code. Use next.config.js to validate env vars at build time.
6. Rate Limiting Exhaustion
- Explanation: GitHub API has strict rate limits for unauthenticated requests, and OpenAI has token-based limits. High traffic can cause service degradation.
- Fix: Implement caching for generated release notes using Redis or Next.js revalidation tags. Use exponential backoff for API retries. Monitor usage and set up alerts for quota thresholds.
7. Inconsistent Categorization
- Explanation: The LLM may inconsistently classify similar commits across different releases, making it hard for users to track changes over time.
- Fix: Provide few-shot examples in the system prompt. Define strict criteria for each category (e.g., "Fixes must resolve a reported issue or bug"). Periodically review and refine the prompt based on output quality.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Internal Tool | Real-time generation with GPT-3.5-turbo | Lower cost is acceptable for internal use; speed is prioritized. | Low |
| Customer-Facing SaaS | Queue-based generation with GPT-4o | Higher quality and reliability justify cost; async processing handles load. | Medium |
| High-Volume Enterprise | Cached results with fallback to GPT-3.5 | Caching drastically reduces API calls; fallback ensures availability. | Low (after cache hit) |
| Strict Compliance | Human-in-the-loop review | LLM output requires verification for regulated industries. | High (Operational) |
Configuration Template
Use this template for a robust environment configuration file that enforces types at runtime.
// src/lib/env.ts
import { createEnv } from '@t3-oss/env-nextjs';
import { z } from 'zod';
export const env = createEnv({
server: {
GITHUB_TOKEN: z.string().min(1),
OPENAI_API_KEY: z.string().min(1),
},
client: {
NEXT_PUBLIC_APP_URL: z.string().url(),
},
runtimeEnv: {
GITHUB_TOKEN: process.env.GITHUB_TOKEN,
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
},
emptyStringAsUndefined: true,
});
Quick Start Guide
- Scaffold Project: Run
npx create-next-app@latest release-engine --typescript --tailwind --app --src-dir and navigate into the directory.
- Install Dependencies: Execute
npm install openai @octokit/rest zod clsx tailwind-merge.
- Configure Secrets: Create
.env.local with your GitHub PAT and OpenAI API key. Ensure the file is in .gitignore.
- Implement Services: Copy the service and schema code blocks into
src/services and src/schemas. Create the server action in src/actions.
- Run Development Server: Execute
npm run dev and test the flow by submitting a public GitHub repository URL and a version tag. Verify the output matches the expected JSON structure.