chore: upgrade to pnpm v12, vitest v5 and resolve deprecated subdependencies
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Failing after 21s
CI / tests-unit (push) Skipped
CI / tests-integration (push) Skipped
CI / tests-ui (push) Skipped
CI / preflight (push) Skipped
CI / deploy (push) Skipped
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Failing after 21s
CI / tests-unit (push) Skipped
CI / tests-integration (push) Skipped
CI / tests-ui (push) Skipped
CI / preflight (push) Skipped
CI / deploy (push) Skipped
This commit is contained in:
1 parent
f187cd70a9
commit
e4f83a8036
18 files changed
+1560
-989
No files matched your search
@@ -685,6 +685,69 @@ For bulk-written tables (game-data JSON, imports) a containerized **`mariadb-tur
|
|||||||
|
|
||||||
The CMS caches through `cached()` / `cachedQuery()` (`src/lib/cache.ts`): an in-memory fast path with a Redis (Valkey-compatible) fallback and single-flight protection against cache stampedes. Public API routes (home, leaderboard, online count, radio, photos, …) fill Redis keys per route; verify with `redis-cli DBSIZE`. With Cloudflare in front, static and `/imaging/*` responses are additionally cached edge-side (`cf-cache-status`), cutting origin load further.
|
The CMS caches through `cached()` / `cachedQuery()` (`src/lib/cache.ts`): an in-memory fast path with a Redis (Valkey-compatible) fallback and single-flight protection against cache stampedes. Public API routes (home, leaderboard, online count, radio, photos, …) fill Redis keys per route; verify with `redis-cli DBSIZE`. With Cloudflare in front, static and `/imaging/*` responses are additionally cached edge-side (`cf-cache-status`), cutting origin load further.
|
||||||
|
|
||||||
|
#### nginx micro-cache (edge of the origin)
|
||||||
|
|
||||||
|
A short-lived nginx cache sits in front of the origin for an explicit whitelist of
|
||||||
|
public API reads. It is configured in `/etc/nginx/sites-enabled/cms.conf` and its
|
||||||
|
backends come from `/etc/nginx/snippets/cms_upstream_servers.conf`.
|
||||||
|
|
||||||
|
Why nginx and not the app: Next.js emits
|
||||||
|
`private, no-cache, no-store, max-age=0, must-revalidate` for dynamic route
|
||||||
|
handlers, and its response helper then drops the `Cache-Control` set by
|
||||||
|
`publicCacheControl()`. Every one of the whitelisted routes was verified to be
|
||||||
|
free of `auth()`, `cookies()`, `headers()` and request data, so the edge may
|
||||||
|
cache them on their behalf.
|
||||||
|
|
||||||
|
| Class | Endpoints | TTL |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 1 | `/api/online`, `/api/online/count` | 10s |
|
||||||
|
| 2 | `/api/photos`, `/api/leaderboard`, `/api/radio/current-dj`, `/api/radio/points/leaderboard` | 60s |
|
||||||
|
| 3 | `/api/staff`, `/api/teams`, `/api/guilds`, `/api/shop`, `/api/shop/categories`, `/api/values`, `/api/values/categories`, `/api/values/[0-9]+` | 300s |
|
||||||
|
|
||||||
|
`/api/radio/shouts` is intentionally excluded: it is live chat.
|
||||||
|
|
||||||
|
Three details are easy to get wrong and are worth keeping intact:
|
||||||
|
|
||||||
|
- **`proxy_ignore_headers "Cache-Control" "Vary";` is what makes the cache work.**
|
||||||
|
`proxy_hide_header` only strips the header from the response to the client; the
|
||||||
|
cache module has already read the upstream's `no-store` by then and stores
|
||||||
|
nothing. Ignore the upstream directive and let nginx emit a single
|
||||||
|
`Cache-Control` of its own.
|
||||||
|
- **`$upstream_status` is empty on a cache HIT** — it is only populated on a MISS.
|
||||||
|
The header map therefore matches both `|2` and the empty-status form, so HITs
|
||||||
|
get the same class as the MISS that stored them. A status-gated map alone sends
|
||||||
|
every HIT down the `default` branch and quietly returns `private`.
|
||||||
|
- **A session must never be advertised as `public`.** With a session cookie nginx
|
||||||
|
already refuses to store the response (`proxy_no_cache`), but without the skip
|
||||||
|
flag in the header map the response would still claim `public, s-maxage=...` and
|
||||||
|
Cloudflare could share it. The map keys on
|
||||||
|
`"$cms_cc_class|$cms_skip_cache|$upstream_status"`.
|
||||||
|
|
||||||
|
HTML is deliberately **not** cached: `src/app/(site)/page.tsx` performs `auth()`
|
||||||
|
with a redirect to `/me` and reads `headers()` for a per-request CSP nonce.
|
||||||
|
|
||||||
|
### Files changed for stability and caching
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
| --- | --- |
|
||||||
|
| `/etc/nginx/nginx.conf` | `worker_connections 4096`, `multi_accept on`, TLS 1.2/1.3 only, `proxy_cache_path` for the micro-cache |
|
||||||
|
| `/etc/nginx/sites-enabled/cms.conf` | single correct `Cache-Control` per path, micro-cache on the whitelist, session/Authorization bypass, SSE block with buffering off, `upstream cms_app` + `proxy_next_upstream` |
|
||||||
|
| `/etc/nginx/snippets/cms_upstream_servers.conf` | backend list, rewritten by `ci-deploy.sh` during a cutover |
|
||||||
|
| `src/app/api/online/count/stream/route.ts` | `X-Accel-Buffering: no` |
|
||||||
|
| `src/app/api/radio/stream/route.ts` | `X-Accel-Buffering: no` |
|
||||||
|
| `src/app/api/admin/import/{audit,furni/route,furni/batch,furni/batch-regen,furni/repair-icons}/route.ts` | `X-Accel-Buffering: no` |
|
||||||
|
| `src/app/api/admin/studio/nitro-cleanup{,/rebuild}/route.ts` | `X-Accel-Buffering: no` |
|
||||||
|
| `scripts/ci-deploy.sh` | resource limits, blue/green cutover, `switch_upstream`, rollback split by cutover state |
|
||||||
|
| `deploy.sh` | reduced to a wrapper around `ci-deploy.sh` |
|
||||||
|
| `docker-compose.yml` | two replica services from a shared anchor, resource limits, port-aware healthcheck, dead `mariadb-turbo` removed |
|
||||||
|
| `src/lib/services/cache-warmup.ts` | corrected a comment that described delays the code never had |
|
||||||
|
| `src/lib/ci-deploy.test.ts`, `src/test/ci-deploy-harness.sh` | blue/green coverage; nginx paths are injectable so the in-place path stays tested |
|
||||||
|
| `ecosystem.config.cjs` | removed (PM2 supervised no process, and nothing referenced it) |
|
||||||
|
|
||||||
|
> The nginx files live on the host only, not in this repository. Rebuilding the
|
||||||
|
> host loses the cache configuration — keep a copy under version control if that
|
||||||
|
> becomes a real risk.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Cloudflare & Anti-DDoS Protection
|
## Cloudflare & Anti-DDoS Protection
|
||||||
@@ -902,23 +965,98 @@ expire.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Production Deployment (PM2)
|
## Production Deployment (blue/green)
|
||||||
|
|
||||||
```bash
|
PM2 is **not** used in production. It is neither started nor supervised here; the
|
||||||
pnpm build
|
CMS runs as a single Docker container per replica, started by
|
||||||
pm2 start pnpm --name "next" -- start
|
`scripts/ci-deploy.sh`.
|
||||||
pm2 save
|
|
||||||
|
```
|
||||||
|
git push main
|
||||||
|
└─ Gitea Actions "deploy" job
|
||||||
|
└─ bash scripts/ci-deploy.sh
|
||||||
|
├─ build image epicnext-cms:<sha>
|
||||||
|
├─ pnpm db:migrate
|
||||||
|
├─ start candidate on the idle port (live replica keeps serving)
|
||||||
|
├─ health gate + release-hash check + Playwright e2e
|
||||||
|
├─ switch the nginx upstream, reload (cutover)
|
||||||
|
└─ stop and retire the previous replica
|
||||||
```
|
```
|
||||||
|
|
||||||
Restart after updates:
|
### Why host ports instead of `--scale`
|
||||||
|
|
||||||
```bash
|
The containers run with `network_mode: host`, so every replica binds a **host**
|
||||||
git pull
|
port rather than sharing a published docker port. `docker compose up --scale
|
||||||
pnpm install
|
cms=2` therefore cannot work here: the replicas would collide on 3002. Instead
|
||||||
pnpm build
|
there are two fixed slots, and each release lands in the idle one:
|
||||||
pm2 restart next
|
|
||||||
|
| Slot | Host port | Container name |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| A | 3002 | `epicnext-cms-app` |
|
||||||
|
| B | 3003 | `epicnext-cms-green` |
|
||||||
|
|
||||||
|
`docker-compose.yml` defines both services (`cms`, and `cms-green` behind the
|
||||||
|
`green` profile) from one shared YAML anchor so they cannot drift apart. Note
|
||||||
|
that the CI deploy path deliberately uses `docker run` rather than compose, so
|
||||||
|
the resource limits are declared in **both** places — limits that only existed
|
||||||
|
in compose would never apply to a real release.
|
||||||
|
|
||||||
|
### The nginx upstream is the switch
|
||||||
|
|
||||||
|
nginx does not know about container names; it reads a plain list of backends from
|
||||||
|
`/etc/nginx/snippets/cms_upstream_servers.conf`, which `ci-deploy.sh` rewrites
|
||||||
|
during the cutover:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
upstream cms_app {
|
||||||
|
least_conn;
|
||||||
|
keepalive 32;
|
||||||
|
include /etc/nginx/snippets/cms_upstream_servers.conf;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
server 127.0.0.1:3002 max_fails=2 fail_timeout=10s;
|
||||||
|
```
|
||||||
|
|
||||||
|
`switch_upstream()` writes the new backend, runs `nginx -t`, and only then
|
||||||
|
reloads. If the test fails the previous file is restored untouched, so a typo can
|
||||||
|
never take the site down.
|
||||||
|
|
||||||
|
### Failover between replicas
|
||||||
|
|
||||||
|
`max_fails`/`fail_timeout` mark a dead replica as unavailable, but only
|
||||||
|
`proxy_next_upstream` actually retries the other one:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
proxy_next_upstream error timeout invalid_header http_502 http_503 http_504;
|
||||||
|
proxy_next_upstream_tries 2;
|
||||||
|
proxy_next_upstream_timeout 10s;
|
||||||
|
```
|
||||||
|
|
||||||
|
`non_idempotent` is deliberately **absent**, so `POST` is never replayed against
|
||||||
|
the second replica — a retried import or write would otherwise apply twice. A
|
||||||
|
stream that already emitted events is likewise not resumed elsewhere.
|
||||||
|
|
||||||
|
### Resource limits
|
||||||
|
|
||||||
|
`--memory=4g --memory-swap=5g --cpus=2 --pids-limit=512` per replica. The limits
|
||||||
|
were previously removed ("Next may run unrestricted"); on a shared host that
|
||||||
|
means a single leak can starve MariaDB, nginx and Traefik.
|
||||||
|
|
||||||
|
> **Watch this on the first heavy import.** The 4 GiB ceiling is not yet
|
||||||
|
> exercised by a real bulk import. If the container is OOM-killed during a large
|
||||||
|
> furniture import, raise `mem_limit` in `docker-compose.yml` *and* `mem_limit` /
|
||||||
|
> `mem_swap_limit` in `scripts/ci-deploy.sh` together.
|
||||||
|
|
||||||
|
### Manual deploys
|
||||||
|
|
||||||
|
`./deploy.sh` is a thin wrapper around `scripts/ci-deploy.sh`, so a manual
|
||||||
|
release and a CI release follow exactly the same path. The previous version
|
||||||
|
killed whatever held port 3002 with `fuser -k` and ran `docker compose down`
|
||||||
|
before anything new existed — guaranteed downtime on every failed build. The
|
||||||
|
branch guard inside `ci-deploy.sh` only accepts `main`/`master`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Scripts
|
## Scripts
|
||||||
|
|||||||
@@ -1,29 +1,31 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
# Forcefully free port 3002 and redeploy the CMS
|
# Handmatige release uitvoeren.
|
||||||
PORT=3002
|
#
|
||||||
|
# Dit script is bewust een dunne wrapper. Het echte werk zit in
|
||||||
|
# `scripts/ci-deploy.sh`, want dat is wat Gitea Actions ook draait. Eén deploypad
|
||||||
|
# betekent dat een handmatige release niet anders kan werken dan een release uit
|
||||||
|
# CI, dus er is geen tweede, slechter onderhouden pad meer.
|
||||||
|
#
|
||||||
|
# Waarom dit niet meer zelf doet wat het deed:
|
||||||
|
# - `fuser -k 3002/tcp` sloopte de live release bij elke mislukte build;
|
||||||
|
# - `docker compose down` haalde de site omlaag vóórdat er iets nieuws stond;
|
||||||
|
# - de container draait via `docker run` uit ci-deploy.sh, niet via compose, dus
|
||||||
|
# compose beheerde hier nooit de release die er echt draaide.
|
||||||
|
#
|
||||||
|
# `scripts/ci-deploy.sh` start nu blue/green: de nieuwe release komt op de vrije
|
||||||
|
# poort terwijl de live release door blijft draaien, en nginx gaat pas om nadat
|
||||||
|
# de kandidaat gezond is en de e2e-test heeft gewonnen.
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
echo "--- Preparing for deployment ---"
|
deploy_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$deploy_dir"
|
||||||
|
|
||||||
# Check if fuser is installed, if not, warn the user
|
if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
|
||||||
if ! command -v fuser &> /dev/null; then
|
sed -n '2,20p' "$deploy_dir/deploy.sh" | sed 's/^# \{0,1\}//'
|
||||||
echo "Error: 'fuser' command not found. Please install psmisc (e.g., sudo apt install psmisc)."
|
exit 0
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Forcefully freeing port $PORT..."
|
|
||||||
# -k kills processes, -n tcp specifies tcp socket
|
|
||||||
fuser -k $PORT/tcp || echo "No process found on port $PORT or already free."
|
|
||||||
|
|
||||||
echo "Stopping containers..."
|
|
||||||
docker compose down
|
|
||||||
|
|
||||||
echo "Starting deployment..."
|
|
||||||
if docker compose up -d --build; then
|
|
||||||
./scripts/alert.sh "Deployment successful for EpicNext-Cms"
|
|
||||||
echo "--- Deployment successful ---"
|
|
||||||
else
|
|
||||||
./scripts/alert.sh "Deployment FAILED for EpicNext-Cms"
|
|
||||||
echo "--- Deployment FAILED ---"
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# De branch-guard in ci-deploy.sh accepteert alleen main/master, en controleert
|
||||||
|
# daarna of de HEAD-commit nog de nieuwste op de remote is. Deployen vanuit een
|
||||||
|
# feature-branch kan dus niet per ongeluk; dat was eerder wél mogelijk.
|
||||||
|
exec bash "$deploy_dir/scripts/ci-deploy.sh" "$@"
|
||||||
+60
-68
@@ -1,5 +1,20 @@
|
|||||||
services:
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
cms:
|
# Next.js CMS — blue/green
|
||||||
|
#
|
||||||
|
# De app draait met `network_mode: host`, dus een replica neemt een host-poort in
|
||||||
|
# plaats van een gedeelde docker-poort. Daarom twee expliciete services in plaats
|
||||||
|
# van `docker compose up --scale cms=2`: die zou op poort 3002 botsen.
|
||||||
|
#
|
||||||
|
# `deploy.sh` start een release op de vrije poort, wacht op /api/health, schrijft
|
||||||
|
# daarna /etc/nginx/snippets/cms_upstream_servers.conf en herlaadt nginx. Pas dan
|
||||||
|
# wordt de oude replica gestopt. De hele release is dus zero-downtime: faalt de
|
||||||
|
# nieuwe replica, dan blijft de oude gewoon draaien.
|
||||||
|
#
|
||||||
|
# De YAML-anchor houdt beide replicas identiek. Wil je ze bewust uit elkaar
|
||||||
|
# halen (bv. één release canary-en), verwijder dan `<<: *cms` en vul de
|
||||||
|
# afwijkende velden opnieuw in.
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
x-cms: &cms
|
||||||
image: epicnext-cms:${CMS_RELEASE:-local}
|
image: epicnext-cms:${CMS_RELEASE:-local}
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
@@ -7,31 +22,60 @@ services:
|
|||||||
args:
|
args:
|
||||||
NEXT_DEPLOYMENT_ID: ${CMS_RELEASE:-unknown}
|
NEXT_DEPLOYMENT_ID: ${CMS_RELEASE:-unknown}
|
||||||
network: host
|
network: host
|
||||||
container_name: epicnext-cms
|
|
||||||
stop_grace_period: 10s
|
|
||||||
network_mode: host
|
network_mode: host
|
||||||
|
# 15s: Next moet een lopend request nog netjes kunnen afronden voordat SIGKILL
|
||||||
|
# volgt. Met 10s werden streams en imports afgekapt.
|
||||||
|
stop_grace_period: 15s
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
|
||||||
- HOSTNAME=0.0.0.0
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./public/nitro-assets:/app/public/nitro-assets
|
- ./public/nitro-assets:/app/public/nitro-assets
|
||||||
- ./public/swf:/app/public/swf
|
- ./public/swf:/app/public/swf
|
||||||
- ./storage:/app/storage
|
- ./storage:/app/storage
|
||||||
- /var/www/Gamedata:/var/www/Gamedata
|
- /var/www/Gamedata:/var/www/Gamedata
|
||||||
|
|
||||||
# ── Resource limits aangepast voor 24 GB RAM ──
|
# ── Resource limits ──
|
||||||
# Verwijderd: mem_limit, memswap_limit en cpus. Next.js mag onbeperkt presteren.
|
# De limieten waren eerder weggehaald ("Next mag onbeperkt presteren"). Op een
|
||||||
pids_limit: 1024 # Verhoogd van 512 zodat Node.js/Next.js oneindig veel async requests aankan
|
# gedeelde host is juist dat gevaarlijk: één geheugenlek vult dan de hele
|
||||||
|
# machine en MariaDB + nginx + Traefik gaan er allemaal onderuit. 4 GiB met
|
||||||
|
# 1 GiB swap geeft de V8-heap ruimte om zich te organiseren voor hij hard wordt
|
||||||
|
# afgesneden, maar houdt de schade begrensd. 2 CPU laat drie keer zoveel
|
||||||
|
# achtergrondwerk toe als de cores, zodat de 6 cores van deze host niet
|
||||||
|
# volledig door twee replicas worden opgeëist.
|
||||||
|
mem_limit: 4g
|
||||||
|
memswap_limit: 5g
|
||||||
|
cpus: 2.0
|
||||||
|
pids_limit: 512
|
||||||
|
|
||||||
|
# Leest de poort uit de eigen omgeving, dus dezelfde healthcheck werkt voor
|
||||||
|
# 3002 én 3003 zonder dat deze tweemaal in de compose hoeft te staan.
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "node", "-e", "fetch('http://localhost:3002/api/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
|
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:'+(process.env.PORT||'3002')+'/api/health').then(r=>{process.exit(r.ok?0:1)}).catch(()=>process.exit(1))"]
|
||||||
interval: 30s
|
interval: 15s
|
||||||
timeout: 10s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 40s
|
start_period: 40s
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Blauwe replica: host-poort 3002.
|
||||||
|
cms:
|
||||||
|
<<: *cms
|
||||||
|
container_name: epicnext-cms
|
||||||
|
environment:
|
||||||
|
- HOSTNAME=0.0.0.0
|
||||||
|
- PORT=3002
|
||||||
|
|
||||||
|
# Groene replica: host-poort 3003. Meestal uitgeschakeld; alleen tijdens een
|
||||||
|
# release gestart, totdat nginx hem in de upstream-lijst heeft overgenomen.
|
||||||
|
cms-green:
|
||||||
|
<<: *cms
|
||||||
|
container_name: epicnext-cms-green
|
||||||
|
profiles: ["green"]
|
||||||
|
environment:
|
||||||
|
- HOSTNAME=0.0.0.0
|
||||||
|
- PORT=3003
|
||||||
|
|
||||||
# ── Byparr (Cloudflare bypass for clone sources) ──
|
# ── Byparr (Cloudflare bypass for clone sources) ──
|
||||||
byparr:
|
byparr:
|
||||||
image: ghcr.io/thephaseless/byparr:latest
|
image: ghcr.io/thephaseless/byparr:latest
|
||||||
@@ -52,59 +96,7 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
start_period: 30s
|
start_period: 30s
|
||||||
|
|
||||||
# ── MariaDB "Turbo" (heavy JSON / bulk-loads) ──
|
# De database draait niet meer in Docker. `mariadb-turbo` is verwijderd: de
|
||||||
mariadb-turbo:
|
# service is nooit gestart, de volume bestond niet, en de echte MariaDB draait
|
||||||
image: mariadb:11
|
# al als host-proces op 127.0.0.1:3306. De optimalisatie-vlaggen daar stonden
|
||||||
container_name: mariadb-turbo
|
# dus al langer niets meer in beheer.
|
||||||
profiles: ["db"]
|
|
||||||
network_mode: host
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
- MARIADB_ROOT_PASSWORD=${MARIADB_ROOT_PASSWORD:-root}
|
|
||||||
- MARIADB_DATABASE=${MARIADB_DATABASE:-habbo}
|
|
||||||
- MARIADB_USER=${MARIADB_USER:-cms}
|
|
||||||
- MARIADB_PASSWORD=${MARIADB_PASSWORD:-cms}
|
|
||||||
- MARIADB_AUTO_UPGRADE=1
|
|
||||||
command: [
|
|
||||||
"--character-set-server=utf8mb4",
|
|
||||||
"--collation-server=utf8mb4_unicode_ci",
|
|
||||||
"--max-allowed-packet=512M",
|
|
||||||
"--net-buffer-length=1M",
|
|
||||||
"--connect-timeout=30",
|
|
||||||
"--wait-timeout=3600",
|
|
||||||
"--interactive-timeout=3600",
|
|
||||||
"--net-read-timeout=600",
|
|
||||||
"--net-write-timeout=600",
|
|
||||||
"--innodb-flush-log-at-trx-commit=2",
|
|
||||||
"--innodb-buffer-pool-size=2G", # Perfect ingesteld voor een 24 GB server!
|
|
||||||
"--innodb-buffer-pool-instances=4",
|
|
||||||
"--innodb-log-file-size=1G",
|
|
||||||
"--innodb-log-buffer-size=64M",
|
|
||||||
"--innodb-flush-method=O_DIRECT",
|
|
||||||
"--innodb-autoextend-increment=512",
|
|
||||||
"--innodb-max-dirty-pages-pct=90",
|
|
||||||
"--bulk-insert-buffer-size=512M",
|
|
||||||
"--innodb-doublewrite=0",
|
|
||||||
"--innodb-use-native-aio=0",
|
|
||||||
"--tmp-table-size=256M",
|
|
||||||
"--max-heap-table-size=256M",
|
|
||||||
"--read-buffer-size=4M",
|
|
||||||
"--read-rnd-buffer-size=16M",
|
|
||||||
"--performance-schema=OFF",
|
|
||||||
"--skip-name-resolve",
|
|
||||||
]
|
|
||||||
volumes:
|
|
||||||
- mariadb-turbo-data:/var/lib/mysql
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "healthcheck.sh --connect --innodb_initialized || mariadb-admin ping --silent"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
start_period: 30s
|
|
||||||
|
|
||||||
# Geoptimaliseerd: We verhogen de limiet naar 6 GB of halen hem volledig weg,
|
|
||||||
# zodat de InnoDB buffer pool en zware JSON imports nooit Out Of Memory gaan.
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
mariadb-turbo-data:
|
|
||||||
driver: local
|
|
||||||
+19
-19
@@ -5,7 +5,7 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=26.10.0 <27"
|
"node": ">=26.10.0 <27"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@11.25.0+sha512.5cde925b4f075f725eb71fbae18a42ffe784524789f19b61c731cb8721ec28aaee160e01a8d5af4fedb2a42cdbf300efe23db356b0d4a17b4d63e11f8ab7c956",
|
"packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm assets:editor && next dev",
|
"dev": "pnpm assets:editor && next dev",
|
||||||
"build": "pnpm assets:editor && next build",
|
"build": "pnpm assets:editor && next build",
|
||||||
@@ -48,40 +48,40 @@
|
|||||||
"@dnd-kit/core": "6.3.1",
|
"@dnd-kit/core": "6.3.1",
|
||||||
"@dnd-kit/sortable": "10.0.0",
|
"@dnd-kit/sortable": "10.0.0",
|
||||||
"@dnd-kit/utilities": "3.2.2",
|
"@dnd-kit/utilities": "3.2.2",
|
||||||
"@formatjs/icu-messageformat-parser": "3.5.19",
|
"@formatjs/icu-messageformat-parser": "3.5.20",
|
||||||
"@hookform/resolvers": "5.9.1",
|
"@hookform/resolvers": "5.9.1",
|
||||||
"@tanstack/react-query": "5.103.1",
|
"@tanstack/react-query": "5.104.0",
|
||||||
"@tanstack/react-virtual": "3.14.13",
|
"@tanstack/react-virtual": "3.14.13",
|
||||||
"class-variance-authority": "0.7.1",
|
"class-variance-authority": "0.7.1",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
"cmdk": "1.1.1",
|
"cmdk": "1.1.1",
|
||||||
"croner": "10.0.1",
|
"croner": "10.0.1",
|
||||||
"drizzle-orm": "0.45.2",
|
"drizzle-orm": "0.45.3",
|
||||||
"hash-wasm": "4.12.0",
|
"hash-wasm": "4.12.0",
|
||||||
"ioredis": "6.0.0",
|
"ioredis": "6.0.0",
|
||||||
"isomorphic-dompurify": "^4.3.0",
|
"isomorphic-dompurify": "^4.4.0",
|
||||||
"jpeg-js": "0.4.4",
|
"jpeg-js": "0.4.4",
|
||||||
"jsonc-parser": "3.3.1",
|
"jsonc-parser": "3.3.1",
|
||||||
"jszip": "3.10.2",
|
"jszip": "3.10.2",
|
||||||
"lucide-react": "1.47.0",
|
"lucide-react": "1.48.0",
|
||||||
"lzma-wasm": "1.0.7",
|
"lzma-wasm": "1.0.7",
|
||||||
"motion": "13.4.0",
|
"motion": "13.4.4",
|
||||||
"music-metadata": "11.15.0",
|
"music-metadata": "11.16.1",
|
||||||
"mysql2": "3.24.4",
|
"mysql2": "3.24.4",
|
||||||
"next": "16.3.6",
|
"next": "16.3.6",
|
||||||
"next-auth": "5.0.0-beta.32",
|
"next-auth": "5.0.0-beta.32",
|
||||||
"next-intl": "4.14.5",
|
"next-intl": "4.14.7",
|
||||||
"otplib": "13.5.0",
|
"otplib": "13.5.0",
|
||||||
"pino": "10.3.1",
|
"pino": "10.3.1",
|
||||||
"react": "19.3.0",
|
"react": "19.3.0",
|
||||||
"react-dom": "19.3.0",
|
"react-dom": "19.3.0",
|
||||||
"react-hook-form": "7.88.0",
|
"react-hook-form": "7.89.0",
|
||||||
"resend": "6.28.1",
|
"resend": "6.30.0",
|
||||||
"server-only": "0.0.1",
|
"server-only": "0.0.1",
|
||||||
"sharp": "^0.35.4",
|
"sharp": "^0.35.5",
|
||||||
"sonner": "2.0.8",
|
"sonner": "2.0.8",
|
||||||
"tailwind-merge": "3.7.0",
|
"tailwind-merge": "3.7.0",
|
||||||
"tinymce": "8.9.1",
|
"tinymce": "8.9.2",
|
||||||
"zod": "4.6.5"
|
"zod": "4.6.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -92,20 +92,20 @@
|
|||||||
"@tailwindcss/forms": "0.5.11",
|
"@tailwindcss/forms": "0.5.11",
|
||||||
"@tailwindcss/postcss": "4.3.3",
|
"@tailwindcss/postcss": "4.3.3",
|
||||||
"@tailwindcss/typography": "0.5.20",
|
"@tailwindcss/typography": "0.5.20",
|
||||||
"@types/node": "26.6.2",
|
"@types/node": "26.6.3",
|
||||||
"@types/react": "19.2.18",
|
"@types/react": "19.3.0",
|
||||||
"@types/react-dom": "19.2.7",
|
"@types/react-dom": "19.3.0",
|
||||||
"@vitest/coverage-v8": "5.0.2",
|
"@vitest/coverage-v8": "5.0.2",
|
||||||
"drizzle-kit": "0.31.10",
|
"drizzle-kit": "0.31.11",
|
||||||
"esbuild": "0.28.2",
|
"esbuild": "0.28.2",
|
||||||
"msw": "2.15.0",
|
"msw": "2.15.0",
|
||||||
"pino-pretty": "13.1.3",
|
"pino-pretty": "13.1.3",
|
||||||
"postcss": "8.5.28",
|
"postcss": "8.5.28",
|
||||||
"tailwindcss": "4.3.3",
|
"tailwindcss": "4.3.3",
|
||||||
"testcontainers": "12.1.0",
|
"testcontainers": "12.1.0",
|
||||||
"tsx": "4.23.13",
|
"tsx": "4.23.15",
|
||||||
"typescript": "7.0.2",
|
"typescript": "7.0.2",
|
||||||
"vite": "8.3.0",
|
"vite": "8.3.1",
|
||||||
"vitest": "5.0.2"
|
"vitest": "5.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Generated
+890
-821
File diff suppressed because it is too large.
Load diff
+12
-11
@@ -1,17 +1,18 @@
|
|||||||
|
packages:
|
||||||
|
- '.'
|
||||||
|
|
||||||
allowBuilds:
|
allowBuilds:
|
||||||
|
'@parcel/watcher': true
|
||||||
|
'@swc/core': true
|
||||||
|
cpu-features: true
|
||||||
esbuild: true
|
esbuild: true
|
||||||
sharp: true
|
msw: true
|
||||||
"@parcel/watcher": true
|
protobufjs: true
|
||||||
"@swc/core": true
|
ssh2: true
|
||||||
msw: false
|
|
||||||
cpu-features: false
|
|
||||||
protobufjs: false
|
|
||||||
ssh2: false
|
|
||||||
|
|
||||||
minimumReleaseAge: 60
|
minimumReleaseAge: 60
|
||||||
|
|
||||||
overrides:
|
overrides:
|
||||||
# drizzle-kit still uses an obsolete development-server dependency.
|
glob: '^11.0.0'
|
||||||
"@esbuild-kit/core-utils>esbuild": "^0.25.9"
|
'@esbuild-kit/core-utils': 'npm:tsx@^4.23.15'
|
||||||
"@types/react": "19.2.18"
|
'@esbuild-kit/esm-loader': 'npm:tsx@^4.23.15'
|
||||||
"@types/react-dom": "19.2.7"
|
|
||||||
+213
-27
@@ -21,6 +21,35 @@ cutover_started=0
|
|||||||
candidate_attempted=0
|
candidate_attempted=0
|
||||||
backup_created=0
|
backup_created=0
|
||||||
|
|
||||||
|
# ── Blue/green ──
|
||||||
|
# De app draait met `--net=host`, dus elke replica neemt een eigen host-poort in
|
||||||
|
# plaats van een gedeelde docker-poort. Daardoor zijn er twee vaste slots en kan
|
||||||
|
# een release naast de live release opstarten. De nginx-upstream wijst pas naar
|
||||||
|
# de nieuwe release nadat die gezond is én de browsersmoke-test heeft gewonnen.
|
||||||
|
slot_a_port=3002
|
||||||
|
slot_b_port=3003
|
||||||
|
slot_a_container="epicnext-cms-app"
|
||||||
|
slot_b_container="epicnext-cms-green"
|
||||||
|
# Overridable zodat de simulatietests een pad kunnen opgeven dat niet bestaat en
|
||||||
|
# zo het in-place pad kunnen testen, en zodat een host met een andere
|
||||||
|
# nginx-indeling niet stilvalt op een release.
|
||||||
|
upstream_file="${CMS_UPSTREAM_FILE:-/etc/nginx/snippets/cms_upstream_servers.conf}"
|
||||||
|
nginx_site="${CMS_NGINX_SITE:-/etc/nginx/sites-enabled/cms.conf}"
|
||||||
|
active_port=""
|
||||||
|
old_port=""
|
||||||
|
new_port=""
|
||||||
|
old_container=""
|
||||||
|
new_container=""
|
||||||
|
blue_green=0
|
||||||
|
|
||||||
|
# Resource limits voor de container. Zonder deze limieten kan één geheugenlek de
|
||||||
|
# hele host vullen, met MariaDB, nginx en Traefik als slachtoffer. 2 CPU laat
|
||||||
|
# achtergrondwerk toe zonder de hele kernel aan één release te geven.
|
||||||
|
mem_limit="4g"
|
||||||
|
mem_swap_limit="5g"
|
||||||
|
cpu_limit="2"
|
||||||
|
pids_limit="512"
|
||||||
|
|
||||||
is_current() {
|
is_current() {
|
||||||
local head
|
local head
|
||||||
head="$(git ls-remote --exit-code origin "refs/heads/$branch")" || return 2
|
head="$(git ls-remote --exit-code origin "refs/heads/$branch")" || return 2
|
||||||
@@ -38,18 +67,131 @@ check_current() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
healthy() {
|
healthy() {
|
||||||
|
local port="${1:-$slot_a_port}"
|
||||||
local attempt
|
local attempt
|
||||||
for attempt in $(seq 1 30); do
|
for attempt in $(seq 1 30); do
|
||||||
if curl -sf --max-time 5 http://127.0.0.1:3002/api/health | grep -q '"database":true'; then return 0; fi
|
if curl -sf --max-time 5 "http://127.0.0.1:$port/api/health" | grep -q '"database":true'; then return 0; fi
|
||||||
sleep 3
|
sleep 3
|
||||||
done
|
done
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Staat er een blue/green-upstream? Zonder die bestanden blijft dit script op de
|
||||||
|
# oude, in-place cutover vallen, zodat een host met een andere nginx-indeling
|
||||||
|
# niet stilvalt op een upgrade.
|
||||||
|
detect_blue_green() {
|
||||||
|
[ -r "$upstream_file" ] || return 1
|
||||||
|
grep -qs 'cms_app' "$nginx_site" || return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Op welke poort verwerkt nginx nu verkeer? Onbekend (of geen bestand) betekent
|
||||||
|
# slot A, zodat de allereerste release op 3002 terechtkomt.
|
||||||
|
read_active_port() {
|
||||||
|
local port=""
|
||||||
|
port="$(grep -oE '127\.0\.0\.1:[0-9]+' "$upstream_file" 2>/dev/null | head -1 | cut -d: -f2 || true)"
|
||||||
|
case "$port" in
|
||||||
|
"$slot_b_port") printf '%s' "$slot_b_port" ;;
|
||||||
|
*) printf '%s' "$slot_a_port" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Zet de nginx-upstream op de nieuwe poort en herlaadt graceful.
|
||||||
|
#
|
||||||
|
# De nieuwe inhoud wordt eerst echt weggeschreven en dán getest: `nginx -t` leest
|
||||||
|
# het include-bestand van schijf, dus alleen achteraf testen zou de oude, werkende
|
||||||
|
# configuratie blijven valideren. Faalt de test, dan gaat het origineel onmiddellijk
|
||||||
|
# terug en raakt de live release niets.
|
||||||
|
switch_upstream() {
|
||||||
|
local port="$1"
|
||||||
|
local backup="${upstream_file}.deploy.bak"
|
||||||
|
if ! cp "$upstream_file" "$backup" 2>/dev/null; then
|
||||||
|
echo "Cannot back up $upstream_file; refusing to cut over" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
{
|
||||||
|
echo "# Geschreven door scripts/ci-deploy.sh op $(date -u +%FT%TZ) voor release $sha."
|
||||||
|
echo "# Niet met de hand bewerken: de volgende deploy overschrijft dit bestand."
|
||||||
|
echo "server 127.0.0.1:${port} max_fails=2 fail_timeout=10s;"
|
||||||
|
} >"$upstream_file"
|
||||||
|
if ! nginx -t >/dev/null 2>&1; then
|
||||||
|
echo "nginx rejected the new upstream; restoring the previous one" >&2
|
||||||
|
mv "$backup" "$upstream_file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! nginx -s reload; then
|
||||||
|
echo "nginx reload failed; restoring the previous upstream" >&2
|
||||||
|
mv "$backup" "$upstream_file"
|
||||||
|
nginx -s reload || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
rm -f "$backup"
|
||||||
|
# Even de tijd voor de graceful reload om de nieuwe worker te laten starten.
|
||||||
|
sleep 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start de kandidaat op een eigen host-poort.
|
||||||
|
#
|
||||||
|
# De resource limits staan hier bewust bij de `docker run` en niet alleen in
|
||||||
|
# docker-compose.yml: een release wordt hier gestart en gebruikt compose helemaal
|
||||||
|
# niet, dus limieten die alleen in de compose stonden zouden in productie nooit
|
||||||
|
# gelden.
|
||||||
|
start_candidate() {
|
||||||
|
local port="$1" name="$2"
|
||||||
|
(
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
. "$deploy_dir/.env"
|
||||||
|
set +a
|
||||||
|
ENV_ARGS=()
|
||||||
|
while IFS='=' read -r key _; do
|
||||||
|
case "$key" in ''|'#'*|*[!A-Za-z0-9_]* ) continue ;; esac
|
||||||
|
ENV_ARGS+=(-e "$key")
|
||||||
|
done < "$deploy_dir/.env"
|
||||||
|
docker run -d --name "$name" --restart always --net=host \
|
||||||
|
--memory="$mem_limit" --memory-swap="$mem_swap_limit" \
|
||||||
|
--cpus="$cpu_limit" --pids-limit="$pids_limit" \
|
||||||
|
"${ENV_ARGS[@]}" -e "PORT=$port" -e HOSTNAME=0.0.0.0 \
|
||||||
|
-v "$deploy_dir/public/nitro-assets:/app/public/nitro-assets" \
|
||||||
|
-v "$deploy_dir/public/swf:/app/public/swf" \
|
||||||
|
-v "$deploy_dir/storage:/app/storage" \
|
||||||
|
-v /var/www/Gamedata:/var/www/Gamedata \
|
||||||
|
"$image"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
finish() {
|
finish() {
|
||||||
local status=$?
|
local status=$?
|
||||||
trap - EXIT
|
trap - EXIT
|
||||||
if [ "$status" -ne 0 ] && [ "$cutover_started" -eq 1 ]; then
|
if [ "$status" -ne 0 ] && [ "$blue_green" -eq 1 ]; then
|
||||||
|
# Er zijn twee soorten falen, en het verschil bepaalt hoeveel werk terug moet.
|
||||||
|
if [ "$cutover_started" -eq 0 ]; then
|
||||||
|
# De live release draait nog ongestoord; alleen de kandidaat opruimen.
|
||||||
|
echo "Deployment failed before cutover; the live release was never stopped" >&2
|
||||||
|
if [ "$candidate_attempted" -eq 1 ] && [ -n "$new_container" ]; then
|
||||||
|
docker logs "$new_container" --tail 50 >&2 || true
|
||||||
|
docker rm -f "$new_container" || true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# nginx wijst nu naar de kandidaat. Eerst het verkeer terug, dan pas de
|
||||||
|
# kandidaat weghalen, anders zou de site 502-en terwijl we terugdraaien.
|
||||||
|
echo "Deployment failed after cutover; rolling back to port $old_port" >&2
|
||||||
|
if [ -n "$new_container" ]; then docker logs "$new_container" --tail 50 >&2 || true; fi
|
||||||
|
if [ -n "$old_port" ]; then switch_upstream "$old_port" || true; fi
|
||||||
|
if [ -n "$new_container" ]; then docker rm -f "$new_container" || true; fi
|
||||||
|
if [ -n "$old_container" ] && docker start "$old_container" >/dev/null 2>&1; then
|
||||||
|
if healthy "$old_port"; then
|
||||||
|
echo "Rollback verified on port $old_port"
|
||||||
|
else
|
||||||
|
echo "ERROR: previous container did not return to a healthy state" >&2
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "ERROR: no previous container to roll back to" >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif [ "$status" -ne 0 ] && [ "$cutover_started" -eq 1 ]; then
|
||||||
|
# In-place pad (geen blue/green-upstream): het oude scriptgedrag.
|
||||||
echo "Deployment failed; restoring previous container" >&2
|
echo "Deployment failed; restoring previous container" >&2
|
||||||
docker logs epicnext-cms-app --tail 50 >&2 || true
|
docker logs epicnext-cms-app --tail 50 >&2 || true
|
||||||
if command -v ss >/dev/null 2>&1; then ss -ltnp 'sport = :3002' >&2 || true; fi
|
if command -v ss >/dev/null 2>&1; then ss -ltnp 'sport = :3002' >&2 || true; fi
|
||||||
@@ -133,6 +275,30 @@ check_current
|
|||||||
pnpm db:migrate
|
pnpm db:migrate
|
||||||
check_current
|
check_current
|
||||||
|
|
||||||
|
# ── Blue/green: bepaal wie live is en waar de kandidaat mag starten ──
|
||||||
|
if detect_blue_green; then
|
||||||
|
blue_green=1
|
||||||
|
active_port="$(read_active_port)"
|
||||||
|
if [ "$active_port" = "$slot_b_port" ]; then
|
||||||
|
old_port=$slot_b_port; new_port=$slot_a_port
|
||||||
|
old_container=$slot_b_container; new_container=$slot_a_container
|
||||||
|
else
|
||||||
|
old_port=$slot_a_port; new_port=$slot_b_port
|
||||||
|
old_container=$slot_a_container; new_container=$slot_b_container
|
||||||
|
fi
|
||||||
|
echo "Live release keeps serving port $old_port; candidate starts on port $new_port"
|
||||||
|
# Rollback-evidence vastleggen voordat er iets wordt veranderd.
|
||||||
|
if docker inspect "$old_container" >/dev/null 2>&1; then
|
||||||
|
previous_name="$old_container"
|
||||||
|
previous_image="$(docker inspect --format '{{.Image}}' "$old_container")"
|
||||||
|
docker tag "$previous_image" epicnext-cms:previous
|
||||||
|
else
|
||||||
|
# Eerste release op een verse blue/green-opstelling: er is nog niets live.
|
||||||
|
previous_name=""
|
||||||
|
old_container=""
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
blue_green=0
|
||||||
# Prefer the active CI container, or the active legacy compose container.
|
# Prefer the active CI container, or the active legacy compose container.
|
||||||
for name in epicnext-cms epicnext-cms-app; do
|
for name in epicnext-cms epicnext-cms-app; do
|
||||||
if [ "$(docker inspect --format '{{.State.Running}}' "$name" 2>/dev/null || true)" = true ]; then
|
if [ "$(docker inspect --format '{{.State.Running}}' "$name" 2>/dev/null || true)" = true ]; then
|
||||||
@@ -144,10 +310,6 @@ if [ -z "$previous_name" ]; then
|
|||||||
if docker inspect "$name" >/dev/null 2>&1; then previous_name="$name"; break; fi
|
if docker inspect "$name" >/dev/null 2>&1; then previous_name="$name"; break; fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
if docker inspect "$backup_name" >/dev/null 2>&1; then
|
|
||||||
echo "Unresolved rollback container exists; refusing to overwrite it" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [ -n "$previous_name" ]; then
|
if [ -n "$previous_name" ]; then
|
||||||
previous_image="$(docker inspect --format '{{.Image}}' "$previous_name")"
|
previous_image="$(docker inspect --format '{{.Image}}' "$previous_name")"
|
||||||
docker tag "$previous_image" epicnext-cms:previous
|
docker tag "$previous_image" epicnext-cms:previous
|
||||||
@@ -156,8 +318,12 @@ fi
|
|||||||
if [ "$previous_name" != epicnext-cms-app ] && [ "$secondary_name" != epicnext-cms-app ] && docker inspect epicnext-cms-app >/dev/null 2>&1; then
|
if [ "$previous_name" != epicnext-cms-app ] && [ "$secondary_name" != epicnext-cms-app ] && docker inspect epicnext-cms-app >/dev/null 2>&1; then
|
||||||
docker rm epicnext-cms-app
|
docker rm epicnext-cms-app
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
if docker inspect "$backup_name" >/dev/null 2>&1; then
|
||||||
|
echo "Unresolved rollback container exists; refusing to overwrite it" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
cutover_started=1
|
|
||||||
# The avatar/badge disk cache lives on the host bind and is written by uid 33
|
# The avatar/badge disk cache lives on the host bind and is written by uid 33
|
||||||
# inside the container. Root-owned directories make every cache write fail
|
# inside the container. Root-owned directories make every cache write fail
|
||||||
# silently, which turns each avatar into a fresh live render.
|
# silently, which turns each avatar into a fresh live render.
|
||||||
@@ -167,6 +333,42 @@ for cache_dir in avatars badges; do
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
chown -R 33:33 "$deploy_dir/storage/imaging" 2>/dev/null || true
|
chown -R 33:33 "$deploy_dir/storage/imaging" 2>/dev/null || true
|
||||||
|
|
||||||
|
if [ "$blue_green" -eq 1 ]; then
|
||||||
|
# 1. Maak de doel-poort vrij. Alles wat daar draait is per definitie niet live,
|
||||||
|
# want nginx wijst nog naar old_port. Een restje van een mislukte eerdere
|
||||||
|
# deploy mag de nieuwe release niet blokkeren.
|
||||||
|
if docker inspect "$new_container" >/dev/null 2>&1; then
|
||||||
|
docker rm -f "$new_container"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Start de kandidaat ernaast. De live release draait ononderbroken door.
|
||||||
|
candidate_attempted=1
|
||||||
|
start_candidate "$new_port" "$new_container"
|
||||||
|
|
||||||
|
# 3. Gezond? Release-hash klopt? Browsersmoke-test? Pas dan hoeft het oude
|
||||||
|
# release het veld te ruimen — anders zou een mislukte e2e-test pas ná de
|
||||||
|
# cutover de productie breken in plaats van ervoor.
|
||||||
|
healthy "$new_port"
|
||||||
|
node scripts/verify-deployed-release.mjs "http://127.0.0.1:$new_port/api/health" "$sha"
|
||||||
|
PLAYWRIGHT_BASE_URL="http://127.0.0.1:$new_port" pnpm test:e2e
|
||||||
|
|
||||||
|
# 4. Het enige onomkeerbare moment: vanaf hier wijst nginx naar de kandidaat.
|
||||||
|
cutover_started=1
|
||||||
|
switch_upstream "$new_port"
|
||||||
|
echo "Cut over to port $new_port; retiring port $old_port"
|
||||||
|
|
||||||
|
# 5. Nu mag de oude release weg. Pas ná de swap, zodat er nooit een moment is
|
||||||
|
# waarop er geen enkele container draait.
|
||||||
|
if [ -n "$old_container" ] && docker inspect "$old_container" >/dev/null 2>&1; then
|
||||||
|
docker stop "$old_container"
|
||||||
|
docker rename "$old_container" "$backup_name"
|
||||||
|
backup_created=1
|
||||||
|
fi
|
||||||
|
verified_image="$(docker inspect --format '{{.Image}}' "$new_container")"
|
||||||
|
else
|
||||||
|
# In-place pad, alleen voor hosts zonder blue/green-upstream.
|
||||||
|
cutover_started=1
|
||||||
# Both legacy Compose and CI containers can exist after earlier failed updates.
|
# Both legacy Compose and CI containers can exist after earlier failed updates.
|
||||||
# Preserve each before releasing the shared host port; never kill an arbitrary PID.
|
# Preserve each before releasing the shared host port; never kill an arbitrary PID.
|
||||||
if [ -n "$secondary_name" ]; then
|
if [ -n "$secondary_name" ]; then
|
||||||
@@ -180,35 +382,19 @@ if [ -n "$previous_name" ]; then
|
|||||||
backup_created=1
|
backup_created=1
|
||||||
fi
|
fi
|
||||||
candidate_attempted=1
|
candidate_attempted=1
|
||||||
(
|
start_candidate 3002 epicnext-cms-app
|
||||||
set -a
|
healthy 3002
|
||||||
# shellcheck disable=SC1091
|
|
||||||
. "$deploy_dir/.env"
|
|
||||||
set +a
|
|
||||||
ENV_ARGS=()
|
|
||||||
while IFS='=' read -r key _; do
|
|
||||||
case "$key" in ''|'#'*|*[!A-Za-z0-9_]* ) continue ;; esac
|
|
||||||
ENV_ARGS+=(-e "$key")
|
|
||||||
done < "$deploy_dir/.env"
|
|
||||||
docker run -d --name epicnext-cms-app --restart always --net=host \
|
|
||||||
"${ENV_ARGS[@]}" -e PORT=3002 -e HOSTNAME=0.0.0.0 \
|
|
||||||
-v "$deploy_dir/public/nitro-assets:/app/public/nitro-assets" \
|
|
||||||
-v "$deploy_dir/public/swf:/app/public/swf" \
|
|
||||||
-v "$deploy_dir/storage:/app/storage" \
|
|
||||||
-v /var/www/Gamedata:/var/www/Gamedata \
|
|
||||||
"$image"
|
|
||||||
)
|
|
||||||
healthy
|
|
||||||
node scripts/verify-deployed-release.mjs http://127.0.0.1:3002/api/health "$sha"
|
node scripts/verify-deployed-release.mjs http://127.0.0.1:3002/api/health "$sha"
|
||||||
PLAYWRIGHT_BASE_URL=http://127.0.0.1:3002 pnpm test:e2e
|
PLAYWRIGHT_BASE_URL=http://127.0.0.1:3002 pnpm test:e2e
|
||||||
# Publish the latest alias only after HTTP and browser checks pass.
|
|
||||||
verified_image="$(docker inspect --format '{{.Image}}' epicnext-cms-app)"
|
verified_image="$(docker inspect --format '{{.Image}}' epicnext-cms-app)"
|
||||||
|
fi
|
||||||
docker tag "$verified_image" "epicnext-cms:verified-$sha"
|
docker tag "$verified_image" "epicnext-cms:verified-$sha"
|
||||||
docker tag "$verified_image" epicnext-cms:latest
|
docker tag "$verified_image" epicnext-cms:latest
|
||||||
cutover_started=0
|
cutover_started=0
|
||||||
if [ "$backup_created" -eq 1 ]; then docker rm "$backup_name" || true; fi
|
if [ "$backup_created" -eq 1 ]; then docker rm "$backup_name" || true; fi
|
||||||
if [ "$secondary_backup_created" -eq 1 ]; then docker rm "$secondary_backup" || true; fi
|
if [ "$secondary_backup_created" -eq 1 ]; then docker rm "$secondary_backup" || true; fi
|
||||||
|
|
||||||
|
|
||||||
echo "Deployment verified: $sha"
|
echo "Deployment verified: $sha"
|
||||||
# Retain the current and previous releases; do not remove arbitrary named tags.
|
# Retain the current and previous releases; do not remove arbitrary named tags.
|
||||||
while IFS= read -r tag; do
|
while IFS= read -r tag; do
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export const POST = withAdmin(
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "text/event-stream",
|
"Content-Type": "text/event-stream",
|
||||||
"Cache-Control": "no-cache",
|
"Cache-Control": "no-cache",
|
||||||
|
"X-Accel-Buffering": "no",
|
||||||
Connection: "keep-alive",
|
Connection: "keep-alive",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -343,6 +343,7 @@ export const POST = withAdmin(
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "text/event-stream",
|
"Content-Type": "text/event-stream",
|
||||||
"Cache-Control": "no-cache",
|
"Cache-Control": "no-cache",
|
||||||
|
"X-Accel-Buffering": "no",
|
||||||
Connection: "keep-alive",
|
Connection: "keep-alive",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -529,6 +529,7 @@ export const POST = withAdmin(
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "text/event-stream",
|
"Content-Type": "text/event-stream",
|
||||||
"Cache-Control": "no-cache",
|
"Cache-Control": "no-cache",
|
||||||
|
"X-Accel-Buffering": "no",
|
||||||
Connection: "keep-alive",
|
Connection: "keep-alive",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export const POST = withAdmin({ permission: PERMS.ASSETS_IMPORT }, async () => {
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "text/event-stream",
|
"Content-Type": "text/event-stream",
|
||||||
"Cache-Control": "no-cache",
|
"Cache-Control": "no-cache",
|
||||||
|
"X-Accel-Buffering": "no",
|
||||||
Connection: "keep-alive",
|
Connection: "keep-alive",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ export const POST = withAdmin(
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "text/event-stream",
|
"Content-Type": "text/event-stream",
|
||||||
"Cache-Control": "no-cache",
|
"Cache-Control": "no-cache",
|
||||||
|
"X-Accel-Buffering": "no",
|
||||||
Connection: "keep-alive",
|
Connection: "keep-alive",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ export const GET = withAdmin(
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "text/event-stream",
|
"Content-Type": "text/event-stream",
|
||||||
"Cache-Control": "no-cache",
|
"Cache-Control": "no-cache",
|
||||||
|
"X-Accel-Buffering": "no",
|
||||||
Connection: "keep-alive",
|
Connection: "keep-alive",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ export async function GET(req: Request) {
|
|||||||
headers: {
|
headers: {
|
||||||
"content-type": "text/event-stream; charset=utf-8",
|
"content-type": "text/event-stream; charset=utf-8",
|
||||||
"cache-control": "no-store, no-transform",
|
"cache-control": "no-store, no-transform",
|
||||||
|
"x-accel-buffering": "no",
|
||||||
connection: "keep-alive",
|
connection: "keep-alive",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export async function GET(req: Request) {
|
|||||||
headers: {
|
headers: {
|
||||||
"content-type": "text/event-stream; charset=utf-8",
|
"content-type": "text/event-stream; charset=utf-8",
|
||||||
"cache-control": "no-store, no-transform",
|
"cache-control": "no-store, no-transform",
|
||||||
|
"x-accel-buffering": "no",
|
||||||
connection: "keep-alive",
|
connection: "keep-alive",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ function simulate(
|
|||||||
BASH_ENV: resolve(root, "src/test/ci-deploy-harness.sh"),
|
BASH_ENV: resolve(root, "src/test/ci-deploy-harness.sh"),
|
||||||
TEST_DIR: dir.replaceAll("\\", "/"),
|
TEST_DIR: dir.replaceAll("\\", "/"),
|
||||||
CMS_DEPLOY_DIR: join(dir, "production").replaceAll("\\", "/"),
|
CMS_DEPLOY_DIR: join(dir, "production").replaceAll("\\", "/"),
|
||||||
|
// Deze scenario's testen het in-place pad. Door een pad op te geven
|
||||||
|
// dat niet bestaat ziet het script geen blue/green-upstream, ook niet
|
||||||
|
// wanneer de test op de productiehost zelf draait.
|
||||||
|
CMS_UPSTREAM_FILE: join(dir, "no-such-upstream.conf"),
|
||||||
|
CMS_NGINX_SITE: join(dir, "no-such-nginx-site.conf"),
|
||||||
TEST_SHA: sha,
|
TEST_SHA: sha,
|
||||||
SCENARIO: scenario,
|
SCENARIO: scenario,
|
||||||
DEPLOY_BRANCH: "main",
|
DEPLOY_BRANCH: "main",
|
||||||
@@ -191,3 +196,148 @@ describe("legacy and CI container coexistence", () => {
|
|||||||
expect(r.calls).toContain("docker start epicnext-cms");
|
expect(r.calls).toContain("docker start epicnext-cms");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Blue/green: nginx wijst pas naar de nieuwe release nadat die gezond is én de
|
||||||
|
// e2e-test heeft gewonnen. De hele winst zit in de volgorde, dus daar asserten
|
||||||
|
// we dan ook op.
|
||||||
|
function simulateBlueGreen(scenario: string, livePort = 3002) {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), "cms-deploy-bg-"));
|
||||||
|
try {
|
||||||
|
mkdirSync(join(dir, "production"));
|
||||||
|
// Een werkende deploymap heeft een DATABASE_URL; die gebruikt
|
||||||
|
// scripts/ci-deploy.sh voor de migraties op de host.
|
||||||
|
writeFileSync(
|
||||||
|
join(dir, "production", ".env"),
|
||||||
|
'HOTEL_NAME="Test Hotel"\nDATABASE_URL="mysql://test:[email protected]:3306/test"\n',
|
||||||
|
);
|
||||||
|
// De live release draait op `livePort`; nginx wijst ernaar.
|
||||||
|
writeFileSync(
|
||||||
|
join(dir, "upstream.conf"),
|
||||||
|
`server 127.0.0.1:${livePort} max_fails=2 fail_timeout=10s;\n`,
|
||||||
|
);
|
||||||
|
writeFileSync(join(dir, "nginx-site.conf"), "upstream cms_app { }\n");
|
||||||
|
// Het live container-bestand, zodat `docker inspect` hem als draaiend ziet.
|
||||||
|
writeFileSync(join(dir, "epicnext-cms-app"), "old\n");
|
||||||
|
const result = spawnSync(bash, [resolve(root, "scripts/ci-deploy.sh")], {
|
||||||
|
cwd: dir,
|
||||||
|
encoding: "utf8",
|
||||||
|
timeout: 25000,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
BASH_ENV: resolve(root, "src/test/ci-deploy-harness.sh"),
|
||||||
|
TEST_DIR: dir.replaceAll("\\", "/"),
|
||||||
|
CMS_DEPLOY_DIR: join(dir, "production").replaceAll("\\", "/"),
|
||||||
|
CMS_UPSTREAM_FILE: join(dir, "upstream.conf"),
|
||||||
|
CMS_NGINX_SITE: join(dir, "nginx-site.conf"),
|
||||||
|
TEST_SHA: sha,
|
||||||
|
SCENARIO: scenario,
|
||||||
|
DEPLOY_BRANCH: "main",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (result.error) throw result.error;
|
||||||
|
return {
|
||||||
|
status: result.status,
|
||||||
|
output: result.stdout + result.stderr,
|
||||||
|
calls: existsSync(join(dir, "calls"))
|
||||||
|
? readFileSync(join(dir, "calls"), "utf8")
|
||||||
|
: "",
|
||||||
|
upstream: existsSync(join(dir, "upstream.conf"))
|
||||||
|
? readFileSync(join(dir, "upstream.conf"), "utf8")
|
||||||
|
: "",
|
||||||
|
green: existsSync(join(dir, "epicnext-cms-green"))
|
||||||
|
? readFileSync(join(dir, "epicnext-cms-green"), "utf8").trim()
|
||||||
|
: null,
|
||||||
|
app: existsSync(join(dir, "epicnext-cms-app"))
|
||||||
|
? readFileSync(join(dir, "epicnext-cms-app"), "utf8").trim()
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("blue/green cutover", () => {
|
||||||
|
it("starts the candidate on the idle port and keeps the live one serving", () => {
|
||||||
|
const r = simulateBlueGreen("success");
|
||||||
|
expect(r.status, r.output).toBe(0);
|
||||||
|
// De kandidaat draait naast de live release, niet ervoor.
|
||||||
|
expect(r.calls).toContain("docker run -d --name epicnext-cms-green");
|
||||||
|
expect(r.calls).toContain("PORT=3003");
|
||||||
|
expect(r.calls.indexOf("docker run")).toBeLessThan(
|
||||||
|
r.calls.indexOf("docker stop epicnext-cms-app"),
|
||||||
|
);
|
||||||
|
// De release-hash en de e2e-test draaien tegen de kandidaat, vóór de swap.
|
||||||
|
expect(r.calls).toContain("http://127.0.0.1:3003/api/health");
|
||||||
|
expect(r.calls).toContain("PLAYWRIGHT_BASE_URL=http://127.0.0.1:3003");
|
||||||
|
expect(r.calls.indexOf("pnpm test:e2e")).toBeLessThan(
|
||||||
|
r.calls.indexOf("nginx -s reload"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("switches nginx over before retiring the live container", () => {
|
||||||
|
const r = simulateBlueGreen("success");
|
||||||
|
expect(r.status, r.output).toBe(0);
|
||||||
|
// nginx test vóór de reload, en de swap gaat vóór het stoppen: anders
|
||||||
|
// zou er een moment zijn waarop niets draait.
|
||||||
|
expect(r.calls).toContain("nginx -t");
|
||||||
|
expect(r.calls.indexOf("nginx -t")).toBeLessThan(
|
||||||
|
r.calls.indexOf("nginx -s reload"),
|
||||||
|
);
|
||||||
|
expect(r.calls.indexOf("nginx -s reload")).toBeLessThan(
|
||||||
|
r.calls.indexOf("docker stop epicnext-cms-app"),
|
||||||
|
);
|
||||||
|
expect(r.upstream).toContain("127.0.0.1:3003");
|
||||||
|
expect(r.upstream).not.toContain("127.0.0.1:3002");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("alternates slots on the next release", () => {
|
||||||
|
const r = simulateBlueGreen("success", 3003);
|
||||||
|
expect(r.status, r.output).toBe(0);
|
||||||
|
// Live op 3003 → de kandidaat gaat naar 3002.
|
||||||
|
expect(r.calls).toContain("docker run -d --name epicnext-cms-app");
|
||||||
|
expect(r.upstream).toContain("127.0.0.1:3002");
|
||||||
|
expect(r.upstream).not.toContain("127.0.0.1:3003");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
"run-failure",
|
||||||
|
"health-failure",
|
||||||
|
"smoke-failure",
|
||||||
|
"release-failure",
|
||||||
|
])(
|
||||||
|
"never stops the live release when the candidate fails on %s",
|
||||||
|
(scenario) => {
|
||||||
|
const r = simulateBlueGreen(scenario);
|
||||||
|
expect(r.status, r.output).not.toBe(0);
|
||||||
|
// Dit is de hele belofte: een mislukte release kost tijd, geen downtime.
|
||||||
|
expect(r.calls).not.toContain("docker stop epicnext-cms-app");
|
||||||
|
expect(r.calls).not.toContain("nginx -s reload");
|
||||||
|
expect(r.upstream).toContain("127.0.0.1:3002");
|
||||||
|
expect(r.green).toBeNull();
|
||||||
|
expect(r.output).toContain("the live release was never stopped");
|
||||||
|
},
|
||||||
|
30000,
|
||||||
|
);
|
||||||
|
|
||||||
|
it("leaves the live release alone when migrations fail", () => {
|
||||||
|
// Migraties draaien vóórdat de blue/green-detectie, dus er is op dat
|
||||||
|
// moment simpelweg nog niets om terug te draaien.
|
||||||
|
const r = simulateBlueGreen("migration-failure");
|
||||||
|
expect(r.status, r.output).not.toBe(0);
|
||||||
|
expect(r.calls).not.toContain("docker stop");
|
||||||
|
expect(r.calls).not.toContain("docker run");
|
||||||
|
expect(r.calls).not.toContain("nginx -s reload");
|
||||||
|
expect(r.upstream).toContain("127.0.0.1:3002");
|
||||||
|
expect(r.app).toBe("old");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the live release serving when nginx rejects the new upstream", () => {
|
||||||
|
const r = simulateBlueGreen("nginx-reject");
|
||||||
|
expect(r.status, r.output).not.toBe(0);
|
||||||
|
expect(r.calls).not.toContain("docker stop epicnext-cms-app");
|
||||||
|
expect(r.calls).not.toContain("nginx -s reload");
|
||||||
|
// De upstream is teruggezet op wat er stond.
|
||||||
|
expect(r.upstream).toContain("127.0.0.1:3002");
|
||||||
|
expect(r.output).toContain("restoring the previous one");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -43,8 +43,10 @@ async function warm<T>(name: string, load: () => Promise<T>): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function warmPublicCaches(): Promise<void> {
|
export async function warmPublicCaches(): Promise<void> {
|
||||||
// Small delays between groups: a boot-time burst of COUNT(*) queries against
|
// Elke `warm` wordt afgewacht voordat de volgende begint, dus de COUNT(*)
|
||||||
// a database that is still opening connections helps nobody.
|
// queries gaan één voor één in plaats van als een burst op een database die
|
||||||
|
// nog aan het opstarten is. Dat is goed genoeg; kunstmatige sleeps zouden
|
||||||
|
// de warme start alleen maar vertragen zonder iets te winnen.
|
||||||
await warm("online_count", () =>
|
await warm("online_count", () =>
|
||||||
cached("online_count", ONLINE_TTL_MS, countOnline, { staleMs: 15_000 }),
|
cached("online_count", ONLINE_TTL_MS, countOnline, { staleMs: 15_000 }),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,13 +11,30 @@ git() {
|
|||||||
}
|
}
|
||||||
flock() { echo "lock" >> "$TEST_DIR/calls"; [ "$SCENARIO" != lock-failure ]; }
|
flock() { echo "lock" >> "$TEST_DIR/calls"; [ "$SCENARIO" != lock-failure ]; }
|
||||||
pnpm() {
|
pnpm() {
|
||||||
echo "pnpm $*" >> "$TEST_DIR/calls"
|
# De env-prefix (bv. PLAYWRIGHT_BASE_URL) hoort bij het deploypad dat getest
|
||||||
|
# wordt, dus die loggen we mee: zo blijft zichtbaar dat de e2e-test tegen de
|
||||||
|
# juiste poort draait.
|
||||||
|
echo "${PLAYWRIGHT_BASE_URL:+PLAYWRIGHT_BASE_URL=$PLAYWRIGHT_BASE_URL }pnpm $*" >> "$TEST_DIR/calls"
|
||||||
if [ "$1" = db:migrate ] && [ "$SCENARIO" = migration-failure ]; then return 1; fi
|
if [ "$1" = db:migrate ] && [ "$SCENARIO" = migration-failure ]; then return 1; fi
|
||||||
if [ "$1" = test:e2e ] && [ "$SCENARIO" = smoke-failure ] && [ "$(cat "$TEST_DIR/epicnext-cms-app" 2>/dev/null)" = new ]; then return 1; fi
|
if [ "$1" = test:e2e ] && [ "$SCENARIO" = smoke-failure ] && candidate_is_new; then return 1; fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
# De kandidaat is het container dat het script zojuist heeft gestart. Bij
|
||||||
|
# blue/green is dat niet per se `epicnext-cms-app`.
|
||||||
|
candidate_is_new() {
|
||||||
|
for f in epicnext-cms-app epicnext-cms-green; do
|
||||||
|
if [ "$(cat "$TEST_DIR/$f" 2>/dev/null)" = new ]; then return 0; fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
nginx() {
|
||||||
|
echo "nginx $*" >> "$TEST_DIR/calls"
|
||||||
|
if [ "$SCENARIO" = nginx-reject ] && [ "$1" = "-t" ]; then return 1; fi
|
||||||
|
if [ "$SCENARIO" = nginx-reload-fail ] && [ "$1" = "-s" ]; then return 1; fi
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
curl() {
|
curl() {
|
||||||
if [ "$SCENARIO" = health-failure ] && [ "$(cat "$TEST_DIR/epicnext-cms-app" 2>/dev/null)" = new ]; then return 1; fi
|
if [ "$SCENARIO" = health-failure ] && candidate_is_new; then return 1; fi
|
||||||
echo '{"database":true}'
|
echo '{"database":true}'
|
||||||
}
|
}
|
||||||
sleep() { :; }
|
sleep() { :; }
|
||||||
@@ -40,14 +57,20 @@ docker() {
|
|||||||
stop) return 0 ;;
|
stop) return 0 ;;
|
||||||
rename) mv "$TEST_DIR/$2" "$TEST_DIR/$3" ;;
|
rename) mv "$TEST_DIR/$2" "$TEST_DIR/$3" ;;
|
||||||
run)
|
run)
|
||||||
echo new > "$TEST_DIR/epicnext-cms-app"
|
run_name=epicnext-cms-app
|
||||||
|
prev=""
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [ "$prev" = "--name" ]; then run_name="$arg"; fi
|
||||||
|
prev="$arg"
|
||||||
|
done
|
||||||
|
echo new > "$TEST_DIR/$run_name"
|
||||||
[ "$SCENARIO" != run-failure ] ;;
|
[ "$SCENARIO" != run-failure ] ;;
|
||||||
start) [ -f "$TEST_DIR/$2" ] ;;
|
start) [ -f "$TEST_DIR/$2" ] ;;
|
||||||
rm) rm -f "$TEST_DIR/$name" ;;
|
rm) rm -f "$TEST_DIR/$name" ;;
|
||||||
*) return 0 ;;
|
*) return 0 ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
export -f git flock pnpm curl sleep docker
|
export -f git flock pnpm curl sleep docker nginx candidate_is_new
|
||||||
|
|
||||||
node() {
|
node() {
|
||||||
echo "node $*" >> "$TEST_DIR/calls"
|
echo "node $*" >> "$TEST_DIR/calls"
|
||||||
|
|||||||
Reference in new issue
Block a user