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
+1692
-1121
No files matched your search
@@ -79,6 +79,7 @@ export const POST = withAdmin(
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
"X-Accel-Buffering": "no",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -343,6 +343,7 @@ export const POST = withAdmin(
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
"X-Accel-Buffering": "no",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -529,6 +529,7 @@ export const POST = withAdmin(
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
"X-Accel-Buffering": "no",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -40,6 +40,7 @@ export const POST = withAdmin({ permission: PERMS.ASSETS_IMPORT }, async () => {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
"X-Accel-Buffering": "no",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -124,6 +124,7 @@ export const POST = withAdmin(
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
"X-Accel-Buffering": "no",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -125,6 +125,7 @@ export const GET = withAdmin(
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
"X-Accel-Buffering": "no",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -103,6 +103,7 @@ export async function GET(req: Request) {
|
||||
headers: {
|
||||
"content-type": "text/event-stream; charset=utf-8",
|
||||
"cache-control": "no-store, no-transform",
|
||||
"x-accel-buffering": "no",
|
||||
connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -61,6 +61,7 @@ export async function GET(req: Request) {
|
||||
headers: {
|
||||
"content-type": "text/event-stream; charset=utf-8",
|
||||
"cache-control": "no-store, no-transform",
|
||||
"x-accel-buffering": "no",
|
||||
connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -50,6 +50,11 @@ function simulate(
|
||||
BASH_ENV: resolve(root, "src/test/ci-deploy-harness.sh"),
|
||||
TEST_DIR: dir.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,
|
||||
SCENARIO: scenario,
|
||||
DEPLOY_BRANCH: "main",
|
||||
@@ -191,3 +196,148 @@ describe("legacy and CI container coexistence", () => {
|
||||
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> {
|
||||
// Small delays between groups: a boot-time burst of COUNT(*) queries against
|
||||
// a database that is still opening connections helps nobody.
|
||||
// Elke `warm` wordt afgewacht voordat de volgende begint, dus de COUNT(*)
|
||||
// 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", () =>
|
||||
cached("online_count", ONLINE_TTL_MS, countOnline, { staleMs: 15_000 }),
|
||||
);
|
||||
|
||||
@@ -11,13 +11,30 @@ git() {
|
||||
}
|
||||
flock() { echo "lock" >> "$TEST_DIR/calls"; [ "$SCENARIO" != lock-failure ]; }
|
||||
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" = 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
|
||||
}
|
||||
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}'
|
||||
}
|
||||
sleep() { :; }
|
||||
@@ -40,14 +57,20 @@ docker() {
|
||||
stop) return 0 ;;
|
||||
rename) mv "$TEST_DIR/$2" "$TEST_DIR/$3" ;;
|
||||
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 ] ;;
|
||||
start) [ -f "$TEST_DIR/$2" ] ;;
|
||||
rm) rm -f "$TEST_DIR/$name" ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
}
|
||||
export -f git flock pnpm curl sleep docker
|
||||
export -f git flock pnpm curl sleep docker nginx candidate_is_new
|
||||
|
||||
node() {
|
||||
echo "node $*" >> "$TEST_DIR/calls"
|
||||
|
||||
Reference in new issue
Block a user