GitHub Push Commenter

Automated comment posting for your team's code pushes

Introduction

This guide walks you through creating a GitHub app that automatically posts comments when code is pushed to your team's repositories. The app listens for push events via webhooks and can post intelligent comments about the changes, notify team members, or provide automated feedback.

What You'll Build

  • A GitHub app integrated with your organization's repositories
  • A webhook server that receives push notifications
  • Automated comment posting logic
  • Team-wide installation and configuration

Prerequisites

Step 1: Create the GitHub App

  1. Navigate to GitHub App Settings

    Go to your GitHub organization settings or personal account settings:

    • For organizations: https://github.com/organizations/YOUR_ORG/settings/apps
    • For personal accounts: https://github.com/settings/apps

    Click "New GitHub App"

  2. Configure Basic Information

    Field Value
    GitHub App name Push Commenter (or your preferred name)
    Homepage URL Your organization website or app documentation URL
    Webhook URL Your server endpoint (e.g., https://your-domain.com/webhook)
    Webhook secret Generate a random string (save this securely!)

    Note: You can use a tool like openssl rand -hex 20 to generate a secure webhook secret.

  3. Set Repository Permissions

    Under "Repository permissions", configure the following:

    Permission Access Level Reason
    Contents Read-only Read commit content and file changes
    Issues Read & write Post comments on issues (if needed)
    Pull requests Read & write Post comments on pull requests
    Commit statuses Read & write Update commit status (optional)
  4. Subscribe to Events

    Under "Subscribe to events", check:

    • Push - This is the primary event for your app
    • Pull request - Optional, if you want to comment on PRs when pushed
  5. Set Installation Permissions

    Under "Where can this GitHub App be installed?"

    • Select "Only on this account" for team-only use
    • Or select "Any account" if you plan to distribute it
  6. Create the App

    Click "Create GitHub App" at the bottom of the page.

  7. Generate a Private Key

    After creation, scroll down to "Private keys" and click "Generate a private key". A .pem file will download automatically. Store this securely - you'll need it to authenticate API requests.

  8. Note Your App ID

    At the top of the app settings page, note your App ID. You'll need this for authentication.

Step 2: Build the Webhook Handler

Create a server that receives push events and posts comments. Here's a complete Node.js implementation:

Project Setup

mkdir github-push-commenter
cd github-push-commenter
npm init -y
npm install express @octokit/app @octokit/rest crypto

Create the Server (server.js)

const express = require('express');
const crypto = require('crypto');
const { App } = require('@octokit/app');
const { Octokit } = require('@octokit/rest');
const fs = require('fs');

const app = express();
const PORT = process.env.PORT || 3000;

// Configuration - set these as environment variables
const APP_ID = process.env.GITHUB_APP_ID;
const PRIVATE_KEY = fs.readFileSync(process.env.PRIVATE_KEY_PATH, 'utf8');
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

// Initialize GitHub App
const githubApp = new App({
  appId: APP_ID,
  privateKey: PRIVATE_KEY,
});

// Middleware to parse JSON with raw body for signature verification
app.use(express.json({
  verify: (req, res, buf) => {
    req.rawBody = buf.toString('utf8');
  }
}));

// Verify webhook signature
function verifySignature(req) {
  const signature = req.headers['x-hub-signature-256'];
  if (!signature) return false;

  const hash = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(req.rawBody)
    .digest('hex');

  return signature === `sha256=${hash}`;
}

// Main webhook endpoint
app.post('/webhook', async (req, res) => {
  // Verify the webhook signature
  if (!verifySignature(req)) {
    console.error('Invalid signature');
    return res.status(401).send('Unauthorized');
  }

  const event = req.headers['x-github-event'];
  const payload = req.body;

  // Only handle push events
  if (event === 'push') {
    await handlePushEvent(payload);
  }

  res.status(200).send('OK');
});

// Handle push events
async function handlePushEvent(payload) {
  const {
    repository,
    pusher,
    ref,
    commits,
    installation,
  } = payload;

  console.log(`Push to ${repository.full_name} on ${ref} by ${pusher.name}`);
  console.log(`Commits: ${commits.length}`);

  // Get installation access token
  const octokit = await githubApp.getInstallationOctokit(installation.id);

  // Generate comment content based on the push
  const comment = generateComment(commits, pusher, ref);

  // Post comment on the latest commit
  const latestCommit = commits[commits.length - 1];

  try {
    await octokit.repos.createCommitComment({
      owner: repository.owner.login,
      repo: repository.name,
      commit_sha: latestCommit.id,
      body: comment,
    });

    console.log(`Posted comment on commit ${latestCommit.id.substring(0, 7)}`);
  } catch (error) {
    console.error('Error posting comment:', error.message);
  }
}

// Generate comment content
function generateComment(commits, pusher, ref) {
  const branch = ref.replace('refs/heads/', '');
  const commitCount = commits.length;
  const commitWord = commitCount === 1 ? 'commit' : 'commits';

  let comment = `šŸš€ **Push detected!**\n\n`;
  comment += `- **Author:** @${pusher.name}\n`;
  comment += `- **Branch:** \`${branch}\`\n`;
  comment += `- **Commits:** ${commitCount} ${commitWord}\n\n`;
  comment += `### Changes:\n`;

  commits.forEach((commit, index) => {
    const shortSha = commit.id.substring(0, 7);
    const message = commit.message.split('\n')[0]; // First line only
    comment += `${index + 1}. [\`${shortSha}\`](${commit.url}) - ${message}\n`;
  });

  comment += `\n---\n`;
  comment += `_Automated comment by Push Commenter_`;

  return comment;
}

// Health check endpoint
app.get('/health', (req, res) => {
  res.status(200).send('OK');
});

app.listen(PORT, () => {
  console.log(`Webhook server listening on port ${PORT}`);
});

Environment Variables

Create a .env file (never commit this!):

GITHUB_APP_ID=your_app_id
PRIVATE_KEY_PATH=./path-to-your-private-key.pem
WEBHOOK_SECRET=your_webhook_secret
PORT=3000

Add .gitignore

node_modules/
.env
*.pem

Step 3: Deploy Your Webhook Handler

Your webhook handler needs to be accessible from the internet. Here are several deployment options:

Option A: Vercel (Serverless)

  1. Install Vercel CLI: npm i -g vercel
  2. Create vercel.json:
    {
      "version": 2,
      "builds": [
        {
          "src": "server.js",
          "use": "@vercel/node"
        }
      ],
      "routes": [
        {
          "src": "/(.*)",
          "dest": "server.js"
        }
      ],
      "env": {
        "GITHUB_APP_ID": "@github-app-id",
        "WEBHOOK_SECRET": "@webhook-secret"
      }
    }
  3. Deploy: vercel --prod
  4. Add secrets: vercel secrets add github-app-id YOUR_APP_ID
  5. Upload private key as environment variable in Vercel dashboard

Option B: Heroku

  1. Install Heroku CLI and login
  2. Create Procfile:
    web: node server.js
  3. Create app: heroku create your-app-name
  4. Set config vars:
    heroku config:set GITHUB_APP_ID=your_app_id
    heroku config:set WEBHOOK_SECRET=your_secret
    heroku config:set PRIVATE_KEY="$(cat your-key.pem)"
  5. Deploy: git push heroku main

Option C: AWS Lambda

Use the Serverless Framework or AWS SAM to deploy the Express app as a Lambda function behind API Gateway.

Update Webhook URL

After deployment, update your GitHub App's webhook URL to point to your deployed endpoint:

Step 4: Install the App for Your Team

  1. Navigate to Installation Page

    Go to your GitHub App's public page:

    https://github.com/apps/YOUR_APP_NAME

  2. Click "Install"

    You'll be prompted to choose where to install the app.

  3. Select Your Organization

    Choose your team's organization from the list.

  4. Configure Repository Access

    Choose which repositories the app can access:

    • All repositories - App works on all current and future repos
    • Only select repositories - Choose specific repos from a list
  5. Review Permissions

    Review the permissions the app will have and click "Install".

  6. Authorize

    You may need to authorize with your GitHub password or 2FA.

Team Member Access

Once installed on your organization, the app will automatically work for all team members when they push to the configured repositories. Individual team members don't need to install anything - the app operates at the organization level.

Step 5: Test Your App

Make a Test Push

  1. Clone one of your configured repositories
  2. Make a small change (e.g., update a README)
  3. Commit and push:
    git add .
    git commit -m "Test push for GitHub app"
    git push origin main
  4. Check the latest commit on GitHub - you should see a comment posted by your app

Monitor Webhook Deliveries

In your GitHub App settings, go to "Advanced" → "Recent Deliveries" to see:

Check Server Logs

Monitor your webhook handler's logs for any errors:

Troubleshooting

Issue Possible Cause Solution
No webhook received Incorrect webhook URL Verify the URL in GitHub App settings points to your deployed server
401 Unauthorized Signature verification failed Check that WEBHOOK_SECRET matches what's configured in GitHub
Cannot post comments Insufficient permissions Ensure the app has "Read & write" access to Issues/Pull requests
Authentication errors Invalid private key or App ID Verify GITHUB_APP_ID and private key are correct
App not triggering on push Not subscribed to push events Check "Subscribe to events" includes "Push"
Works locally, fails in production Environment variables not set Verify all environment variables are configured on your hosting platform

Debugging Tips

Customization Ideas

Enhance your push commenter with these features:

1. Conditional Comments

Only comment on specific conditions:

// Only comment on main branch pushes
if (ref === 'refs/heads/main') {
  await postComment(...);
}

// Only comment on large pushes
if (commits.length > 5) {
  await postComment(...);
}

2. Code Analysis

Analyze commits and provide insights:

// Count files changed
const filesChanged = commits.reduce((acc, commit) => {
  return acc + commit.added.length + commit.modified.length + commit.removed.length;
}, 0);

comment += `\n- **Files changed:** ${filesChanged}`;

// Detect breaking changes
const hasBreakingChange = commits.some(c =>
  c.message.toLowerCase().includes('breaking')
);
if (hasBreakingChange) {
  comment += `\n\nāš ļø **WARNING:** This push may contain breaking changes!`;
}

3. Team Mentions

Automatically mention team members:

// Mention reviewers for production pushes
if (ref === 'refs/heads/production') {
  comment += `\n\n@teamlead @devops-team Please review this production push.`;
}

4. External Integrations

Connect to other tools:

5. Rich Formatting

Use GitHub's Markdown for better comments:

comment += `
## šŸ“Š Push Summary

| Metric | Value |
|--------|-------|
| Commits | ${commits.length} |
| Author | @${pusher.name} |
| Branch | \`${branch}\` |

### šŸ” Files Changed
${commits.map(c => c.modified.map(f => `- \`${f}\``).join('\n')).join('\n')}
`;

Security Best Practices

Additional Resources