Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

git-deployer

A tiny, self-hosted CI/CD daemon — turns a GitHub push into a deploy.sh run on your server, with Discord notifications and structured logs.

  • One Go binary, no runtime dependencies.
  • HMAC-verified GitHub webhooks (X-Hub-Signature-256).
  • Built-in git fetch + reset --hard <sha> before invoking your script, so deploy scripts never have to handle their own clone logic.
  • Per-project mutex: pushes to the same project queue serially; pushes to different projects run in parallel.
  • Discord embed for each deploy: blurple while running, green on success, red on failure, with the last 1.4 KB of the deploy log inline.

Designed to scale from one repo to dozens; one process, one config file, one webhook per repo.


Table of contents


How it works

GitHub repo  ──push──►  https://<your-host>/webhook/<project>
                                    │
                                    ▼
                  HMAC-verify body against WEBHOOK_SECRET
                                    │
                                    ▼
                git fetch --prune && git reset --hard <sha>
                                    │
                                    ▼
              bash <project.dir>/<deploy_script>   ──── stdout/stderr ─►  /var/log/git-deployer/<project>-<ts>.log
                                    │
                                    ▼
                          Discord webhook
                       (running → ok / failed)

The handler:

  1. Validates the X-Hub-Signature-256 HMAC.
  2. Returns 202 Accepted immediately.
  3. Spawns a goroutine that:
    • Acquires the project mutex.
    • Posts a "🚀 Deploying" embed to Discord (blurple).
    • Runs git fetch + reset --hard <sha> inside the project's working directory (skippable with skip_fetch: true).
    • Execs bash <deploy_script> with cwd = project.dir.
    • Captures full stdout/stderr to a log file plus tail.
    • Posts a "✅ Deployed" or "❌ Deploy failed" embed.

Only push events to the configured branch trigger a deploy. ping returns 200. Any other event is acked and ignored.


Quick start

The fastest path: standalone Linux box, single project, fronted by nginx.

# 1. Build the binary
go install github.com/mrvcoder/git-deployer/cmd/git-deployer@latest
sudo install -m 0755 "$(go env GOPATH)/bin/git-deployer" /usr/local/bin/git-deployer

# 2. Create config dirs
sudo mkdir -p /etc/git-deployer /var/log/git-deployer

# 3. Drop in your config.json (see below)
sudo cp config.example.json /etc/git-deployer/config.json
sudo $EDITOR /etc/git-deployer/config.json

# 4. Drop in your .env with secrets
sudo cp .env.example /etc/git-deployer/.env
sudo chmod 600 /etc/git-deployer/.env
sudo $EDITOR /etc/git-deployer/.env
#   set WEBHOOK_SECRET to a long random string
#   set GITHUB_TOKEN if any project is a private repo
#   set DISCORD_WEBHOOK_URL if you want notifications

# 5. Install + start the systemd unit
sudo cp deploy/systemd/git-deployer.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now git-deployer
sudo systemctl status git-deployer

# 6. Front it with nginx (or Caddy / Traefik / whatever)
sudo cp deploy/nginx/git-deployer.conf /etc/nginx/conf.d/
sudo sed -i 's/git\.example\.com/<your-real-host>/' /etc/nginx/conf.d/git-deployer.conf
sudo nginx -t && sudo systemctl reload nginx

Now point a GitHub webhook at https://<your-host>/webhook/<project-name> (see Adding a new project).


Configuration

git-deployer reads runtime state from two files:

  • /etc/git-deployer/config.json — the project list. Public-safe; can be committed.
  • /etc/git-deployer/.env — secrets (WEBHOOK_SECRET, GITHUB_TOKEN, DISCORD_WEBHOOK_URL). 0600, never commit.

Both paths are overridable with -config / -env flags.

config.json

{
  "projects": [
    {
      "name": "my-app",
      "repo": "your-org/my-app",
      "branch": "main",
      "dir":  "/srv/my-app",
      "deploy_script": "deploy.sh",
      "skip_fetch": false
    }
  ]
}
Field Required Default Notes
name yes URL slug for the webhook (/webhook/<name>). Also the project mutex key.
repo yes owner/name on GitHub. Used to build the clone URL and link to commits in Discord.
branch no main Only pushes to this ref fire a deploy.
dir yes Absolute path on disk. The deploy script runs here.
deploy_script no deploy.sh File inside dir to exec.
skip_fetch no false Set true if your deploy.sh handles its own clone/checkout. Otherwise the deployer does it.

.env

WEBHOOK_SECRET=replace-with-a-long-random-string
DISCORD_WEBHOOK_URL=
GITHUB_TOKEN=
DEPLOYER_HOST=
LISTEN_ADDR=127.0.0.1:8081
LOG_DIR=/var/log/git-deployer
Variable Required Default Notes
WEBHOOK_SECRET yes Shared secret with GitHub. Generate: openssl rand -hex 32.
DISCORD_WEBHOOK_URL no (disabled) Set to a Discord channel webhook URL to receive embeds. Empty = no notifications.
GITHUB_TOKEN no (public-only) PAT (fine-grained or classic). Required for private-repo clones. Exposed as $GITHUB_TOKEN to deploy.sh.
DEPLOYER_HOST no OS hostname Brand shown on / and in Discord footers (e.g. git.example.com).
LISTEN_ADDR no 127.0.0.1:8081 Bind address. Front it with nginx / Caddy.
LOG_DIR no /var/log/git-deployer One file per deploy run: <project>-<UTC-timestamp>.log.

Per-project deploy.sh

Lives at <project.dir>/<deploy_script>. Executed with cwd = project.dir. The deployer exports:

Var Meaning
GIT_REPO <owner>/<name> — matches config.json
GIT_BRANCH branch from config
GIT_REF refs/heads/<branch>
GIT_SHA commit being deployed (payload.after)
GIT_PUSHER GitHub username that pushed
GITHUB_TOKEN the PAT from .env, if set — useful for npm install of private packages, etc.

By default git fetch + reset --hard $GIT_SHA && git clean -fd runs before deploy.sh, so when your script starts the working tree already matches the pushed commit. The remote URL is reset to the plain (no-token) HTTPS form afterwards so the PAT is never persisted on disk.

Exit 0 → green Discord embed. Anything else → red.

Minimal deploy.sh for a Node app:

#!/usr/bin/env bash
set -euo pipefail
echo "[deploy] building ${GIT_SHA:-HEAD}"
npm ci --no-audit --no-fund
npm run build
pm2 reload my-app --update-env

Adding a new project

  1. Drop a deploy.sh at the repo root. Make it executable in repo, or invoke it via bash; the deployer uses bash <script>.

  2. Make sure the project dir exists on the server:

    sudo mkdir -p /srv/my-app
    sudo chown <runtime-user>:<runtime-user> /srv/my-app   # if not running as root
  3. Add an entry to /etc/git-deployer/config.json and reload:

    sudo systemctl restart git-deployer
  4. Create the GitHub webhook — repository → Settings → Webhooks → Add webhook:

    • Payload URL: https://<your-host>/webhook/<project-name>
    • Content type: application/json
    • Secret: the same value as WEBHOOK_SECRET in /etc/git-deployer/.env
    • SSL verification: Enable
    • Events: "Just the push event"
  5. Push a commit and watch:

    • journalctl -u git-deployer -f for service logs
    • tail -f /var/log/git-deployer/<project>-*.log for the deploy stdout
    • Your Discord channel for the embed

If the first push fails:

  • 403/bad signature → secret mismatch between GitHub and .env
  • 404 unknown projectname in config.json doesn't match the URL slug
  • deploy script exit 1 → check the linked log file in the Discord embed footer

Self-hosting checklist

  • git-deployer running under systemd (systemctl is-active git-deployer)
  • HTTPS-fronted by nginx / Caddy / Traefik
  • /etc/git-deployer/.env mode 0600, owned by service user
  • LISTEN_ADDR=127.0.0.1:8081 (don't expose the Go HTTP port directly)
  • WEBHOOK_SECRET is high-entropy random (≥32 hex chars)
  • Service user can git, bash, and run whatever your deploy scripts need
  • Log dir on a partition that won't fill up (or set up logrotate on /var/log/git-deployer/*.log)
  • Each project's GitHub webhook uses the same secret as .env

A simple logrotate snippet:

/var/log/git-deployer/*.log {
    weekly
    rotate 8
    compress
    missingok
    notifempty
    copytruncate
}

Endpoints

Method Path Behavior
GET / Plain-text status + project list.
GET /health Returns ok (200). Use for nginx upstream / monitoring.
POST /webhook/<project> GitHub push webhook. HMAC-verified. Returns 202 and runs in goroutine.

Webhook behaviour:

  • X-GitHub-Event: ping200 {"ok":true,"pong":<delivery-id>}.
  • X-GitHub-Event: push, branch matches → 202 {"ok":true,...}, deploy fires asynchronously.
  • X-GitHub-Event: push, branch mismatch → 200 {"skipped":"ref mismatch",...}.
  • Any other event → 202 {"ok":true,"skipped":"<event>"}.
  • Bad HMAC → 401 bad signature (logged with remote IP).
  • Unknown project name → 404 unknown project "...".

Security notes

  • HMAC verification is mandatory. The handler rejects any POST whose X-Hub-Signature-256 doesn't match hmac_sha256(WEBHOOK_SECRET, raw_body).
  • The PAT is never persisted in remote.origin.url. The deployer uses a one-shot x-access-token:<pat>@github.com/... URL on the git fetch command only; afterwards it resets origin to the plain HTTPS URL.
  • Deploy logs may contain secrets if your deploy.sh echoes them. Treat /var/log/git-deployer/ as sensitive.
  • deploy.sh runs as the service user (default root in the shipped unit file). Run as a less-privileged user if your scripts don't need root.
  • Body size is capped at 1 MiB. GitHub push payloads are well below this; the cap blocks accidental DoS via large bogus POSTs.
  • The legacy GitHub X-Hub-Signature (sha1) is not accepted — only X-Hub-Signature-256.

Development

git clone https://github.com/mrvcoder/git-deployer
cd git-deployer
go build ./cmd/git-deployer

# Run with example config + your own .env
./git-deployer -config ./config.example.json -env ./.env

Repository layout:

cmd/git-deployer/main.go        # binary entrypoint
internal/config/                # config.json + .env loader
internal/server/                # http handlers, HMAC verify, webhook router
internal/deploy/                # per-project runner, git sync, log tailer
internal/discord/               # webhook embed sender
deploy/systemd/                 # example systemd unit
deploy/nginx/                   # example nginx vhost
deploy.sh                       # self-deploy script for git-deployer itself
config.example.json
.env.example

The webhook listener is in internal/server/server.go — HMAC verification, push payload parsing, branch matching, and the project lookup happen there. The actual git sync and script exec live in internal/deploy/deploy.go. Both files are small (~250 lines each) and self-contained.

PRs welcome.

License

MIT — see LICENSE.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages