Skip to content

feat(engines): propose runtime bumps, which were resolved and never proposed - #1428

Merged
glennmichael123 merged 2 commits into
mainfrom
feat/engine-updates
Aug 29, 2026
Merged

feat(engines): propose runtime bumps, which were resolved and never proposed#1428
glennmichael123 merged 2 commits into
mainfrom
feat/engine-updates

Conversation

@glennmichael123

Copy link
Copy Markdown
Member

extractEngines put every known runtime under engines on the dashboard. resolveEngineVersion knew which registry to ask for each. bumpEngineConstraint knew how to raise a constraint without changing its shape. Both were tested and exported from the public API.

No scan step called either. So an engine was listed and never proposed.

Had one been proposed, it would not have been written: generateAllFileUpdates rewrites the four dependency sections and nothing else, so an engines update entered that loop, logged Package node not found, and produced no change.

What's wired

packages: { engines: true }   // opt-in, default off
Scan checkEnginesForUpdates beside the Docker/Zig checks; npm engines via the registry client (shared cache), Node/Deno via GitHub releases
Constraint shape preserved at the author's precision — >=20>=22, never 22.3.0
Write applyEngineUpdates, scoped to the engines object
Config packages.engines typed, validated as a boolean, documented

Off by default because the module's own docstring already said why: "bumping it is a deployment decision". Raising node: '>=20' to '>=22' is a statement about every environment the project runs in.

Two things worth your eye

Scoping the rewrite. npm, pnpm and yarn are often both an engine and a devDependency in the same file. A rewrite matching by name alone would move the wrong one — there's a test where both are present and only the engine moves.

Precision, not satisfies. bumpEngineConstraint compares at the author's precision, so ^9.0.0 follows 9.1.0 to ^9.1.0 the way a dependency's caret would, while >=9 isn't moved by a 9.x release at all. That's the existing function's behaviour — its docstring says "null when it already admits latest", which isn't quite what it does. I kept the behaviour and made the tests state it explicitly rather than silently pick a side.

Not addressed

Node's latest release is the current line, not LTS — so >=20 is offered >=23 when 23 is current. Preferring even majors would be an opinion the resolver hasn't expressed; the opt-in makes the proposal deliberate either way. Flagging rather than deciding.

Also cosmetic: an engines row lands in the npm table with Mend badges keyed on the npm package name, which for node is a different package. Harmless, wrong-looking.

Tests

2127 → 2143.

Lint ✓ · typecheck ✓ · check:docs ✓ (222 config keys now) · 0 fail

🤖 Generated with Claude Code

https://claude.ai/code/session_01UWouahBJT3msK7V2VDYY6T

glennmichael123 and others added 2 commits August 29, 2026 02:25
…the file

`resolveDockerDigest` was written with a docstring explaining exactly why it
matters — "moving the tag without moving the digest would leave the old image
running while the file claims otherwise" — and never called. Three defects
followed, two of them silent.

Parsing split on the *last* colon, and a digest reference contributes two.
`FROM node:20@sha256:abc…` therefore parsed as the image `node:20@sha256` at
version `abc…`. That is not an image, is never found in any registry, and
appears on the dependency dashboard as though it were real. The guard meant
to catch this tested `version.startsWith('sha256:')`, which the bare hex
never does. The digest is now split off before the tag is read, and a
reference pinned by digest alone — with no tag to move — is skipped as it was
always meant to be.

Updating then dropped the pin. `[^\s]+` swallowed `20@sha256:abc…` as the
version and replaced the lot, so `FROM node:20@sha256:…` became
`FROM node:22` — silently unpinning an image somebody pinned on purpose. The
tag and digest are now captured separately and moved together, with the new
digest resolved through the OCI client. Where it cannot be resolved the line
is left alone: writing the new tag beside the old digest would produce
exactly the mismatch the original docstring warns about.

Registry credentials are threaded through, so a private image's digest is
re-resolved with the same credentials the scan used to find its tag.

The third was pre-existing and unrelated to digests. The trailing group was
`(\s.*)?$`, and `\s` matches a newline — so with the `m` flag the match ran
past the end of its line and took every following line with it. A
multi-stage Dockerfile building from the same image twice had both `FROM`
lines collapsed into one, deleting a build stage. Horizontal whitespace only
now, with a regression test that reads the surviving `FROM` lines back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWouahBJT3msK7V2VDYY6T
…roposed

`extractEngines` put every known runtime under `engines` on the dashboard.
`resolveEngineVersion` knew which registry to ask for each — GitHub for
Node and Deno, npm for the package managers — and `bumpEngineConstraint`
knew how to raise a constraint without changing its shape. Both were tested
and exported from the public API. No scan step called either, so an engine
was listed and never proposed.

Had one been proposed, it would not have been written. `generateAllFileUpdates`
rewrites the four dependency sections and nothing else, so an `engines`
update entered that loop, logged "Package node not found", and produced no
change.

Opt-in via `packages.engines: true`, off by default, because the module's
own docstring already said why: an engine constraint is a deployment
decision rather than a dependency one. Raising `node: '>=20'` to `'>=22'`
is a statement about every environment the project runs in.

The constraint keeps its shape at the author's precision — `>=20` becomes
`>=22`, never `22.3.0`, which would break every contributor on a different
patch. `bumpEngineConstraint` compares at that precision rather than by
`satisfies`, so `^9.0.0` follows 9.1.0 to `^9.1.0` the way a dependency's
caret range would, while `>=9` is not moved by a 9.x release at all. That is
the existing function's behaviour, and the tests now say so explicitly.

Rewriting is scoped to the `engines` object. `npm`, `pnpm` and `yarn` are
often both an engine and a devDependency in the same file, and a rewrite
that matched by name alone would move the wrong one.

The GitHub lookup reuses `fetchLatestActionVersion`: it takes `owner/repo`
and returns the latest release tag, which is exactly the shape a runtime's
GitHub releases have. The npm lookup is the instance's registry client, so
it shares the scan's cache.

Not addressed: Node's latest release is the current line, not the LTS one,
so `>=20` is offered `>=23` when 23 is current. Preferring even majors for
Node would be an opinion the resolver has not expressed, and the opt-in
makes the proposal deliberate either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWouahBJT3msK7V2VDYY6T
@glennmichael123
glennmichael123 merged commit 12a063d into main Aug 29, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant