chore: pin the Node base image to an exact patch, align .nvmrc and engines (#3779)

* chore: pin the Node base image to an exact patch and align .nvmrc / engines

The root Dockerfile built on `node:24-alpine3.24`, which resolves at build
time. The same Dockerfile rebuilt tomorrow can therefore produce a different
Node — and a different V8 — with no commit recording that it changed, which
leaves "did our fix miss a case?" and "did the engine change under us?" with
no evidence separating them.

Pin both stages to `node:24.19.0-alpine3.24`, set `.nvmrc` to match, and
narrow `engines.node` from `>=22.0.0` to `>=24.0.0 <25`.

No-op today, verified rather than assumed: `node:24-alpine3.24` and
`node:24.19.0-alpine3.24` resolve to the same manifest
(sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43),
that image's config carries NODE_VERSION=24.19.0, and the image currently
serving production reports v24.19.0.

`engines` is bracketed at MAJOR granularity on purpose: a patch-exact floor
would make it a fourth place to bump on every base-image patch, which is the
drift this change is about rather than a fix for it. `engine-strict` is not
set, so pnpm warns rather than failing, and nothing in CI reads the field.

Add src/__tests__/node-version-consistency.test.ts to hold the invariant:
every root-Dockerfile node stage pins an exact patch, the stages agree, and
.nvmrc and engines agree with them — plus a population floor, because every
one of those verdicts is universally quantified over the extractor's output
and an empty list satisfies all of them. Blinding the extractor was watched
to leave the pin verdict green and red only that control.

Only the root Dockerfile is pinned. The apps/* and containers/* images float
in the same way but are separate services on separate cadences, so pinning
them is a real behaviour change and belongs in its own PR.

Also removes deployment-specific identifiers from three comments in
src/instrumentation.node.ts — this is a public repo — and corrects two
comments in src/server/liveness-heartbeat.ts that restated a base-image tag
(one of them made stale by this very change). Comments only; no code change.

* test(node-version): close two Dockerfile spellings the extractor was blind to

The `FROM node:` extractor anchored at `/^FROM\s+.../gm`, which Docker's own
grammar is looser than. Two legal forms slipped an UNPINNED floating stage past
the whole file with all seven tests green — verified by appending each to the
real Dockerfile and watching the suite pass:

  from node:24-alpine3.24 AS sneaky       (lowercase directive)
    FROM node:24-alpine3.24 AS indented   (leading whitespace)

Both were built under Docker to confirm they are valid: the lowercase form
builds with only a ConsistentInstructionCasing warning, the indented form with
no warning at all. Nothing else covered this — there is no Dockerfile linter in
CI.

The pattern is now `/^[ \t]*FROM[ \t]+(?:--\S+[ \t]+)*node:(\S+)/gim`. `[ \t]`
rather than `\s` because under `m` a `\s` spans newlines and can join a bare
`FROM` to a `node:` on the next line into a stage that does not exist. The `i`
flag's reach over `node:` is defensive only: an uppercase image reference is not
legal, so the only thing it can match is a stage that could never build.

The count control was the deeper problem: an equality on the MATCHED count
cannot move when the extractor goes blind, so an added-but-unmatched stage left
it sitting at 2. Two controls replace that reasoning with a measurement — the
total `FROM` directive count, which moves for any stage in any spelling, and a
coverage control comparing the extractor against a deliberately dumber second
reader that tokenises instead of pattern-matching the image reference.

Also closed a vacuity hole found while re-running the mutant battery: the pin
verdict was a for-of loop, and a loop over an empty list is green, so an
extractor returning nothing produced the file's strongest-sounding pass while
checking no stage at all. It now asserts its population first, and the pin rule
moved into a named function driven over a fixture — asserted only against live
data, which does not violate it, the rule could be neutered to a constant and
stay green forever.

Message fixes, no assertion weakened:
  - Moving both stages to `node@sha256:...` is STRONGER than a tag pin, but red
    several tests with wording blaming the pattern. The failure now says so.
  - `>=24.0.0 <25.0.0` denotes exactly the range `>=24.0.0 <25` does and was
    rejected on spelling. Accepted now; `<25.0.1` is a genuinely different range
    that admits major 25, and is still rejected, with a control pinning both.
  - Corrected a control comment that cited a double space after `FROM` as the
    likely gap. `[ \t]+` has always handled runs of spaces and tabs; the comment
    pointed the next reader away from the two gaps that were open.

Separately, a docblock still named a private infrastructure repository and one
of its manifest filenames. This is a public repository; the reference carried no
value to a public reader and is gone.
This commit is contained in:
Zachary Lowden
2026-08-10 13:02:22 -05:00
committed by GitHub
parent 6bf01a274d
commit 819c35a242
6 changed files with 486 additions and 17 deletions
+1 -1
View File
@@ -1 +1 @@
24.18.0 24.19.0
+2 -2
View File
@@ -1,6 +1,6 @@
##### DEPENDENCIES ##### DEPENDENCIES
FROM node:24-alpine3.24 AS deps FROM node:24.19.0-alpine3.24 AS deps
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
WORKDIR /app WORKDIR /app
@@ -110,7 +110,7 @@ COPY --from=builder /app/server-maps/ /server-maps/
##### RUNNER ##### RUNNER
FROM node:24-alpine3.24 AS runner FROM node:24.19.0-alpine3.24 AS runner
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
+1 -1
View File
@@ -4,7 +4,7 @@
"private": true, "private": true,
"packageManager": "pnpm@10.28.1", "packageManager": "pnpm@10.28.1",
"engines": { "engines": {
"node": ">=22.0.0" "node": ">=24.0.0 <25"
}, },
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
@@ -0,0 +1,467 @@
import fs from 'fs';
import path from 'path';
import { describe, expect, it } from 'vitest';
/**
* 🔴 THE NODE VERSION THE APP RUNS ON MUST BE PINNED TO AN EXACT PATCH, AND THE
* THREE PLACES THAT DECLARE IT MUST AGREE.
*
* The three places, and who reads each:
* Dockerfile `FROM node:<tag>` the runtime that actually serves production
* .nvmrc CI (`actions/setup-node` `node-version-file`)
* and every local `nvm use`
* package.json `engines.node` the declared supported range
*
* WHAT THIS GUARD IS FOR — it is not tidiness. A base tag of `node:24-alpine3.24`
* floats: the same Dockerfile, rebuilt tomorrow, can produce a different Node and
* therefore a different V8, with no commit anywhere recording that it changed. When
* a regression shows up after a deploy, "did our fix miss a case?" and "did the
* engine change under us?" then have no evidence that separates them, because the
* only artefact that moved is an image digest nobody diffed. Pinning the patch puts
* every engine change in the history as a reviewable one-line diff.
*
* The agreement half matters for a different reason: CI compiles and runs the test
* suite on whatever `.nvmrc` says, and production runs whatever the Dockerfile says.
* When those two drift apart the suite is testing a runtime nothing ships.
*
* WHAT IS DELIBERATELY OUT OF SCOPE, stated rather than left implicit — a guard that
* looks broader than it is, is worse than one that admits its edges:
*
* - Only the ROOT Dockerfile. The per-service Dockerfiles under `apps/` and
* `containers/` build separate images on separate release cadences and float
* today; pinning them changes what those services run, so it belongs in its own
* change rather than riding along in a no-op.
* - `flake.nix`, which is a FOURTH declaration (the NixOS dev shell) and is on a
* different major. It cannot be brought into line by editing one line: the
* pinned nixpkgs' newest Node 24 is older than the version pinned here, so
* agreeing would mean moving the nixpkgs pin as well. Asserting it here would
* only produce a red gate nobody can fix in the change that trips it.
* `engines.node` is bracketed at MAJOR granularity partly for this reason: a
* patch-exact floor would be violated by every dev shell on day one.
*/
const REPO_ROOT = path.resolve(__dirname, '../..');
const DOCKERFILE = 'Dockerfile';
/**
* How many `FROM node:` stages the root Dockerfile has today (deps + runner), and
* how many `FROM` directives it has in TOTAL (those two, plus `FROM deps` and
* `FROM scratch`).
*
* Together these are the controls that stop the whole file passing vacuously. Every
* verdict below is universally quantified over the tags the extractor returns — and
* a universally quantified assertion over an EMPTY list is true.
*
* The node-stage count ALONE is not enough, and the gap was real rather than
* theoretical: it is an equality on the MATCHED count, so a node stage the extractor
* cannot see leaves it sitting at 2 and nothing anywhere notices. Measured on the
* previous pattern: with `from node:24-alpine3.24 AS sneaky` appended to this very
* Dockerfile, all seven tests in this file passed.
*
* The TOTAL directive count is the half that closes it. A stage added in ANY
* spelling moves the total, whether or not the extractor understood it — so an
* added-but-unmatched stage is a red, not a silent pass. It is independent of the
* coverage control below (which compares two readers of the same file) precisely so
* that a blind spot shared by both readers still trips something.
*/
const EXPECTED_NODE_STAGES = 2;
const EXPECTED_FROM_DIRECTIVES = 4;
/**
* `FROM node:<tag>`, tolerating the spellings Docker accepts and a reader writing
* this pattern by eye does not:
*
* - flags between `FROM` and the image reference (`FROM --platform=$BUILDPLATFORM
* node:...`), the shape a build-matrix change is likely to introduce;
* - a LOWERCASE directive (`from node:...`) — Dockerfile directives are
* case-insensitive. Verified by building one: it succeeds, with only a
* `ConsistentInstructionCasing` warning;
* - LEADING WHITESPACE (` FROM node:...`) — also builds, no warning at all.
*
* The last two were live holes, not hypotheticals: see the measurement quoted above.
*
* The `i` flag necessarily makes `node:` case-insensitive as well. That reach is
* DEFENSIVE rather than required — an uppercase image reference is not legal
* (`FROM ALPINE:3.20` fails to build with `repository name ... must be lowercase`),
* so the only thing the extra reach can match is a stage that could never build,
* which is worth naming rather than skipping.
*
* `[ \t]` rather than `\s`: under the `m` flag `\s` spans newlines, so a bare `FROM`
* ending one line could join a `node:` beginning the next into a match for a stage
* that does not exist.
*
* Deliberately NOT matching a digest-pinned `node@sha256:...`: that form is already
* immutable, so it does not have the defect this guard exists for, and silently
* accepting it under a rule written for tags would be a lie about what was checked.
* If the repo ever moves to digest pins, this guard should be rewritten, not widened.
* The controls below detect that case explicitly and say so, so that a move to
* digests reads as "out of scope", not as "the pattern is broken".
*/
const FROM_NODE_TAG = /^[ \t]*FROM[ \t]+(?:--\S+[ \t]+)*node:(\S+)/gim;
/**
* EVERY `FROM` directive, whatever it builds on — a deliberately DUMBER second
* reader whose only job is to disagree with FROM_NODE_TAG.
*
* It recognises the directive and then splits the remainder into tokens, instead of
* encoding the image reference's shape into the pattern, so the two readers do not
* share a blind spot in how an image reference is recognised. When a `FROM node:`
* stage exists that the extractor above did not return, the two disagree and the
* coverage control names the offending reference.
*/
const ANY_FROM = /^[ \t]*FROM[ \t]+(.+)$/gim;
/** An exact patch pin: `24.19.0-alpine3.24`. NOT `24-alpine3.24`, NOT `24.19-alpine3.24`. */
const EXACT_PATCH_TAG = /^(\d+\.\d+\.\d+)(-\S+)?$/;
/**
* `>=X.Y.Z <W` — a floor and a major ceiling, which is the shape asserted below.
*
* The ceiling may equivalently be written `<W.0.0`, which denotes exactly the same
* set of versions; rejecting one spelling of a range while accepting the other made
* the failure read as a real violation when nothing was wrong. Nothing looser is
* accepted: `<W.0.1` is NOT the same range — it admits `W.0.0`, a major that is
* neither built nor tested — so it must keep failing.
*/
const ENGINES_RANGE = /^>=(\d+)\.(\d+)\.(\d+)\s+<(\d+)(?:\.0\.0)?$/;
function extractNodeTags(dockerfile: string): string[] {
return [...dockerfile.matchAll(FROM_NODE_TAG)].map((m) => m[1]);
}
/**
* The image reference of every `FROM` directive, found without FROM_NODE_TAG:
* the first token after the directive that is not a `--flag`.
*/
function extractFromRefs(dockerfile: string): string[] {
return [...dockerfile.matchAll(ANY_FROM)].map(
(m) =>
m[1]
.trim()
.split(/[ \t]+/)
.filter((token) => !token.startsWith('--'))[0] ?? ''
);
}
/**
* The tags that are NOT exact patch pins — i.e. exactly what the verdict below
* forbids.
*
* Split out as a named function so the verdict's rule is driven over a fixture in a
* control, not only over whatever the repo happens to contain today. Asserted only
* against live data, the rule could be neutered to a constant `true` and stay green
* forever, because the live data does not violate it.
*/
function floatingTags(tags: string[]): string[] {
return tags.filter((tag) => !EXACT_PATCH_TAG.test(tag));
}
/**
* The version a tag declares, WITHOUT re-testing whether it is a proper pin.
*
* Deliberately lenient, and the reason is worth stating: if these two helpers also
* enforced the pin rule, then unpinning the Dockerfile would red three tests at once
* — and the two extra failures would be about `undefined` and `NaN` rather than
* about the float. A mutant should die to the guard that OWNS it, with that guard's
* message, and stay silent everywhere else.
*/
function versionOf(tag: string | undefined): string | undefined {
return tag?.match(EXACT_PATCH_TAG)?.[1] ?? tag;
}
function majorOf(tag: string | undefined): number {
return Number(tag?.match(/^(\d+)/)?.[1]);
}
function read(rel: string): string {
return fs.readFileSync(path.join(REPO_ROOT, rel), 'utf8');
}
const dockerfileText = read(DOCKERFILE);
const nodeTags = extractNodeTags(dockerfileText);
const nvmrc = read('.nvmrc').trim();
const enginesNode: unknown = JSON.parse(read('package.json'))?.engines?.node;
/** Every `FROM` image reference, and the `node:` subset, as the DUMB reader sees them. */
const fromRefs = extractFromRefs(dockerfileText);
const independentNodeTags = fromRefs
.filter((ref) => /^node:/i.test(ref))
.map((ref) => ref.slice('node:'.length));
const digestPinnedNodeRefs = fromRefs.filter((ref) => /^node@/i.test(ref));
/**
* Appended to the messages that go strange when the extractor returned nothing.
*
* Without it, moving both stages to `node@sha256:...` — which is STRONGER than a tag
* pin, not weaker — reds several tests with wording that points at a broken pattern.
* This is message text only: it changes no assertion, it just stops the failure
* misdiagnosing its own input.
*/
function formatEmptyExtractionNote(tags: string[], digestRefs: string[]): string {
if (tags.length > 0) return '';
const digests = digestRefs.length
? ` The Dockerfile DOES have ${digestRefs.length} digest-pinned node ` +
`stage(s) (${digestRefs.join(', ')}). A digest is a STRONGER pin than an ` +
`exact tag, so this is not a violation and not a bug in the pattern — this guard is ` +
`written for tags and does not read digests. Moving the image to digests means ` +
`rewriting this guard, not widening it.`
: '';
return (
` NOTE: the extractor returned NO node tags at all, so the versions quoted in this ` +
`message are absent rather than wrong — read the CONTROL failures first.${digests}`
);
}
const emptyExtractionNote = () => formatEmptyExtractionNote(nodeTags, digestPinnedNodeRefs);
describe('node version is pinned and consistent across the repo', () => {
// ---------------------------------------------------------------------------
// Controls first. Everything below quantifies over `nodeTags`, and an empty
// list satisfies all of it — so prove the extractor found the stages, prove it
// did not MISS any, and prove it can distinguish a pinned tag from a floating
// one, before reading any verdict.
//
// The evasions these controls exist for are the ones Docker accepts and a
// pattern written by eye does not: a LOWERCASE `from node:` directive, and a
// directive with LEADING WHITESPACE. Both were verified to build, and both
// were verified to slip an unpinned stage past the previous pattern with all
// seven tests still green. (Not, as an earlier version of this comment
// claimed, a double space after `FROM` — `[ \t]+` has always handled runs of
// spaces and tabs, and citing it here sent the reader away from the two gaps
// that were actually open.)
// ---------------------------------------------------------------------------
it('CONTROL: the extractor finds every node stage in the root Dockerfile', () => {
expect(
nodeTags.length,
`Expected ${EXPECTED_NODE_STAGES} \`FROM node:\` stages in ${DOCKERFILE}, found ` +
`${nodeTags.length}. If a stage was added, raise EXPECTED_NODE_STAGES so the new ` +
`stage is covered. If this dropped to 0, FROM_NODE_TAG no longer matches and every ` +
`assertion in this file is passing over an empty list.${emptyExtractionNote()}`
).toBe(EXPECTED_NODE_STAGES);
});
it('CONTROL: every FROM directive in the root Dockerfile is accounted for', () => {
expect(
fromRefs.length,
`${DOCKERFILE} has ${fromRefs.length} \`FROM\` directives (${fromRefs.join(', ')}); this ` +
`guard was written against ${EXPECTED_FROM_DIRECTIVES}. A stage was added or removed. ` +
`This control is deliberately separate from the node-stage count: that one counts what ` +
`the extractor MATCHED, so a node stage written in a spelling the extractor cannot see ` +
`leaves it unchanged and unnoticed. This one moves for any stage in any spelling. If ` +
`the new stage is a node stage, make sure it appears in the coverage control below and ` +
`raise EXPECTED_NODE_STAGES too; then raise EXPECTED_FROM_DIRECTIVES.`
).toBe(EXPECTED_FROM_DIRECTIVES);
});
it('CONTROL: the extractor sees every node stage a dumber reader can find', () => {
// Two readers of the same file, built differently on purpose: FROM_NODE_TAG
// encodes the image reference's shape in the pattern, extractFromRefs finds
// the directive and then tokenises. When they disagree, the extractor has a
// blind spot — which is exactly how a floating stage stayed invisible.
expect(
nodeTags,
`An independent scan of ${DOCKERFILE} finds \`FROM node:\` stages the extractor did not ` +
`return. Independent scan: [${independentNodeTags.join(', ')}]. Extractor: ` +
`[${nodeTags.join(', ')}]. FROM_NODE_TAG is blind to a spelling that is in the file, so ` +
`the verdicts below are not covering every stage.`
).toEqual(independentNodeTags);
});
it('CONTROL: the extractor reads tags, and only node tags, out of a Dockerfile', () => {
// Drive the real function over a fixture rather than re-implementing its
// rules. The `FROM deps` / `FROM scratch` lines are the ones a sloppier
// pattern would sweep up; `python:3.12` is the cross-image negative.
const fixture = [
'FROM node:24.19.0-alpine3.24 AS deps',
'RUN echo not-a-from-line node:99-alpine',
'FROM deps AS builder',
'FROM scratch AS maps',
'FROM --platform=$BUILDPLATFORM node:22-alpine AS other',
'FROM python:3.12 AS unrelated',
].join('\n');
expect(extractNodeTags(fixture)).toEqual(['24.19.0-alpine3.24', '22-alpine']);
expect(extractNodeTags('FROM scratch\nFROM python:3.12\n')).toEqual([]);
});
it('REGRESSION: a LOWERCASE `from node:` stage is not invisible to the extractor', () => {
// Docker directives are case-insensitive: this builds (only a
// ConsistentInstructionCasing warning), and under the previous `/gm` pattern
// it was extracted as nothing at all — an unpinned stage, seven green tests.
expect(
extractNodeTags('from node:24-alpine3.24 AS sneaky\n'),
'a lowercase `from node:` directive builds, so it must be extracted'
).toEqual(['24-alpine3.24']);
expect(extractNodeTags('From node:24-alpine3.24 AS mixed\n')).toEqual(['24-alpine3.24']);
// …and in the position it would really appear: appended after real stages.
expect(
extractNodeTags('FROM node:24.19.0-alpine3.24 AS deps\nfrom node:24-alpine3.24 AS sneaky\n')
).toEqual(['24.19.0-alpine3.24', '24-alpine3.24']);
});
it('REGRESSION: an INDENTED `FROM node:` stage is not invisible to the extractor', () => {
// Leading whitespace before a directive is legal and builds without even a
// warning; the previous pattern anchored hard at `^FROM` and missed it.
expect(
extractNodeTags(' FROM node:24-alpine3.24 AS indented\n'),
'an indented FROM directive builds, so it must be extracted'
).toEqual(['24-alpine3.24']);
expect(extractNodeTags('\tfrom node:24-alpine3.24 AS both\n')).toEqual(['24-alpine3.24']);
expect(
extractNodeTags('FROM node:24.19.0-alpine3.24 AS deps\n\t FROM node:22-alpine AS tabbed\n')
).toEqual(['24.19.0-alpine3.24', '22-alpine']);
});
it('REGRESSION: widening for those two did not start matching non-directives', () => {
// The `i` flag and the leading-`[ \t]*` are the widenings; these are the
// lines in the real Dockerfile that sit closest to them.
expect(extractNodeTags('# FROM node:24-alpine3.24 AS commented-out\n')).toEqual([]);
expect(extractNodeTags(' # from node:24-alpine3.24\n')).toEqual([]);
expect(extractNodeTags('COPY --from=deps /app/node_modules ./node_modules\n')).toEqual([]);
expect(extractNodeTags('RUN echo "derived from node:99-alpine"\n')).toEqual([]);
// `\s` would span the newline and invent a stage that is not there.
expect(extractNodeTags('FROM\nnode:24-alpine3.24\n')).toEqual([]);
});
it('CONTROL: the failure note diagnoses an empty extraction instead of blaming the pattern', () => {
// Message text, so nothing else in this file can catch it going wrong — which
// is exactly why it gets its own assertion rather than being trusted.
expect(formatEmptyExtractionNote(['24.19.0-alpine3.24'], [])).toBe('');
expect(formatEmptyExtractionNote([], [])).toContain('returned NO node tags');
const digestNote = formatEmptyExtractionNote([], ['node@sha256:abc', 'node@sha256:def']);
expect(digestNote).toContain('2 digest-pinned node stage(s)');
expect(digestNote).toContain('STRONGER pin');
// A digest-pinned tree must not be told its pattern is broken.
expect(digestNote).toContain('not a bug in the pattern');
});
it('CONTROL: the dumb second reader finds every FROM, and only FROMs', () => {
// If this reader were blind in the same places as FROM_NODE_TAG, the
// coverage control comparing them would agree vacuously.
const fixture = [
'FROM node:24.19.0-alpine3.24 AS deps',
'from node:24-alpine3.24 AS sneaky',
' FROM scratch AS maps',
'FROM --platform=$BUILDPLATFORM node:22-alpine AS other',
'# FROM node:1-commented',
'COPY --from=builder /app/x ./x',
].join('\n');
expect(extractFromRefs(fixture)).toEqual([
'node:24.19.0-alpine3.24',
'node:24-alpine3.24',
'scratch',
'node:22-alpine',
]);
});
it('CONTROL: the pin predicate rejects the floating forms it exists to catch', () => {
// The exact tags this repo has used. `24-alpine3.24` is what was there before
// the pin, and is the mutation this whole file is meant to fail on.
expect(EXACT_PATCH_TAG.test('24.19.0-alpine3.24')).toBe(true);
expect(EXACT_PATCH_TAG.test('24.19.0')).toBe(true);
expect(EXACT_PATCH_TAG.test('24-alpine3.24')).toBe(false);
expect(EXACT_PATCH_TAG.test('24.19-alpine3.24')).toBe(false);
expect(EXACT_PATCH_TAG.test('lts-alpine')).toBe(false);
expect(EXACT_PATCH_TAG.test('latest')).toBe(false);
// The `$` anchor, which nothing else here exercises: without it the predicate
// accepts anything that merely BEGINS with a patch version.
expect(EXACT_PATCH_TAG.test('24.19.0.1')).toBe(false);
expect(EXACT_PATCH_TAG.test('24.19.0 24.20.0')).toBe(false);
});
it('CONTROL: the pin verdict picks the floating tags OUT of a mixed list', () => {
// The verdict below applies this rule to whatever the Dockerfile contains, and
// the Dockerfile does not violate it — so on live data alone the rule could be
// replaced by a constant `true` and never be noticed. Drive it over a fixture
// that DOES violate it.
expect(floatingTags(['24.19.0-alpine3.24', '24.19.0'])).toEqual([]);
expect(floatingTags(['24.19.0-alpine3.24', '24-alpine3.24', 'latest'])).toEqual([
'24-alpine3.24',
'latest',
]);
});
it('CONTROL: the engines-range predicate accepts both spellings of one ceiling, and nothing looser', () => {
expect(ENGINES_RANGE.test('>=24.0.0 <25')).toBe(true);
// Identical set of versions, so identical verdict. Rejecting this spelling
// reported a violation where there was none.
expect(ENGINES_RANGE.test('>=24.0.0 <25.0.0')).toBe(true);
// NOT identical: `<25.0.1` admits 25.0.0, a major nobody builds or tests.
expect(ENGINES_RANGE.test('>=24.0.0 <25.0.1')).toBe(false);
expect(ENGINES_RANGE.test('>=24.0.0 <25.1.0')).toBe(false);
expect(ENGINES_RANGE.test('>=24.0.0')).toBe(false);
expect(ENGINES_RANGE.test('^24.0.0')).toBe(false);
expect(ENGINES_RANGE.test('>=24 <25')).toBe(false);
});
// ---------------------------------------------------------------------------
// Verdicts.
// ---------------------------------------------------------------------------
it('every root Dockerfile node stage pins an EXACT patch version', () => {
// Assert the population FIRST. This verdict used to be a loop, and a loop over
// an empty list is green — so an extractor that returned nothing produced the
// repo's strongest-sounding pass while checking no stage at all.
expect(
nodeTags.length,
`This verdict examined NO node tags — it did not pass, it never ran. Read the ` +
`CONTROL failures above.${emptyExtractionNote()}`
).toBe(EXPECTED_NODE_STAGES);
const floating = floatingTags(nodeTags);
expect(
floating,
`${DOCKERFILE} has ${floating.map((tag) => `\`FROM node:${tag}\``).join(', ')}, which ` +
`does not pin a patch version. A tag like \`24-alpine3.24\` resolves to whatever the ` +
`newest 24.x is at BUILD time, so production's Node — and its V8 — can change with no ` +
`commit recording it, which makes an engine change indistinguishable from an ` +
`application regression. Write the full version, e.g. \`node:24.19.0-alpine3.24\`.`
).toEqual([]);
});
it('all root Dockerfile node stages use the SAME tag', () => {
expect(
[...new Set(nodeTags)],
`${DOCKERFILE} builds its stages on different node tags (${nodeTags.join(', ')}). The ` +
`build stage and the runtime stage must be the same engine, or the app is compiled ` +
`against one Node and executed on another.${emptyExtractionNote()}`
).toHaveLength(1);
});
it('.nvmrc matches the Dockerfile pin', () => {
const version = versionOf(nodeTags[0]);
expect(
nvmrc,
`.nvmrc says ${nvmrc}, the ${DOCKERFILE} base image is ${version}. CI installs the ` +
`.nvmrc version and runs the suite on it; production runs the Dockerfile version. ` +
`While these differ, the suite is testing a runtime that never ships.` +
`${emptyExtractionNote()}`
).toBe(version);
});
it('engines.node brackets exactly the major that is pinned', () => {
const pinnedMajor = majorOf(nodeTags[0]);
expect(typeof enginesNode, 'package.json has no engines.node').toBe('string');
const parsed = String(enginesNode).match(ENGINES_RANGE);
expect(
parsed,
`engines.node is "${String(enginesNode)}", which is not of the form ">=X.Y.Z <W" (the ` +
`exclusive ceiling may equivalently be written "<W.0.0"; anything else — "<W.0.1", ` +
`"<W.1.0" — denotes a DIFFERENT range that admits major W, so it is rejected on ` +
`purpose rather than on spelling). An open-ended range permits a major nobody builds, ` +
`tests or runs.`
).not.toBeNull();
const floorMajor = Number(parsed?.[1]);
const ceiling = Number(parsed?.[4]);
expect(
floorMajor,
`engines.node starts at major ${floorMajor} but the image pins ` +
`${pinnedMajor}.${emptyExtractionNote()}`
).toBe(pinnedMajor);
expect(
ceiling,
`engines.node permits up to major ${ceiling}; the only major that is built and tested ` +
`is ${pinnedMajor}, so the exclusive ceiling must be ${pinnedMajor + 1}.`
).toBe(pinnedMajor + 1);
});
});
+6 -6
View File
@@ -58,8 +58,8 @@ registerCpuProfiler();
// watches the pod's OWN event-loop lag and auto-arms V8's (separate-thread) // watches the pod's OWN event-loop lag and auto-arms V8's (separate-thread)
// sampler when lag crosses a threshold — the one mechanism that captures a 504 // sampler when lag crosses a threshold — the one mechanism that captures a 504
// wave's pin. DISARMED by default (no timer, no histogram, zero overhead) unless // wave's pin. DISARMED by default (no timer, no histogram, zero overhead) unless
// CPU_PROFILE_LAG_TRIGGER_MS is set (suggested: 1000) on the dp-prod-api // CPU_PROFILE_LAG_TRIGGER_MS is set (suggested: 1000) in the deployment
// deployment. See src/server/cpu-profiler.ts. // environment. See src/server/cpu-profiler.ts.
registerEventLoopStallProfiler(); registerEventLoopStallProfiler();
// Arm Grafana Pyroscope CONTINUOUS wall+cpu profiling. DARK by default: a // Arm Grafana Pyroscope CONTINUOUS wall+cpu profiling. DARK by default: a
@@ -90,7 +90,7 @@ registerEventLoopLongTaskDetector();
// pinned pod (loop still flushing timers) from a truly-wedged one — retiring the // pinned pod (loop still flushing timers) from a truly-wedged one — retiring the
// ~15min probe-tolerance band-aid that the httpGet `/api/live` liveness needed // ~15min probe-tolerance band-aid that the httpGet `/api/live` liveness needed
// because it's served by the same saturated loop. See liveness-heartbeat.ts and // because it's served by the same saturated loop. See liveness-heartbeat.ts and
// the liveness history in datapacket-talos deployment-api.yaml. // the liveness-probe history in the deployment manifests.
registerLivenessHeartbeat(); registerLivenessHeartbeat();
// Spawn the off-loop event-loop wedge detector. Everything else that watches the // Spawn the off-loop event-loop wedge detector. Everything else that watches the
@@ -111,9 +111,9 @@ registerEventLoopWatchdog();
// loop thread → pin → 504/502/499 on every rollout. The warmer self-requests // loop thread → pin → 504/502/499 on every rollout. The warmer self-requests
// the hot routes over localhost during startup and flips /api/ready's warm gate // the hot routes over localhost during startup and flips /api/ready's warm gate
// only once warm (fail-open). It is OPT-IN via WARMUP_ENABLED (default FALSE — // only once warm (fail-open). It is OPT-IN via WARMUP_ENABLED (default FALSE —
// runs ONLY when WARMUP_ENABLED='true', set on the dp-prod SSR/API/heavy pools; // runs ONLY when WARMUP_ENABLED='true', set on the production request-serving
// elsewhere it no-ops + flips warm immediately). It self-imports lazily so the // workloads; elsewhere it no-ops + flips warm immediately). It self-imports
// fetch/route code isn't pulled into the boot path needlessly. // lazily so the fetch/route code isn't pulled into the boot path needlessly.
// //
// CRITICAL: do NOT await this. register() must return so Next can start the // CRITICAL: do NOT await this. register() must return so Next can start the
// HTTP listener — the warmer needs that listener up to self-request, so // HTTP listener — the warmer needs that listener up to self-request, so
+9 -7
View File
@@ -13,10 +13,10 @@ import {
* event loop pins, and an httpGet liveness probe (served by that SAME loop) * event loop pins, and an httpGet liveness probe (served by that SAME loop)
* times out — so the kubelet SIGKILLs a pod that is busy-but-ALIVE, which * times out — so the kubelet SIGKILLs a pod that is busy-but-ALIVE, which
* cold-restarts it and AMPLIFIES the wave (cold-start module compilation re-pins * cold-restarts it and AMPLIFIES the wave (cold-start module compilation re-pins
* the loop → fails again → cascade). See the liveness history in * the loop → fails again → cascade). The prior mitigation just widened the probe
* datapacket-talos `deployment-api.yaml`. The prior mitigation just widened the * tolerance to ~15min — a band-aid on a signal (probe-response latency) that
* probe tolerance to ~15min — a band-aid on a signal (probe-response latency) * fundamentally can't tell "busy" from "dead"; the deployment configuration's
* that fundamentally can't tell "busy" from "dead". * own history is the record of that, and is where the probe is defined.
* *
* This writes the current epoch-SECONDS to a file every 2s, ON the event loop. * This writes the current epoch-SECONDS to a file every 2s, ON the event loop.
* A k8s EXEC liveness probe then checks the file's staleness, e.g.: * A k8s EXEC liveness probe then checks the file's staleness, e.g.:
@@ -27,8 +27,10 @@ import {
* is reaped. That is the correct "tolerate busy, detect dead" semantics, with a * is reaped. That is the correct "tolerate busy, detect dead" semantics, with a
* real loop-liveness signal instead of probe-latency tolerance tuning. * real loop-liveness signal instead of probe-latency tolerance tuning.
* *
* Epoch-SECONDS (not ms) because the runner image is node:24-alpine3.24 → busybox * Epoch-SECONDS (not ms) because the runner image is Alpine-based → busybox `date`
* `date` has no `%N`; the probe reads seconds with `date +%s`. * has no `%N`; the probe reads seconds with `date +%s`. (Deliberately not naming a
* Node tag here: the exact base image is pinned in one place, the Dockerfile, and a
* copy of it in a comment is a copy that goes stale without anything noticing.)
* *
* The same tick also stores a MILLISECOND timestamp into the event-loop watchdog's * The same tick also stores a MILLISECOND timestamp into the event-loop watchdog's
* SharedArrayBuffer, which a worker thread reads to detect a wedge off-loop. One * SharedArrayBuffer, which a worker thread reads to detect a wedge off-loop. One
@@ -57,7 +59,7 @@ export function registerLivenessHeartbeat() {
if (started) return; if (started) return;
started = true; started = true;
// Ensure the parent dir exists so the first write doesn't ENOENT. In the prod node:20-alpine pods `/tmp` // Ensure the parent dir exists so the first write doesn't ENOENT. In the Alpine runtime image `/tmp`
// already exists (mkdir recursive is a no-op), so the probe path is unchanged; on a Windows dev box // already exists (mkdir recursive is a no-op), so the probe path is unchanged; on a Windows dev box
// `/tmp/heartbeat` resolves to `C:\tmp\heartbeat`, whose parent doesn't exist — without this every 2s tick // `/tmp/heartbeat` resolves to `C:\tmp\heartbeat`, whose parent doesn't exist — without this every 2s tick
// logged an ENOENT. Best-effort: if mkdir fails, write() below still logs the persistent failure once. // logged an ENOENT. Best-effort: if mkdir fails, write() below still logs the persistent failure once.