ops: un deploiement qui refuse de partir casse, et qui se verifie
OPS-011, en partie. Ce que le workflow garantit maintenant : - rien ne part d un arbre casse. La suite, ruff check et ruff format tournent sur le runner de deploiement avant tout envoi. Une CI verte sur GitHub ne prouve rien ici : le deploiement se declenche a la main, sur ce que la branche contient a cet instant ; - seuls les fichiers nommes partent. La charge est une liste blanche — app/, wsgi.py, requirements.txt — et non l arbre de travail moins neuf exclusions. C est par cette porte que clear_db.py, la suite de tests et les definitions de CI se sont retrouves sur le noeud de production ; - le deploiement est verifie. /health est interroge pendant deux minutes apres l envoi et le job echoue s il ne repond jamais « healthy ». Avant, un arbre a moitie televerse etait un deploiement vert. Ce qui n est pas garanti, et c est ecrit dans le fichier : la bascule n est pas atomique. Le miroir se fait sur place, donc pendant le transfert la production execute un melange de deux versions. En cherchant a fermer ce point, un defaut a part entiere est apparu. Les contrats etaient ranges a os.getcwd()/documents et leur chemin absolu ecrit en base. La racine de stockage suivait donc le repertoire depuis lequel le processus avait ete lance : redemarrer le serveur ailleurs envoie les nouveaux contrats dans un nouvel arbre et rend les anciens illisibles — la base continuant d affirmer qu ils sont la, la panne se manifeste par un 500 au telechargement, pas par quelque chose d actionnable. app/storage.py fixe la racine et DOCUMENTS_ROOT la deplace. Les nouvelles lignes gardent un chemin relatif, les anciennes gardent leur chemin absolu et continuent de resoudre : aucune migration de donnees n est necessaire, donc ce changement n attend pas Alembic. C etait aussi le troisieme pre-requis de la bascule par repertoires de version. Les deux autres sont hors d atteinte d ici — la commande de demarrage Pterodactyl doit pointer sur current/, et les repertoires partages doivent etre installes sur le noeud. Les deux sont decrits dans docs/deployment.md, avec la procedure de retour arriere qui manquait. 511 tests.
This commit is contained in:
@@ -145,6 +145,93 @@ python security_scan.py --url https://yourdomain.com
|
||||
|
||||
All checks must pass before deployment.
|
||||
|
||||
## Deploying, and undoing a deployment (OPS-011)
|
||||
|
||||
Deployment is the Gitea workflow `.gitea/workflows/git-to-ptero.yaml`, run by
|
||||
hand (`workflow_dispatch`). It mirrors files over SFTP to the Pterodactyl
|
||||
node.
|
||||
|
||||
### What the workflow guarantees
|
||||
|
||||
1. **Nothing ships from a broken tree.** The suite, `ruff check` and
|
||||
`ruff format --check` all run on the deploy runner first. A green CI on
|
||||
GitHub proves nothing here: the deploy is triggered by hand, on whatever
|
||||
the branch currently holds.
|
||||
2. **Only named files ship.** The payload is an allowlist — `app/`,
|
||||
`wsgi.py`, `requirements.txt` — not the working tree minus exclusions.
|
||||
A new file at the repository root does not reach production unless
|
||||
somebody adds it. The old form is how `clear_db.py`, the test suite and
|
||||
the CI definitions got onto the production node.
|
||||
3. **The deployment is verified.** `/health` is polled for two minutes after
|
||||
the upload and the job fails if it never reports healthy. Set the
|
||||
`HEALTH_URL` repository secret to `https://<host>/health`; without it the
|
||||
workflow warns that the deployment went out unverified.
|
||||
|
||||
Deliberately *not* shipped: `run.py` (development entry point), `tests/`,
|
||||
`migrations/add_tryout_coaches.py` (a one-off, already applied — run it by
|
||||
hand if a fresh database ever needs it), `docs/`, `audit/`, `clear_db.py`.
|
||||
|
||||
### What it does not guarantee
|
||||
|
||||
**The switch is not atomic.** Files are mirrored in place, so for the length
|
||||
of the transfer the node runs a mixture of two versions. And because
|
||||
`--delete` is off — uploaded contracts, logs and the server's `.env` live
|
||||
under the deployment root and are absent from the repository — a file
|
||||
removed from the repository stays on the server for ever.
|
||||
|
||||
### Rolling back
|
||||
|
||||
There is no previous release on the node to switch back to, so a rollback is
|
||||
a forward deployment of a known-good commit:
|
||||
|
||||
```bash
|
||||
# 1. Find the last deployment that was verified healthy — the workflow run
|
||||
# log names the commit.
|
||||
git log --oneline
|
||||
|
||||
# 2. Deploy that commit. In the Gitea UI, run the "Push to SFTP" workflow
|
||||
# against the tag or branch pointing at it. Tag known-good releases so
|
||||
# this step does not depend on reading a log:
|
||||
git tag -a deploy-2026-08-11 -m "verified healthy" <commit>
|
||||
git push origin deploy-2026-08-11 # requires the push freeze to be lifted
|
||||
|
||||
# 3. Confirm.
|
||||
curl -fsS https://<host>/health
|
||||
```
|
||||
|
||||
A rollback does **not** undo a database migration. If the deployment that
|
||||
broke production also changed the schema, restore from backup first —
|
||||
`docs/database-restore.md`.
|
||||
|
||||
### Making the switch atomic
|
||||
|
||||
The remaining work, and why it is not done here. A release-directory layout
|
||||
looks like this on the node:
|
||||
|
||||
```
|
||||
/home/container/
|
||||
├── releases/
|
||||
│ ├── 2026-08-11-a1b2c3/
|
||||
│ └── 2026-08-10-9f8e7d/
|
||||
├── current -> releases/2026-08-11-a1b2c3
|
||||
├── documents/ # shared, never inside a release
|
||||
├── logs/ # shared
|
||||
└── .env # shared
|
||||
```
|
||||
|
||||
Three prerequisites. One is done, two are not:
|
||||
|
||||
| # | Prerequisite | State |
|
||||
|---|---|---|
|
||||
| 1 | The Pterodactyl startup command must run the app from `current/`, and the server must be restarted on switch | **Panel change.** Cannot be made or verified from the repository |
|
||||
| 2 | `documents/`, `logs/` and `.env` must sit beside the releases, not inside one. `DOCUMENTS_ROOT` points the document store at a fixed path (`app/storage.py`) | Mechanism ready, **not yet configured on the node** |
|
||||
| 3 | Contract paths must be relative to that root, or the first switch strands every contract ever uploaded | **Done.** New rows store a relative path; rows written earlier keep their absolute one and still resolve, so no data migration is needed |
|
||||
|
||||
Prerequisite 3 was a defect on its own, not just a blocker: paths were built
|
||||
from `os.getcwd()`, so starting the server from a different directory would
|
||||
have sent new contracts to a new tree and made the existing ones unreadable
|
||||
— with the database still claiming they were there.
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- [ ] `.env` is not committed to repository
|
||||
|
||||
Reference in New Issue
Block a user