From ef51c01dd775db7b261fafd36217a4e81664fcf4 Mon Sep 17 00:00:00 2001 From: Chrilleweb Date: Fri, 20 Mar 2026 23:32:10 +0200 Subject: [PATCH 1/3] fix: expire date bug --- .../cli/src/services/detectEnvExpirations.ts | 39 ++++++++++++++++--- .../services/detectEnvExpirations.test.ts | 22 +++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/services/detectEnvExpirations.ts b/packages/cli/src/services/detectEnvExpirations.ts index e1591068..f98626d8 100644 --- a/packages/cli/src/services/detectEnvExpirations.ts +++ b/packages/cli/src/services/detectEnvExpirations.ts @@ -1,6 +1,9 @@ import fs from 'fs'; import type { ExpireWarning } from '../config/types.js'; +// Number of milliseconds in a day +const MS_PER_DAY = 1000 * 60 * 60 * 24; + /** * Detects expiration warnings in a dotenv file. * fx: @@ -27,7 +30,7 @@ export function detectEnvExpirations(filePath: string): ExpireWarning[] { const expireMatch = line.match(reg); if (expireMatch) { - pendingExpire = expireMatch[2]!; // capture dato + pendingExpire = expireMatch[2]!; // capture date continue; } @@ -37,10 +40,12 @@ export function detectEnvExpirations(filePath: string): ExpireWarning[] { const key = line.split('=')[0]; if (key && pendingExpire) { - const expireDate = new Date(pendingExpire); - const now = new Date(); - const diffMs = expireDate.getTime() - now.getTime(); - const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24)); + const diffDays = calculateDaysLeft(pendingExpire, new Date()); + + if (diffDays === null) { + pendingExpire = null; + continue; + } warnings.push({ key, @@ -55,3 +60,27 @@ export function detectEnvExpirations(filePath: string): ExpireWarning[] { return warnings; } + +/** + * Calculates remaining days from today (UTC day) to a YYYY-MM-DD expiration date. + * Using UTC day boundaries avoids timezone and time-of-day drift. + * @param expireDateStr - Expiration date in YYYY-MM-DD format + * @param now - Current date + * @returns Number of days left until expiration, or null if invalid date + */ +function calculateDaysLeft(expireDateStr: string, now: Date): number | null { + const parts = expireDateStr.split('-').map(Number); + if (parts.length !== 3) return null; + + const [year, month, day] = parts; + if (!year || !month || !day) return null; + + const expireUtc = Date.UTC(year, month - 1, day); + const todayUtc = Date.UTC( + now.getUTCFullYear(), + now.getUTCMonth(), + now.getUTCDate(), + ); + + return Math.ceil((expireUtc - todayUtc) / MS_PER_DAY); +} diff --git a/packages/cli/test/unit/services/detectEnvExpirations.test.ts b/packages/cli/test/unit/services/detectEnvExpirations.test.ts index 33c4ae87..06b7d907 100644 --- a/packages/cli/test/unit/services/detectEnvExpirations.test.ts +++ b/packages/cli/test/unit/services/detectEnvExpirations.test.ts @@ -125,4 +125,26 @@ describe('detectEnvExpirations', () => { { key: 'B', date: '2024-12-20', daysLeft: 19 }, ]); }); + + it('is stable across time-of-day for the same calendar date', () => { + vi.setSystemTime(new Date('2024-12-01T23:59:59.000Z')); + + fs.writeFileSync( + envPath, + ` + # @expire 2024-12-31 + API_KEY=123 + `, + ); + + const result = detectEnvExpirations(envPath); + + expect(result).toEqual([ + { + key: 'API_KEY', + date: '2024-12-31', + daysLeft: 30, + }, + ]); + }); }); From 7892a902909b5b2a8169e700606991f92cc24838 Mon Sep 17 00:00:00 2001 From: Chrilleweb Date: Fri, 20 Mar 2026 23:51:24 +0200 Subject: [PATCH 2/3] chore: docs --- docs/expiration_warnings.md | 18 ++++++------------ packages/cli/src/config/constants.ts | 11 +++++++---- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/docs/expiration_warnings.md b/docs/expiration_warnings.md index 671ecdfb..1d2789de 100644 --- a/docs/expiration_warnings.md +++ b/docs/expiration_warnings.md @@ -40,28 +40,22 @@ TOKEN_D=... - The annotation applies to the **next env key only** - If no key follows, no warning is created -## Warning output +## Behavior -When expiration annotations are found, CLI output includes an `Expiration warnings` section. +`dotenv-diff` uses two expiration thresholds: -Severity is shown from `daysLeft`: - -- `< 0`: expired (`EXPIRED ... days ago`) -- `0`: `EXPIRES TODAY` -- `1`: `expires tomorrow` -- `2-3`: urgent warning -- `4-7`: warning -- `> 7`: still shown as informational warning +- `<= 7 days`: treated as error and returns exit code `1` +- `<= 20 days`: treated as warning (error in strict mode) ## Strict mode -In strict mode, expiration warnings are treated as blocking warnings and return exit code `1`. +In strict mode, expiration warnings at `<= 20 days` are treated as blocking warnings and return exit code `1`. ```bash dotenv-diff --strict ``` -If expiration warnings exist, strict mode includes them in the strict error summary. +If expiration warnings at `<= 20 days` exist, strict mode includes them in the strict error summary. ## Enable / disable diff --git a/packages/cli/src/config/constants.ts b/packages/cli/src/config/constants.ts index 00e4418c..e5b164a4 100644 --- a/packages/cli/src/config/constants.ts +++ b/packages/cli/src/config/constants.ts @@ -65,12 +65,15 @@ export const ALLOWED_CATEGORIES = [ 'gitignore', ] as const; -/** * Threshold in days for showing expiration warnings for environment variables. - * Variables expiring within this number of days or already expired will trigger a warning. +/** + * Threshold in days for showing expiration warnings for environment variables. + * Expiration warnings are shown for variables with expiration dates within this threshold. + * Expiration warnings do not trigger an error exit by default, but can be promoted to errors with strict mode. */ export const EXPIRE_THRESHOLD_DAYS = 20; -/** Threshold in days for showing urgent expiration warnings. - * Variables expiring within this number of days or already expired will trigger a high-severity warning. +/** + * Threshold in days for urgent expiration checks. + * Expiration warnings at or below this value trigger an error exit even without strict mode. */ export const URGENT_EXPIRE_DAYS = 7; From 936c00c2dd9f0e7996bfedefdf2bd8db0e26023f Mon Sep 17 00:00:00 2001 From: Chrilleweb Date: Fri, 20 Mar 2026 23:53:41 +0200 Subject: [PATCH 3/3] chore: docs --- docs/expiration_warnings.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/expiration_warnings.md b/docs/expiration_warnings.md index 1d2789de..6bbeefc2 100644 --- a/docs/expiration_warnings.md +++ b/docs/expiration_warnings.md @@ -6,7 +6,6 @@ By annotating a key with an expiration date, `dotenv-diff` can: - warn when a key is expired - warn when a key is close to expiration -- fail the process in `--strict` mode ## Syntax @@ -55,8 +54,6 @@ In strict mode, expiration warnings at `<= 20 days` are treated as blocking warn dotenv-diff --strict ``` -If expiration warnings at `<= 20 days` exist, strict mode includes them in the strict error summary. - ## Enable / disable Expiration warnings are enabled by default.