Skip to content

Quality checks and CI

Terminal window
./vendor/bin/pest --parallel --processes=8 # backend tests on PostgreSQL, no network
./vendor/bin/pest tests/Race # contested-row tests: two real processes per case
./vendor/bin/pint --test # PHP style
npm run types # generates Wayfinder routes, then tsc --noEmit
npm run lint:check # eslint
npm run format:check # prettier
npm run build # production assets
composer audit && npm audit # dependency advisories

scripts/check-all runs all of the above in CI’s order, plus the golden scenario, the Stage 3A checkout scenarios (G1 and G6), the race suite and the reconciling demo seed against the *_test database (it refuses any other). --stamp then marks the pushed HEAD for the local deploy gate.

Each worker rebuilds the schema in its own database. With PostgreSQL’s default max_locks_per_transaction = 64, sixteen workers can exhaust the lock table (“out of shared memory”) since the schema grew in Stage 2; eight workers are safe, or raise that setting in your local PostgreSQL. CI runs two workers.

The Race suite (tests/Race/) uses DatabaseTruncation instead of a wrapping transaction so its fixtures are committed and visible to the php artisan orders:reserve processes it starts. Ordinary pest --parallel runs it too.

One workflow, .github/workflows/ci.yml, on every push and pull request to main and develop:

Job What it proves
quality Pint, Prettier, ESLint, production build, tsc, then git diff --exit-code to prove no checker modified the tree
tests Pest in parallel on production’s PHP (8.4) against a PostgreSQL 17 service (production’s version), then the named gates: the Stage 2 golden scenario, the Stage 3A checkout scenarios (tests/Feature/Checkout/GoldenScenariosTest.php: G1, an offline request submitted twice, confirmed and paid; G6, price change and malicious input), the race suite (two real connections per case, queued behind a lock holder that proves they contended), and a fresh seed that must reconcile
audit composer audit and npm audit --audit-level=moderate; an unreachable advisory database fails. What happens next, and the weekly and quarterly updates, are the dependency policy
docs The documentation site builds
landing The landing page: Prettier, astro check, the static build and npm audit
storefront The Astro storefront: SDK types match the OpenAPI contract, theme boundaries, Prettier, ESLint, astro check, Vitest, the server build and npm audit (details)
deploy Only on pushes to main, after the other jobs pass (storefront included); see Deployment
deploy-local Instead of deploy when the repository variable DEPLOY_GATE=local: requires a local-checks stamp on the same tree (ADR-012)

The audit fails on advisories published after a commit merged, so a red audit on an unchanged branch means a new advisory, not a regression. Fix it with a lock-only update in its own pull request. Found so far: league/commonmark ≤ 2.10.1 (GHSA-97jj-33gv-5xf9 raw-HTML filter bypass, GHSA-3q6v-r5mr-hxv8 table-extension quadratic DoS; published 30 September 2026), a transitive dependency of laravel/framework, updated to 2.10.3 on 4 October 2026. The application renders no user Markdown, so exposure was limited. In the same pull request, brace-expansion (GHSA-q2hr-2g5m-vwhr, GHSA-qhr7-859c-m2p7, GHSA-6j4f-fj2g-mc7p; denial of service) was raised to 1.1.21 and 5.0.12 with npm audit fix; it is reached only through build and lint tooling, not the shipped bundle. On 6 October 2026 shell-quote 1.9.0 (GHSA-pqg4-j6r4-53mv, command injection in quote(); critical) came through concurrently, which pins that exact version even in its latest release, so no lock-only update could fix it: package.json overrides raises it to 1.12.0. It is a development tool (composer dev), not in the shipped bundle; concurrently was checked to still run.

Nothing in CI auto-fixes. CI must be green before a pull request is merged; branch protection is not available, so this is a rule, not an enforcement (ADR-004).

Terminal window
php artisan orders:report-totals [--workspace=slug] # read-only; exit 1 if any stored total ≠ subtotal + delivery fee
Terminal window
php artisan orders:backfill-revisions # idempotent; migrated revision 1 for orders without one
php artisan inventory:backfill-opening-balances # idempotent; opening movements and legacy review batches
php artisan inventory:reconcile [--workspace=slug] [--json] # read-only; CORRUPTION lines fail (exit 1), exception lines only report
php artisan orders:reserve {id} [--key=] # reserve an order's stems (exit 0 reserved, 2 shortage, 3 refused)
php artisan orders:transition {id} {status} [--key=] [--reason=] # status change with its stock effect (exit 0 applied, 2 refused, 3 conflict)
php artisan inventory:write-off {batch} {qty} [--return] [--reason=] # write-off or supplier return through the ledger
php artisan lang:check [uk] # server strings missing a translation (also a unit test)

lang:check scans __() calls in app/ only. Strings in Laravel’s own mail layout (“Regards,”, the button fallback line, “All rights reserved.”) are not found by it. They were untranslated in the verification email until 6 October 2026 (found while checking the order-request emails), and are now in lang/uk.json.

Legacy orders created before BRV-03 omitted the delivery fee. Amendable ones (pending, confirmed) are corrected on their next edit; frozen ones keep their agreed amounts and stay listed.

  • No any, no @ts-ignore, no eslint-disable in the front end.
  • Generated Wayfinder helpers are gitignored and never edited.
  • Tests fake every outbound HTTP call; a stray request fails the test.
  • Factories default to the visible, normal state; tests pin every column they search or assert on.
  • Never assert on ->first() of a relation with several rows and no orderBy: PostgreSQL returns them in any order. OrderReservationTest failed on main on 4 October 2026 because an order’s first allocation was sometimes the tulip (15.00), not the rose (20.00); select the row you mean.
  • Cross-tenant access asserts 404 for records and 403 for actions the role lacks.
  • Commit messages have a plain subject and body, without attribution trailers.
  • Every server-side __() string has a Ukrainian entry in lang/uk.json (TranslationsTest fails otherwise); every new interface string goes into both resources/js/locales/*.json.