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.
179 lines
8.0 KiB
Markdown
179 lines
8.0 KiB
Markdown
# my-cd-webhook
|
|
|
|
Fleet-wide continuous deployment built on [adnanh/webhook](https://github.com/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 0`s 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`](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`.
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
./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 `21600` is 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-21699` is the lowest free block in the `21500-21999` pool;
|
|
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:21600`** over
|
|
WireGuard. Verify before adding grindex.
|
|
- **`adnanh/webhook` is 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/domaindingo` still carries `scripts/deploy-*.sh`.** They should be
|
|
deleted there once this is live, the way paradicsomleves' were.
|