Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 5 additions & 14 deletions docs/expiration_warnings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
11 changes: 7 additions & 4 deletions packages/cli/src/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
39 changes: 34 additions & 5 deletions packages/cli/src/services/detectEnvExpirations.ts
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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;
}

Expand All @@ -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,
Expand All @@ -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);
}
22 changes: 22 additions & 0 deletions packages/cli/test/unit/services/detectEnvExpirations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
]);
});
});
Loading