Bot hosting: releases are self-contained CI bundles, and the bot serves its own web UI
The image no longer installs the bot's dependencies: its CI builds one tar.gz holding the app plus a
vendor/ directory of prebuilt wheels, and PYTHONPATH=/app/vendor makes the code import them. A new
dependency therefore needs no image rebuild and the server contacts no package index.
- deployBotBundle.sh: install / --latest / --status / --rollback, verifies every file against BUILD.json
and keeps the previous release beside the live one.
- compose: mount ${BOT_APP_DIR:-/srv/bot/app} at /app, set DB_BACKEND, DATA_DIR, WEB_ALLOWED_GROUPS,
WEB_SERVE_UI and PYTHONPATH. bot/app placeholder untracked; install.sh creates /srv/bot/app.
- The placeholder requirements would have crashed the bot on import (no sqlglot), which is why the
image now ships none of them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
46
bot/AGENT.md
46
bot/AGENT.md
@@ -15,23 +15,43 @@ What you are given:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Runtime** | `python:3.12-slim` + ffmpeg + curl. Dependencies from `app/requirements.txt`, installed at image build (`docker compose build discord-bot`). |
|
||||
| **Your code** | bind-mounted at `/app` from `bot/app/`. Not baked into the image. Edit, then `docker compose restart discord-bot`. |
|
||||
| **Entry point** | the container runs `python -u /app/main.py` and restarts it if it exits. With no `main.py` it idles. |
|
||||
| **Runtime** | `python:3.12-slim` + ffmpeg + curl, and nothing else. **Your dependencies ship with your code** (see §1a), imported from `/app/vendor` via `PYTHONPATH`. Adding one needs no image rebuild. |
|
||||
| **Your code** | a release bundle from your CI, installed at `/srv/bot/app` and bind-mounted at `/app` (§1a). Treat `/app` as read-only; write to `/data`. |
|
||||
| **Entry point** | the container runs `python -u /app/main.py` and restarts it if it exits. With no bundle installed it idles and logs a hint. |
|
||||
| **User** | uid 1000 |
|
||||
| **Database** | MariaDB, database `vanguardbot`, user `bot`, reachable at `DB_HOST:DB_PORT`. Yours alone. Other databases on the same server belong to other services; never touch them. |
|
||||
| **Persistent disk** | `/data`, yours |
|
||||
| **Web UI** | bind `WEB_HOST:WEB_PORT` (0.0.0.0:8000). The proxy publishes it as `https://bot.<domain>/` behind login (§3). |
|
||||
| **Web UI** | bind `WEB_HOST:WEB_PORT` (0.0.0.0:8000) — one port for your API *and* your pages. The proxy publishes it as `https://bot.<domain>/` behind login (§3). |
|
||||
| **Discord** | `DISCORD_BOT_TOKEN`, `DISCORD_GUILD_ID`. The bot user belongs to the community's existing Discord application. |
|
||||
| **Clock** | `TZ=America/New_York` |
|
||||
|
||||
### Environment variables
|
||||
`DISCORD_BOT_TOKEN`, `DISCORD_GUILD_ID`, `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`,
|
||||
`WEB_HOST`, `WEB_PORT`, `WEB_PUBLIC_URL`, `AUTH_HEADER_USER`, `AUTH_HEADER_GROUPS`, `AUTH_HEADER_EMAIL`,
|
||||
`MEDIA_CLIPS`, `TZ`. Secrets arrive only through env. Never write them to the repo, `/data`, or logs.
|
||||
`DISCORD_BOT_TOKEN`, `DISCORD_GUILD_ID`, `DB_BACKEND=mariadb`, `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`,
|
||||
`DB_PASSWORD`, `DATA_DIR=/data`, `WEB_HOST`, `WEB_PORT`, `WEB_PUBLIC_URL`, `WEB_SERVE_UI=true`,
|
||||
`WEB_ALLOWED_GROUPS=bot-users,admins`, `AUTH_HEADER_USER`, `AUTH_HEADER_GROUPS`, `AUTH_HEADER_EMAIL`,
|
||||
`MEDIA_CLIPS`, `PYTHONPATH=/app/vendor`, `TZ`. Secrets arrive only through env. Never write them to the repo,
|
||||
`/data`, or logs.
|
||||
|
||||
SQLAlchemy URL: `mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}` (async: `mysql+aiomysql`).
|
||||
|
||||
## 1a. Releases: one bundle, dependencies included
|
||||
|
||||
Your CI (`.gitea/workflows/ci.yml`, job `bundle`) runs after the tests and produces the artifact
|
||||
`vanguard-bot-bundle` — a single tar.gz whose contents **are** `/app`:
|
||||
|
||||
main.py app/ website/ version.json BUILD.json vendor/
|
||||
|
||||
`vendor/` is `pip install --only-binary=:all: --target vendor -r requirements.txt`, built on `python:3.12`
|
||||
so the wheels match the runtime image's ABI. `BUILD.json` records the version, commit, build time, the
|
||||
package list and a sha256 of every file. The admin installs it with `./deployBotBundle.sh <bundle.tar.gz>`,
|
||||
which checks the bundle against `BUILD.json`, keeps the previous release for `--rollback`, and restarts the
|
||||
container. Rules that follow from this:
|
||||
|
||||
- Keep `requirements.txt` resolvable with **prebuilt wheels only**; a dependency that must compile will fail
|
||||
the build. Ask the admin if you truly need one.
|
||||
- Don't import anything that is not in `requirements.txt` or the standard library.
|
||||
- Nothing is installed on the server at deploy time, and the server reaches no package index.
|
||||
|
||||
## 2. Process model
|
||||
|
||||
- One process, one asyncio loop: the Discord gateway client and the web server run together
|
||||
@@ -52,6 +72,10 @@ Every request to `https://bot.<domain>/` passes through Caddy, which sends the u
|
||||
| `X-Authentik-Groups` | group names, separated by `\|` or `,` — the community uses `admins`, `editors`, `streamers`; others may be added |
|
||||
| `X-Authentik-Email`, `X-Authentik-Name`, `X-Authentik-Uid` | extras |
|
||||
|
||||
`WEB_SERVE_UI=true` is set on the server, so this process serves your HTML pages at `/` and their assets
|
||||
at `/assets/` as well as `/api/…` — same origin, one port, no second service and no proxy of your own.
|
||||
Locally the separate website launcher still works; it is off unless `WEB_SERVE_UI` says otherwise.
|
||||
|
||||
Rules:
|
||||
1. **Implement no login page, no sessions, no passwords.** Identity is the headers, per request.
|
||||
2. Port 8000 is loopback-only on the host, so the headers can be trusted. Still fail closed: a request
|
||||
@@ -99,10 +123,12 @@ message content; Message Content is a privileged intent, enable it only if you n
|
||||
|
||||
## 6. Working with the admin
|
||||
|
||||
- **Source**: push to the community Gitea, `ssh://git@git.<domain>:2222/vanguard/discord-bot.git`.
|
||||
The admin deploys by pulling into `bot/app` and restarting. CI on the `python` runner label runs your tests.
|
||||
- **Source**: push to the community Gitea. CI runs your tests on the `python` runner label, then builds the
|
||||
release bundle (§1a); the admin installs it with `deployBotBundle.sh`. Pushes to `main` can install it
|
||||
automatically once the repository variable `BOT_AUTO_DEPLOY` is set to `1`; it is unset by default.
|
||||
- **Need infra?** Open a Gitea issue titled `infra: …` stating the exact change: a new env var and why,
|
||||
a mount, a Caddy path that should skip login (e.g. a public webhook endpoint), a new database.
|
||||
Don't work around the proxy or the firewall.
|
||||
- **Dependencies changed?** Say so in the PR; the image must be rebuilt.
|
||||
- **Dependencies changed?** Just push: they travel inside the bundle. Only a new *system* package (beyond
|
||||
ffmpeg and curl) needs the admin to rebuild the image.
|
||||
- Keep `README.md` updated with every command, env var, and table you add.
|
||||
|
||||
Reference in New Issue
Block a user