n8n GitHub Issue Automation
Webhook-triggered GitHub issue processing via n8n + Cloudflare Tunnel + local Claude agent.
Overview
When a GitHub issue receives the label auto or auto-local, a webhook fires to n8n on the Hostinger VPS. n8n routes the event to the appropriate worker environment:
auto-local— tunnelled to the local dev machine (Windows 10) via Cloudflare Tunnel, where a Python listener spawnsclaude /process-issues-remote.auto— sent directly to the claude-proxy service on vps-h1, which runsclaude -p "process issue #N".
Both paths use Supabase agent_sessions with claim_task() (FOR UPDATE SKIP LOCKED) to prevent double-processing when multiple environments are active.
Architecture
GitHub issue labeled "auto" / "auto-local"
│
└─→ POST → n8n.vps-h1 /webhook/github-issues
│ responds 200 immediately
│
├── label = "auto-local"
│ └─→ Cloudflare Tunnel → localhost:3500 (local-listener.py)
│ ├─ INSERT agent_sessions (worker_env='local')
│ └─ spawn: claude /process-issues-remote
│ └─ claim_task() [FOR UPDATE SKIP LOCKED]
│ └─ parallel worktrees (MAX=4)
│
└── label = "auto"
└─→ claude-proxy:9999 (vps-h1)
└─ claude -p "process issue #N"
Label Routing
| Label | Routes to | worker_env |
|---|---|---|
auto | VPS claude-proxy (vps-h1, port 9999) | vps-h1 |
auto-local | Local dev machine via Cloudflare Tunnel (port 3500) | local |
Supabase Coordination
agent_sessions table
Every pipeline run inserts a row into agent_sessions before spawning Claude. This provides:
- Visibility — active/completed sessions visible in Supabase dashboard
- Deduplication —
claim_task()usesFOR UPDATE SKIP LOCKEDso concurrent workers skip already-claimed issues - Capacity cap —
MAX_PARALLEL=4enforced by countingstatus='active'rows before accepting new work
worker_env column
The worker_env column (type text) identifies which environment created the session.
| Value | Environment |
|---|---|
local | Local dev machine (Windows 10, radieu) |
vps-h1 | Hostinger VPS — claude-proxy |
vps-i1 | IONOS VPS — AI-Dev-IO1 |
ci | GitHub Actions runner |
The column was added by migration 002_agent_worker_env.sql (see file inventory below).
claim_task() flow
-- Simplified pseudocode
BEGIN;
SELECT id FROM agent_sessions
WHERE status = 'pending'
LIMIT 1
FOR UPDATE SKIP LOCKED;
-- If row found AND active_count < MAX_PARALLEL:
UPDATE agent_sessions SET status = 'active', worker_env = $1 WHERE id = $2;
COMMIT;If MAX_PARALLEL (4) active sessions already exist, the caller receives no row and must back off.
File Inventory
| File | Purpose |
|---|---|
monitoring/supabase/migrations/002_agent_worker_env.sql | Adds worker_env column to agent_sessions |
infra-src/n8n-workflows/github-auto-trigger.json | n8n workflow — import via n8n UI |
infra-src/github-agent/local-listener.py | Local HTTP server on port 3500 |
infra-src/github-agent/cloudflared-config.yml.template | Cloudflare Tunnel config template |
scripts/setup-cloudflare-tunnel.ps1 | One-shot tunnel setup (idempotent, Windows PowerShell) |
Setup Checklist
Complete these steps in order on first setup.
- Apply SQL migration — run
002_agent_worker_env.sqlagainst the Supabase project (mwkqmgadqnkkihjdeqsi) via the Supabase SQL editor orsupabase db push - Configure Cloudflare Tunnel — from the local dev machine:
cloudflared tunnel login # one-time browser auth .\scripts\setup-cloudflare-tunnel.ps1 - Start the tunnel (keep running in a terminal or add as a Windows service):
cloudflared tunnel --config "$env:USERPROFILE\.cloudflared\local-dev-config.yml" run local-dev - Set environment variables and start the local listener:
$env:PROXY_SECRET = "your-secret" # must match n8n GITHUB_TRIGGER_SECRET $env:SUPABASE_URL = "https://mwkqmgadqnkkihjdeqsi.supabase.co" $env:SUPABASE_SERVICE_ROLE_KEY = "your-service-role-key" python infra-src/github-agent/local-listener.py - Import n8n workflow — in the n8n UI on vps-h1, import
infra-src/n8n-workflows/github-auto-trigger.json; set theGITHUB_TRIGGER_SECRETcredential/env var to matchPROXY_SECRET; activate the workflow - Register GitHub webhook — in
radieu/p24-infra→ Settings → Webhooks → Add webhook:- URL:
https://n8n.vps-h1.infra.zintegrowana.online/webhook/github-issues - Content type:
application/json - Events: Issues only
- URL:
- Smoke test — create a throwaway issue in
radieu/p24-infra, add labelauto-local, then verify:- HTTP 202 logged by
local-listener.py - New row in
agent_sessionstable (Supabase dashboard → Table Editor →agent_sessions) claude /process-issues-remoteprocess spawned (visible in Task Manager /ps aux)
- HTTP 202 logged by
Environment Variables
Required by infra-src/github-agent/local-listener.py:
| Variable | Description |
|---|---|
PROXY_SECRET | Shared secret validated against the X-Proxy-Secret header sent by n8n. Must match the value configured in the n8n workflow. |
SUPABASE_URL | Full Supabase project URL, e.g. https://mwkqmgadqnkkihjdeqsi.supabase.co |
SUPABASE_SERVICE_ROLE_KEY | Supabase service_role key — required for server-side INSERT into agent_sessions (bypasses RLS) |
The listener will exit immediately at startup if any of these variables are absent.