Files
redefined-designs/.gitea/workflows/sonarqube.yml
T
bermudalambandClaude Opus 5 fe28c97e0f
Linting / lint (pull_request) Successful in 2m7s
SonarQube Analysis / sonarqube (pull_request) Successful in 25m21s
chore(sonar): remove the rejected Tinqer spike, clear the lint debt, and report measures in CI (#261)
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 <noreply@anthropic.com>
2026-09-01 13:30:29 -05:00

278 lines
12 KiB
YAML
Executable File

name: SonarQube Analysis
# The only workflow that runs the test suites. tests.yml used to run the unit
# and end-to-end suites as well, on identical triggers, so every pull request
# installed, migrated, built, started the backend and ran the whole end-to-end
# suite twice. With one runner the second copy did not run in parallel, it
# queued. It was deleted and its two summarize steps folded in here. See #123.
#
# Note that the integration suite DOES run here, on every push and pull request,
# despite backend-integration.yml describing it as manual. That quarantine only
# ever applied to tests.yml. It is survivable here because test:integration:cov
# passes --forceExit, which papers over the post-run hang, and because of the
# timeout below.
on:
push:
branches: [main]
pull_request:
types: [opened, synchronize, reopened]
workflow_dispatch:
jobs:
sonarqube:
runs-on: ubuntu-latest
# The scanner needs every coverage report in one workspace, so the suites run
# here rather than being passed between jobs as artifacts. That makes this the
# long job. The timeout is a stop, not a budget: the integration suite has
# hung after completing before (see backend-integration.yml) and cost three
# hours of runner time.
timeout-minutes: 30
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: redefined_test
POSTGRES_PASSWORD: redefined_test
POSTGRES_DB: redefined_test
options: >-
--health-cmd "pg_isready -U redefined_test"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
# Consumed by the integration suite's setup files.
TEST_PGHOST: postgres
TEST_PGPORT: 5432
TEST_PGUSER: redefined_test
TEST_PGPASSWORD: redefined_test
TEST_PGDATABASE: redefined_test
# Consumed by the backend process the end-to-end run drives.
PGHOST: postgres
PGPORT: 5432
PGUSER: redefined_test
PGPASSWORD: redefined_test
PGDATABASE: redefined_test
PORT: 3000
DEMO_MODE: 'true'
UPLOADS_DIR: /tmp/redefined-uploads
# NODE_ENV is deliberately unset: `npm install` omits devDependencies when
# NODE_ENV=production, which strips tsc/vite/@playwright/test and breaks the
# build. It would also flip the session cookie to Secure, which the e2e run
# serves over plain http.
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install backend deps
run: npm install
working-directory: backend
- name: Install frontend deps
run: npm install
working-directory: frontend
- name: TypeScript build check (backend)
run: npm run build
working-directory: backend
- name: TypeScript build check (frontend)
run: npm run build
working-directory: frontend
# The frontend's build was the only thing this workspace ran, so the unit
# suite #188 added over the filter dimensions was run by nothing but the
# author's terminal. A suite CI never runs decays into a record of what
# the code used to do, and its value is highest exactly here: chips() is
# pure, and the end-to-end run reaches it only through a browser.
#
# Guarded and named in the gate like every suite: a failing test should
# fail the job at the end, not abort it and take the scan with it.
- name: Frontend unit tests
id: frontend_unit
continue-on-error: true
run: npm run test:unit
working-directory: frontend
- name: Check the Sonar tsconfig has not drifted
run: node scripts/check-sonar-tsconfig.js
- name: Run migrations
run: node migrate.js up
working-directory: backend
# --json/--outputFile appended rather than baked into the script: the
# coverage run and the results file are wanted together here, and nowhere
# else. summarize-jest.js below reads that file.
- name: Backend unit tests with coverage
id: unit
continue-on-error: true
run: npm run test:unit:cov -- --json --outputFile=unit-results.json
working-directory: backend
# Covers everything in src/routes, which the unit suite does not touch —
# without this the backend reports around 11% rather than the ~73% it
# actually has.
#
# Guarded like the other two suites. It was the only one that was not, so
# it was the only one whose failure aborted the job — taking the scan, the
# end-to-end run and the coverage merge with it. That is what #154 has
# cost on every push since it started: not a degraded analysis, none at
# all. See #174.
- name: Backend integration tests with coverage
id: integration
continue-on-error: true
run: npm run test:integration:cov -- --json --outputFile=integration-results.json
working-directory: backend
# Guarded because the scan does not depend on it. A backend that will not
# start fails the end-to-end run below on its own, and the gate records
# both — there is no reason for it to also cost the analysis.
- name: Start backend for the end-to-end run
id: backend
continue-on-error: true
run: |
mkdir -p /tmp/redefined-uploads
node dist/server.js > /tmp/backend.log 2>&1 &
for i in $(seq 1 30); do
if node -e "require('http').get('http://localhost:3000/api/config', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"; then
echo "Backend ready after ${i}s"
exit 0
fi
sleep 1
done
echo "Backend did not become ready within 30s:"
cat /tmp/backend.log
exit 1
working-directory: backend
# A network fetch, and so the least interesting way to lose an analysis.
- name: Install Playwright browsers
id: browsers
continue-on-error: true
run: npx playwright install --with-deps chromium
working-directory: frontend
# Runs against an istanbul-instrumented dev server, which is what produces
# window.__coverage__ for the fixture to collect.
- name: Frontend end-to-end tests with coverage
id: e2e
continue-on-error: true
env:
PLAYWRIGHT_JSON_OUTPUT_NAME: playwright-results.json
# list as well as json, so the log still shows which test failed rather
# than only a file nobody reads until the summary step.
run: npm run test:e2e:cov -- --reporter=list,json
working-directory: frontend
# Keyed off the step outcome rather than failure(). continue-on-error above
# means the job is not in a failed state at this point, so failure() would
# never fire and the log that explains an end-to-end failure would go
# unprinted precisely when it is wanted.
- name: Backend log
if: steps.e2e.outcome == 'failure' || steps.backend.outcome == 'failure'
run: cat /tmp/backend.log
# Fails when nothing was collected rather than writing an empty report. An
# uninstrumented dev server lets every test pass while gathering nothing,
# and the resulting 0% reads as "the tests stopped covering things".
- name: Merge frontend coverage
id: coverage
continue-on-error: true
run: npm run coverage:report
working-directory: frontend
# Guarded so that a scanner error does not take the summaries below with
# it. The gate still records it, so a failed scan fails the job.
#
# If a coverage report is missing — the end-to-end run collecting nothing,
# say — this scans anyway and SonarQube reports those files as uncovered,
# which reads as a regression rather than as a missing input. Accepted:
# jest still writes coverage for a suite whose tests fail, so the failure
# actually occurring produces all three reports; there is no
# sonar.qualitygate.wait, so a degraded run marks the dashboard and is
# overwritten by the next good one rather than blocking anything; and the
# job fails regardless, so no run in this state reads as clean.
- name: SonarQube Scan
id: scan
continue-on-error: true
uses: sonarsource/sonarqube-scan-action@v4
env:
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
# Readable pass and fail counts, which the raw jest and Playwright output
# does not give at a glance. Both run with if: always() so a failing suite
# is still summarised — which is the case they exist for.
- name: Summarize unit tests
if: always()
run: node scripts/summarize-jest.js backend/unit-results.json "Backend Unit Test"
# The suite this workflow has been failing on for weeks, and the only one
# that had no summary — so it presented as 36 assertion errors about
# categories and price filters rather than as a count. #154 records how
# expensive that misdirection was to read.
- name: Summarize integration tests
if: always()
run: node scripts/summarize-jest.js backend/integration-results.json "Backend Integration Test"
- name: Summarize end-to-end tests
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
# do real damage.
#
# Every guarded step is listed. That is the invariant — a step carrying
# continue-on-error and missing from here cannot fail the job at all — and
# tests/unit/workflowGate.test.ts asserts it, because the integration
# suite going unlisted is exactly how #174 happened.
#
# always() is load-bearing. A step whose `if:` omits it still implicitly
# requires every previous step to have succeeded, so this was skipped in
# exactly the case it exists for: the summarise step above used to exit 1
# on a failing suite, which failed the job first and left this gate as dead
# code. The job then reported its failure under a name that describes
# summarising rather than testing. The summarisers exit 0 now; this is what
# fails the run. See #142.
- name: Fail if any guarded step failed
if: >-
always() && (
steps.frontend_unit.outcome == 'failure' ||
steps.unit.outcome == 'failure' ||
steps.integration.outcome == 'failure' ||
steps.backend.outcome == 'failure' ||
steps.browsers.outcome == 'failure' ||
steps.e2e.outcome == 'failure' ||
steps.coverage.outcome == 'failure' ||
steps.scan.outcome == 'failure'
)
run: exit 1