diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aca2e23366..78f8bef729 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -398,6 +398,21 @@ jobs: - name: UI build if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' }} run: npm run extension:build && npm run miner-extension:build && npm --workspace @loopover/ui run build + # Bundle-size regression tracking for the deployed marketing/product site (loopover-ui only -- + # loopover-miner-ui is a self-hosted operator dashboard with no build step in this job at all, see + # "UI build" above). Fork-excluded (no secrets.CODECOV_TOKEN on a fork PR, same boundary as the + # coverage-upload steps below) and non-blocking (continue-on-error): unlike coverage, nothing gates + # merges on bundle size, so an upload hiccup must never fail CI. Uses @codecov/bundle-analyzer's + # standalone CLI against the built dist/client directly rather than a bundler-plugin hook -- + # @codecov/vite-plugin's latest release doesn't support this repo's Vite 8 yet (peer range caps at + # 6.x; this repo's Vite 8 runs on rolldown, not classic rollup, so @codecov/rollup-plugin doesn't fit + # either) -- revisit wiring the plugin in once upstream catches up. + - name: Upload UI bundle stats to Codecov + if: ${{ (github.event_name == 'push' || needs.changes.outputs.ui == 'true') && github.event.pull_request.head.repo.fork != true }} + continue-on-error: true + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + run: npm run bundle-analysis --workspace @loopover/ui # The full-suite coverage run, sharded (#ci-shard-coverage). This alone was ~9-10 of the ~11 minutes a # typical backend PR spent in validate-code, because vitest schedules whole test FILES atomically to diff --git a/apps/loopover-ui/package.json b/apps/loopover-ui/package.json index 6ea37c586d..b543d28872 100644 --- a/apps/loopover-ui/package.json +++ b/apps/loopover-ui/package.json @@ -11,6 +11,7 @@ "build": "vite build", "build:cloudflare": "npm --prefix ../.. run ui:build", "build:dev": "vite build --mode development", + "bundle-analysis": "bundle-analyzer ./dist/client --bundle-name=loopover-ui --upload-token=$CODECOV_TOKEN", "deploy:built": "wrangler deploy --config dist/server/wrangler.json", "preview": "vite preview", "version:built": "wrangler versions upload --config dist/server/wrangler.json", @@ -76,6 +77,7 @@ "zod": "^4.4.3" }, "devDependencies": { + "@codecov/bundle-analyzer": "^2.0.1", "@eslint/js": "^9.39.4", "@lovable.dev/vite-tanstack-config": "2.6.5", "@testing-library/dom": "^10.4.1", diff --git a/package-lock.json b/package-lock.json index b42e2b1705..add9d54021 100644 --- a/package-lock.json +++ b/package-lock.json @@ -542,6 +542,7 @@ "zod": "^4.4.3" }, "devDependencies": { + "@codecov/bundle-analyzer": "^2.0.1", "@eslint/js": "^9.39.4", "@lovable.dev/vite-tanstack-config": "2.6.5", "@testing-library/dom": "^10.4.1", @@ -782,6 +783,72 @@ "dev": true, "license": "MIT" }, + "node_modules/@actions/core": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.1.tgz", + "integrity": "sha512-a6d/Nwahm9fliVGRhdhofo40HjHQasUPusmc7vBfyky+7Z+P2A1J68zyFVaNcEclc/Se+eO595oAr5nwEIoIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/exec": "^3.0.0", + "@actions/http-client": "^4.0.0" + } + }, + "node_modules/@actions/exec": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-3.0.0.tgz", + "integrity": "sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/io": "^3.0.2" + } + }, + "node_modules/@actions/github": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-9.1.1.tgz", + "integrity": "sha512-tL5JbYOBZHc0ngEnCsaDcryUizIUIlQyIMwy1Wkx93H5HzbBJ7TbiPx2PnFjBwZW0Vh05JmfFZhecE6gglYegA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/http-client": "^3.0.2", + "@octokit/core": "^7.0.6", + "@octokit/plugin-paginate-rest": "^14.0.0", + "@octokit/plugin-rest-endpoint-methods": "^17.0.0", + "@octokit/request": "^10.0.7", + "@octokit/request-error": "^7.1.0", + "undici": "^6.23.0" + } + }, + "node_modules/@actions/github/node_modules/@actions/http-client": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.2.tgz", + "integrity": "sha512-JP38FYYpyqvUsz+Igqlc/JG6YO9PaKuvqjM3iGvaLqFnJ7TFmcLyy2IDrY0bI0qCQug8E9K+elv5ZNfw62ZJzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^6.23.0" + } + }, + "node_modules/@actions/http-client": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.1.tgz", + "integrity": "sha512-+Nvd1ImaOZBSoPbsUtEhv+1z99H12xzncCkz0a3RuehINE81FZSe2QTj3uvAPTcJX/SCzUQHQ0D1GrPMbrPitg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^6.23.0" + } + }, + "node_modules/@actions/io": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-3.0.2.tgz", + "integrity": "sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -1604,6 +1671,193 @@ "license": "MIT OR Apache-2.0", "peer": true }, + "node_modules/@codecov/bundle-analyzer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@codecov/bundle-analyzer/-/bundle-analyzer-2.0.1.tgz", + "integrity": "sha512-SkL0+Ccs6VuHIA9Bxb8/J110CffRXpqtKt7m18FTWC0P6gDKEsoieUerDdcd6K9BFKujj6gaUdTt5i4I/v8GTA==", + "dev": true, + "license": "MIT", + "os": [ + "darwin", + "linux" + ], + "dependencies": { + "@codecov/bundler-plugin-core": "^2.0.1", + "micromatch": "^4.0.8", + "yargs": "^17.7.2" + }, + "bin": { + "bundle-analyzer": "dist/cli.cjs" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@codecov/bundle-analyzer/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@codecov/bundle-analyzer/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@codecov/bundle-analyzer/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@codecov/bundle-analyzer/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@codecov/bundle-analyzer/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@codecov/bundle-analyzer/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@codecov/bundle-analyzer/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@codecov/bundle-analyzer/node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@codecov/bundle-analyzer/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@codecov/bundler-plugin-core": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@codecov/bundler-plugin-core/-/bundler-plugin-core-2.0.1.tgz", + "integrity": "sha512-TkdKn/rEwZQ723M7DDUmHe5r0IJa23rUT4TAx5jXmg12wGZGAHGWWU7LKeQsYCsKdLMxK7bLaGk9M++4wSRD5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/core": "^3.0.0", + "@actions/github": "^9.0.0", + "chalk": "4.1.2", + "semver": "^7.5.4", + "unplugin": "^1.10.1", + "zod": "^3.22.4" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@codecov/bundler-plugin-core/node_modules/unplugin": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz", + "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@codecov/bundler-plugin-core/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -3345,6 +3599,38 @@ "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", "license": "MIT" }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz", + "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz", + "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, "node_modules/@octokit/request": { "version": "10.0.9", "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.9.tgz", @@ -14920,6 +15206,16 @@ "fsevents": "~2.3.3" } }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, "node_modules/tw-animate-css": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", @@ -15889,7 +16185,7 @@ "version": "3.0.0", "license": "AGPL-3.0-only", "dependencies": { - "@loopover/engine": "^3.1.1", + "@loopover/engine": "^1.0.0", "@modelcontextprotocol/sdk": "1.29.0", "zod": "^4.4.3" }, @@ -15920,7 +16216,7 @@ "version": "3.0.0", "license": "AGPL-3.0-only", "dependencies": { - "@loopover/engine": "^3.1.1", + "@loopover/engine": "^3.0.0", "@modelcontextprotocol/sdk": "1.29.0", "zod": "^4.4.3" }, diff --git a/test/unit/codecov-policy.test.ts b/test/unit/codecov-policy.test.ts index 23909efd69..f2d3da8ff8 100644 --- a/test/unit/codecov-policy.test.ts +++ b/test/unit/codecov-policy.test.ts @@ -164,4 +164,36 @@ describe("Codecov policy", () => { const trustedWith = record(trustedCoverageUpload!.with, "trusted coverage upload with"); expect(trustedWith.token).toBe("${{ secrets.CODECOV_TOKEN }}"); }); + + it("uploads loopover-ui's bundle stats without letting an upload hiccup fail CI", () => { + // loopover-miner-ui deliberately has no build step in this job at all (self-hosted operator + // dashboard, not something we deploy -- see "UI build"'s own comment), so there is nothing to + // upload bundle stats for on that app here. + const workflow = readYaml(".github/workflows/ci.yml"); + const validateCode = nestedRecord(workflow, ["jobs", "validate-code"]); + const steps = recordArray(validateCode.steps, "jobs.validate-code.steps"); + + const buildIndex = steps.findIndex((step) => step.name === "UI build"); + const bundleIndex = steps.findIndex((step) => step.name === "Upload UI bundle stats to Codecov"); + expect(buildIndex).toBeGreaterThan(-1); + expect(bundleIndex).toBeGreaterThan(buildIndex); + + const bundleStep = steps[bundleIndex]!; + // Unlike the coverage uploads above, nothing gates a merge on bundle size -- a Codecov outage or a + // missing token must never fail CI for an otherwise-honest PR. + expect(bundleStep["continue-on-error"]).toBe(true); + expect(String(bundleStep.if)).toContain("github.event.pull_request.head.repo.fork != true"); + const bundleEnv = record(bundleStep.env, "bundle step env"); + expect(bundleEnv.CODECOV_TOKEN).toBe("${{ secrets.CODECOV_TOKEN }}"); + expect(bundleStep.run).toBe("npm run bundle-analysis --workspace @loopover/ui"); + + const uiPkg = JSON.parse(readFileSync("apps/loopover-ui/package.json", "utf8")) as { + scripts: Record; + devDependencies: Record; + }; + expect(uiPkg.scripts["bundle-analysis"]).toBe( + "bundle-analyzer ./dist/client --bundle-name=loopover-ui --upload-token=$CODECOV_TOKEN", + ); + expect(uiPkg.devDependencies["@codecov/bundle-analyzer"]).toBeDefined(); + }); });