# Git & Repository Activity

> Notify your team about pull requests, merges, releases, and code reviews using GitHub webhooks, local git post-merge hooks, and CI release steps.

Source: https://docs.alphorn.dev/use-cases/git-activity/

Keep your team in the loop on repository activity without everyone watching GitHub. Route important events - merged PRs, new releases, review requests - to the channels where your team already communicates.

## GitHub Webhooks

GitHub can send webhook events straight to Alphorn. Pick the **GitHub** source preset when creating the webhook (under **Advanced: field templates**), or see [Webhook templates](/webhook-templates#github) for per-event templates. GitHub can't send an `Authorization` header, so create that webhook with **No API key (URL is the secret)**.

In GitHub (**Settings → Webhooks → Add webhook**):

- **Payload URL**: your Alphorn webhook URL
- **Content type**: `application/json` (GitHub's default, `application/x-www-form-urlencoded`, arrives as plain text, so the preset never applies)
- **Events**: the events you care about

A small proxy is still useful if you want to format each event type differently. The example below skips signature verification to stay short; Alphorn doesn't check `X-Hub-Signature-256`, so for a proxy that verifies it, see the [GitHub Webhooks proxy](/use-cases/github-webhooks#proxy-with-signature-verification-nodejs).

### Proxy example (Node.js)

```javascript
const express = require("express");
const app = express();
app.use(express.json()); // parses the JSON body into req.body

app.post("/github-webhook", (req, res) => {
  const event = req.headers["x-github-event"];
  const payload = req.body;
  let title, message, priority, tags;

  switch (event) {
    case "pull_request":
      if (payload.action === "opened") {
        title = `PR opened: ${payload.pull_request.title}`;
        message = `${payload.pull_request.user.login} opened #${payload.number} in ${payload.repository.full_name}`;
        priority = 2;
        tags = ["git", "pr", "opened"];
      } else if (payload.action === "closed" && payload.pull_request.merged) {
        title = `PR merged: ${payload.pull_request.title}`;
        message = `#${payload.number} merged by ${payload.pull_request.merged_by.login}`;
        priority = 2;
        tags = ["git", "pr", "merged"];
      } else {
        return res.sendStatus(200);
      }
      break;

    case "release":
      title = `Release: ${payload.release.tag_name}`;
      message = `${payload.repository.full_name} - ${payload.release.name || "New release"}`;
      priority = 3;
      tags = ["git", "release"];
      break;

    case "push":
      if (payload.ref === "refs/heads/main") {
        title = `Push to main: ${payload.repository.name}`;
        message = `${payload.commits.length} commit(s) by ${payload.pusher.name}`;
        priority = 2;
        tags = ["git", "push", "main"];
      } else {
        return res.sendStatus(200);
      }
      break;

    default:
      return res.sendStatus(200);
  }

  fetch("https://app.alphorn.dev/n/V1StGXR8_Z5jdHi6B-myT", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.ALPHORN_API_KEY}`,
    },
    body: JSON.stringify({ title, message, priority, tags }),
  }).catch((err) => console.error("Forwarding to Alphorn failed:", err));

  res.sendStatus(200);
});

app.listen(8080);
```

## Post-merge hook (local)

Notify after merging to main locally. The hook needs `jq` and `curl`, and `ALPHORN_API_KEY` must be exported in the shell you run git from (or replace the `alp_...` fallback with your key):

```bash title=".git/hooks/post-merge"
#!/bin/bash
BRANCH=$(git rev-parse --abbrev-ref HEAD)

if [ "$BRANCH" = "main" ]; then
  COMMIT=$(git log -1 --pretty=format:"%s (%an)")
  jq -n --arg m "$COMMIT" \
    '{title: "Merged to main", message: $m, priority: 2, tags: ["git", "merge", "main"]}' |
  curl -fsS -X POST "https://app.alphorn.dev/n/V1StGXR8_Z5jdHi6B-myT" \
    -H "Authorization: Bearer ${ALPHORN_API_KEY:-alp_...}" \
    -H "Content-Type: application/json" \
    --data-binary @-
fi
```

Git only runs executable hooks, so make it executable with `chmod +x .git/hooks/post-merge`.

## Release notifications (CI)

After creating a Git tag, notify the team:

```bash
VERSION=$(git describe --tags --exact-match 2>/dev/null)

if [ -n "$VERSION" ]; then
  PREV=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null)
  # First tag: no previous tag, so use the full history
  CHANGELOG=$(git log --oneline ${PREV:+"$PREV..HEAD"} | head -10)
  # jq escapes the multi-line changelog into valid JSON
  jq -n --arg v "$VERSION" --arg c "$CHANGELOG" \
    '{title: "Release \($v)", message: "Changes:\n\($c)", priority: 3, tags: ["git", "release", $v]}' |
  curl -fsS -X POST "https://app.alphorn.dev/n/V1StGXR8_Z5jdHi6B-myT" \
    -H "Authorization: Bearer $ALPHORN_API_KEY" \
    -H "Content-Type: application/json" \
    --data-binary @-
fi
```

## Routing examples

| Channel | Filter | Purpose |
|---|---|---|
| Slack (#dev) | Tags has all of `pr`, `merged` | Merged PRs only |
| Slack (#releases) | Tags has any of `release` | New releases |
| Discord | Tags has any of `git` | All repository activity |
| Email | Tags has any of `release` | Release archive |
