From fe28c97e0f48181de646fd1c78f22d73bdc4dd34 Mon Sep 17 00:00:00 2001 From: Thom Lamb Date: Tue, 1 Sep 2026 13:30:29 -0500 Subject: [PATCH] chore(sonar): remove the rejected Tinqer spike, clear the lint debt, and report measures in CI (#261) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The standing cleanup, three features behind. Four changes. Report the measures in CI. This is the one that matters, because the rest was only findable by reading the tree. SonarQube here is 9.9 Community: no Bearer auth, so the official MCP cannot connect, and the host is a CI secret, so hotspots, duplication, debt and coverage existed only on a dashboard — which made "reduce the debt" an instruction nobody could act on without a browser open beside them. scripts/summarize-sonar.js queries the measures API with the secrets the workflow already holds and prints the result into the job log. The scanner masks the URL and token; measures are not secret. It polls the compute task before reading. The workflow does not set sonar.qualitygate.wait, so the scan step returns once the report is uploaded and the server computes measures afterwards — reading immediately would return the previous analysis, indistinguishable from this one and quietly wrong. When it cannot confirm, it says so in the output rather than presenting stale numbers as current. It is deliberately not guarded with continue-on-error: it exits 0 on every path, and guarding it would oblige it to appear in the final gate, whose job is to fail the build. Remove the Tinqer spike. #216 evaluated Drizzle against Tinqer and rejected Tinqer, and its closing comment said the throwaway src/db-tinqer/ probe must not reach main. The whole spike commit was merged, so it did. The probe is 71 lines imported by nothing, and @tinqerjs/tinqer, @tinqerjs/pg-promise-adapter and pg-promise were dependencies for a library nobody chose. The condition_note column that warning also named did not reach main. Clear the lint debt, both projects now at zero warnings from six and two. One of these was a real defect rather than tidiness: the third catch block in shippingAddresses.ts rolled back and returned 500 while discarding the error, so a failed default-address change left nothing behind to say why — the two catch blocks above it in the same file already logged, and this one had simply been missed. The Express namespace augmentation is a false positive and is disabled with the reason written beside it, because an interface that must merge into one Express declares inside a namespace has no ES module spelling. Dedupe the extension map. backfillImageReencode.ts kept its own .jpg/.png/.webp table whose comment named uploadTypes.ts as the source of truth, directly above duplicating it. That file rewrites stored images, so the two disagreeing would silently skip files it should re-encode. src/db-drizzle/ deliberately stays. #217 is open to promote exactly those files properly, with tablesFilter and the sql.param() array rule; deleting them here would be doing #217 badly in the wrong issue. Only their unused-symbol warnings are fixed, and if drizzle-kit pull regenerates schema.ts the table warning returns — worth #217 knowing. Hotspots and coverage are untouched because both numbers are still invisible. They are the next pass, once the step above has printed them once. Closes #261 Co-Authored-By: Claude Opus 5 --- .gitea/workflows/sonarqube.yml | 18 + backend/package-lock.json | 554 ------------------ backend/package.json | 3 - backend/src/backfillImageReencode.ts | 17 +- backend/src/db-drizzle/itemFilters.drizzle.ts | 2 +- backend/src/db-drizzle/schema.ts | 2 +- backend/src/db-tinqer/probe.ts | 71 --- backend/src/middleware/customerAuth.ts | 5 + backend/src/routes/shippingAddresses.ts | 4 + frontend/src/filters.ts | 9 +- scripts/summarize-sonar.js | 196 +++++++ 11 files changed, 240 insertions(+), 641 deletions(-) delete mode 100644 backend/src/db-tinqer/probe.ts create mode 100644 scripts/summarize-sonar.js diff --git a/.gitea/workflows/sonarqube.yml b/.gitea/workflows/sonarqube.yml index 78b86c2..d2c7310 100755 --- a/.gitea/workflows/sonarqube.yml +++ b/.gitea/workflows/sonarqube.yml @@ -227,6 +227,24 @@ jobs: if: always() run: node scripts/summarize-playwright.js frontend/playwright-results.json + # The measures, printed into the log because that is the only place this + # project can read them. SonarQube 9.9 Community has no Bearer auth, so the + # official MCP cannot connect, and the host is a CI secret — so security + # hotspots, duplication, debt and coverage lived solely on a dashboard, and + # "reduce the debt" was an instruction nobody could act on without opening a + # browser. The scanner masks the URL and token; the measures are not secret. + # + # Deliberately not guarded with continue-on-error. The script exits 0 on + # every path, so it cannot fail the job anyway, and guarding it would + # oblige it to appear in the gate below — which exists to fail the job, + # the opposite of what a report should do. See #261. + - name: Report SonarQube measures + if: always() + env: + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: node scripts/summarize-sonar.js + # Last, so a failing step still produces coverage, a scan and every # summary first. Without this step continue-on-error above would turn a # failing suite into a passing job, which is the one way this change could diff --git a/backend/package-lock.json b/backend/package-lock.json index 2668c3b..77e41bf 100755 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -26,8 +26,6 @@ }, "devDependencies": { "@eslint/js": "^9.39.5", - "@tinqerjs/pg-promise-adapter": "^0.0.27", - "@tinqerjs/tinqer": "^0.0.27", "@types/bcryptjs": "^2.4.6", "@types/cookie-parser": "^1.4.7", "@types/express": "^4.17.21", @@ -43,7 +41,6 @@ "eslint-plugin-sonarjs": "^4.2.0", "globals": "^17.11.0", "jest": "^29.7.0", - "pg-promise": "^12.7.1", "supertest": "^7.0.0", "ts-jest": "^29.2.4", "tsx": "^4.16.5", @@ -635,40 +632,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@esbuild-kit/core-utils": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", @@ -2748,28 +2711,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz", - "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=23.5.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", - "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" - } - }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", @@ -2783,358 +2724,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@oxc-parser/binding-android-arm-eabi": { - "version": "0.134.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.134.0.tgz", - "integrity": "sha512-N9Us7l/X9ZC3LA6eWSzPyduvBPXV1eRyDPwM6/UWpxwwXGsatb8131+d2L8UsmyHrixnKLHBd6UeH8wangV7fw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-android-arm64": { - "version": "0.134.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.134.0.tgz", - "integrity": "sha512-Ic2oPZESeCaD4+9cKRqp1GMYsTO9Q3Yi9HdY2x9x75ozbnC20sybFHzeBklmaVD9PBzd8KbkmNN0gy+SVlm7zw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-darwin-arm64": { - "version": "0.134.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.134.0.tgz", - "integrity": "sha512-1z9+nVJ1Awq4CPyHAthx5zOUrg5T1zc3dWt6juxwDcuejFGbdYzWJITkS1rv4DCdSTphDU3IW71MzyLV3BjRGQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-darwin-x64": { - "version": "0.134.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.134.0.tgz", - "integrity": "sha512-MpofofaRZnxmYrY3lE8RTLHmE1KkX2q0elEeJ8sMcLbS8At76BjYSL6axssqhx29prGGDzIZ5lYFD+IXqaTzFA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-freebsd-x64": { - "version": "0.134.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.134.0.tgz", - "integrity": "sha512-OqnPQY27vqWAbMnHfLcF8CVOUv2cCvdlTiqyK5qz5WCbH3XOLpYVQATv8S5UrR8bbEJCAqJLsI7W6cRFXAxCoA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { - "version": "0.134.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.134.0.tgz", - "integrity": "sha512-0eqWl+PWrcwGC3b8DCB58w3QINAuZfpX2ULTGpI01GUMBc6zDKSpttWxvqPIydxuQEkGQTQRAXLLvc+vTDZbQw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { - "version": "0.134.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.134.0.tgz", - "integrity": "sha512-YToasuDmyzpyTC1ztrvkaSXz8tP+YUbx041M/4SGxaRGiyMzsKkQ869KPUTGA57A6aVsb1/DiPX8XZQQeSFkiw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-arm64-gnu": { - "version": "0.134.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.134.0.tgz", - "integrity": "sha512-qMS7NLc2o8G7LLz53wisol+O7/YbMhtaGVhlfsTVLnrraf9kLFgzpiGjtQALLbdxX8uhx0Zmd4l+vY3s1/K4Gg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-arm64-musl": { - "version": "0.134.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.134.0.tgz", - "integrity": "sha512-jUEsnxPXhrCYxswQLYvUOxZEE5UWFMkK5kBnrcPMj7TONz1pD0yKgPmOQCAx2LPoysJ/v2Sjg4RBUVoO2VXoZQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { - "version": "0.134.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.134.0.tgz", - "integrity": "sha512-FU5xMUsXnMuWKVCGo43c6SJsnqHcsioqm32PHdDY39cIRJa/AZbo7RMYW0W5gYcNZqb8EMhELkMDwbJOFbUhtg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { - "version": "0.134.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.134.0.tgz", - "integrity": "sha512-tPc7OAhslHVmNebho1RSEGL//7i7Nm39gbQ+gYreBYwzvDVqNwdQH3S13ccM+R1TpimQ+3bIyFMfnilJIGRtjQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-riscv64-musl": { - "version": "0.134.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.134.0.tgz", - "integrity": "sha512-TtWEC4MUAHodX5a5kGsmK8g5K49V5ewWfwGrXdbw+gXWLXWXuhi+ectNELmOvYIwaqPSTAry1HNS/hfXssaPHQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-s390x-gnu": { - "version": "0.134.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.134.0.tgz", - "integrity": "sha512-mF4uls8TA8SPXSDLpclJ6z9S+vaeUnNp95iUEfqwv9oVJUAE7B2j4crOj8UvByRXpbO5N+aAlJadQEyFlnXU6g==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-x64-gnu": { - "version": "0.134.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.134.0.tgz", - "integrity": "sha512-hieAQplyJeCvzqdSAxpOOGvVCBVXo/ioIBxioHfsUrQu93Et7Hy52cCG/GHEnjImVyeqEIViyyjuB0nKghxz/w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-x64-musl": { - "version": "0.134.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.134.0.tgz", - "integrity": "sha512-OQnJAK4sFuNSLgsh2s/K+14a3kwbmqf2yh1JiADU9XSfDuRooZYbAxmmBVPiyQ97+jBIIA1x2oPZeYNto3Ioow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-openharmony-arm64": { - "version": "0.134.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.134.0.tgz", - "integrity": "sha512-RYLooe6g31q/PqNFN0NjR80IK/ARGsfLasAXL42LXonL+5Cyy9Or76rjBKQLAjERikJwbRU/sYW9Q5Tnpt1A4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-wasm32-wasi": { - "version": "0.134.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.134.0.tgz", - "integrity": "sha512-h8bmHyvc0PxA811prUjfVpJlQAurOOiRbohY4QNGjCEz+L72G9CmWl28OOz3mevrYlURgUsprNuH8hDJXh1VOw==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-win32-arm64-msvc": { - "version": "0.134.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.134.0.tgz", - "integrity": "sha512-EPTfanpBMLNnSAWCDYpbJp1stmsf5x6hMsAwymf2J8ylRupIvO6FtKmHBMdc/wt43y08iz6ILlzFGIXe32/Kbw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-win32-ia32-msvc": { - "version": "0.134.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.134.0.tgz", - "integrity": "sha512-yAwetF+fTr9lTMtmqvkkWiiMXvH/yjMxGzUDG2rTL/LV1lL37eZ2enUGIlIo0gwhBIKWgabDEidtil+kiqNpgQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-win32-x64-msvc": { - "version": "0.134.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.134.0.tgz", - "integrity": "sha512-4VY39G5tuGlBYiH13XbWqfLcwKygQEv0iyf8vtZ5NyLAGjI8M0MiYyLhj91GkWRznr+5VC0I+5R55HUDV/Rklw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.134.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.134.0.tgz", - "integrity": "sha512-T0xuRRKrQFmocH8y+jGfpmSkGcheaJExY9lEihmR1Gm2aH+75B8CzgU2rABRQSzzDxLjZ15Sc0bRVLj5lVeNXQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, "node_modules/@paralleldrive/cuid2": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", @@ -3177,37 +2766,6 @@ "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==" }, - "node_modules/@tinqerjs/pg-promise-adapter": { - "version": "0.0.27", - "resolved": "https://registry.npmjs.org/@tinqerjs/pg-promise-adapter/-/pg-promise-adapter-0.0.27.tgz", - "integrity": "sha512-GF2KDUWmavLDCrYRhisvwGR5cah5sSuo3loSunUVTITGvYivlrYl58zOwBKMkC0WfiVyrJ5nVAJ3E/Spt6NH9A==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@tinqerjs/tinqer": "^0.0.27" - } - }, - "node_modules/@tinqerjs/tinqer": { - "version": "0.0.27", - "resolved": "https://registry.npmjs.org/@tinqerjs/tinqer/-/tinqer-0.0.27.tgz", - "integrity": "sha512-Rf9zNwstFD1mb8BO6RkRatPbx0rJ0VXHrS9pk8doeVv6sCTgo6wyFR/8hQPYe1LTblJUV3SFQNAfJpvsVbNgrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "oxc-parser": "0.134.0" - } - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4103,16 +3661,6 @@ "dev": true, "license": "MIT" }, - "node_modules/assert-options": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/assert-options/-/assert-options-0.8.3.tgz", - "integrity": "sha512-s6v4HnA+vYSGO4eZX+F+I3gvF74wPk+m6Z1Q3w1Dsg4Pnv/R24vhKAasoMVZGvDpOOfTg1Qz4ptZnEbuy95XsQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -8279,44 +7827,6 @@ "node": ">= 0.8.0" } }, - "node_modules/oxc-parser": { - "version": "0.134.0", - "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.134.0.tgz", - "integrity": "sha512-Hs8fRG6A94BzMrMkGOtrUS7JQjmslfF+IvIXslf3QURzK3ud0QmFJRiYZjTe4TzAQnTfvlk4AwZnqIbrUjiE4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "^0.134.0" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/sponsors/Boshen" - }, - "optionalDependencies": { - "@oxc-parser/binding-android-arm-eabi": "0.134.0", - "@oxc-parser/binding-android-arm64": "0.134.0", - "@oxc-parser/binding-darwin-arm64": "0.134.0", - "@oxc-parser/binding-darwin-x64": "0.134.0", - "@oxc-parser/binding-freebsd-x64": "0.134.0", - "@oxc-parser/binding-linux-arm-gnueabihf": "0.134.0", - "@oxc-parser/binding-linux-arm-musleabihf": "0.134.0", - "@oxc-parser/binding-linux-arm64-gnu": "0.134.0", - "@oxc-parser/binding-linux-arm64-musl": "0.134.0", - "@oxc-parser/binding-linux-ppc64-gnu": "0.134.0", - "@oxc-parser/binding-linux-riscv64-gnu": "0.134.0", - "@oxc-parser/binding-linux-riscv64-musl": "0.134.0", - "@oxc-parser/binding-linux-s390x-gnu": "0.134.0", - "@oxc-parser/binding-linux-x64-gnu": "0.134.0", - "@oxc-parser/binding-linux-x64-musl": "0.134.0", - "@oxc-parser/binding-openharmony-arm64": "0.134.0", - "@oxc-parser/binding-wasm32-wasi": "0.134.0", - "@oxc-parser/binding-win32-arm64-msvc": "0.134.0", - "@oxc-parser/binding-win32-ia32-msvc": "0.134.0", - "@oxc-parser/binding-win32-x64-msvc": "0.134.0" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -8523,17 +8033,6 @@ "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", "license": "MIT" }, - "node_modules/pg-cursor": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/pg-cursor/-/pg-cursor-2.22.0.tgz", - "integrity": "sha512-knzXLKqarTjOvb3qDSW0JiGsazmxwEKXrqHfWRte7XUsOYccQRafn3BLnQobWwInkzFJSyOej8y8cQRh2z3kGw==", - "dev": true, - "license": "MIT", - "peer": true, - "peerDependencies": { - "pg": "^8" - } - }, "node_modules/pg-int8": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", @@ -8543,16 +8042,6 @@ "node": ">=4.0.0" } }, - "node_modules/pg-minify": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/pg-minify/-/pg-minify-1.8.0.tgz", - "integrity": "sha512-jO/oJOununpx8DzKgvSsWm61P8JjwXlaxSlbbfTBo1nvSWoo/+I6qZYaSN96jm/KDwa5d+JMQwPGgcP6HXDRow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/pg-pool": { "version": "3.14.0", "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", @@ -8562,45 +8051,12 @@ "pg": ">=8.0" } }, - "node_modules/pg-promise": { - "version": "12.7.1", - "resolved": "https://registry.npmjs.org/pg-promise/-/pg-promise-12.7.1.tgz", - "integrity": "sha512-tsemfuBRsHMGaFZKMpkmCAA9iLeHXuuTq+0DucJwJyftyXk7crlMFXq7SY0AOVelIrdSL8qwdl5omfj/R8JN6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-options": "0.8.3", - "pg": "8.23.0", - "pg-minify": "1.8.0", - "spex": "4.1.0" - }, - "engines": { - "node": ">=16.0" - }, - "peerDependencies": { - "pg-query-stream": "4.17.0" - } - }, "node_modules/pg-protocol": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz", "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==", "license": "MIT" }, - "node_modules/pg-query-stream": { - "version": "4.17.0", - "resolved": "https://registry.npmjs.org/pg-query-stream/-/pg-query-stream-4.17.0.tgz", - "integrity": "sha512-69bug2CH/AJi1BN8r08eZ8tAGdbZZVp9kB0FG9OC2VkJPHIyj7d3Nk9V8tmporO60wqeWvhn7qtQ5oNimwlFug==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "pg-cursor": "^2.22.0" - }, - "peerDependencies": { - "pg": "^8" - } - }, "node_modules/pg-types": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", @@ -9285,16 +8741,6 @@ "source-map": "^0.6.0" } }, - "node_modules/spex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/spex/-/spex-4.1.0.tgz", - "integrity": "sha512-ktgNAQ1X9x1A3IMChM6XBDeVjhGPbLgPQ8aEzGOaUIhZTnLeJSBApvi3gXT789hee6h73N3jOeWkXDwoPbYT/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", diff --git a/backend/package.json b/backend/package.json index 04fbdd0..0fa2b68 100755 --- a/backend/package.json +++ b/backend/package.json @@ -45,8 +45,6 @@ }, "devDependencies": { "@eslint/js": "^9.39.5", - "@tinqerjs/pg-promise-adapter": "^0.0.27", - "@tinqerjs/tinqer": "^0.0.27", "@types/bcryptjs": "^2.4.6", "@types/cookie-parser": "^1.4.7", "@types/express": "^4.17.21", @@ -62,7 +60,6 @@ "eslint-plugin-sonarjs": "^4.2.0", "globals": "^17.11.0", "jest": "^29.7.0", - "pg-promise": "^12.7.1", "supertest": "^7.0.0", "ts-jest": "^29.2.4", "tsx": "^4.16.5", diff --git a/backend/src/backfillImageReencode.ts b/backend/src/backfillImageReencode.ts index 32bf5b4..3a38347 100644 --- a/backend/src/backfillImageReencode.ts +++ b/backend/src/backfillImageReencode.ts @@ -43,19 +43,11 @@ import { promises as fs } from 'fs'; import path from 'path'; import { pool } from './db'; import { needsProcessing, reencodeInPlace } from './imageProcessing'; +import { typeForExtension } from './uploadTypes'; const UPLOADS_DIR = process.env.UPLOADS_DIR || '/app/uploads'; const APPLY = process.argv.includes('--apply'); -// The stored extension is the file's real type — uploadTypes.ts derives it from -// the validated content type on the way in, so it can be trusted on the way -// back out. Anything else is a file this application would refuse to serve. -const TYPE_FOR_EXTENSION: Readonly> = { - '.jpg': 'image/jpeg', - '.png': 'image/png', - '.webp': 'image/webp' -}; - interface Totals { seen: number; missing: number; @@ -78,7 +70,12 @@ async function handleRow(imagePath: string, totals: Totals): Promise { // basename only: image_path is '/uploads/', and the directory it is // served from is a server constant rather than part of the stored value. const filePath = path.join(UPLOADS_DIR, path.basename(imagePath)); - const mimetype = TYPE_FOR_EXTENSION[path.extname(filePath).toLowerCase()]; + // typeForExtension rather than a copy of its table. The stored extension is + // the file's real type — uploadTypes.ts derives it from the validated content + // type on the way in — and this rewrites stored images, so a private copy + // drifting from the real one would silently skip files it should re-encode. + // It lowercases its own input, so the call site does not. + const mimetype = typeForExtension(path.extname(filePath)); if (!mimetype) { console.warn(`[backfill] unrecognised extension, skipping: ${imagePath}`); diff --git a/backend/src/db-drizzle/itemFilters.drizzle.ts b/backend/src/db-drizzle/itemFilters.drizzle.ts index ac03283..0f4b7b5 100644 --- a/backend/src/db-drizzle/itemFilters.drizzle.ts +++ b/backend/src/db-drizzle/itemFilters.drizzle.ts @@ -5,7 +5,7 @@ // match with a count equality, and array parameters. If this cannot be said // cleanly, nothing else in the conversion matters. -import { SQL, and, eq, gte, lte, sql, inArray, exists } from 'drizzle-orm'; +import { SQL, and, gte, lte, sql, inArray, exists } from 'drizzle-orm'; import { items, itemTags, favorites, categories } from './schema'; export interface SpikeFilters { diff --git a/backend/src/db-drizzle/schema.ts b/backend/src/db-drizzle/schema.ts index 8bd9138..b769a0e 100644 --- a/backend/src/db-drizzle/schema.ts +++ b/backend/src/db-drizzle/schema.ts @@ -77,7 +77,7 @@ export const tags = pgTable("tags", { name: text().notNull(), color: text().notNull(), createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), -}, (table) => [ +}, () => [ uniqueIndex("tags_name_uniq").using("btree", sql`lower(name)`), ]); diff --git a/backend/src/db-tinqer/probe.ts b/backend/src/db-tinqer/probe.ts deleted file mode 100644 index 2a84318..0000000 --- a/backend/src/db-tinqer/probe.ts +++ /dev/null @@ -1,71 +0,0 @@ -// Spike (#216): what Tinqer can and cannot express, recorded as runnable cases. -// -// Every case is wrapped in a function rather than evaluated at module level. -// defineSelect() parses the lambda eagerly and THROWS on a shape it cannot -// handle, so a top-level call turns an unsupported query into an import-time -// crash. That is itself a finding worth keeping: the failure is a runtime throw -// at load, not a type error, so an unsupported query compiles cleanly and takes -// the process down when the module is first required. - -import { createSchema, defineSelect } from '@tinqerjs/tinqer'; -import { toSql } from '@tinqerjs/pg-promise-adapter'; - -interface Db { - items: { id: number; name: string; price_cents: number; status: string; category_id: number | null }; - categories: { id: number; parent_id: number | null }; -} - -const schema = createSchema(); - -export type Outcome = { label: string; ok: boolean; sql?: string; error?: string }; - -function attempt(label: string, build: () => unknown, params: Record): Outcome { - try { - const r = toSql(build() as never, params as never); - return { label, ok: true, sql: r.sql }; - } catch (e) { - return { label, ok: false, error: (e as Error).message.split('\n')[0] }; - } -} - -export function runCases(): Outcome[] { - return [ - // Supported. - attempt( - 'compound condition', - () => - defineSelect(schema, (q, p: { min: number; max: number }) => - q.from('items').where((i) => i.price_cents >= p.min && i.price_cents <= p.max).select((i) => ({ id: i.id })) - ), - { min: 1, max: 2 } - ), - attempt( - 'array membership', - () => - defineSelect(schema, (q, p: { statuses: string[] }) => - q.from('items').where((i) => p.statuses.includes(i.status)).select((i) => ({ id: i.id })) - ), - { statuses: ['available', 'sold'] } - ), - - // NOT supported — and these are the two shapes buildItemFilterSql needs. - attempt( - 'ternary (clause optional at run time)', - () => - defineSelect(schema, (q, p: { apply: boolean; min: number }) => - q.from('items').where((i) => (p.apply ? i.price_cents >= p.min : true)).select((i) => ({ id: i.id })) - ), - { apply: true, min: 1 } - ), - attempt( - 'block body with if (clause optional at run time)', - () => - defineSelect(schema, (q, p: { apply: boolean; min: number }) => { - let x = q.from('items'); - if (p.apply) x = x.where((i) => i.price_cents >= p.min); - return x.select((i) => ({ id: i.id })); - }), - { apply: true, min: 1 } - ) - ]; -} diff --git a/backend/src/middleware/customerAuth.ts b/backend/src/middleware/customerAuth.ts index 793a0f4..37ffe84 100755 --- a/backend/src/middleware/customerAuth.ts +++ b/backend/src/middleware/customerAuth.ts @@ -2,6 +2,11 @@ import { Request, Response, NextFunction } from 'express'; import { pool } from '../db'; declare global { + // A namespace is the only way to spell an Express type augmentation — the + // interface has to merge into the one Express declares, and Express declares + // it inside a namespace. There is no ES module form of this, so the rule is + // disabled here rather than worked around. + // eslint-disable-next-line @typescript-eslint/no-namespace namespace Express { interface Request { customerId?: number; diff --git a/backend/src/routes/shippingAddresses.ts b/backend/src/routes/shippingAddresses.ts index 4b20ffc..108b157 100644 --- a/backend/src/routes/shippingAddresses.ts +++ b/backend/src/routes/shippingAddresses.ts @@ -118,6 +118,10 @@ router.post('/:id/set-default', requireCustomer, asyncRoute(async (req: Request, res.json(rows[0]); } catch (err) { await client.query('ROLLBACK'); + // Logged like the two catch blocks above it in this file, which this one + // was simply missing. Without it a failed default-address change rolls + // back and returns 500 leaving nothing behind to say why. + console.error(err); res.status(500).json({ error: 'internal error' }); } finally { client.release(); diff --git a/frontend/src/filters.ts b/frontend/src/filters.ts index a928915..180dce1 100644 --- a/frontend/src/filters.ts +++ b/frontend/src/filters.ts @@ -56,8 +56,15 @@ function isPublicStatus(value: string): value is ItemStatus { return value === 'available' || value === 'reserved' || value === 'sold'; } +// Sorted with an explicit comparator rather than a bare .sort(). The default +// sorts by UTF-16 code unit, which is a perfectly good total order for the +// ASCII status values passed here — but the rule exists because that stops +// being true the moment a non-ASCII value appears, and a set comparison that +// silently depends on its inputs staying ASCII is not worth keeping. const sameSet = (a: readonly string[], b: readonly string[]) => - a.length === b.length && [...a].sort().join() === [...b].sort().join(); + a.length === b.length && + [...a].sort((x, y) => x.localeCompare(y)).join() === + [...b].sort((x, y) => x.localeCompare(y)).join(); // Which preset a status list corresponds to, for showing the control's current // position. Null means no preference, which each screen renders as its default. diff --git a/scripts/summarize-sonar.js b/scripts/summarize-sonar.js new file mode 100644 index 0000000..ac3faae --- /dev/null +++ b/scripts/summarize-sonar.js @@ -0,0 +1,196 @@ +#!/usr/bin/env node +// Prints the project's SonarQube measures into the job log. +// +// This exists because the numbers are otherwise unreachable. The server is +// SonarQube 9.9 Community: it has no Bearer auth, so the official MCP cannot +// connect, and the host URL is a CI secret, so there is no way to query it from +// a developer's machine either. The measures lived only on a dashboard, which +// made "reduce the debt" an instruction nobody could act on without a browser +// open beside them. See #261. +// +// The scanner masks SONAR_HOST_URL and SONAR_TOKEN in the log. Measures are not +// secret, so they print fine — only the URL they came from is redacted. +// +// Authentication is HTTP Basic with the token as the username and an empty +// password. That is the documented scheme for 9.9; the `Authorization: Bearer` +// form was added later and returns 401 here. +// +// Runs with `if: always()` and deliberately WITHOUT `continue-on-error`. A +// cleanup report that can break a build is worse than no report, so instead of +// being guarded this script simply never fails: every path exits 0 and problems +// are described rather than thrown. Guarding it would also oblige it to be named +// in the workflow's final gate — workflowGate.test.ts asserts that pairing — and +// that gate exists to fail the job, which is the opposite of what is wanted for +// a report. Same shape as the summarize-jest.js steps beside it. + +const https = require('https'); +const http = require('http'); +const fs = require('fs'); +const path = require('path'); + +const HOST = process.env.SONAR_HOST_URL; +const TOKEN = process.env.SONAR_TOKEN; +const PROJECT = process.argv[2] || 'redefined-designs'; + +// Ordered so the four things the cleanup issue asks about come first, rather +// than alphabetically. Each row is [metric key, label, formatter]. +const METRICS = [ + ['security_hotspots', 'Security hotspots', (v) => v], + ['vulnerabilities', 'Vulnerabilities', (v) => v], + ['bugs', 'Bugs', (v) => v], + ['duplicated_lines_density', 'Duplicated lines', (v) => `${v}%`], + ['duplicated_blocks', 'Duplicated blocks', (v) => v], + ['sqale_index', 'Technical debt', formatDebt], + ['code_smells', 'Code smells', (v) => v], + ['coverage', 'Coverage', (v) => `${v}%`], + ['line_coverage', 'Line coverage', (v) => `${v}%`], + ['branch_coverage', 'Branch coverage', (v) => `${v}%`], + ['uncovered_lines', 'Uncovered lines', (v) => v], + ['ncloc', 'Lines of code', (v) => v], + ['new_coverage', 'Coverage on new code', (v) => `${v}%`], + ['new_duplicated_lines_density', 'Duplication on new code', (v) => `${v}%`], + ['new_code_smells', 'Code smells on new code', (v) => v] +]; + +/** sqale_index is minutes. Days here are Sonar's 8-hour working days. */ +function formatDebt(minutes) { + const total = Number(minutes); + if (!Number.isFinite(total)) return String(minutes); + const days = Math.floor(total / (8 * 60)); + const hours = Math.floor((total % (8 * 60)) / 60); + const mins = total % 60; + return `${total} min (${days}d ${hours}h ${mins}m)`; +} + +function get(url) { + return new Promise((resolve) => { + const client = url.startsWith('https:') ? https : http; + const request = client.get( + url, + // The empty password is deliberate: 9.9 expects `token:` rather than a + // password, and omitting the colon sends the token as a username with no + // password field at all, which it rejects. + { auth: `${TOKEN}:`, timeout: 30000 }, + (res) => { + let body = ''; + res.on('data', (chunk) => (body += chunk)); + res.on('end', () => resolve({ status: res.statusCode, body })); + } + ); + request.on('timeout', () => { + request.destroy(); + resolve({ status: 0, body: 'timed out after 30s' }); + }); + request.on('error', (err) => resolve({ status: 0, body: err.message })); + }); +} + +function parse(body) { + try { + return JSON.parse(body); + } catch { + return null; + } +} + +/** + * Waits for the server to finish processing this run's analysis. + * + * The workflow does not set `sonar.qualitygate.wait`, so the scan step returns + * as soon as the report is uploaded and the server computes measures in the + * background. Querying immediately therefore returns the *previous* analysis, + * which would be indistinguishable from the current one and quietly wrong — the + * worst kind of number to publish into a cleanup report. + * + * The scanner writes the task id to .scannerwork/report-task.txt. Polling it + * costs a few seconds and makes the difference between "these are this commit's + * numbers" and "these are probably this commit's numbers". + */ +async function waitForAnalysis(base) { + const reportPath = path.join(process.cwd(), '.scannerwork', 'report-task.txt'); + if (!fs.existsSync(reportPath)) { + return 'no report-task.txt — reporting the last completed analysis, which may predate this commit'; + } + + const taskId = /^ceTaskId=(.+)$/m.exec(fs.readFileSync(reportPath, 'utf8'))?.[1]?.trim(); + if (!taskId) { + return 'no ceTaskId in report-task.txt — reporting the last completed analysis'; + } + + // Twenty attempts at three seconds. A minute is far longer than this project + // takes to process and short enough not to stretch the job noticeably. + for (let attempt = 0; attempt < 20; attempt++) { + const res = await get(`${base}/api/ce/task?id=${encodeURIComponent(taskId)}`); + const status = parse(res.body)?.task?.status; + if (status === 'SUCCESS') return null; + if (status === 'FAILED' || status === 'CANCELED') { + return `analysis ${status} on the server — reporting the last completed analysis instead`; + } + await new Promise((resolve) => setTimeout(resolve, 3000)); + } + return 'analysis still processing after 60s — reporting the last completed analysis'; +} + +async function main() { + if (!HOST || !TOKEN) { + // Not an error. A fork or a run without the secrets configured reaches this + // legitimately, and saying so is more useful than a stack trace. + console.log('SonarQube measures: SONAR_HOST_URL or SONAR_TOKEN not set, skipping.'); + return; + } + + const base = HOST.replace(/\/+$/, ''); + + const staleness = await waitForAnalysis(base); + + const keys = METRICS.map(([key]) => key).join(','); + const measuresUrl = `${base}/api/measures/component?component=${encodeURIComponent(PROJECT)}&metricKeys=${keys}`; + + const res = await get(measuresUrl); + if (res.status !== 200) { + console.log(`SonarQube measures: request failed (status ${res.status}). ${res.body.slice(0, 300)}`); + return; + } + + const json = parse(res.body); + const measures = json?.component?.measures; + if (!Array.isArray(measures)) { + console.log('SonarQube measures: unexpected response shape.'); + console.log(res.body.slice(0, 300)); + return; + } + + // Keyed by metric so the display order above is what prints, rather than + // whatever order the API happened to return. + const byKey = new Map(measures.map((m) => [m.metric, m])); + + const lines = ['', '=== SonarQube measures ===', '']; + // Said plainly rather than omitted. A reader acting on these numbers needs to + // know whether they describe this commit. + if (staleness) lines.push(`NOTE: ${staleness}`, ''); + for (const [key, label, format] of METRICS) { + const measure = byKey.get(key); + // A metric can be legitimately absent — new-code metrics do not exist until + // a second analysis, and branch_coverage is missing when nothing branches. + if (!measure) { + lines.push(`${label.padEnd(26)} —`); + continue; + } + const value = measure.period?.value ?? measure.value; + lines.push(`${label.padEnd(26)} ${format(value)}`); + } + + const gate = await get( + `${base}/api/qualitygates/project_status?projectKey=${encodeURIComponent(PROJECT)}` + ); + const gateStatus = parse(gate.body)?.projectStatus?.status; + lines.push('', `Quality gate ${gateStatus ?? 'unknown'}`, ''); + + console.log(lines.join('\n')); +} + +main().catch((err) => { + // Reported rather than thrown, for the reason in the header: this step must + // not be able to fail the job. + console.log(`SonarQube measures: ${err instanceof Error ? err.message : String(err)}`); +}); -- 2.54.0