As part of future changes, we're introducing a means to parse
Dockerfiles for any `apk add` commands, so we can propose updates to any
packages using the APK datasource.
Before we do this, we need to correct a couple of bugs in our
implementation of the APK versioning scheme, so we can handle i.e.
apk add curl=~8.12.1
When the `apk` CLI builds a version constraint, it `OR`s one flag per
operator character[0].
For instance, `~` sets both "fuzzy" and "equal".
These operator characters may be in any order and may repeat, so `~`,
`=~` and `~=` are all the same operator, and `>>` and `>` are the same.
To correct our parsing, we should take this into account, rather than
using known patterns.
When we perform a fuzzy match, this is performed over version _tokens_
not the version itself, which means that:
- `~1.6` matches `1.6`, `1.6.0_pre1`, `1.6.0`, `1.6.5` and `1.6.9_p1`
- `~1.6` does not match `1.60`
- `~8.12.1` matches every `8.12.1-rN` i.e. `8.12.1-r1` and `8.12.1-r100`
While doing this, we also fix related issues:
- `~` previously treated this as an npm-style tilde ("same major and
minor, and greater than or equal"),
- admitted fuzzy versions that `apk` rejects - `~2.39.0-r0` should not have
matched `2.39.1-r0`
- rejected fuzzy versions that `apk` accepts - `~2.39.0` should have
matched `2.39.0-r1`
- `=~` and `~=` were not allowed
- `isSingleVersion('=~1.2.3')` was true, so a range looked like a pin
- `isLessThanRange` compared against the raw range, so `_parse` failed on
the operator and it always returned false
- `minSatisfyingVersion` was inherited, so it only ever found an exact
match
- `getNewValue` dropped the operator entirely, rewriting `~8.12.1` to a
bare version
We also set `supportsRanges`, which we'd forgotten to set despite `~`
and `>=` being ranges, and note that we support `pin`, `replace` and
`bump` strategies.
When using `getNewValue`, we now keep the precision the constraint was
written with, as otherwise we'd quietly narrow what the user asked for:
- `~8.12.1` becomes `~8.13.0` (previously `~8.13.0-r0`)
- `~8.12` becomes `~8.13`
- `~8.12.1-r0` becomes `~8.13.0-r2` (with a new revision number)
For `bump`, we raise a lower bound which includes its boundary, as with
`npm` and `pep440`, so:
- `>=8.12.1` becomes `>=8.13.0`
- `>~8.12` becomes `>~8.13`
Otherwise we return `null`, logging which constraint we couldn't handle
in the `bump` case, as `npm` does:
- `>` on its own can't be bumped, as `>8.13.0` would exclude the version
we bump to
- `<` and `<=` are upper bounds, so there's no lower bound to raise
- for `replace`, there's no single obvious new bound for `<`, `<=`, `>`,
`>=`, `>~` or `<~`, and a bound which already admits the new version
doesn't need one
- a prefix constraint whose version doesn't parse, i.e. `~foo`, which
would otherwise throw
`><` constrains a package to an identity hash rather than to a version,
so it's rejected outright.
Additionally, `isLessThanRange` and `matches` weren't agreeing on how to
resolve a fuzzy constraint.
For instance, `_compare` ranks `2.39.0_git20240101` below `2.39.0`, but
`~2.39.0` matches it, this would lead to a rollback of sorts.
Instead, we can ignore the `_git` and `_p` suffixes, which are common in
Alpine and Wolfi packages.
[0]: https://gitlab.alpinelinux.org/alpine/apk-tools/-/blob/44dcdfc255c26a4e19a4a27f66a54169cce5ca72/src/version.c
Co-authored-by: Claude Opus 5 <jamie.tanna+claude-code@mend.io>
Mend's GitLab.com support has been around for a little bit, but we've
not explicitly documented it.
While we're doing this, we should make sure the outdated references to
GitLab security is updated in line with the current state of affairs.
With some additional research from Claude Opus 5, but with Claude Sonnet
5 driving the changes.
We can also make sure to clarify that folks may want to use Mend-hosted
against GitLab.com, and that there are security implications when
running Renovate on GitLab (more generally).
Researched-by: Claude Opus 5 <jamie.tanna+claude-code@mend.io>
Co-authored-by: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io>
* fix(manager/npm): pass --no-frozen-lockfile to pnpm lock file updates
pnpm applies an explicitly-set frozenLockfile: true from
pnpm-workspace.yaml even to 'pnpm install --lockfile-only', so lock file
regeneration always fails with ERR_PNPM_OUTDATED_LOCKFILE in repos that
set it: pnpm's CI frozen default has a --lockfile-only carve-out, but
explicitly-set values skip that carve-out, and only CLI flags take
precedence over pnpm-workspace.yaml settings. 'pnpm update' is not
affected by frozen mode (and rejects the flag), so only the install
command gets it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: less verbose comment
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The internal preset validation ran `validateConfig()` with `isPreset`,
which allows selectors such as `matchManagers` at the top level of a
preset. That is how #39738 shipped `helpers:goXPackages*` with a bare
`matchManagers` and broke `inheritConfig` users: the real resolution
paths validate the resolved config without that leniency.
Resolve each preset through `extends`, as a repository or inherited
config would, and validate the result without `isPreset`. `packages:`
and `monorepo:` presets are selector-only `packageRules` fragments, so
they are resolved from within a `packageRules` entry instead.
* support AWS S3 as a chart repository
* support creds via hostRules
* apply suggestions
* fix(datasource/helm): classify s3 lookup errors
* test: use await expect().resolves
* fix(datasource/aws-machine-image): return all AMI releases for minimumReleaseAge filtering
Previously, _getReleases only returned the latest image in the releases array,
which broke minimumReleaseAge filtering. When the latest AMI was too new,
there were no older candidates to fall back to.
Fixed by:
- _getReleases now returns all matching AMIs as separate releases (sorted oldest→newest)
- _getDigest simplified to read directly from the images array to ensure latest digest is still returned
- Added reproducer test verifying all 3 images are returned
This aligns with how other datasources (npm, PyPI, etc.) work — they return
complete version history to allow minimumReleaseAge filtering to find an
appropriately-aged candidate.
Co-authored-by: Claude Code <claude@anthropic.com>
* chore: apply suggestion
* fixup! chore: apply suggestion
---------
Co-authored-by: Claude Code <claude@anthropic.com>
* fix(deno): only collect workspace members when workspaces are declared
A package.json with no workspaces field has no members to find, but the member
scan ran anyway, and find-packages with no patterns returns every package.json
in the tree. Standalone subprojects were picked up as members of the root lock,
which is what breaks lockFileMaintenance.
The scan now runs only when workspaces are declared.
* test(deno): use await expect().resolves
Required by the `vitest(prefer-expect-resolves)` oxlint rule enabled on
main since this branch was created.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Michael Kriese <michael.kriese@mend.io>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
When we introduced the `apk` datasource, we missed that the default for
the Datasource was the `loose` versioning, which doesn't make as much
sense as `apk`.
This may be a small breaking change for users who are working with the
APK datasource already, but this is a minimal enough change that this is
fine, and can be restored with a Package Rule.
Co-authored-by: Claude Opus 5 <jamie.tanna+claude-code@mend.io>
fix(manager/dockerfile): only relabel last `stage` as `final`
If using a Docker image that only declares a `syntax`, with no base
image, we would incorrectly set the `depType: final`, when this isn't
actually an image.
Instead, we should make sure that we find the last `stage` dependency,
and then override that.
Co-authored-by: Claude Opus 5 <jamie.tanna+claude-code@mend.io>
* fix(docker): adjust tag caching for private registries
Tag caching for private registries currently has two issues, observed in
issues #45249 and #40734:
1. `getTags` has no `cacheable` guard (unlike `getReleases`)
2. `getTags` caches `null` tag results
These two combined mean that regardless of `cachePrivatePackages`
settings, private registry tag lookups which fail due to transient/auth
errors are cached. This results in the cache being "poisoned" by
misconfigured jobs, with well-configured jobs unable to recover. This is
especially relevant for the hosted app where caching is shared by
different tenants.
This commit:
1. Aligns `getTags` caching with `getReleases`, ensuring caching
behaviour is driven by `cachePrivatePackages`.
2. Prevents `null` tag results being cached, allowing for recovery
regardless of `cachePrivatePackages` configuration.
Note that I have _not_ applied (2) to `getReleases`, because
1. `getReleases` has a slightly different contract in that it sets
`fallback: true` to intentionally cache failures
2. I have not observed any actual failures in `getReleases` due to (20,
so it hasn't been empirically proven as requiring a fix
3. `getTags` is internal so is a lower-risk change
Fixes
- #45249
Relates to
- #45249
- #40734
* refactor(docker): undefined for no-auth
Previously `getAuthHeaders()` used `null` as an auth-failed sentinel,
which was inconsistent with `withCache()`, and led to the original "fix"
where nulls were explicitly omitted from the cache.
Instead this implements the suggestion from https://github.com/renovatebot/renovate/pull/45348#issuecomment-5340524967
to uniformly return undefined for `getTags` auth errors, which allows
the cache to recognise failures itself. Then cacheability ends up being
conditional on dockerhub as always.
This also rectifies a mismatch between auth-failure signals, where other
functions like `_getImageConfig` were returning `undefined` when headers
were unavailable.
IMPORTANT: once this lands, since there aren't any cache invalidations,
the existing issue will still be observed until the cache TTL expires
and old `null` entries are evicted.
* fix: whoops, sort out the vacuous tests
I failed to realise caching isn't automatically orchestrated in these
tests. As an eagle-eyed observer [pointed out][1], this meant the tests
weren't actually exercising anything.
This commit fixes that by adding a new test block which does the cache
orchestration using a temp dir cache. I've also opted to add assertions
against the mock server's http trace counter directly to ensure the mock
really is/isn't being hit. This should also serve to make the tests fail
fast if the underlying impl flow (auth probe -> get tags) is adjusted in
the future.
I've also brought in some caching dependencies into the test file, but
those are used elsewhere too (e.g. `withCache` tests).
This time, I have actually verified the tests which we'd expect to fail
do fail against main. Note that the first 'caches tags for Docker Hub'
is expected to pass on both branches (the control test).
```
❯ getTags caching (3)
✓ caches tags for Docker Hub 21ms 249 MB heap used
× does not cache tags for a private registry 20ms 212 MB heap used
× does not cache failed lookups when cachePrivatePackages is enabled 15ms 215 MB heap used
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
FAIL lib/modules/datasource/docker/index.spec.ts > modules/datasource/docker/index > getTags caching > does not cache tags for a private registry
AssertionError: expected [ '1.0.0' ] to deeply equal [ '1.0.0', '2.0.0' ]
- Expected
+ Received
[
"1.0.0",
- "2.0.0",
]
❯ lib/modules/datasource/docker/index.spec.ts:3694:72
3692| expect(httpMock.getTrace()).toHaveLength(2);
3693|
3694| expect(await ds.getTags('https://registry.company.com', 'node')).toEqual([
| ^
3695| '1.0.0',
3696| '2.0.0',
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/4]⎯
FAIL lib/modules/datasource/docker/index.spec.ts > modules/datasource/docker/index > getTags caching > does not cache tags for a private registry
Error: *** Unused HTTP mocks ***
- GET https://registry.company.com/v2/node/tags/list
Requests done:
- GET https://registry.company.com/v2/node/tags/list?n=10000 [200]
- GET https://registry.company.com/v2/node/tags/list?n=10000 [200]
---
Renovate testing strategy requires that every HTTP request
has a corresponding mock.
This error occurs because some of the created mocks are unused.
In most cases, you simply need to remove them.
+++
❯ <test> lib/modules/datasource/docker/index.spec.ts:3675:9
3673| });
3674|
3675| it('does not cache tags for a private registry', async () => {
| ^
3676| httpMock
3677| .scope('https://registry.company.com/v2')
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/4]⎯
FAIL lib/modules/datasource/docker/index.spec.ts > modules/datasource/docker/index > getTags caching > does not cache failed lookups when cachePrivatePackages is enabled
AssertionError: expected null to be undefined
- Expected:
undefined
+ Received:
null
❯ lib/modules/datasource/docker/index.spec.ts:3717:9
3715| expect(
3716| await ds.getTags('https://registry.company.com', 'node'),
3717| ).toBeUndefined();
| ^
3718| expect(httpMock.getTrace()).toHaveLength(1);
3719|
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/4]⎯
FAIL lib/modules/datasource/docker/index.spec.ts > modules/datasource/docker/index > getTags caching > does not cache failed lookups when cachePrivatePackages is enabled
Error: *** Unused HTTP mocks ***
- GET https://registry.company.com/v2/node/tags/list
Requests done:
- GET https://registry.company.com/v2/node/tags/list?n=10000 [403]
---
Renovate testing strategy requires that every HTTP request
has a corresponding mock.
This error occurs because some of the created mocks are unused.
In most cases, you simply need to remove them.
+++
❯ <test> lib/modules/datasource/docker/index.spec.ts:3701:9
3699| });
3700|
3701| it('does not cache failed lookups when cachePrivatePackages is enabled', async () => {
| ^
3702| GlobalConfig.set({ cachePrivatePackages: true });
3703|
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[4/4]⎯
Test Files 1 failed (1)
Tests 2 failed | 313 passed (315)
```
[1]: https://github.com/renovatebot/renovate/pull/45348#pullrequestreview-5092478059
* oxlint-fix
---------
Co-authored-by: Jamie Tanna <jamie.tanna@mend.io>
* feat(data): automatic update of static data
* test(manager/mise): drop stale cargo-backend registry test
The mise registry now lists a github mirror for magika alongside its
cargo backend, so a bare `magika` entry resolves via the
github-prioritised branch (already covered by the
'prioritising the github backend over others' test) instead of cargo.
No tool in the current registry data reaches the cargo case through
the unprefixed lookup path anymore, and the cargo backend itself
stays covered by the explicit `cargo:` prefix test.
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: secustor <sebastian@poxhofer.at>
For organisations who wish to ensure that their repositories have no
configuration validation warnings (by forcing them to be errors), we can
make this inherited configuration that can run separate to a global
self-hosted administrator's choice.
As this slipped past our code review, but `matchPaths` has been
deprecated for ~3 years, replaced by `matchFileNames`.
Co-authored-by: Claude Opus 5 <jamie.tanna+claude-code@mend.io>
For instance, when a repository is onboarding with many dependency
updates (some of which have many security releases), it would be
useful to let the user know that they're not unlimited, but that the
rate limit is taken into account.
Now we have the capability to specify these, we should:
- take them into account when determining how many PR(s) may be raised
at a given time
- point users to the documentation when they may want to limit them, if
the number of security PRs is more than the
`prConcurrentLimit`/`branchConcurrentLimit`
Co-authored-by: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io>
* feat(datasource): add APK datasource support
* remove missing manager link for now
* lint docs
* update tests
* replace binary fixtures
* fix coverage
* Update lib/modules/datasource/apk/readme.md
Co-authored-by: Jamie Tanna <github@jamietanna.co.uk>
* Apply suggestion from @viceice
Co-authored-by: Michael Kriese <michael.kriese@gmx.de>
* use tar instead of tar-stream
* use codeBlock
* add ignore back
* make method private
* use an async extract instead of list
* add usage example
* stream apkindex download and parse line-by-line
* refactor(datasource): address review comments on APK datasource
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: sync pnpm-lock.yaml after merge
Co-authored-by: Cursor <cursoragent@cursor.com>
* test: remove ignored lines
As they seem to be covered, anyway.
* fixup! Merge remote-tracking branch 'origin/HEAD' into apk-datasource
* fixup! fixup! Merge remote-tracking branch 'origin/HEAD' into apk-datasource
* lint: use `node:` prefix
* Update lib/modules/datasource/apk/index.ts
Co-authored-by: Michael Kriese <michael.kriese@gmx.de>
* refactor(datasource/apk): use asTimestamp millis instead of new Date
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(datasource/apk): rename uncached method to _getPackages
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(datasource/apk): encode registry URL parts as query parameters
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(datasource/apk): reject invalid registry URLs, skip caching empty indexes
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(datasource/apk): throw on unparseable registry URL and drop unset sourceUrl support
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(datasource/apk): show how registry URL parameters build the index URLs
Co-authored-by: Cursor <cursoragent@cursor.com>
* Apply suggestions from code review
Co-authored-by: RahulGautamSingh <rahultesnik@gmail.com>
* fix(datasource/apk): keep credentials and port in component URLs
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(datasource/apk): extract APKINDEX by basename and key packages by name
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(datasource/apk): report a version served by two components only once
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Jamie Tanna <github@jamietanna.co.uk>
Co-authored-by: Michael Kriese <michael.kriese@gmx.de>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Jamie Tanna <jamie.tanna@mend.io>
Co-authored-by: RahulGautamSingh <rahultesnik@gmail.com>
* fix(config/validation): allow only capturing `currentDigest` in Custom Managers
As reported in #45665, it was previously possible - through a preset
prior to 44.60.1 - to specify a Custom Manager (in this case, with
Regex) that only captured the `currentDigest` for a dependency.
This causes config validation in a repository, but as it appears to be
valid - with `currentValue` defaulting to `latest` - we should make it
possible to use either `currentValue` or `currentDigest`.
* fixup! fix(config/validation): allow only capturing `currentDigest` in Custom Managers
When a datasource uses the 'merge' registryStrategy (e.g. maven),
mergeRegistries() tags each release with the registryUrl of the repo
that actually reported it. postprocessRelease() ignored this and used
the first configured/default registryUrl instead, causing the Maven
datasource to probe the wrong repository for release verification.
If the artifact didn't exist in that first repo, the release was
wrongly rejected and no update was proposed, even though a valid
release existed in another configured repository.
* test: reduce oversized expectation literals to representative examples
* test: shrink oversized fixtures instead of trimming assertions
Cut the real-world fixtures down to a representative set of entries that still covers every parser shape, and assert the complete output again with a single toEqual.
* test(core): replace file snapshots with explicit assertions
* test(core): replace inline snapshots with explicit assertions
* test(core): assert on relevant sections instead of full bodies
* test(core): drop assertions implied by full equality
* test(core): address review feedback
* test(core): use resolves instead of awaiting inside expect
* fix(bump-versions): collect bumpVersions from all upgrades, not just upgrades[0]
* chore: add test for duplicate bumpversion configs
* chore: use suggested safeStringify
* chore: use coerceArray
* test: validate minimumReleaseAge does not apply to security updates
* test: add end-to-end coverage for security-update minimumReleaseAge bypass
Drive the real vulnerability flow (appendVulnerabilityPackageRules) through the
real lookupUpdates, instead of only simulating the generated packageRule. Also
reword the inline comment and use luxon/asTimestamp for timestamps.
* test: fix type-check on e2e lookup config
Cast the spread config to satisfy partial() overload resolution, matching the
existing `getConfig() as never` idiom in the lookup spec.
* test: drop redundant inline lookup test in favor of end-to-end coverage
The simulated lookup test hand-built the security packageRule; the end-to-end
test plus the generated-rule assertion in vulnerabilities.spec.ts cover the same
behavior against the real flow, so the inline test added no unique coverage.
* test(vulnerability): align lookup config types
As per #45568 the OpenSSF Security Scorecard project is moving from the
Google Container Registry (GCR) to the GitHub Container Registry
(GHCR)[0].
This may not work if users are on 5.5.0, as [0] notes that it already
existed prior to the migration, but otherwise we shouldn't need to
specify a version, given it's a like-for-like replacement.
[0]: https://github.com/ossf/scorecard/issues/5208
Co-authored-by: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io>