Skip to content

sharpsir-group/github-watcher

Repository files navigation

Sharp Sotheby's International Realty

GitHub Watcher

Zero-dependency webhook server that auto-deploys your repos on git push.
No CI provider needed — just Node.js, a JSON config, and a server.

License Node >= 14 Zero Dependencies Stars

Node.js HMAC-SHA256 PM2 Apache GitHub Webhooks CloudFront Cloudflare Vite Lovable


Why

We build apps in Lovable, which syncs every change to a GitHub repo. GitHub Watcher bridges the gap between Lovable's cloud development and our self-hosted infrastructure: every time Lovable pushes to main, this server pulls the code, patches it for our subpath deployment (e.g. /hrms/, /pipeline/), builds it, and copies the output to the web server — all without touching the Lovable project files.

No GitHub Actions YAML, no build minutes to burn, no vendor lock-in. Just a single Node.js process, a JSON config, and a GitHub webhook.

The Problem

Scenario GitHub Actions GitHub Watcher
Build minutes Limited free tier, then paid Unlimited — your own CPU
Self-hosted deploy Needs SSH keys, runners, or third-party actions Built-in — deploys locally
Subpath SPA patching Custom scripts in YAML First-class preBuild config
CloudFront invalidation Extra action + AWS credentials in secrets Built-in, one config key
Cloudflare cache purge Extra action + API token in secrets Built-in, one config key
Webhook secret rotation Update repo settings + re-deploy secrets Edit .secrets, restart PM2
Debugging deploys Scroll through action logs in browser pm2 logs github-watcher or ./logs/

Features

  • Zero dependencies — runs on Node.js standard library only
  • Multi-repo — deploy any number of repositories from one instance
  • Branch filtering — only deploy pushes to the branch you care about
  • Signature verification — validates X-Hub-Signature-256 using HMAC-SHA256
  • Pre-build patching — apply find/replace patches before build, auto-reverted after
  • Post-deploy hooks — run arbitrary commands after deployment (restart services, notify, etc.)
  • CloudFront invalidation — optional CDN cache busting via AWS CLI
  • Cloudflare cache purge — optional edge + Worker Cache API purge via Cloudflare API
  • Deploy queue — concurrent pushes to the same repo are queued, not dropped
  • Deploy stamping — injects commit hash + timestamp into index.html for traceability
  • Health checkGET /health endpoint for uptime monitoring
  • PM2 ready — ships with an ecosystem.config.js for production process management

Architecture

sequenceDiagram
    participant L as Lovable
    participant GH as GitHub
    participant WH as webhook-server.js
    participant DS as deploy.sh
    participant CF as CloudFront
    participant CFL as Cloudflare

    L->>GH: git push
    GH->>WH: webhook POST
    WH->>WH: verify HMAC-SHA256
    WH->>DS: spawn

    DS->>DS: git pull
    DS->>DS: pre-build patches
    DS->>DS: build
    DS->>DS: copy to deploy path
    DS->>DS: stamp index.html
    DS->>DS: post-deploy hooks
    DS->>DS: revert patches
    DS->>CF: invalidation request
    CF-->>DS: invalidation created
    DS->>CFL: purge cache API
    CFL-->>DS: cache purged
    DS-->>WH: exit 0
Loading

Quick Start

1. Clone

git clone https://github.com/sharpsir-group/github-watcher.git
cd github-watcher

2. Configure

cp config.example.json config.json

Edit config.json with your repositories:

{
  "repos": {
    "your-org/your-repo": {
      "name": "My App",
      "localPath": "/home/deploy/your-repo",
      "deployPath": "/var/www/my-app",
      "branch": "main",
      "preBuild": [],
      "buildCmd": "npm install --include=dev && npm run build",
      "distFolder": "dist",
      "postDeploy": [],
      "cloudfront": {},
      "secret": "WEBHOOK_SECRET_MY_APP"
    }
  }
}

Note: Use npm install --include=dev instead of npm ci in buildCmd. PM2 sets NODE_ENV=production, which causes npm install / npm ci to skip devDependencies (including build tools like Vite). The --include=dev flag ensures they are always installed.

3. Create .env

cp .env.example .env
chmod 600 .env

Generate a webhook secret and add it:

openssl rand -hex 32
# Paste the output as WEBHOOK_SECRET_MY_APP= in .env

AWS and Cloudflare credentials go in the same file (see Configuration Reference below).

4. Clone your target repo

git clone git@github.com:your-org/your-repo.git /home/deploy/your-repo
mkdir -p /var/www/my-app

5. Start

# Direct
node webhook-server.js

# Or with PM2 (recommended)
pm2 start ecosystem.config.js
pm2 save

6. Add the webhook on GitHub

Go to your repository Settings > Webhooks > Add webhook:

Field Value
Payload URL http://your-server:9001/
Content type application/json
Secret The WEBHOOK_SECRET_* value from your .env file
Events Just the push event

Or use the GitHub CLI:

gh api repos/your-org/your-repo/hooks --method POST \
  -f 'name=web' \
  -f 'config[url]=https://your-server/webhook/github-watcher' \
  -f 'config[content_type]=json' \
  -f 'config[secret]=YOUR_SECRET_VALUE' \
  -f 'config[insecure_ssl]=0' \
  -f 'events[]=push' \
  -F 'active=true'

Configuration Reference

Repository Config (config.json)

Field Type Description
name string Display name used in logs
localPath string Absolute path to the cloned repository
deployPath string Where built files are copied to
branch string Only deploy pushes to this branch
preBuild array Find/replace patches applied before build (auto-reverted)
buildCmd string Shell command to build the project
distFolder string Build output directory (relative to repo root)
postDeploy array Shell commands to run after deployment
cloudfront object Optional CloudFront CDN invalidation config
cloudflare object Optional Cloudflare cache purge config
secret string Key name in .env for webhook signature verification

Pre-Build Patches

Patches let you modify source files before build without polluting your git history. They are automatically reverted after the build completes (or fails).

{
  "preBuild": [
    {
      "file": "vite.config.ts",
      "find": "export default defineConfig({",
      "replace": "export default defineConfig({\n  base: \"/app/\","
    },
    {
      "file": "src/lib/matrix-sso.ts",
      "find": "const BASE_PATH = '/matrix-apps-template'",
      "replace": "const BASE_PATH = '/app'"
    }
  ]
}
Deploying SPAs to a Subpath

When deploying a Vite + React Router app to a subpath (e.g. /app/), three patches are typically needed:

  1. Vite base — so asset URLs (JS, CSS, images) resolve correctly
  2. React Router basename — so the client-side router matches routes under the subpath
  3. SSO BASE_PATH — so OAuth redirect URIs point to the correct callback URL (Matrix SSO apps only)
{
  "preBuild": [
    {
      "file": "vite.config.ts",
      "find": "export default defineConfig(({ mode }) => ({",
      "replace": "export default defineConfig(({ mode }) => ({\n  base: \"/app/\","
    },
    {
      "file": "src/App.tsx",
      "find": "<BrowserRouter>",
      "replace": "<BrowserRouter basename=\"/app\">"
    },
    {
      "file": "src/lib/matrix-sso.ts",
      "find": "const BASE_PATH = '/matrix-apps-template'",
      "replace": "const BASE_PATH = '/app'"
    }
  ]
}

Without the basename patch, the app will load but the router will show a 404 because it doesn't know its routes are prefixed.

Without the BASE_PATH patch, the app will redirect to SSO login with the wrong redirect_uri (e.g. /matrix-apps-template/auth/callback instead of /app/auth/callback), causing an "Invalid redirect_uri" error after authentication.

CloudFront Invalidation

If your deploy path is behind a CloudFront distribution, configure automatic cache invalidation:

{
  "cloudfront": {
    "distributionId": "E1XXXXXXXXXX",
    "invalidationPaths": ["/*"]
  }
}

Requires AWS CLI installed and credentials in .env:

AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...
AWS_DEFAULT_REGION=us-east-1

Cloudflare Cache Purge

If your site uses a Cloudflare Worker for prerendering or caching, configure automatic cache purge after deploy:

{
  "cloudflare": {
    "zoneId": "your-zone-id",
    "purgeEverything": true,
    "apiTokenKey": "CF_API_TOKEN"
  }
}
Field Type Description
zoneId string Cloudflare zone ID for the domain
purgeEverything boolean When true, purges all cached content for the zone
apiTokenKey string Key name in .env whose value is the Cloudflare API token

The API token needs Zone > Cache Purge > Purge permission. Add it to .env:

CF_API_TOKEN=your-cloudflare-api-token

Repos without a cloudflare block are unaffected — the purge step is silently skipped.

API

Method Path Description
GET / or /health Health check — returns {"status":"ok"}
POST / Webhook receiver — accepts GitHub push events

Deployment Pipeline

When a valid push event is received, the deploy script runs these steps in order:

  1. Git pullfetch + reset --hard to the configured branch
  2. Pre-build patches — apply configured find/replace transformations
  3. Build — run the configured build command
  4. Deploy — copy build output to the deploy path
  5. Stamp — inject deploy timestamp and commit hash into index.html
  6. Post-deploy hooks — run any configured post-deploy commands
  7. Revert patches — restore patched files to their original state
  8. CloudFront invalidation — create CloudFront invalidation if configured
  9. Cloudflare cache purge — purge Cloudflare edge + Worker Cache API if configured

If the build fails at any step, patches are reverted and the deploy is aborted.

Reverse Proxy Setup

In production, place the webhook server behind a reverse proxy (Apache, Nginx) with TLS.

Apache

ProxyPass /webhook/github-watcher http://127.0.0.1:9001/
ProxyPassReverse /webhook/github-watcher http://127.0.0.1:9001/

GitHub webhook Payload URL: https://your-domain/webhook/github-watcher

SPA .htaccess

Each deploy path serving a single-page app needs an .htaccess for client-side routing:

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteBase /app/
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule . /app/index.html [L]
</IfModule>

<IfModule mod_headers.c>
    <FilesMatch "^index\.html$">
        Header set Cache-Control "no-cache, no-store, must-revalidate"
    </FilesMatch>
    <FilesMatch "\.(js|css|woff2)$">
        Header set Cache-Control "public, max-age=31536000, immutable"
    </FilesMatch>
</IfModule>

Note: deploy.sh runs rm -rf "$DEPLOY_PATH"/* before copying, but the * glob does not match dotfiles, so .htaccess survives redeploys.

Running with PM2

The included ecosystem.config.js is ready for production use:

pm2 start ecosystem.config.js
pm2 save
pm2 startup
pm2 monit
pm2 logs github-watcher

Manual Deploy

Trigger a deploy without a webhook:

./deploy.sh "your-org/your-repo"

File Structure

github-watcher/
├── webhook-server.js      # HTTP server — receives and validates webhooks
├── deploy.sh              # Build and deploy pipeline
├── config.json            # Repository configurations (git-ignored)
├── config.example.json    # Example configuration
├── ecosystem.config.js    # PM2 process manager config
├── package.json           # npm metadata and keywords
├── .env                   # All secrets and credentials (git-ignored, chmod 600)
├── .env.example           # Template for .env
├── logs/                  # Deployment logs (git-ignored)
└── README.md

Security

  • Webhook signatures are verified using HMAC-SHA256 (X-Hub-Signature-256)
  • All secrets and credentials live in a single .env file with 600 permissions
  • Sensitive files (.env, config.json, logs/) are git-ignored
  • Request body size is capped at 10 MB
  • The server binds to 0.0.0.0 — use a firewall or reverse proxy to restrict access

Who Is This For?

  • Lovable developers deploying to self-hosted infrastructure
  • Indie hackers who want CI/CD without GitHub Actions limits
  • Self-hosters who prefer control over third-party services
  • Teams deploying multiple Vite/React SPAs from one server

Requirements

  • Node.js >= 14
  • Git (on the server)
  • PM2 (optional, recommended for production)
  • AWS CLI (optional, only for CloudFront invalidation)

Contributing

See CONTRIBUTING.md for development setup and guidelines.

License

MIT


Part of the Sharp Matrix platform · sharpsir.group

About

Zero-dependency GitHub webhook server that auto-deploys your repos on git push. Built for Lovable + Vite SPAs on self-hosted infrastructure. No CI needed — just Node.js, a JSON config, and a server.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors