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, which is worse than not updating at all.
— and never called. Three defects followed, two of them silent.
1. A pinned reference parsed into nonsense
Parsing split on the last colon, and a digest contributes two:
FROM node:20@sha256:aaaa… → name: "node:20@sha256" version: "aaaa…"Not an image, never found in any registry, and it appears on the dependency dashboard as though 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. A reference pinned by digest alone, with no tag to move, is skipped as it was always meant to be.
2. Updating dropped the pin
[^\s]+ swallowed 20@sha256:aaaa… as the version and replaced the lot:
- FROM node:20@sha256:aaaa…
+ FROM node:22Silently unpinning an image somebody pinned on purpose. Tag and digest are now captured separately and moved together, with the new digest resolved through the OCI client. Where it can't be resolved the line is left alone — writing the new tag beside the old digest produces exactly the mismatch the 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.
3. The regex ate the rest of the file
Pre-existing, unrelated to digests, and found on the way. The trailing group was (\s.*)?$ — and \s matches a newline. With the m flag the match ran past the end of its line:
{"full":"FROM node:20@sha256:aaaa\nFROM node:20 as tools", "rest":"\nFROM node:20 as tools"}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.
Tests
2115 → 2127. Two of these I found by writing the test and watching it fail against my own first fix — the digest-only case and the line-spanning one.
Lint ✓ · typecheck ✓ · check:docs ✓ · 0 fail
🤖 Generated with Claude Code