Step 5: Deploy as GitHub App
Transform your webhook server into a proper GitHub App with installation flows, JWT authentication, per-repository tokens, production deployment, and monitoring.
Why a GitHub App?
Until now, we have used a Personal Access Token (PAT), which has several limitations:
- PATs are tied to a user account — if the user leaves, the bot breaks
- PATs have broad permissions that cannot be scoped per-repository
- No installation marketplace — each repo needs manual setup
- Rate limits are tied to the user, not the app
A GitHub App solves all of these. It has its own identity, scoped permissions, per-installation tokens, and a proper installation flow.
Step 1: Register the GitHub App
Go to GitHub Settings → Developer settings → GitHub Apps → New GitHub App and fill in:
Homepage URL: Your app's landing page or GitHub repo
Webhook URL:
https://your-domain.com/webhookWebhook secret: Generate a strong random string
Permissions:
• Pull requests: Read & write
• Contents: Read-only (to fetch .ai-review.yml)
• Metadata: Read-only (required)
Subscribe to events: Pull request
Where can this app be installed? Any account (or "Only on this account" for private use)
After creating the app, note down the App ID and generate a private key (download the .pem file).
Step 2: JWT Authentication
GitHub Apps authenticate using JWTs signed with the private key. Install the required package:
npm install jsonwebtoken
Create src/auth.js to handle GitHub App authentication:
// src/auth.js
const jwt = require('jsonwebtoken');
const { Octokit } = require('@octokit/rest');
const fs = require('fs');
const APP_ID = process.env.GITHUB_APP_ID;
const PRIVATE_KEY = process.env.GITHUB_PRIVATE_KEY ||
fs.readFileSync(process.env.GITHUB_PRIVATE_KEY_PATH, 'utf8');
/**
* Generate a JWT for authenticating as the GitHub App.
* JWTs are valid for up to 10 minutes.
*/
function generateAppJWT() {
const now = Math.floor(Date.now() / 1000);
const payload = {
iat: now - 60, // Issued 60 seconds ago (clock skew buffer)
exp: now + (10 * 60), // Expires in 10 minutes
iss: APP_ID, // GitHub App ID
};
return jwt.sign(payload, PRIVATE_KEY, { algorithm: 'RS256' });
}
/**
* Get an installation access token for a specific repository.
* These tokens are scoped to the installation and expire in 1 hour.
*/
async function getInstallationToken(installationId) {
const appJWT = generateAppJWT();
const appOctokit = new Octokit({ auth: appJWT });
const response = await appOctokit.apps.createInstallationAccessToken({
installation_id: installationId,
});
return response.data.token;
}
/**
* Create an Octokit instance authenticated for a specific installation.
*/
async function getInstallationOctokit(installationId) {
const token = await getInstallationToken(installationId);
return new Octokit({ auth: token });
}
module.exports = { generateAppJWT, getInstallationToken, getInstallationOctokit };
Step 3: Update the Webhook Handler
With GitHub App auth, the webhook payload includes an installation field. Update the server to use installation tokens:
// Updated webhook handler for GitHub App
app.post('/webhook', async (req, res) => {
if (process.env.GITHUB_WEBHOOK_SECRET && !verifySignature(req)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = req.headers['x-github-event'];
if (event !== 'pull_request') {
return res.status(200).json({ ignored: true });
}
const { action, pull_request, repository, installation } = req.body;
if (!['opened', 'synchronize', 'reopened'].includes(action)) {
return res.status(200).json({ ignored: true });
}
res.status(202).json({ processing: true });
try {
// Get installation-scoped Octokit
const octokit = await getInstallationOctokit(installation.id);
await handlePullRequest({
octokit, // Pass the scoped client
owner: repository.owner.login,
repo: repository.name,
pullNumber: pull_request.number,
headSha: pull_request.head.sha,
});
} catch (error) {
console.error(`Error reviewing PR #${pull_request.number}:`, error);
}
});
Step 4: Deploy to a Cloud Provider
We will cover three deployment options, from simplest to most production-ready:
Option A: Railway (Simplest)
# Install Railway CLI
npm install -g @railway/cli
# Login and initialize
railway login
railway init
# Set environment variables
railway variables set GITHUB_APP_ID=your_app_id
railway variables set GITHUB_PRIVATE_KEY="$(cat private-key.pem)"
railway variables set GITHUB_WEBHOOK_SECRET=your_secret
railway variables set OPENAI_API_KEY=your_key
# Deploy
railway up
Option B: Fly.io
# Install Fly CLI and login
flyctl auth login
# Create the app
flyctl launch --name ai-code-reviewer
# Set secrets
flyctl secrets set GITHUB_APP_ID=your_app_id
flyctl secrets set GITHUB_PRIVATE_KEY="$(cat private-key.pem)"
flyctl secrets set GITHUB_WEBHOOK_SECRET=your_secret
flyctl secrets set OPENAI_API_KEY=your_key
# Deploy
flyctl deploy
Create a Dockerfile for Fly.io:
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY src/ ./src/
EXPOSE 3000
CMD ["node", "src/server.js"]
Option C: AWS Lambda + API Gateway
For a serverless approach, wrap the Express app with a Lambda handler:
npm install serverless-http
// src/lambda.js
const serverless = require('serverless-http');
const { app } = require('./server');
module.exports.handler = serverless(app);
Step 5: Add Health Monitoring
Add basic monitoring to track reviews and catch errors in production:
// src/monitor.js
const stats = {
reviews_total: 0,
reviews_success: 0,
reviews_failed: 0,
issues_found: 0,
last_review: null,
uptime_start: new Date().toISOString(),
};
function recordReview(success, issueCount = 0) {
stats.reviews_total++;
if (success) {
stats.reviews_success++;
stats.issues_found += issueCount;
} else {
stats.reviews_failed++;
}
stats.last_review = new Date().toISOString();
}
function getStats() {
return {
...stats,
uptime_hours: Math.round(
(Date.now() - new Date(stats.uptime_start).getTime()) / 3600000
),
};
}
module.exports = { recordReview, getStats };
Add a stats endpoint to the server:
const { getStats } = require('./monitor');
app.get('/stats', (req, res) => {
res.json(getStats());
});
Step 6: Update the Webhook URL
Once deployed, update your GitHub App's webhook URL:
- Go to GitHub Settings → Developer settings → GitHub Apps
- Click your app, then Edit
- Update the Webhook URL to your production URL (e.g.,
https://ai-code-reviewer.railway.app/webhook) - Click Save changes
Step 7: Install the App
Install the app on your repositories:
- Go to your app's public page:
https://github.com/apps/your-app-name - Click Install
- Choose which repositories to install on (start with one test repo)
- Click Install
Now open a PR in the installed repository. You should see the AI review appear within seconds.
What Is Next
Your AI code review tool is deployed and live. In the final lesson, we will cover Enhancements and Next Steps — auto-fix suggestions, learning from team feedback, caching, multi-language support, and frequently asked questions.
Lilly Tech Systems