GitHub Integration — Setup Guide
Phase 49.3 (Light) — webhook-based notifications + UI sidebar feed for linked GitHub repos. No bidirectional sync (that is Phase 49.5+ Heavy).
ARC OS receives webhook events from linked GitHub repos and simultaneously:
- Sends a Telegram notification to the project owner
- Updates the sidebar feed in the Workspace ContextRail (30s polling)
Architecture
GitHub repo
│ (push / PR / CI / issue events)
▼
arc-os.co/api/webhooks/github
│ HMAC-SHA256 timing-safe verify (X-Hub-Signature-256)
│ Rate limit 100 req/min/project
│ Payload max 50KB
▼
shared/routes/github.ts handleGithubWebhook
├─→ DB: github_events (for the UI feed)
└─→ shared/github-notifier.ts → Telegram (master bot token)
Supported events: push, pull_request, workflow_run, issues. The rest (releases, deployments, discussions) are ignored.
Multi-repo: one project can link multiple repos (e.g. frontend + backend + docs). Constraint: UNIQUE(project_name, owner, repo).
Quick setup (3 minutes)
1. Generate the webhook URL+secret
arc github link <project-name> <owner/repo>
Example:
arc github link arc-v2 SerhiiInUa/citadel-v2
The CLI returns:
- Webhook URL —
https://arc-os.co/api/webhooks/github - Secret — 32-byte hex (unique per link)
- Step-by-step instructions for the GitHub UI
2. Add the webhook in the GitHub repo
On the page https://github.com/<owner>/<repo>/settings/hooks:
- Click Add webhook
- Payload URL:
https://arc-os.co/api/webhooks/github - Content type:
application/json(important!) - Secret: paste the value from the CLI output
- Which events? → "Let me select individual events":
- ☑ Pushes
- ☑ Pull requests
- ☑ Workflow runs
- ☑ Issues
- ☑ Active
- Click Add webhook
GitHub immediately sends a test ping event — it will be silently rejected (since ARC only expects supported types). This is normal.
3. Verify it works
Push to the repo, open a PR, or run a workflow. Within ~1-3 seconds:
- A Telegram notification arrives for the project owner with icon + summary + GitHub link
- The sidebar feed in the Workspace ContextRail updates (via ~30s polling)
Managing links
List linked repos
arc github links arc-v2
Remove a link
arc github unlink arc-v2 <id>
The ID comes from the arc github links output. The webhook on GitHub is not deleted automatically — invalid signatures will be silently rejected. Better workflow: first delete the webhook in the GitHub UI, then arc github unlink.
Security
- HMAC-SHA256 signature — every webhook secret =
crypto.randomBytes(32).toString('hex')(256 bits of entropy) - Timing-safe compare —
node:crypto timingSafeEqualprevents timing attacks on signature verification - Silent reject — invalid signatures get
401 ""with no body (no info leak to scanners) - Rate limit — 100 req/min per project (in-memory window). Exceeding it →
429 Rate limited - Payload size limit — 50KB in the handler, 64KB in nginx (DoS protection)
- Multi-repo signature routing — the handler looks up candidates by
repository.full_name, then verifies each one's signature before accepting (prevents cross-project secret reuse) - Public endpoint isolation — the nginx config for
/api/webhooks/githubhasauth_basic offandproxy_read_timeout 5s
What you see in the sidebar
ContextRail (right panel in the Workspace) shows the GitHub section ⤵
- Auto-hidden if the project has no linked repos
- Last 8 events (newest first)
- Per-event icon: GitBranch (push) / GitPullRequest (PR) / CircleCheck (CI success) / CircleAlert (CI failure) / CircleDot (issues)
- Relative time format: 30s, 5m, 2h, 1d
- Click a row → opens the GitHub URL in a new tab
- Polling every 30s (fresh data without manual refresh)
Telegram messages
The master bot sends a formatted message to the project owner:
🔀 [arc-v2] SerhiiInUa/citadel-v2
PR #42 opened: feat: add lazy worker lifecycle by @Sergei89
View on GitHub
Icons:
- 📦 push
- 🔀 pull_request
- ✅ workflow_run (success)
- ❌ workflow_run (failure)
- ⚙️ workflow_run (other)
- 🐛 issues
Troubleshooting
Webhook returns 401 on the test ping
Expected — ARC silently rejects unsupported events (e.g. ping). Only production events (push, PR, workflow_run, issues) are processed.
Webhook returns 401 on a push event
- Check that the GitHub webhook is configured with
Content type: application/json(notform-urlencoded) - Check the secret — it must exactly match what
arc github linkreturned - If the secret is lost — delete the link and create it again (
arc github unlink→arc github link)
Sidebar feed is empty although Telegram notifications arrive
- Check that the ContextRail is visible at all (≥1280px viewport)
- Hard refresh the frontend (Ctrl+Shift+R) — the component is cached
- Check the DB:
sqlite3 data/citadel.db "SELECT COUNT(*) FROM github_events WHERE project_name='<name>';"
Telegram notifications don't arrive
- Check that the project has an
owner_idin the DB:sqlite3 ... "SELECT name, owner_id FROM projects;" - Check the master bot token in the vault:
grep MASTER_BOT_TOKEN config/vault.json(must be encrypted) - The webhook event is in the DB but notify failed → check the master logs:
tmux capture-pane -t citadel-master -p | grep github-notifier
Rate limit hit
Hard limit 100 req/min/project. For CI with thousands of runners — raise it in shared/routes/github.ts:RATE_MAX.
API endpoints
Details: API Reference.
| Endpoint | Auth | Description |
|---|---|---|
POST /api/crm/projects/:name/github |
JWT | Link repo |
GET /api/crm/projects/:name/github |
JWT | List links |
DELETE /api/crm/projects/:name/github/:id |
JWT | Unlink |
GET /api/crm/projects/:name/github/events?limit=N |
JWT | List events (max 200) |
POST /api/webhooks/github |
HMAC | Public receiver |
Roadmap
| Phase | Scope | Status |
|---|---|---|
| 49.3 | Webhook receiver + Telegram | ✅ DONE |
| 49.3.1 | UI sidebar feed | ✅ DONE |
| 49.4 | API polling, Workspace GitHub tab, multi-repo dashboard | BACKLOG (P2) |
| 49.5 | Bidirectional issue sync, auto-PR review workers | BACKLOG (P2) |