882f42447b477076e0041e227b00a9fb160073dd
15
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f7d85736dd |
ci: fold the Tests workflow into SonarQube Analysis and delete it (#123)
tests.yml and sonarqube.yml had identical triggers and ran the same tests. Where tests.yml ran test:unit:json and playwright test, sonarqube.yml ran test:unit:cov and test:e2e:cov: the same two suites, differing only in reporter and instrumentation. Every pull request therefore installed dependencies twice, migrated twice, built and started the backend twice, installed a Playwright browser twice and ran the whole end-to-end suite twice. With one runner the second copy did not run in parallel, it queued. Two things in tests.yml were not duplicates and are folded in rather than dropped. The summarize steps, which render pass and fail counts and name the failing tests, where sonarqube.yml offered only raw jest and Playwright output. And the continue-on-error plus fail-at-the-end pattern that lets those summaries render on a failing run at all: sonarqube.yml used to abort at the first failing step, so a broken unit test meant no end-to-end results, no coverage and no scan, which is one fact per run when a run costs many minutes. The trade-off is fast feedback. A broken unit test used to fail tests.yml in a couple of minutes; now it is reported when the whole pipeline finishes. That is worth it with one runner, where the fast job was queued behind the slow one anyway, and where a single run reporting unit results, end-to-end results, coverage and the scan together beats two runs each reporting a fragment. Worth revisiting if the runner count changes. Two details that would each have silently broken something: The coverage scripts do not emit the JSON the summarizers read - test:unit:cov has no --json, and test:e2e:cov does not set the JSON reporter. Both are appended at the step rather than baked into package.json, since coverage and a results file are only wanted together in this one place. The Playwright step uses --reporter=list,json so the log still names the failing test rather than only writing a file. The "Backend log" step was keyed on failure(). With continue-on-error the job is not in a failed state when it runs, so failure() would never fire and the log explaining an end-to-end failure would have gone unprinted exactly when it was wanted. It is now keyed on the step outcome. The header comment also records something the old comments obscured: the integration suite runs here on every push and pull request, despite backend-integration.yml describing it as manual. That quarantine only ever applied to tests.yml. Verification, run rather than assumed: both suites executed locally with the exact commands the workflow now uses. The unit run wrote unit-results.json alongside its coverage and summarize-jest.js rendered 199 passed from it. The end-to-end run wrote playwright-results.json and summarize-playwright.js rendered 100 passed, 3 failed, 2 skipped and named all three failures - a failing run, which is the case if: always() exists for. The three failures are pre-existing and fail identically on main. Parsing the remaining workflows confirms sonarqube.yml declares one job carrying both new step ids, lint.yml and backend-integration.yml are untouched, and tests.yml is gone. The two results files are added to .gitignore. CI never committed them, but the commands are now documented and runnable locally, which makes an accidental commit a matter of time. Closes #123 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
005146742e |
ci: give linting its own workflow (#113)
The lint job moves out of tests.yml into lint.yml, unchanged in what it runs. Lint is the fastest check in the pipeline and the one most often broken, and reporting it as one job among the suites made a lint failure and a test failure look alike at a glance. The split left two comments in the wrong place, both artifacts of the copy rather than of the intent. tests.yml kept the six-line note explaining the lint policy — that only defect-catching rules fail the build, that everything else warns, and why no --max-warnings flag appears. With the job gone it sat directly above backend-unit, where a reader would fairly take it as describing the unit tests. It has moved to lint.yml, with the job it actually describes. lint.yml inherited tests.yml's header about backend-integration living in its own manual workflow after it held the runner for three hours. That is worth saying where someone might expect integration tests to run; in a workflow that only runs ESLint it explains the absence of something nobody was looking for. Replaced with why this workflow exists at all. Verified by parsing both files rather than by reading them: lint.yml declares one job, lint; tests.yml declares backend-unit and frontend-e2e. No job was lost in the move and none is now declared twice, which is the failure a copy-and-delete edit invites. Refs #113 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
332c1e7cd0 |
feat(ci): import test coverage into SonarQube (#61)
SonarQube reported 0% coverage for 78 unit, 134 integration and 83 end-to-end tests, so the coverage-on-new-code gate — the most useful thing SonarQube offers a project this size — has been failing permanently while looking configured. It now reports 69.6%, verified by a real scan. Backend coverage comes from both suites, written to separate directories because jest writes coverage/lcov.info by default and the second run would silently overwrite the first. Both are needed rather than just the fast one: the unit suite alone reports 11%, because everything in src/routes is exercised by the integration suite. That suite is manual-only after hanging for 3h12m post-run, so it runs here with --forceExit and the job carries a hard timeout; jest confirmed during testing that it would otherwise have hung. The frontend had no unit tests at all, so its coverage comes from Playwright driving an istanbul-instrumented dev server, collected per test by an auto-fixture and merged with nyc. The 17 specs now import from a local fixtures module that re-exports @playwright/test, which is what lets the fixture attach without touching each test body. Instrumentation is gated behind COVERAGE=true and loaded by dynamic import, since vite-plugin-istanbul is ESM-only while vite.config.ts evaluates as CommonJS. Both directions were checked rather than assumed: a normal build contains no instrumentation, and the dev server instruments nested modules as well as top-level ones — the first attempt used an include glob of src/* which would have silently missed everything under src/admin and src/cart. coverage:report fails when nothing was collected instead of writing an empty report, and that guard was fired deliberately to confirm it works. This project has been bitten twice by tools succeeding while measuring nothing — SonarQube skipping the whole frontend and still exiting EXECUTION SUCCESS in #67, and an ESLint matcher silently matching no files during #60 — and coverage has exactly that shape: an uninstrumented dev server lets every test pass while gathering nothing, and the 0% that follows reads as lost coverage rather than broken collection. Worth knowing when reading the numbers: end-to-end coverage flatters. Istanbul marks a line covered when the browser ran it, so a component rendered during a test counts as covered with nothing asserting anything about it. Recorded in the design doc and the project context rather than left to be discovered. Also declares sonar.tests so test files are analysed under the test rule set rather than as production code. Closes #61 |
||
|
|
ae8f0eb12c |
fix(ci): make SonarQube actually analyse the frontend (#67)
SonarQube was analysing none of frontend/src. All 34 files were indexed and reported ncloc 0, so the quality gate had been measuring roughly a third of the codebase while appearing to cover it. The scanner log gives the cause: frontend/tsconfig.json sets "moduleResolution": "bundler", which is correct for Vite, and the TypeScript bundled with SonarQube 9.9 predates 5.0 and rejects it. Building the frontend program throws, every frontend file is dropped, and the scan still exits EXECUTION SUCCESS — which is why a green job hid this indefinitely. Adds frontend/tsconfig.sonar.json, an analysis-only mirror that differs only in using "node", and points the scan at it via sonar.typescript.tsconfigPaths. The app's own tsconfig is deliberately untouched: "bundler" is right for the build, and changing it to satisfy an old analyser would let the tool dictate the build. The mirror cannot use `extends` — the old compiler validates the base file while reading it, so the error just moves to pointing at tsconfig.json. Verified locally against a scratch project: 59/59 files analysed, no skips. Analysed lines go from 2,069 to 5,481, code smells from 2 to 14, security hotspots from 3 to 4, and technical debt from 21 to 85 minutes. The frontend had been hiding twelve code smells and a hotspot, which is part of why the React problems behind #60 and #62 had to be found by hand. A standalone copy drifts, and drift here does not fail anything — it silently returns to skipping the frontend while reporting success. scripts/check-sonar-tsconfig.js compares the two and fails when they diverge in anything but moduleResolution, and runs before the scan so the scan is never what discovers it. Confirmed it catches drift by introducing some. Moves scan settings into sonar-project.properties at the repo root so a local scan and the CI scan analyse the same thing, leaving only the host and token in secrets. Adds scripts/scan-local.sh, which runs the scanner in Docker because it needs Java 11+ and the dev machine has Java 8, and which defaults to a scratch project key: the server is Community edition with no branch analysis, so any scan overwrites the single main analysis of whichever key it is given. Closes #67 |
||
|
|
c058b3ed2e |
feat(ci): add ESLint to both workspaces with a defect-only CI gate (#60)
TypeScript's strict mode checks types and nothing else, so nothing enforced the React hook rules, the SonarJS rules, or unhandled-promise detection. Adds a flat config per workspace, a lint script in each, and a lint job in tests.yml. The rule selection is the substance of this change and is measured rather than guessed. A full-strength config reports 435 violations across 50 files, but 325 of those are the no-unsafe-* family from recommendedTypeChecked, every one downstream of pool.query() returning any rows and untyped fetch responses. Typing those boundaries is the whole of #65, so enabling the rules here would ship a linter whose output is three-quarters another issue's backlog — the reliable way to teach everyone to ignore lint output. This enables recommended plus the two type-aware rules that catch defects rather than describe type debt, which leaves 110 findings. Both configs downgrade every preset to a warning and then list the error rules explicitly at the bottom, so the CI gate is readable in one place instead of inferred from four presets' defaults. Errors are no-floating-promises, no-misused-promises, rules-of-hooks, exhaustive-deps and jsx-a11y/alt-text; everything else warns. No --max-warnings flag is needed because ESLint already exits non-zero on errors and zero on warnings. no-misused-promises runs with checksVoidReturn.attributes false, since onClick={async () => ...} is idiomatic React and safe when the handler catches its own errors — at the default it flags every antd button in the admin screens, 25 of its 28 hits, and a rule that is 89% noise gets switched off within a week. The 37 errors this surfaced were mostly not the mechanical fix they looked like. The plan assumed the 30 floating promises were fire-and-forget loaders that already handled their own failures, which was true of the one sampled when writing the design and false for most of the rest: Admin, Categories, Customers, Tags, Settings, Account and CustomerAuthContext all had no rejection handling at all, so `void` on them would have hidden real failures rather than annotated deliberate ones. Each of those loaders now catches and surfaces the failure before the call site voids it. The CustomerAuthContext one was a live bug — a rejected fetchMe left loading true forever, rendering as a permanent spinner instead of a signed-out page. Admin's load became a useCallback so its effect can name it honestly rather than suppress the dependency, Categories' drop handler was split so the function antd receives returns void as its type says, and Cart's effect now names refreshCartContext, which is a useCallback with an empty dependency list and so cannot re-run it. The only disable added is in asyncRoute, where returning a promise where Express expects void is the entire point of the wrapper and the promise cannot reject. Two of the issue's premises did not survive measurement, both recorded in the spec: exhaustive-deps flags 2 cases rather than the 10 inferred from empty dependency arrays, and the backend was already clean on the defect rules because #59 wrapped every async route. Verified: lint, build, 78 unit, 134 integration and 83 e2e all pass in both workspaces, and the CI gate was confirmed to fail by introducing a deliberate violation rather than by assuming the job is wired correctly. Closes #60 |
||
|
|
5a9ecefeba |
ci: build QA in Portainer from a git stack, drop the build workflow
The QA image is now built by Portainer from this repository rather than by Gitea Actions. Deployed as a Git repository stack, "Pull and redeploy" pulls the repo, builds from the Dockerfile, and recreates the containers in one action. This removes the runner from the loop entirely. Three dispatches failed without ever building: the runner refuses privileged containers, so the dind service was never created. Working around that needed either the host Docker socket mounted into the runner or privileged containers enabled runner-wide, and both hand every workflow on every branch root-equivalent control of the NAS, production included. Portainer already holds the socket — that is how it manages containers — so building there needs no new privilege at all. pull_policy: build is what keeps it honest. Without it the stack reuses whatever is tagged redefined-designs:qa, which is exactly how a redeploy appears to succeed while still serving old code — a failure this project has already hit twice. Deleting qa-build.yml also drops the registry, the REGISTRY_TOKEN and BREVO_API_KEY secrets, and the notification email. The email existed because CI worked asynchronously and had to tell you when it finished; redeploying from Portainer is synchronous, so the browser already does. Losing the per-commit image tags is a real cost — rollback becomes "rebuild from the ref you want" rather than retagging a specific build. README changes for this are deliberately not in this commit: that file also carries uncommitted work of Thom's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8d082d100f |
ci: run backend integration tests as a separate manual workflow
The job was holding the runner for hours on every push and pull request. On 2026-08-18 it ran 3h12m, and because the runner is sequential it kept frontend-e2e queued behind it for that whole time. It is not a slow suite. That run executed 89 tests in 87 seconds, then logged "Jest did not exit one second after the test run has completed" and sat there until something killed it — an open handle keeping the event loop alive after the run finishes. The same run also failed with relation "orders" does not exist, so the schema was missing in CI even though globalSetup migrates. Neither is root-caused yet; both follow the job into its new home. So the new workflow carries a 15-minute timeout. The suite needs about 90 seconds, so this is not a performance budget — it is a stop that turns a post-run hang into minutes of wasted runner time rather than hours. Tests.yml keeps a comment saying where the job went, so its absence reads as deliberate rather than as something lost in an edit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c350f0a588 |
ci: distinguish between missing secrets and variables in QA build
Separate error messages and guidance for missing secrets versus variables in the QA build configuration check to improve clarity for developers. |
||
|
|
711f59c0a1 |
ci: distinguish why the QA build daemon is unreachable (#25)
The first dispatch failed at the wait step with nothing to act on. The run logs showed the dind service container was never created — no service startup output at all, and teardown reporting "No such container" for the ID it had recorded — which is what act_runner does when it refuses a privileged container. From the failing step, that is indistinguishable from dockerd simply being slow, so the step now says which one it is: if the service host resolves, the container exists and dockerd is not serving plain TCP on 2375; if it does not resolve, the service never started and the runner needs container.privileged. Also raises the wait from 30s to 90s. The NAS took the full 30s before failing, so the old ceiling was too close to the observed time to distinguish slow from broken. The docker CLI fallback is retained: the runner image has no docker binary, and the static install worked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2a20c0e05b |
ci: manual workflow to build and publish the QA image (#25)
Adds a workflow_dispatch job that builds a chosen ref, pushes it to the Gitea container registry as :qa plus a commit-sha tag, and emails when it is ready. It deliberately does not restart the QA stack — redeploying stays a human action in Portainer. The build runs against a Docker-in-Docker service rather than the NAS's Docker socket. Mounting the host socket into the runner would give every workflow on every branch root-equivalent control of the NAS, production included; pushing to a registry means the image does not need to survive in the build daemon. The QA stack now pulls that image instead of requiring a local build. The previous arrangement meant the image existed only if someone remembered to build it, which produced two confusing failures already: a Docker Hub "pull access denied" when the tag was missing, and a silent stale-image deploy when the build had not been rerun. Two runner capabilities cannot be verified from here — privileged service containers for dind, and a docker CLI in the runner image. The workflow checks both and fails with an explanation rather than a connection refused, and validates all five required secrets and variables up front rather than part-way through a build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ec63b9cdb0 |
fix: unset NODE_ENV in e2e job so devDependencies install
npm treats NODE_ENV=production as --omit=dev, so `npm install` in the frontend-e2e job skipped typescript and the build died on `tsc: not found`. The same env var would have stripped vite and @playwright/test from the frontend install, and flipped the session cookie to Secure on a run served over plain http. The reported summarize crash was a symptom: the job aborted before Playwright ran, but Summarize is `if: always()` and threw ENOENT on the missing JSON, burying the real failure. Both summarize scripts now report the missing file and exit 0 -- the job still fails via its own step. Also split build from start, replaced `sleep 3` with a readiness poll against /api/config, and dump the backend log when e2e fails. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
483b1391a5 | fix: use node-pg-migrate instead of deleted init.sql in e2e workflow | ||
|
|
2bff3a231f | ci: fix Postgres service port conflict in CI | ||
|
|
234330e482 | Add CI test workflow with job summaries | ||
|
|
2adbf24b23 |
Add SonarQube CI workflow
SonarQube Analysis / sonarqube (push) Successful in 11m16s
|