diff --git a/docs/expiration_warnings.md b/docs/expiration_warnings.md index 671ecdf..6bbeefc 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 @@ -40,29 +39,21 @@ 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. - ## Enable / disable Expiration warnings are enabled by default. diff --git a/packages/cli/src/config/constants.ts b/packages/cli/src/config/constants.ts index 00e4418..e5b164a 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; diff --git a/packages/cli/src/services/detectEnvExpirations.ts b/packages/cli/src/services/detectEnvExpirations.ts index e159106..f98626d 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 33c4ae8..06b7d90 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, + }, + ]); + }); });