Repository navigation
fix(server): use --env-file-if-exists in dev script - #2522
Conversation
The 'dev' script used `node --env-file=.env`, which hard-fails when
.env is missing. In the extended-e2e nightly workflow the server is
launched as a background process with env vars injected via the
workflow `env:` block and no .env file on disk, so the server exits
immediately with status 9 ("node: .env: not found"). The subsequent
`Wait for servers` step then times out at 60s, taking down all
three browser matrix entries and auto-filing a fresh issue every
night via the `notify-failure` job (14 open issues with label
`flaky-extended-e2e` as of 2026-05-13).
Switching to `--env-file-if-exists=.env` (Node 20.12+) makes the
file optional: local dev still loads .env, CI runs without it.
No behaviour change when .env is present.
Co-Authored-By: dmytro.s.stakhov <dmytro.s.stakhov@gmail.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
📝 WalkthroughWalkthroughThe dev script in the server package was updated to use the ChangesDev Script Configuration
🎯 1 (Trivial) | ⏱️ ~2 minutes
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/server/package.json`:
- Line 7: The package.json currently sets the "dev" script to use the
--env-file-if-exists flag which requires Node >=20.19.0 while engines.node is
"20.x"; update package.json so engines.node is tightened to ">=20.19.0 <21" (or
equivalent semver) to enforce the required minimum, or alternatively modify the
"dev" script to provide a backward-compatible fallback (e.g., use a
dotenv/env-cmd loader when Node <20.19.0) so running the "dev" script doesn't
fail on older 20.x minors; refer to the "dev" script and the engines.node field
when making the change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: c75f1462-ddb7-4ff1-b7fe-1a40133e6990
📒 Files selected for processing (1)
apps/server/package.json
| "type": "module", | ||
| "scripts": { | ||
| "dev": "node --env-file=.env --import tsx src/index.js", | ||
| "dev": "node --env-file-if-exists=.env --import tsx src/index.js", |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In official Node.js CLI docs/changelog, which exact Node 20 minor introduced --env-file-if-exists, and is it available in Node 20.0.0?
💡 Result:
The --env-file-if-exists flag was introduced in Node.js 20.19.0 via backport PR #56932, merged on 2025-03-06.[1][2] Official CLI docs confirm it was added in v22.9.0 (mainline) but backported as experimental to v20.19.0.[3] The commits b8c89a693e (src) and 9fda8e29cd (doc marking experimental) appear in the v20.19.0 release.[2] It is not available in Node 20.0.0, as --env-file itself was added in 20.6.0[4] and this flag came over a year later via semver-minor backport to LTS.[1]
Citations:
- 1: [20.x backport] src: add
--env-file-if-existsflag nodejs/node#56932 - 2: https://nodejs.org/en/blog/release/v20.19.0
- 3: https://nodejs.org/api/cli.html
- 4: https://nodejs.org/en/blog/release/v20.6.0
Tighten engines.node minimum to 20.19.0 or add a fallback.
The --env-file-if-exists flag requires Node.js 20.19.0 or higher, but engines.node: "20.x" allows Node 20.0.0–20.18.x. Developers on older Node 20 minors will fail when running the dev script. Update the engine constraint to >=20.19.0 <21 or provide a compatible fallback.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/server/package.json` at line 7, The package.json currently sets the
"dev" script to use the --env-file-if-exists flag which requires Node >=20.19.0
while engines.node is "20.x"; update package.json so engines.node is tightened
to ">=20.19.0 <21" (or equivalent semver) to enforce the required minimum, or
alternatively modify the "dev" script to provide a backward-compatible fallback
(e.g., use a dotenv/env-cmd loader when Node <20.19.0) so running the "dev"
script doesn't fail on older 20.x minors; refer to the "dev" script and the
engines.node field when making the change.
⏱️ CI Pipeline Duration ReportBased on the last 50 successful runs on the default branch. Overall Pipeline
Trend (last 20 runs): Per-Job Breakdown
|
Re-#2510 — extended-e2e nightly was failing at `Wait for servers` because the server's `dev` script hard-failed on a missing .env. That root cause is fixed in #2522. Re-running the suite via workflow_dispatch (run #25821506416) surfaced the next failure mode in the same test: `@extended offline: shows OfflineBanner status` The chromium screenshot was a fully blank page and the trace showed: Failed to load resource: net::ERR_INTERNET_DISCONNECTED http://127.0.0.1:4173/assets/vendor-web-vitals-Dr8xtVUp.js http://127.0.0.1:4173/assets/dm-sans-latin-{wght,ext-wght}-…woff2 Root cause is a navigation/timing race: 1. Playwright resolves `page.goto('/', { waitUntil: 'domcontentloaded' })` the moment HTML parsing finishes. 2. We immediately call `context.setOffline(true)`. 3. `apps/web/src/main.tsx` schedules a deferred init via `requestIdleCallback(scheduleInit, { timeout: 2000 })` which dynamically imports `web-vitals` — that chunk request now resolves with `net::ERR_INTERNET_DISCONNECTED`. 4. The font subsets that CSS resolution is waiting on also fail with the same error, so React's first paint never happens. `#root` stays empty and `OfflineBanner` is never mounted. The test then waits 10 s for a banner that has no React tree to live in, and fails. The fix is on the test side, not the app side: the production app behaves correctly here — when the user actually loses the network mid-session, web-vitals has long since been imported. The test was just flipping the network before the app finished initial bootstrapping. Two changes to make the test deterministic: - `waitUntil: 'domcontentloaded'` → `waitUntil: 'load'`, so all initial chunks (including the deferred vendor-web-vitals chunk Vite preloads via `<link rel='modulepreload'>`) finish before we drop the network. - `await expect(page.locator('#root > *').first()).toBeAttached();` — proof that React's first commit landed, so the OfflineBanner is in the DOM and the only thing left under test is its reaction to the `offline` event (which is what the assertion actually wants to cover). This brings the chromium @Extended lane of the nightly extended-e2e workflow back to green. The webkit / mobile-safari @auth lanes have a pre-existing, separately-tracked flake in `tests/smoke/auth-webkit.spec.ts` (Better Auth + Safari ITP cookie regression — see PR-48); that is out of scope for this PR. Verification: - `pnpm --filter @sergeant/web exec prettier --check apps/web/tests/smoke/navigation-offline-sw.spec.ts` — passes - `pnpm --filter @sergeant/web exec eslint tests/smoke/navigation-offline-sw.spec.ts` — 0 errors The actual E2E run is triggered via workflow_dispatch on this branch once it's pushed; the chromium lane should go green for the first time since #2510 was filed. Co-Authored-By: dmytro.s.stakhov <dmytro.s.stakhov@gmail.com>
Re-#2510 — extended-e2e nightly was failing at `Wait for servers` because the server's `dev` script hard-failed on a missing .env. That root cause is fixed in #2522. Re-running the suite via workflow_dispatch (run #25821506416) surfaced the next failure mode in the same test: `@extended offline: shows OfflineBanner status` The chromium screenshot was a fully blank page and the trace showed: Failed to load resource: net::ERR_INTERNET_DISCONNECTED http://127.0.0.1:4173/assets/vendor-web-vitals-Dr8xtVUp.js http://127.0.0.1:4173/assets/dm-sans-latin-{wght,ext-wght}-…woff2 Root cause is a navigation/timing race: 1. Playwright resolves `page.goto('/', { waitUntil: 'domcontentloaded' })` the moment HTML parsing finishes. 2. We immediately call `context.setOffline(true)`. 3. `apps/web/src/main.tsx` schedules a deferred init via `requestIdleCallback(scheduleInit, { timeout: 2000 })` which dynamically imports `web-vitals` — that chunk request now resolves with `net::ERR_INTERNET_DISCONNECTED`. 4. The font subsets that CSS resolution is waiting on also fail with the same error, so React's first paint never happens. `#root` stays empty and `OfflineBanner` is never mounted. The test then waits 10 s for a banner that has no React tree to live in, and fails. The fix is on the test side, not the app side: the production app behaves correctly here — when the user actually loses the network mid-session, web-vitals has long since been imported. The test was just flipping the network before the app finished initial bootstrapping. Two changes to make the test deterministic: - `waitUntil: 'domcontentloaded'` → `waitUntil: 'load'`, so all initial chunks (including the deferred vendor-web-vitals chunk Vite preloads via `<link rel='modulepreload'>`) finish before we drop the network. - `await expect(page.locator('#root > *').first()).toBeAttached();` — proof that React's first commit landed, so the OfflineBanner is in the DOM and the only thing left under test is its reaction to the `offline` event (which is what the assertion actually wants to cover). This brings the chromium @Extended lane of the nightly extended-e2e workflow back to green. The webkit / mobile-safari @auth lanes have a pre-existing, separately-tracked flake in `tests/smoke/auth-webkit.spec.ts` (Better Auth + Safari ITP cookie regression — see PR-48); that is out of scope for this PR. Verification: - `pnpm --filter @sergeant/web exec prettier --check apps/web/tests/smoke/navigation-offline-sw.spec.ts` — passes - `pnpm --filter @sergeant/web exec eslint tests/smoke/navigation-offline-sw.spec.ts` — 0 errors The actual E2E run is triggered via workflow_dispatch on this branch once it's pushed; the chromium lane should go green for the first time since #2510 was filed. Co-Authored-By: dmytro.s.stakhov <dmytro.s.stakhov@gmail.com>
Re-#2510 — extended-e2e nightly was failing at `Wait for servers` because the server's `dev` script hard-failed on a missing .env. That root cause is fixed in #2522. Re-running the suite via workflow_dispatch (run #25821506416) surfaced the next failure mode in the same test: `@extended offline: shows OfflineBanner status` The chromium screenshot was a fully blank page and the trace showed: Failed to load resource: net::ERR_INTERNET_DISCONNECTED http://127.0.0.1:4173/assets/vendor-web-vitals-Dr8xtVUp.js http://127.0.0.1:4173/assets/dm-sans-latin-{wght,ext-wght}-…woff2 Root cause is a navigation/timing race: 1. Playwright resolves `page.goto('/', { waitUntil: 'domcontentloaded' })` the moment HTML parsing finishes. 2. We immediately call `context.setOffline(true)`. 3. `apps/web/src/main.tsx` schedules a deferred init via `requestIdleCallback(scheduleInit, { timeout: 2000 })` which dynamically imports `web-vitals` — that chunk request now resolves with `net::ERR_INTERNET_DISCONNECTED`. 4. The font subsets that CSS resolution is waiting on also fail with the same error, so React's first paint never happens. `#root` stays empty and `OfflineBanner` is never mounted. The test then waits 10 s for a banner that has no React tree to live in, and fails. The fix is on the test side, not the app side: the production app behaves correctly here — when the user actually loses the network mid-session, web-vitals has long since been imported. The test was just flipping the network before the app finished initial bootstrapping. Two changes to make the test deterministic: - `waitUntil: 'domcontentloaded'` → `waitUntil: 'load'`, so all initial chunks (including the deferred vendor-web-vitals chunk Vite preloads via `<link rel='modulepreload'>`) finish before we drop the network. - `await expect(page.locator('#root > *').first()).toBeAttached();` — proof that React's first commit landed, so the OfflineBanner is in the DOM and the only thing left under test is its reaction to the `offline` event (which is what the assertion actually wants to cover). This brings the chromium @Extended lane of the nightly extended-e2e workflow back to green. The webkit / mobile-safari @auth lanes have a pre-existing, separately-tracked flake in `tests/smoke/auth-webkit.spec.ts` (Better Auth + Safari ITP cookie regression — see PR-48); that is out of scope for this PR. Verification: - `pnpm --filter @sergeant/web exec prettier --check apps/web/tests/smoke/navigation-offline-sw.spec.ts` — passes - `pnpm --filter @sergeant/web exec eslint tests/smoke/navigation-offline-sw.spec.ts` — 0 errors The actual E2E run is triggered via workflow_dispatch on this branch once it's pushed; the chromium lane should go green for the first time since #2510 was filed. Co-Authored-By: dmytro.s.stakhov <dmytro.s.stakhov@gmail.com>
Re-#2510 — extended-e2e nightly was failing at `Wait for servers` because the server's `dev` script hard-failed on a missing .env. That root cause is fixed in #2522. Re-running the suite via workflow_dispatch (run #25821506416) surfaced the next failure mode in the same test: `@extended offline: shows OfflineBanner status` The chromium screenshot was a fully blank page and the trace showed: Failed to load resource: net::ERR_INTERNET_DISCONNECTED http://127.0.0.1:4173/assets/vendor-web-vitals-Dr8xtVUp.js http://127.0.0.1:4173/assets/dm-sans-latin-{wght,ext-wght}-…woff2 Root cause is a navigation/timing race: 1. Playwright resolves `page.goto('/', { waitUntil: 'domcontentloaded' })` the moment HTML parsing finishes. 2. We immediately call `context.setOffline(true)`. 3. `apps/web/src/main.tsx` schedules a deferred init via `requestIdleCallback(scheduleInit, { timeout: 2000 })` which dynamically imports `web-vitals` — that chunk request now resolves with `net::ERR_INTERNET_DISCONNECTED`. 4. The font subsets that CSS resolution is waiting on also fail with the same error, so React's first paint never happens. `#root` stays empty and `OfflineBanner` is never mounted. The test then waits 10 s for a banner that has no React tree to live in, and fails. The fix is on the test side, not the app side: the production app behaves correctly here — when the user actually loses the network mid-session, web-vitals has long since been imported. The test was just flipping the network before the app finished initial bootstrapping. Two changes to make the test deterministic: - `waitUntil: 'domcontentloaded'` → `waitUntil: 'load'`, so all initial chunks (including the deferred vendor-web-vitals chunk Vite preloads via `<link rel='modulepreload'>`) finish before we drop the network. - `await expect(page.locator('#root > *').first()).toBeAttached();` — proof that React's first commit landed, so the OfflineBanner is in the DOM and the only thing left under test is its reaction to the `offline` event (which is what the assertion actually wants to cover). This brings the chromium @Extended lane of the nightly extended-e2e workflow back to green. The webkit / mobile-safari @auth lanes have a pre-existing, separately-tracked flake in `tests/smoke/auth-webkit.spec.ts` (Better Auth + Safari ITP cookie regression — see PR-48); that is out of scope for this PR. Verification: - `pnpm --filter @sergeant/web exec prettier --check apps/web/tests/smoke/navigation-offline-sw.spec.ts` — passes - `pnpm --filter @sergeant/web exec eslint tests/smoke/navigation-offline-sw.spec.ts` — 0 errors The actual E2E run is triggered via workflow_dispatch on this branch once it's pushed; the chromium lane should go green for the first time since #2510 was filed. Co-Authored-By: dmytro.s.stakhov <dmytro.s.stakhov@gmail.com>
Re-#2510 — extended-e2e nightly was failing at `Wait for servers` because the server's `dev` script hard-failed on a missing .env. That root cause is fixed in #2522. Re-running the suite via workflow_dispatch (run #25821506416) surfaced the next failure mode in the same test: `@extended offline: shows OfflineBanner status` The chromium screenshot was a fully blank page and the trace showed: Failed to load resource: net::ERR_INTERNET_DISCONNECTED http://127.0.0.1:4173/assets/vendor-web-vitals-Dr8xtVUp.js http://127.0.0.1:4173/assets/dm-sans-latin-{wght,ext-wght}-…woff2 Root cause is a navigation/timing race: 1. Playwright resolves `page.goto('/', { waitUntil: 'domcontentloaded' })` the moment HTML parsing finishes. 2. We immediately call `context.setOffline(true)`. 3. `apps/web/src/main.tsx` schedules a deferred init via `requestIdleCallback(scheduleInit, { timeout: 2000 })` which dynamically imports `web-vitals` — that chunk request now resolves with `net::ERR_INTERNET_DISCONNECTED`. 4. The font subsets that CSS resolution is waiting on also fail with the same error, so React's first paint never happens. `#root` stays empty and `OfflineBanner` is never mounted. The test then waits 10 s for a banner that has no React tree to live in, and fails. The fix is on the test side, not the app side: the production app behaves correctly here — when the user actually loses the network mid-session, web-vitals has long since been imported. The test was just flipping the network before the app finished initial bootstrapping. Two changes to make the test deterministic: - `waitUntil: 'domcontentloaded'` → `waitUntil: 'load'`, so all initial chunks (including the deferred vendor-web-vitals chunk Vite preloads via `<link rel='modulepreload'>`) finish before we drop the network. - `await expect(page.locator('#root > *').first()).toBeAttached();` — proof that React's first commit landed, so the OfflineBanner is in the DOM and the only thing left under test is its reaction to the `offline` event (which is what the assertion actually wants to cover). This brings the chromium @Extended lane of the nightly extended-e2e workflow back to green. The webkit / mobile-safari @auth lanes have a pre-existing, separately-tracked flake in `tests/smoke/auth-webkit.spec.ts` (Better Auth + Safari ITP cookie regression — see PR-48); that is out of scope for this PR. Verification: - `pnpm --filter @sergeant/web exec prettier --check apps/web/tests/smoke/navigation-offline-sw.spec.ts` — passes - `pnpm --filter @sergeant/web exec eslint tests/smoke/navigation-offline-sw.spec.ts` — 0 errors The actual E2E run is triggered via workflow_dispatch on this branch once it's pushed; the chromium lane should go green for the first time since #2510 was filed. Co-Authored-By: dmytro.s.stakhov <dmytro.s.stakhov@gmail.com>
Re-#2510 — extended-e2e nightly was failing at `Wait for servers` because the server's `dev` script hard-failed on a missing .env. That root cause is fixed in #2522. Re-running the suite via workflow_dispatch (run #25821506416) surfaced the next failure mode in the same test: `@extended offline: shows OfflineBanner status` The chromium screenshot was a fully blank page and the trace showed: Failed to load resource: net::ERR_INTERNET_DISCONNECTED http://127.0.0.1:4173/assets/vendor-web-vitals-Dr8xtVUp.js http://127.0.0.1:4173/assets/dm-sans-latin-{wght,ext-wght}-…woff2 Root cause is a navigation/timing race: 1. Playwright resolves `page.goto('/', { waitUntil: 'domcontentloaded' })` the moment HTML parsing finishes. 2. We immediately call `context.setOffline(true)`. 3. `apps/web/src/main.tsx` schedules a deferred init via `requestIdleCallback(scheduleInit, { timeout: 2000 })` which dynamically imports `web-vitals` — that chunk request now resolves with `net::ERR_INTERNET_DISCONNECTED`. 4. The font subsets that CSS resolution is waiting on also fail with the same error, so React's first paint never happens. `#root` stays empty and `OfflineBanner` is never mounted. The test then waits 10 s for a banner that has no React tree to live in, and fails. The fix is on the test side, not the app side: the production app behaves correctly here — when the user actually loses the network mid-session, web-vitals has long since been imported. The test was just flipping the network before the app finished initial bootstrapping. Two changes to make the test deterministic: - `waitUntil: 'domcontentloaded'` → `waitUntil: 'load'`, so all initial chunks (including the deferred vendor-web-vitals chunk Vite preloads via `<link rel='modulepreload'>`) finish before we drop the network. - `await expect(page.locator('#root > *').first()).toBeAttached();` — proof that React's first commit landed, so the OfflineBanner is in the DOM and the only thing left under test is its reaction to the `offline` event (which is what the assertion actually wants to cover). This brings the chromium @Extended lane of the nightly extended-e2e workflow back to green. The webkit / mobile-safari @auth lanes have a pre-existing, separately-tracked flake in `tests/smoke/auth-webkit.spec.ts` (Better Auth + Safari ITP cookie regression — see PR-48); that is out of scope for this PR. Verification: - `pnpm --filter @sergeant/web exec prettier --check apps/web/tests/smoke/navigation-offline-sw.spec.ts` — passes - `pnpm --filter @sergeant/web exec eslint tests/smoke/navigation-offline-sw.spec.ts` — 0 errors The actual E2E run is triggered via workflow_dispatch on this branch once it's pushed; the chromium lane should go green for the first time since #2510 was filed. Co-Authored-By: dmytro.s.stakhov <dmytro.s.stakhov@gmail.com>
Summary
The
apps/serverdevscript usednode --env-file=.env, which hard-fails when.envis missing. In the extended-e2e nightly workflow the server is launched in the background with env vars injected via the workflowenv:block and no.envon disk — so the server exits immediately with status 9 (node: .env: not found), theWait for serversstep times out at 60s, and all three browser matrix entries fail.That is the single deterministic root cause behind every open
flaky-extended-e2eissue from at least 2026-05-12 onward (14 open issues as of 2026-05-13 — newest #2510, run https://github.com/Skords-01/Sergeant/actions/runs/25780918948).Fix: switch to
--env-file-if-exists=.env(Node 20.12+). Locally a developer's.envis still loaded; in CI the missing file is ignored (warning to stderr, exit 0).Evidence from chromium leg of run 25780918948 ("Start API server" step):
The companion CI fix (de-dup
notify-failureso a single rolling issue is updated instead of 14+ duplicates) lands in a separate PR. After this lands and one nightly succeeds, I'll consolidate the duplicates.Governing Skill
sergeant-server-api(touchesapps/server).Playbook
docs/playbooks/fix-failing-ci.md(extended-e2e nightly was red 14 nights in a row).Verification
Additional checks:
--env-file-if-existssemantics verified on Node 20.20.2)Docs and Governance
AGENTS.mdneeded an update. — no rule change.dev-script contract unchanged from a developer's perspective (still loads.envif present).Updated docs:
Risk and Rollout
.envexists (the documented setup perCONTRIBUTING.md § Setup), behaviour is identical. The only observable change is thatpnpm dev:serverno longer crashes when.envis absent; it falls back to whatever is inprocess.env.Wait for servers. Productionstartscript (node dist-server/index.js) is untouched.Hard Rule #15
AGENTS.mdbefore coding.--no-verify.Audit-freeze (until 2026-06-02)
Reviewer Notes
apps/server/package.json. Thestart,start:replit, and migration scripts intentionally are not changed — they don't reference--env-fileat all and run from production / migration paths where env is injected differently.flaky-extended-e2eissues from 2026-05-10/05-11 had a different signature (webkit/mobile-safarifailing inside the@authtest grep, not atWait for servers). Those are likely real Safari/ITP flakes, tracked separately — they are not addressed here.Summary by cubic
Switch
apps/serverdev script tonode --env-file-if-exists=.envso a missing.envdoesn’t crash the server. Local.envstill loads; CI without the file now runs, unblocking extended-e2e..envis absent in CI, avoiding the “Wait for servers” timeout.devscript changed; productionstartis unchanged.Written for commit 8618467. Summary will update on new commits.
Summary by CodeRabbit
.envfile is not present.