Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a296551f6 | ||
|
|
e70b96e3a7 |
No files matched your search
@@ -0,0 +1,42 @@
|
||||
name: Renovate
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "17 4 * * 1-5"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
debug:
|
||||
description: "Debug-loglevel"
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
# Niet overlappen met zichzelf; een tweede start wacht tot de eerste klaar is.
|
||||
concurrency:
|
||||
group: renovate
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
RENOVATE_VERSION: 44.164.10 # renovate: datasource=npm depName=renovate
|
||||
|
||||
jobs:
|
||||
renovate:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Run Renovate
|
||||
env:
|
||||
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
||||
LOG_LEVEL: ${{ github.event.inputs.debug == 'true' && 'debug' || 'info' }}
|
||||
shell: bash
|
||||
run: |
|
||||
docker run --rm \
|
||||
--memory=6g \
|
||||
--memory-swap=6g \
|
||||
-e RENOVATE_PLATFORM=gitea \
|
||||
-e RENOVATE_ENDPOINT=https://gitlab.epicnabbo.nl/ \
|
||||
-e RENOVATE_TOKEN \
|
||||
-e RENOVATE_REPOSITORIES='["remco/EpicNext-Cms"]' \
|
||||
-e RENOVATE_AUTODISCOVER=false \
|
||||
-e RENOVATE_BINARY_SOURCE=install \
|
||||
-e LOG_LEVEL \
|
||||
"renovate/renovate:${RENOVATE_VERSION}"
|
||||
@@ -56,9 +56,18 @@ Lenis has been removed; the public site now uses native scrolling. Other used
|
||||
runtime libraries remain. Vitest and its coverage provider are upgraded together;
|
||||
`clearMocks: false` preserves initialization-time permission contract assertions.
|
||||
|
||||
Dependency updates are manual: Renovate has been removed, so there is no bot
|
||||
opening upgrade pull requests. Node/pnpm upgrades remain coordinated with
|
||||
Docker and the runner toolchain.
|
||||
Dependency updates run through self-hosted Renovate: `renovate.json` plus
|
||||
`.gitea/workflows/renovate.yaml` run the pinned `renovate/renovate` image on the
|
||||
host runner every weekday at 04:17 (and via `workflow_dispatch`). Minor and patch
|
||||
updates are grouped into one PR and automerged only after CI is green
|
||||
(`platformAutomerge: false`, so Renovate waits on the checks itself); major
|
||||
updates require approval on the dependency dashboard. The job authenticates with
|
||||
the `renovate-bot` account token stored as the repo Actions secret
|
||||
`RENOVATE_TOKEN`; rotation only requires overwriting that secret (the bot
|
||||
password lives in `/root/.gitea-renovate-bot.env` on the host). `minimumReleaseAge`
|
||||
is 3 days in `renovate.json` and 60 hours in `pnpm-workspace.yaml`, so neither the
|
||||
installer nor the bot picks up a fresh release on day zero. Node/pnpm upgrades
|
||||
remain coordinated with Docker and the runner toolchain.
|
||||
Obsolete global overrides were removed; a scoped esbuild override remains because
|
||||
Drizzle Kit's loader still resolves a vulnerable legacy development-server build.
|
||||
The two deprecated esbuild-kit packages remain upstream dependencies of Drizzle
|
||||
|
||||
+13
-11
@@ -1,4 +1,4 @@
|
||||
# Informational route JavaScript budgets
|
||||
# Route JavaScript budgets (enforced)
|
||||
|
||||
Run after the existing production build; no second build or server is needed:
|
||||
|
||||
@@ -28,20 +28,22 @@ The definition follows the data exposed by the installed Next 16.3.8 build and t
|
||||
|
||||
## Initial limits
|
||||
|
||||
The first limits are **baseline bytes × 1.15, rounded upward to the next 10 KiB (10,240 bytes)** independently for raw and gzip. They are provisional size alerts, not validated speed targets. Baseline: local production build `build-TfctsWXpff2fKS`, Next 16.3.4 **Turbopack**; its source commit was not inferred.
|
||||
The limits are **baseline bytes × 1.15, rounded upward to the next 10 KiB (10,240 bytes)** independently for raw and gzip. They are size guardrails, not validated speed targets.
|
||||
|
||||
The production build now runs webpack, so the numbers it reports are not directly comparable to the baseline below. Re-measured on the current webpack build the routes land at `/me` 786138/247738, `/news` 781601/245672, `/events` 782011/245923, `/search` 783262/246578, `/admin/catalog` 1172089/370843, `/admin/studio/furni` 1374128/440546 (raw/gzip). All remain inside the limits below, but `/admin/studio/furni` sits at ~98% of its gzip limit, so the next dependency added to that route will trip it. Recalibrate the table and `scripts/performance-budgets.json` together if the intent is to reset the baseline on webpack.
|
||||
Baselines were last recalibrated against a local webpack production build (Next 16.4.1, `next build --webpack`) and cross-checked against the report the Docker image build emits: `/me` 621460/195411, `/news` 616712/193199, `/events` 617121/193450, `/search` 618536/194216, `/admin/catalog` 1049094/330369, `/admin/studio/furni` 1397566/447765 (raw/gzip). The earlier Turbopack numbers (previous revision) were **not** comparable and were dropped.
|
||||
|
||||
The image build compiles the app, then runs `scripts/performance-report.mjs` in the same `RUN` layer (`Dockerfile:48-53`). In `mode: enforce` a failing route therefore fails the image build, which blocks both the PR `preflight` job and the `deploy` job. Recalibrate the table and `scripts/performance-budgets.json` **together** in the same commit whenever the envelope legitimately grows (new route dependency, Next upgrade).
|
||||
|
||||
| Route | Baseline raw bytes | Baseline gzip bytes | Raw limit | Gzip limit |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| `/me` | 767156 | 238571 | 890880 | 276480 |
|
||||
| `/news` | 765367 | 237599 | 880640 | 276480 |
|
||||
| `/events` | 765851 | 237964 | 890880 | 276480 |
|
||||
| `/search` | 765851 | 237964 | 890880 | 276480 |
|
||||
| `/admin/catalog` | 1654898 | 492619 | 1904640 | 573440 |
|
||||
| `/admin/studio/furni` | 1241312 | 391541 | 1433600 | 450560 |
|
||||
| `/me` | 621460 | 195411 | 716800 | 225280 |
|
||||
| `/news` | 616712 | 193199 | 716800 | 225280 |
|
||||
| `/events` | 617121 | 193450 | 716800 | 225280 |
|
||||
| `/search` | 618536 | 194216 | 716800 | 225280 |
|
||||
| `/admin/catalog` | 1049094 | 330369 | 1208320 | 389120 |
|
||||
| `/admin/studio/furni` | 1397566 | 447765 | 1607680 | 522240 |
|
||||
|
||||
Configured limits are positive integer bytes; `null` explicitly means observe-only. `scripts/performance-budgets.json` remains `mode: informational`. Exceeding a limit produces `over-budget` and a warning, with exit code 0. Missing production `BUILD_ID`, unsupported manifests, missing routes or missing referenced assets produce `unavailable` with a reason and **no partial/zero total**, also exit code 0. Malformed budget configuration or an unwritable output directory fails the command. This keeps initial CI reporting non-blocking while preventing invalid configuration from quietly disabling limits.
|
||||
Configured limits are positive integer bytes; `null` explicitly means observe-only. `scripts/performance-budgets.json` runs `mode: enforce`: an `over-budget` route prints `Performance budgets enforced: <route> (over-budget)` and exits non-zero. Missing production `BUILD_ID`, unsupported manifests, missing routes or missing referenced assets produce `unavailable` with a reason and **no partial/zero total**; under `enforce` an `unavailable` route also fails the command, so a bundler change that breaks measurement cannot pass quietly. Malformed budget configuration or an unwritable output directory always fails the command. Set `mode` back to `informational` only while recalibrating, never as a permanent state.
|
||||
|
||||
Synthetic tests cover shared-chunk deduplication, exact byte/gzip calculations, route bootstrap selection, missing data, safe parsing and CLI exit behavior. Run `pnpm exec vitest run --coverage.enabled=false scripts/performance-report.test.mjs`.
|
||||
Synthetic tests cover shared-chunk deduplication, exact byte/gzip calculations, route bootstrap selection, missing data, safe parsing and CLI exit behavior in both modes. Run `pnpm exec vitest run --coverage.enabled=false scripts/performance-report.test.mjs`.
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
":timezone(Europe/Amsterdam)",
|
||||
":semanticCommits",
|
||||
"workarounds:all"
|
||||
],
|
||||
"labels": ["dependencies"],
|
||||
"dependencyDashboard": true,
|
||||
"prConcurrentLimit": 5,
|
||||
"prHourlyLimit": 3,
|
||||
"minimumReleaseAge": "3 days",
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "Minor en patch: één groeps-PR, automerge pas na groene CI",
|
||||
"matchUpdateTypes": ["minor", "patch", "pin", "digest"],
|
||||
"groupName": "all non-major dependencies",
|
||||
"automerge": true,
|
||||
"platformAutomerge": false
|
||||
},
|
||||
{
|
||||
"description": "Major updates vragen goedkeuring via het dependency-dashboard",
|
||||
"matchUpdateTypes": ["major"],
|
||||
"labels": ["major"],
|
||||
"dependencyDashboardApproval": true
|
||||
},
|
||||
{
|
||||
"description": "Docker-compose draait op :latest; geen digest-pin-ruis",
|
||||
"matchManagers": ["docker-compose"],
|
||||
"pinDigests": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,36 +1,36 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"mode": "informational",
|
||||
"mode": "enforce",
|
||||
"routes": [
|
||||
{
|
||||
"route": "/me",
|
||||
"maxInitialRawBytes": 890880,
|
||||
"maxInitialGzipBytes": 276480
|
||||
"maxInitialRawBytes": 716800,
|
||||
"maxInitialGzipBytes": 225280
|
||||
},
|
||||
{
|
||||
"route": "/news",
|
||||
"maxInitialRawBytes": 880640,
|
||||
"maxInitialGzipBytes": 276480
|
||||
"maxInitialRawBytes": 716800,
|
||||
"maxInitialGzipBytes": 225280
|
||||
},
|
||||
{
|
||||
"route": "/events",
|
||||
"maxInitialRawBytes": 890880,
|
||||
"maxInitialGzipBytes": 276480
|
||||
"maxInitialRawBytes": 716800,
|
||||
"maxInitialGzipBytes": 225280
|
||||
},
|
||||
{
|
||||
"route": "/search",
|
||||
"maxInitialRawBytes": 890880,
|
||||
"maxInitialGzipBytes": 276480
|
||||
"maxInitialRawBytes": 716800,
|
||||
"maxInitialGzipBytes": 225280
|
||||
},
|
||||
{
|
||||
"route": "/admin/catalog",
|
||||
"maxInitialRawBytes": 1904640,
|
||||
"maxInitialGzipBytes": 573440
|
||||
"maxInitialRawBytes": 1208320,
|
||||
"maxInitialGzipBytes": 389120
|
||||
},
|
||||
{
|
||||
"route": "/admin/studio/furni",
|
||||
"maxInitialRawBytes": 1433600,
|
||||
"maxInitialGzipBytes": 450560
|
||||
"maxInitialRawBytes": 1607680,
|
||||
"maxInitialGzipBytes": 522240
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -7,12 +7,12 @@ const LIMIT_KEYS = ["route", "maxInitialRawBytes", "maxInitialGzipBytes"];
|
||||
export function validateBudgets(config) {
|
||||
if (
|
||||
config?.schemaVersion !== 1 ||
|
||||
config.mode !== "informational" ||
|
||||
(config.mode !== "informational" && config.mode !== "enforce") ||
|
||||
!Array.isArray(config.routes) ||
|
||||
config.routes.length === 0
|
||||
)
|
||||
throw new Error(
|
||||
"Budget config requires schemaVersion 1, mode informational and nonempty routes.",
|
||||
"Budget config requires schemaVersion 1, mode informational or enforce, and nonempty routes.",
|
||||
);
|
||||
if (
|
||||
Object.keys(config).some(
|
||||
@@ -288,7 +288,7 @@ export function collectReport(nextDir, config, metadata = {}) {
|
||||
});
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
mode: "informational",
|
||||
mode: config.mode,
|
||||
generatedAt: new Date().toISOString(),
|
||||
buildId,
|
||||
commitSha: metadata.commitSha ?? null,
|
||||
@@ -308,6 +308,10 @@ export function collectReport(nextDir, config, metadata = {}) {
|
||||
}
|
||||
|
||||
export function markdownReport(report) {
|
||||
const modeLine =
|
||||
report.mode === "enforce"
|
||||
? `Build: ${report.buildId ?? "unavailable"}. Declared commit: ${report.commitSha ?? "not supplied"}. Mode: enforce; an over-budget or unavailable route fails the command and the image build.`
|
||||
: `Build: ${report.buildId ?? "unavailable"}. Declared commit: ${report.commitSha ?? "not supplied"}. Mode: informational; exceeded budgets and unavailable metrics do not fail CI.`;
|
||||
const lines = [
|
||||
"# Route JavaScript size report",
|
||||
"",
|
||||
@@ -315,7 +319,7 @@ export function markdownReport(report) {
|
||||
"",
|
||||
report.gzip,
|
||||
"",
|
||||
`Build: ${report.buildId ?? "unavailable"}. Declared commit: ${report.commitSha ?? "not supplied"}. Mode: informational; exceeded budgets and unavailable metrics do not fail CI.`,
|
||||
modeLine,
|
||||
"",
|
||||
"| Route | State | Unique JS | Raw bytes | Gzip bytes | Raw budget | Gzip budget |",
|
||||
"| --- | --- | ---: | ---: | ---: | ---: | ---: |",
|
||||
@@ -370,6 +374,17 @@ export function main(argv = process.argv.slice(2)) {
|
||||
console.warn(
|
||||
`Performance warning: ${row.route} ${row.status}${row.reason ? `: ${row.reason}` : ""}`,
|
||||
);
|
||||
if (report.mode === "enforce") {
|
||||
const failed = report.routes.filter((row) => row.status !== "measured");
|
||||
if (failed.length > 0) {
|
||||
console.error(
|
||||
`Performance budgets enforced: ${failed
|
||||
.map((row) => `${row.route} (${row.status})`)
|
||||
.join(", ")}`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
return report;
|
||||
}
|
||||
if (
|
||||
|
||||
@@ -373,7 +373,10 @@ describe("route JS measurement", () => {
|
||||
expect(() =>
|
||||
validateBudgets({ ...config, routes: [budget, budget] }),
|
||||
).toThrow();
|
||||
expect(() => validateBudgets({ ...config, mode: "enforce" })).toThrow();
|
||||
expect(validateBudgets({ ...config, mode: "enforce" }).mode).toBe(
|
||||
"enforce",
|
||||
);
|
||||
expect(() => validateBudgets({ ...config, mode: "strict" })).toThrow();
|
||||
expect(() => validateBudgets({ ...config, typo: true })).toThrow();
|
||||
});
|
||||
it("emits warnings and artifacts with exit zero when a build is unavailable", () => {
|
||||
@@ -423,4 +426,51 @@ describe("route JS measurement", () => {
|
||||
expect(markdown).toContain("not measured browser traffic");
|
||||
expect(markdown).toContain("Raw bytes");
|
||||
});
|
||||
describe("enforce mode", () => {
|
||||
const run = (dir, budgets) => {
|
||||
const write = (relative, value) => {
|
||||
const file = path.join(dir, relative);
|
||||
mkdirSync(path.dirname(file), { recursive: true });
|
||||
writeFileSync(file, JSON.stringify(value));
|
||||
};
|
||||
write("budgets.json", budgets);
|
||||
return spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"scripts/performance-report.mjs",
|
||||
"--next-dir",
|
||||
dir,
|
||||
"--config",
|
||||
path.join(dir, "budgets.json"),
|
||||
"--output-dir",
|
||||
path.join(dir, "reports"),
|
||||
],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
};
|
||||
it("passes when every route is measured within budget", () => {
|
||||
const enforce = { ...config, mode: "enforce" };
|
||||
const runResult = run(fixture().dir, enforce);
|
||||
expect(runResult.status).toBe(0);
|
||||
expect(runResult.stdout).toContain("Mode: enforce");
|
||||
expect(runResult.stderr).not.toContain("Performance budgets enforced");
|
||||
});
|
||||
it("fails when a route is over budget", () => {
|
||||
const enforce = {
|
||||
schemaVersion: 1,
|
||||
mode: "enforce",
|
||||
routes: [{ ...budget, maxInitialRawBytes: 1, maxInitialGzipBytes: 1 }],
|
||||
};
|
||||
const runResult = run(fixture().dir, enforce);
|
||||
expect(runResult.status).toBe(1);
|
||||
expect(runResult.stderr).toContain("Performance budgets enforced");
|
||||
expect(runResult.stderr).toContain("/news (over-budget)");
|
||||
});
|
||||
it("fails when a route cannot be measured, so broken manifests do not pass quietly", () => {
|
||||
const enforce = { ...config, mode: "enforce" };
|
||||
const runResult = run(path.join(fixture().dir, "missing"), enforce);
|
||||
expect(runResult.status).toBe(1);
|
||||
expect(runResult.stderr).toContain("/news (unavailable)");
|
||||
});
|
||||
});
|
||||
});
|
||||
+4
-4
@@ -38,10 +38,10 @@ export default defineConfig({
|
||||
provider: "v8",
|
||||
reporter: ["text", "lcov", "html"],
|
||||
thresholds: {
|
||||
statements: 12,
|
||||
branches: 9,
|
||||
functions: 10,
|
||||
lines: 13,
|
||||
statements: 36,
|
||||
branches: 29,
|
||||
functions: 30,
|
||||
lines: 37,
|
||||
},
|
||||
include: ["src/**/*.{ts,tsx}"],
|
||||
exclude: [
|
||||
|
||||
Reference in new issue
Block a user