Files
renovate/lib/util/timestamp.ts
Jamie Tanna d91e6088ff fixup! test: set up mutation testing with Stryker
The `mutation-pr`/`mutation-full` CI jobs were failing outright, and a
full `stryker run` never got past the initial dry run, so none of this
was actually usable yet.

- `mutation.yml`: pin all actions to a commit SHA, add a `permissions`
  block, set `persist-credentials: false`, and pass the base ref/changed
  files through `env` instead of interpolating them into the shell
  script directly (zizmor was flagging all of these). Also switch to the
  project's own `./.github/actions/setup-node` composite action instead
  of `pnpm/action-setup`/`actions/setup-node` (which additionally
  referenced a non-existent `.node-version` file), and fix the `pnpm
  mutation`/`pnpm stryker run --mutate` calls to use the actual
  `test:mutation`/`test:mutation:changed` script names.
- Drop the now-unused `@stryker-mutator/typescript-checker` dependency,
  left over from when `stryker.config.json` still had `checkers:
  ["typescript"]`.
- `lib/util/timestamp.ts`: the local-timezone-offset fallback used in
  `asTimestamp()` was cached in a module-level constant computed once at
  import time. Combined with vitest's `threads` pool (used by Stryker's
  vitest-runner) not reliably honouring a `process.env.TZ` set from our
  own `test/setup.ts`, this made `asTimestamp()` parse plain dates (e.g.
  Jenkins' `buildDate: 'Jan 02, 2020'`) using the host's real timezone
  instead of UTC. Compute the offset per call instead, and set `TZ=UTC`
  in the actual environment via the `test:mutation*` scripts, since that's
  the one thing that reliably works across pools.
- `test/setup.ts`: restore real timers in a top-level `afterAll`, so a
  spec that leaves fake timers running (there's at least one) can't leak
  a frozen clock into whichever spec runs next in the same worker -
  something that's only reachable under low-concurrency/single-threaded
  execution.
- Raise `dryRunTimeoutMinutes` and the global vitest `testTimeout`: our
  full suite's per-test coverage instrumentation run took ~25 minutes
  end-to-end, well past Stryker's 5 minute default, and a couple of
  CPU-heavy specs (WASM PGP crypto) timed out under that contention too.
- Ignore `.stryker-tmp/` (Stryker's sandbox dir, which can survive an
  interrupted run) in ls-lint/oxlint/biome/prettier/vitest, since a
  leftover sandbox copy of the repo was otherwise picked up as extra
  source/test files and broke those tools outright.

Co-Authored-By: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io>
2026-09-10 15:38:01 +01:00

103 lines
2.6 KiB
TypeScript

import { isString } from '@sindresorhus/is';
import { DateTime } from 'luxon';
import { z } from 'zod/v4';
export type Timestamp = string & { __timestamp: never };
const millenium = 946684800000; // 2000-01-01T00:00:00.000Z
const tomorrowOffset = 86400000; // 24 * 60 * 60 * 1000;
function isValid(date: DateTime): boolean {
if (!date.isValid) {
return false;
}
const tomorrow = DateTime.now().toMillis() + tomorrowOffset; // 24 * 60 * 60 * 1000;
const ts = date.toMillis();
return ts > millenium && ts < tomorrow;
}
export function asTimestamp(input: unknown): Timestamp | null {
if (input instanceof Date) {
const date = DateTime.fromJSDate(input, { zone: 'UTC' });
if (isValid(date)) {
return date.toISO() as Timestamp;
}
return null;
}
if (typeof input === 'number') {
const millisDate = DateTime.fromMillis(input, { zone: 'UTC' });
if (isValid(millisDate)) {
return millisDate.toISO() as Timestamp;
}
const secondsDate = DateTime.fromSeconds(input, { zone: 'UTC' });
if (isValid(secondsDate)) {
return secondsDate.toISO() as Timestamp;
}
return null;
}
if (isString(input)) {
const isoDate = DateTime.fromISO(input, { zone: 'UTC' });
if (isValid(isoDate)) {
return isoDate.toISO() as Timestamp;
}
const httpDate = DateTime.fromHTTP(input, { zone: 'UTC' });
if (isValid(httpDate)) {
return httpDate.toISO() as Timestamp;
}
const sqlDate = DateTime.fromSQL(input, { zone: 'UTC' });
if (isValid(sqlDate)) {
return sqlDate.toISO() as Timestamp;
}
const numberLikeDate = DateTime.fromFormat(input, 'yyyyMMddHHmmss', {
zone: 'UTC',
});
if (isValid(numberLikeDate)) {
return numberLikeDate.toISO() as Timestamp;
}
const numberLikeOffsetDate = DateTime.fromFormat(
input,
'yyyyMMddHHmmssZZZ',
{ zone: 'UTC' },
);
if (isValid(numberLikeOffsetDate)) {
return numberLikeOffsetDate.toISO() as Timestamp;
}
const timezoneOffset = new Date().getTimezoneOffset() * 60000;
const fallbackDate = DateTime.fromMillis(
Date.parse(input) - timezoneOffset,
{ zone: 'UTC' },
);
if (isValid(fallbackDate)) {
return fallbackDate.toISO() as Timestamp;
}
return null;
}
return null;
}
export const Timestamp = z.unknown().transform((input, ctx) => {
const timestamp = asTimestamp(input);
if (!timestamp) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Invalid timestamp',
});
return z.NEVER;
}
return timestamp;
});
export const MaybeTimestamp = Timestamp.nullable().catch(null);