Files
EpicNext-Cms/src/lib/deploy-workflow-contract.test.ts
T
openhands 108c6ce03d
Gitea Actions Runner Test / test-job (push) Successful in 2s
CI / check (push) Successful in 36s
CI / tests-integration (push) Successful in 2m3s
CI / tests-unit (push) Successful in 2m18s
CI / tests-ui (push) Successful in 3m6s
CI / preflight (push) Skipped
CI / deploy (push) Failing after 3m14s
fix(ci): make the lint gate fail for real and stop byparr leaking disk
The CI lint step was `biome check . || true`, so it could never fail: 14 real
violations were passing unnoticed. Drop the `|| true` and fix what it found.

Lint fixes, none of which change behaviour:
- give list items their natural identity instead of the array index
  (key={c} / key={char}, key={`skeleton-${i}`})
- document the two useEffect dependency lists that must keep their
  function-declaration handlers, with the reasoning that dropping them broke
  the tree and save-on-Ctrl+S once already (704e3363)
- scope the remaining noArrayIndexKey / useExhaustiveDependencies exemptions to
  the three files that need them, in biome.json instead of scattered comments

Storage, on a host that had grown to 81% disk:
- byparr starts a Firefox per request and never removes the profile it leaves in
  the container's writable layer. With no volume mounted, nothing else reclaimed
  it: 716 profiles / 6.8 GB in two days, ~1.7 GB/day. docker-prune.sh now removes
  orphaned profiles, identifying live ones by the open fd in /proc/<pid>/fd rather
  than by age, because browsers stay warm for ~27 hours here — longer than the
  leak window, so no age threshold can be both safe and useful.
- bound the build cache properly: buildx treats --max-used-space and --filter as
  mutually exclusive, so passing both silently dropped the 4 GB cap and the cache
  reached 49 GB.
- escalate to the emergency prune when / drops below 8 GB free, so the bound holds
  even if the schedule stops.
- clear multi-GB tmp_pack files left behind by a gc that was OOM-killed
  mid-repack; git only removes those on the next successful gc.
- make setup-cron.sh append instead of replacing the crontab (`crontab -`
  overwrites the whole file, which had been dropping the other scheduled jobs),
  and run the prune daily rather than weekly to match the leak rate.

Volumes are still never pruned: mariadb-turbo-data is a database.
2026-10-05 17:24:12 +02:00

83 lines
4.1 KiB
TypeScript

// @ts-nocheck
import { readFileSync } from "node:fs";
import { expect, it } from "vitest";
const workflow = readFileSync(".gitea/workflows/ci.yaml", "utf8");
const deploy = readFileSync("scripts/ci-deploy.sh", "utf8");
it("uses two test workers and runs smoke checks in the deployment transaction", () => {
expect(workflow).toContain("pnpm test:coverage --maxWorkers=4");
expect(workflow).not.toContain("\n e2e:");
expect(workflow).toContain("bash scripts/ci-deploy.sh");
expect(deploy).toContain(
"node scripts/verify-deployed-release.mjs http://127.0.0.1:3002/api/health",
);
});
it("locks CI and scheduled deployments using the same production lock", () => {
expect(deploy).toContain('exec 9>"$deploy_dir/.deploy.lock"');
expect(deploy).toContain("flock -w 1800 9");
const scheduled = readFileSync("scripts/docker-update.sh", "utf8");
expect(scheduled).toContain('exec 9>"$DIR/.deploy.lock"');
expect(scheduled.indexOf("flock -w 1800 9")).toBeLessThan(
scheduled.indexOf("git pull --ff-only"),
);
});
it("preserves production runtime configuration and recent cache", () => {
expect(deploy).toContain("--net=host");
expect(deploy).toContain("--restart always");
expect(deploy).toContain("/var/www/Gamedata:/var/www/Gamedata");
expect(deploy).toContain("/app/storage");
expect(deploy).not.toContain("--env-file");
// The scoped prune lives in docker-prune.sh; deploys invoke it for both CI
// and scheduled updates. It reclaims build cache + old unreferenced images
// but never touches volumes.
expect(deploy).toContain('bash "$deploy_dir/scripts/docker-prune.sh"');
const prune = readFileSync("scripts/docker-prune.sh", "utf8");
// The build cache is bounded by the cap alone. buildx treats --max-used-space
// and --filter as mutually exclusive: combining them silently dropped the
// cap, so the cache grew unbounded (49 GB observed on this host).
expect(prune).toContain("docker builder prune -af --max-used-space=");
expect(prune).toContain('docker image prune -af --filter "until=168h"');
expect(prune).toContain('docker container prune -f --filter "until=24h"');
// Emergency `--force` mode drops every age window to reclaim unused bytes,
// but even then volumes are off-limits.
expect(prune).toContain('== "--force" ]]');
expect(prune).toContain("FORCE=1");
expect(prune).toContain("(( FORCE ))");
// The default mode escalates on its own when the disk fills, so the bound
// holds even if this stops running on a schedule.
expect(prune).toContain("FREE_KB");
expect(prune).toMatch(/if\s*\(\(\s*FREE_KB\s*</);
expect(deploy).not.toContain("--force");
expect(deploy).not.toContain("docker volume prune");
expect(prune).not.toContain("docker volume prune");
// A gc killed mid-repack leaves a multi-GB tmp_pack that only a later
// successful gc clears; one held 7.7 GB while the object store was 83 MB.
expect(prune).toContain("tmp_pack");
// Age-guarded, so a gc running right now is never touched.
expect(prune).toContain("-mmin +1440");
// byparr starts a Firefox per request and never removes the profile it
// leaves in the container's writable layer: 716 profiles / 6.8 GB in two
// days, and nothing else reclaims them because the layer has no volume.
expect(prune).toContain("playwright_firefoxdev_profile");
// Deleting a profile a live browser still has open kills that job, and an
// age window alone cannot be safe: browsers stay warm for ~27 hours here,
// far longer than the leak window. Liveness comes from the open fd instead.
expect(prune).toContain("/proc/$p/fd");
expect(prune).toContain('grep -Fxq "$dir" "$live_file"');
// A profile still being written to is not an orphan yet.
expect(prune).toContain("BYPARR_TMP_MIN_AGE_MIN");
});
it("builds the checked out source without fetching a moving remote branch", () => {
const dockerfile = readFileSync("Dockerfile", "utf8");
expect(dockerfile).toContain("COPY . .");
expect(dockerfile).not.toMatch(
/^ADD\s+https?:\/\/.*(?:repository|branches)/m,
);
expect(dockerfile).not.toMatch(/^RUN\s+git\s+(?:pull|fetch|clone)\b/m);
});
it("no longer publishes container images in CI", () => {
expect(workflow).not.toContain("publish-container");
expect(workflow).not.toContain("publish-container.sh");
});