The registry reserves 20000-20099 for Gitea and requires new blocks to come from the 21500-21999 free pool, claimed only once a service is actually listening. Also corrects the clone URL to fisher/my-cd-webhook.
my-cd-webhook
Fleet-wide continuous deployment built on adnanh/webhook, replacing the per-project deploy scripts and hand-rolled webhook receivers that used to live inside each application repository.
This repository is checked out on every deploying host (currently s4 and s5)
and updated independently of the projects it deploys.
Why this exists
Every project had grown its own deployment mechanism, and each one was a slightly different 200-line bash script plus, in some cases, a bespoke Python HTTP server. They shared no code, drifted apart, and could only be changed by committing to the application repository and redeploying it — which is exactly the thing that is hardest to do when deployment is broken.
Concretely, the patterns this replaces:
| Project | What was there | Problem |
|---|---|---|
webdev/paradicsomleves |
scripts/webhook.py (275 lines), webhook-install.sh, webhook-uninstall.sh |
A whole HTTP server, HMAC implementation, systemd watchdog integration and unit generator, maintained inside an image-gallery app. Removed in 660a309. |
webdev/domaindingo |
scripts/deploy-{dev,test,prod}.sh |
Three near-identical scripts, run by hand. A hostname != s2 guard that exit 0s on mismatch — so a wrong-host run reports success — and which is now simply wrong, because test and prod both moved to s5. deploy-prod.sh has no host guard at all. |
webdev/grindex |
nothing | The pipeline stops at "image pushed"; deployment is a human running docker compose up -d on s4. |
The webhook was never the problem. A self-contained webhook, reimplemented per project, was.
Design
One manifest. targets.json maps (repo, branch) onto a
host, an environment, a compose stack and an image. It is the only file that
knows this, and adding an environment is an edit here — never a change to an
application repository.
Host-aware by construction. Every host runs the same daemon from the same
checkout and acts only on the targets whose host matches. A push for another
host's target is a logged no-op, never a silent success. Host matching accepts
both s5 and s5.fisher.hu, because the fleet uses both.
Deploys are immutable and provenance-checked. The receiver deploys the
<branch>-sha-<short7> tag, verifies the image's
org.opencontainers.image.revision label equals the commit that triggered it,
then re-pins to the resolved digest before handing it to compose. Mutable tags
have already drifted once in this fleet and served a broken build for months.
It never touches git. The compose stacks live in sysadmin/uas-ng and are
refreshed by that repository's own updater timer. A deploy script that also runs
git reset --hard is doing two jobs badly.
It never runs docker compose down. up -d recreates exactly the services
whose image changed. The old test script used down -v, which destroys data
volumes.
Secrets stay on the host. secrets.env (mode 0600) holds one webhook secret
per repository. The rendered hooks file is generated on the host and is never
committed.
Layout
targets.json the manifest — the single source of truth
bin/cd-target manifest queries (resolve / list / host-config)
bin/cd-deploy the one generic deploy script
bin/cd-render-hooks targets.json -> adnanh/webhook hooks.json for this host
bin/cd-status what is this host deploying, and is it healthy
etc/cd-webhook.service systemd user unit template
install/install.sh idempotent installer
install/uninstall.sh removal (--purge also drops secrets and logs)
secrets.env.example template for the host-local secrets file
How a deploy runs
git push origin test
│
▼
Gitea push webhook ──► http://10.255.255.1:21600/hooks/webdev-domaindingo
│ (WireGuard only; never exposed publicly)
▼
adnanh/webhook verify HMAC-SHA256, require X-Gitea-Event: push,
require a ref we actually deploy
│
▼
bin/cd-deploy --repo webdev/domaindingo --ref refs/heads/test --sha <40-hex>
│
├─ resolve (repo, branch, this host) in targets.json ──► no match? log, exit 0
├─ take the per-target lock
├─ poll the registry for <branch>-sha-<short7> (CI is still building)
├─ verify the revision label matches the pushed commit
├─ resolve tag -> digest, and deploy the digest
├─ docker compose up -d
├─ health check
└─ roll back to the previous digest if health fails
│
▼
ntfy
Installing on a host
Requires adnanh/webhook on PATH (or WEBHOOK_BIN set), plus docker,
python3, curl and flock.
git clone ssh://git@gitea.fisher.hu:2221/fisher/my-cd-webhook.git ~/S/my-cd-webhook
cd ~/S/my-cd-webhook
./install/install.sh # creates the secrets template on the first run
$EDITOR ~/.config/cd-webhook/secrets.env
./install/install.sh # renders hooks, installs and starts the service
The installer is idempotent — re-run it after editing targets.json, rotating a
secret, or pulling a new version of this repository.
It prints the exact webhook URL for each repository at the end. In Gitea, under Repository → Settings → Webhooks → Add Webhook → Gitea:
| Field | Value |
|---|---|
| Target URL | http://<host wg ip>:21600/hooks/<id> |
| HTTP Method | POST |
| POST Content Type | application/json |
| Secret | the matching value from secrets.env |
| Trigger On | Push Events |
| Branch filter | * (branch selection lives in targets.json) |
Adding a target
Add an object to targets.json and re-run install/install.sh on the owning
host. Nothing else. If the repository is new to that host, add its secret to
secrets.env first and configure the webhook in Gitea.
Fields: name, enabled, repo, branch, env, host, stack_dir,
compose_file, compose_project, container, image_repo,
image_tag_template, image_env_var, pull_policy_env_var, health_url,
health_expect_key, health_expect_value, rollback_on_failure.
image_tag_template understands {branch}, {env}, {sha}, {short7} and
{short12}.
Current targets
| Target | Repo | Branch | Host | Route |
|---|---|---|---|---|
domaindingo-test |
webdev/domaindingo |
test |
s5 | ddt.fisher.hu |
domaindingo-prod |
webdev/domaindingo |
prod |
s5 | dd.fisher.hu |
Testing without deploying
./bin/cd-status # health of this host's receiver
./bin/cd-target list --host s5.fisher.hu # what s5 would deploy
./bin/cd-render-hooks --host s5.fisher.hu --redact # the hooks file, secrets masked
./bin/cd-deploy --repo webdev/domaindingo --ref refs/heads/test \
--sha $(git rev-parse HEAD) --dry-run
CD_TARGETS_FILE points the tooling at a scratch manifest for testing. It
changes which manifest is read, never which host this machine may act as.
Not yet done
- Port
21600is proposed, not allocated. The Port registry's rule is to claim a block only once the service is deployed and listening, as part of the same change.21600-21699is the lowest free block in the21500-21999pool; verify it is still free on the host (ss -lntup) when installing, then add both the block row and a per-project table to the registry. - Gitea (s5) must be able to reach s4 on
10.255.255.12:21600over WireGuard. Verify before adding grindex. adnanh/webhookis not installed on s4 or s5. Nothing runs yet.- grindex is deliberately absent from the manifest. Its stack lives at
/home/fisher/S/traefik-systems/grindex/on s4, which is not in git and whose compose project names have not been verified on the host. Adding it from guesswork would be exactly the sloppiness this repo exists to remove. webdev/domaindingostill carriesscripts/deploy-*.sh. They should be deleted there once this is live, the way paradicsomleves' were.