Quality checks and CI
./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 stylenpm run types # generates Wayfinder routes, then tsc --noEmitnpm run lint:check # eslintnpm run format:check # prettiernpm run build # production assetscomposer audit && npm audit # dependency advisoriesscripts/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).
Operator reports
Section titled “Operator reports”php artisan orders:report-totals [--workspace=slug] # read-only; exit 1 if any stored total ≠ subtotal + delivery feephp artisan orders:backfill-revisions # idempotent; migrated revision 1 for orders without onephp artisan inventory:backfill-opening-balances # idempotent; opening movements and legacy review batchesphp artisan inventory:reconcile [--workspace=slug] [--json] # read-only; CORRUPTION lines fail (exit 1), exception lines only reportphp 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 ledgerphp 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.
Conventions the checks enforce
Section titled “Conventions the checks enforce”- No
any, no@ts-ignore, noeslint-disablein 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 noorderBy: PostgreSQL returns them in any order.OrderReservationTestfailed onmainon 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 inlang/uk.json(TranslationsTestfails otherwise); every new interface string goes into bothresources/js/locales/*.json.