* feat(datasource): add GraalVM version datasource with support for JDK and JRE releases
* feat(datasource): add GraalVM version datasource with support for JDK and JRE releases
* feat(datasource): add GraalVM version datasource with support for JDK and JRE releases
* feat(datasource): add GraalVM version datasource with support for JDK and JRE releases
* feat(datasource): add GraalVM version datasource with support for JDK and JRE releases
* add coverage
* fix
* fix
* fix
* code cov
* code cov
* code cov
* refactor(datasource): integrate GraalVM into java-version datasource
Merges graalvm-version datasource into java-version as a unified multi-vendor
Java distribution datasource. Implements vendor-based modular architecture with
routing layer for Adoptium (Eclipse Temurin) and Oracle GraalVM.
Architecture:
- Vendor-specific handlers (adoptium.ts, graalvm.ts) with routing in index.ts
- Vendor-aware system detection with different arch/os mappings per vendor
- Custom registry support for GraalVM's mise-java API
- Adoptium remains the default registry for backwards compatibility
Package names:
- New explicit: adoptium-jdk, adoptium-jre, oracle-graalvm-jdk, oracle-graalvm-jre
- Legacy (map to Adoptium): java, java-jdk, java-jre
Changes:
- Created adoptium.ts and graalvm.ts vendor handlers
- Enhanced common.ts with vendor detection and vendor-specific system mappings
- Updated index.ts with routing layer and custom registry support
- Merged test suites with comprehensive coverage for both vendors
- Updated mise manager integration to use JavaVersionDatasource
- Removed graalvm-version datasource directory
All 65 datasource tests and 93 mise manager tests passing.
* fix
* test(datasource): add coverage for non-HttpError paths in java-version
* fix(datasource): remove dead code and add coverage for java-version
Remove unreachable HttpError handling from _getReleases catch block
since both getAdoptiumReleases and getGraalvmReleases handle HttpError
internally. Add test for the graalvmRegistryUrl fallback when
registryUrl is undefined.
* fix(datasource): address review comments for GraalVM datasource
- Remove unused 'datasource-graalvm-version' cache namespace
- Convert MiseJavaRelease interface to Zod schema and derive type from it
- Use http.getJson() with the Zod schema in getGraalvmReleases instead
of getJsonUnchecked, letting the schema handle invalid/empty responses
- Update getGraalvmReleases test mock to use getJson
* docs: remove unused datasource-graalvm-version from cacheTtlOverride docs
* fix(datasource): preserve Adoptium config typing
* fix(datasource): harden GraalVM release parsing
* fix(datasource): address GraalVM review feedback
* test: cover GraalVM mise package mapping
* test(datasource): cover default GraalVM registry fallback
* test(datasource): cover legacy registry resolution
* test(datasource): type legacy datasource mocks
* fix(datasource): harden GraalVM defaults and responses
* refactor(datasource): use function-only registry accessors
* test(datasource): cover registry accessor functions
* test: restore coverage exclusions
* chore: retrigger CI checks
* test(versioning): cover four uncovered guards
- `cargo`: a converted range whose operators are already attached to
their versions, so `npm2cargo` leaves the elements alone
- `hashicorp`: a widen on a complex range ending in `>=`, which the
underlying semver implementation refuses and returns null for
- `poetry`: a version list holding an unparseable entry, for both
`getSatisfyingVersion` and `minSatisfyingVersion`, plus the same widen
refusal, which falls back to the current value
- `swift`: an open-ended range whose bound is not a valid semver
Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* refactor(versioning): drop two guards that cannot be false
- `hermit`: `major` is the only mandatory group in the version regex, so
the `if (major)` around the first release segment always holds; the
sibling parser already pushes it unconditionally
- `rpm`: after `c1 === c2` is ruled out, one of `c1 > c2` and `c1 < c2`
must hold, so the second comparison becomes a ternary
Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* test(versioning/gradle): cover three uncovered guards
- `getMajor` on a version whose leading token is not a number
- `getSatisfyingVersion` with a later match that is not greater than the
one already held
- `minSatisfyingVersion` with a later match that is not smaller than the
one already held
Two branches in `compare.ts` are still open and are looked at next.
Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* test(versioning/ivy): cover a later match that is not the greatest
`getSatisfyingVersion` kept every matching version it walked past, so the
guard that holds on to the one already found was never taken.
Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* refactor(versioning): drop three guards that cannot be false
- `gradle`: `tokenCmp` compares two tokens whose only types are `Number`
and `String`, so once both-numbers and both-strings are handled
exactly one side is a number; the chain becomes a ternary and the
unreachable `return 0` goes
- `ivy`: `tokenize` always yields at least one token for a non-empty
version, so the length check around the last one always holds
- `ivy`: `rangeToStr` only returns null for a null range, which the
`length === 1` check above it already rules out
Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* test(versioning/nuget): cover a later match that is not the greatest
Both loops in `getSatisfyingVersion` walked version lists that only ever
climbed, so the guard that holds on to the candidate already found was
never taken -- once for a bracket range, once for a bare version acting
as a minimum.
Also pin down that a floating range needs a major component: `-*` alone
does not parse.
Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* refactor(versioning): drop three more guards that cannot be false
- `maven`: the interval picked for auto-extension is only picked when the
new value is at or past its upper bound, and that upper bound is the
lower one incremented, so the new value never sorts below the lower
bound
- `maven`: an interval never has both bounds open, because the parser
only nulls one side when the other is a version
- `nuget`: a floating range always carries a major component, so neither
the parser's NaN check nor the serializer's undefined check can fail
Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* refactor(versioning/pep440): drop three unreachable range paths
`updateRangeValue` handles `!=`, a `.*` prefix, `~=`, `==` and `<=`
itself, `handleUpperBound` handles `<`, and `parseCurrentRange` rejects
`===`. Only `>` and `>=` can reach `handleLowerBound`, so its operator
check always holds and it never returns null. Making it total removes
the null check at the call site as well.
This does drop the `logger.error` that followed, which was guarded by a
comment saying it could not happen unless pep440 itself changed.
`isLessThanRange` classifies the same fixed operator set, so the last
check there is the only one left and its fallback was dead.
`divideCompatibleReleaseRange` only ever sees a `~=` version, and pep440
requires at least two release components after that operator, so there
is always a component to drop. A new test pins down that `~=7` is not a
valid range.
Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* test(versioning): cover three more uncovered guards
- `elm`: a widen where the new version sits below the lower bound, so
the upper bound is kept rather than extended
- `hex`: a widen on a complex range ending in `>=`, which the underlying
semver implementation refuses and returns null for
- `rust-release-channel`: a concrete current version paired with a
channel name as the new version, which falls through to the new
version unchanged
Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* refactor(versioning): drop two more guards that cannot be false
- `composer`: `getNewValue` falls back to the new version whenever
nothing else matched, so it never returns null and the recursive call
in the or-range path always has a value to work with
- `docker`: a version's prerelease is always a string, so once an empty
one on either side is handled, the remaining pair is both non-empty
and unequal
Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* test(config,datasource): cover two more uncovered guards
- `config/validation`: `hostRules[].allowInternal` nested inside a
`packageRules[]` entry, which reports a plain configuration error
rather than a security one, because nested host rules are never applied
- `datasource/docker`: a non-list manifest whose response carries no
`docker-content-digest` header, so the digest falls back to the hash of
the manifest body
Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* refactor(config,datasource): unwind three branches v8 could not count
- `config/decrypt` and `datasource/docker/common` each picked a response
with an `await` inside a ternary. v8 gives the branch that follows such
a ternary a negative hit count, which the reporters read as uncovered
even though both paths run. Spelling the choice as an if/else makes the
count come out right; behaviour is unchanged.
- `datasource/index` and `datasource/npm/npmrc` each ended an exhaustive
chain with an `else if` whose implicit else v8 counts but nothing can
reach: `registryStrategy` is one of three literals, and `npmrc` starts
as an object and is only ever reassigned to one.
Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* test(workers/global): cover seven uncovered guards
The config parsers rewrite legacy boolean spellings of `dryRun`,
`requireConfig` and `platformCommit`, but nothing passed a value that was
already current, so the fall-through of each chain went untaken. Cover it
from both the CLI and the environment.
Also cover a datasource env var that names no auth field, one whose value
is undefined, a default config file that cannot be read, and the error
`isLimitReached` throws when asked for a concurrent limit without a
branch config. That last one had an ignore hint saying it should not
happen; the error message documents the contract, so it is now tested and
the hint is gone.
Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* refactor(workers/global): drop a guard that cannot be false
`getConfig` in the additional-config-file parser returns early unless
`RENOVATE_ADDITIONAL_CONFIG_FILE` is set, so by the time a parse error is
being classified the variable is always present.
Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* refactor(workers/repository): drop a dead changelog file lookup
The gitea, gitlab and forgejo changelog sources each declared `files` as
an empty array and then guarded the only assignment to it with
`if (!files.length)`, which is always true one line after the
declaration. Collapse it to the filter itself.
Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* ci: ratchet the branch coverage threshold
A full run on this branch reports 100% statements (49471/49471), 100%
functions (7381/7381), 100% lines (48961/48961) and 99.73% branches
(25690/25759), up from 99.53% before this branch's work. The threshold
moves to 99.71, keeping the same rounding-down margin as before.
Statements, functions and lines are already pinned at 100 and stay
there.
Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* refactor(config,datasource): say why the ternary was unwound
Both sites now carry a comment explaining that the if/else is deliberate:
v8 gives the branch following an `await` inside a ternary a negative hit
count, which the coverage reporters read as uncovered.
`apiCheckResponse` also gets an explicit `HttpResponse<unknown>` type,
which is what the two branches have in common and what
`isECRMaxResultsResponse` already expects.
Also fix a type error in the new config file spec: the `pathExists` spy
needs the same `as never` cast the surrounding tests use.
Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* refactor(versioning/pep440): use a template literal in handleLowerBound
The exclude-style return is the one line this branch rewrote in that
function, so it now builds its string with a template literal instead of
the `+` operator.
Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* test(workers/global): drop the cast from the limits throw test
Calling the overload with a non-null assertion on the config argument
says the same thing without retyping the whole function.
Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io>
---------
Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* test(versioning/aws-machine-image): cover the empty version guard
`_parse()` returns null for a falsy version before it tries the AMI id
regex, and no test passed one. Checking that an empty string is not
valid closes the only branch the module was missing.
Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* test(datasource/buildpacks-registry): cover a response without a homepage
`latest.homepage` is optional in the registry response, but every test
supplied one, so the release result was never built without it. A
buildpack whose latest entry carries no homepage now checks that the
releases still come back and no source url is derived.
Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* test(datasource): cover optional-field branches in four datasources
Each of these datasources builds its release result by attaching fields
only when the registry supplies them, and the "not supplied" path was
never exercised:
- cdnjs: a library with neither homepage nor repository
- cpan: a search result where no release is marked latest, so no tag
- dart: a latest release carrying no pubspec
- ruby-version: a release table row whose first column is not a version
Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* test(datasource): cover missing-metadata branches in hex and pypi
Two more datasources only attached fields when the registry supplied
them, and the absent case was untested: a hex package with neither an
html_url nor a github link in its meta, and a pypi response carrying no
releases at all.
Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* test(datasource): cover galaxy and repology fallbacks, mark unreachable guards
Adds a galaxy role with empty github fields, so no source url is
derived, and a repology lookup where the api fallback after a 403 finds
no matching package.
Marks four guards that are not worth a test: cpan and dart both check a
variable that is only null when the request threw, and
`handleGenericErrors()` always rethrows; conan's registry url is always
supplied by the datasource layer; and repology's resolver either returns
a body or throws.
Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* test(datasource): cover pod CDN branches, mark the sbt listing guards
Adds two pod lookups: an empty CDN body, and a CDN index that lists no
matching pod.
Marks the guards whose tests would need multi-level directory listing
fixtures: both sbt datasources walk nested index pages, and pod's github
helper checks a body that an empty json response never produces because
parsing throws first.
Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* test(datasource): cover maven metadata without a latest tag
Adds maven metadata carrying a release but no latest, so respectLatest
is left alone. Marks the three deb guards, which would each need a
crafted package index: a block missing a required key, a repeated
package in the final block, and two component indexes whose meta agrees.
Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* test(datasource): cover npmrc registry fallback, mark cache-bound guards
Adds a resolveRegistryUrl lookup for a package no npmrc rule matches.
Marks the guards whose tests would need a primed cache or a specific
blob shape: nuget's services index, deno's release cache, the docker hub
cache window, docker's manifest and config handling, go's pseudo-version
fallback, and both rubygems cache layers.
Three implicit elses are left counted: a coverage-ignore hint does not
attach to an `else if`, so the registry strategy chain, the npmrc reset
and docker's digest-header branch carry a plain note instead.
Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io>
* fix(datasource/repology): pass the mock body as a string
`ResponseMock.body` is typed as a string, and every other call in the
spec passes `'[]'`. The new api fallback test passed an array literal,
which tsc rejected.
Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io>
---------
Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io>
To harden Renovate from being able to access internal hosts, we can add
a level of enforcement on all outgoing requests.
We can split this into two types of traffic:
- where the response becomes configuration (i.e. `extends`)
- where the response does not become configuration (i.e.
`customDatasources`)
To harden this, we enforce on every outbound HTTP request:
- check the resolved URL before anything, including cache lookups, can
proceed
- catching IP-literal hosts in every spelling the WHATWG URL parser normalizes
- blocking known Cloud metadata IPs, too
- a `dnsLookup` wrapper validates every address a hostname resolves to,
on every new connection, which also defeats DNS rebinding
- a `beforeRedirect` hook re-runs the URL check on each redirect
target, with the new host getting its own grant lookup rather than
inheriting the original host's verdict
We default this enforcement to `warn`, to not break existing deployments
and instead give them visibility into required configuration before a
future major version (major+1) enables `block` by default.
We'll also remove allow in a future major version (major+2), to give
time for admins to migrate over.
When a request is blocked, this degrades like an unreachable registry,
so a Renovate run isn't aborted by a blocked request.
As part of this, we can also provide a worked example of the different
cases that do/do not need allowing via `hostRules`.
Requests through an admin-configured proxy resolve DNS at the proxy, so
`dnsLookup` never runs for them, so we can resolve the hostname locally
and validate every address before the request instead.
This doesn't catch everything, especially as the proxy may resolve the
name to a different result, and if we can't resolve the name - possible
in locked-down corporate environments - we fail open.
Child processes aren't currently able to route traffic through
Renovate's HTTP layer, so are unrestricted.
Co-authored-by: Claude Fable 5 <jamie.tanna+claude-code@mend.io>
Co-authored-by: Claude Opus 5 <jamie.tanna+claude-code@mend.io>
Co-authored-by: Claude Sonnet 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>
* refactor(lint): enable oxlint prefer-named-capture-group at error
Numbered capture groups (m[1], $1) break silently when a regex is
edited and group order shifts. Named groups (?<name>...) make intent
explicit and accesses (m.groups.name) resilient to reordering.
This enables the built-in eslint rule prefer-named-capture-group,
surfacing 321 pre-existing unnamed-group violations across ~121
files to be burned down incrementally in follow-up commits.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk
* refactor(versioning): name capture groups for prefer-named-capture-group
Burns down all prefer-named-capture-group violations in
lib/modules/versioning/. Unused capture groups (only reachable via
.test() or whole-match) become non-capturing (?:...); groups whose
value is read become named (?<name>...), with accesses rewritten to
match.groups!.<name> (or destructured from .groups!) instead of
positional match[n]/$n so a future edit that reorders groups cannot
silently break the reader.
Two cases needed extra care to preserve behavior:
- semver/common.ts and poetry/patterns.ts split() on a regex whose
capture group is relied upon by String.prototype.split to keep the
separator in the output array; the group is kept capturing and
named, not converted to non-capturing.
- hashicorp/convertor.ts's semverRegex is only ever embedded via
.source into a named `(?<version>...)` group elsewhere and never
matched directly, so its internal groups are safe to make
non-capturing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk
* refactor(datasource): name capture groups for prefer-named-capture-group
Burns down all prefer-named-capture-group violations under
lib/modules/datasource/. Same treatment as the versioning module:
unused groups become non-capturing, used groups get names, and
positional match[n] reads move to match.groups!.<name>.
docker/ecr.ts and pypi/common.ts keep their positional destructuring
(`const [, region] = ...`, `!!m &&`) working unchanged since a named
capture group is still addressable by its numeric index in
JavaScript — only pypi/common.ts's access was rewritten to
m.groups!.owner for clarity since it read a single group directly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk
* refactor(platform): name capture groups for prefer-named-capture-group
Burns down all prefer-named-capture-group violations under
lib/modules/platform/. Uses the repo's established idiom of
`match.groups?.<name>` after an optional-chained exec()/match() (see
bitbucket/index.ts hostnameWithoutApiPrefix, gerrit/utils.ts
extractSourceBranch) and `$<name>` in String.replace replacement
strings. Unused groups (markdown/HTML massaging regexes matched only
to be stripped) become non-capturing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk
* refactor(manager): name capture groups in ansible-galaxy for prefer-named-capture-group
Burns down prefer-named-capture-group violations in
lib/modules/manager/ansible-galaxy/. newBlockRegEx and blockLineRegEx
dropped their unused outer wrapping group (only the inner key/value
groups were ever read) and named the key/value groups; nameMatchRegex's
remaining unused groups (protocol/user-info/port alternatives) become
non-capturing since only source/hostname/depName/version are read via
.groups. Accesses in collections.ts and roles.ts move from
lineMatch[2]/lineMatch[3] to lineMatch.groups!.key/value.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk
* refactor(manager): name capture groups for prefer-named-capture-group (batch 1)
Burns down prefer-named-capture-group violations across the first
alphabetical slice of lib/modules/manager/ (ansible through mix).
Used groups get names with accesses rewritten to match.groups!.<name>
(or $<name> in replacement strings); groups whose value is never read
become non-capturing (?:...). Two String.split() call sites
(git-submodules/extract.ts, meteor/extract.ts) keep the established
"named group still readable positionally from the split() result
array" idiom instead of restructuring the destructuring.
bazel/rules/go.ts and buildpacks/extract.ts dockerRef verified
byte-for-byte equivalent before/after via a quick node script since
several nested groups collapsed to non-capturing at once.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk
* refactor(manager): name capture groups for prefer-named-capture-group (batch 2)
Burns down prefer-named-capture-group violations across the remaining
alphabetical slice of lib/modules/manager/ (npm through terragrunt).
Same treatment as batch 1: used groups get names with accesses moved
to match.groups!.<name> or $<name> replacement strings; unused groups
(wrapping alternations, quantifier repetition groups only consumed via
.test(), or values never read from a match) become non-capturing.
terraform/extractors/others/modules.ts and terragrunt/modules.ts share
near-identical URL-parsing regexes (github/bitbucket/git/azure-devops
ref matchers) — the same set of always-unused `depth=`/`&depth=`
query-string groups is converted to non-capturing in both files.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk
* refactor(config): name capture groups for prefer-named-capture-group
Burns down prefer-named-capture-group violations in lib/config/.
Used groups get names with replacement strings switched from $1/$2 to
$<name>; the unused separator group in schedule-migration.ts's
afterBeforeRegex becomes non-capturing. presets/parse.ts keeps
assigning through the already-declared `repo` variable rather than
array-destructuring the exec() result, matching this file's existing
style for the other named-group regexes just above it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk
* refactor(util): name capture groups for prefer-named-capture-group
Burns down prefer-named-capture-group violations across lib/util/ and
lib/logger/. Same treatment as previous batches: named groups for
values that are read (via $<name> replacement strings, .groups!.<name>,
or destructured from .groups), non-capturing groups where nothing reads
the captured value (e.g. semantic.ts's angular-commit .test()-only
regex). www-authenticate.ts's tokenizer entries each get their own
named group but keep accessing the match generically via match[1],
since a positionally-indexed named group is still readable by index.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk
* refactor(workers): name capture groups for prefer-named-capture-group
Burns down prefer-named-capture-group violations in lib/workers/.
Used groups get names with $<name> replacement strings; unused
wrapper/alternation groups (e.g. semantic-commit-message.ts's optional
scope/issue wrappers, release-notes.ts's date-heading and
horizontal-rule matchers that are only probed with .search()/replaced
wholesale) become non-capturing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk
* refactor(test,tools): name capture groups for prefer-named-capture-group
Finishes the prefer-named-capture-group burndown: test/, tools/, and
vitest.config.mts. Groups whose value is read get names ($<name>
replacement strings or .groups.<name> access); groups only used via
.test()/.search() or that feed a .match(/…/g) call (whose return value
is the array of whole matches, not captured groups) become
non-capturing.
This is the last remaining slice of pre-existing violations, so the
rule (already enabled at error repo-wide) now passes with zero
findings across the whole codebase.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk
* fix prettier failure
* refactor(lint): use a named capture group in `v8-ignore-no-count`
New rule from main trips the newly enabled `prefer-named-capture-group` oxlint rule.
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Refs #45216
The object counterpart to `prefer-coerce-array`: `x ?? {}` becomes
`coerceObject(x)`. The rule flags a `??` whose default is a bare `{}`
literal, so a populated default or an asserted `({} as Foo)` still
passes; a `x ?? y ?? {}` chain maps onto `coerceObject(x, y)`.
Every hit across `lib/` converted cleanly, so no disable comments were
needed.
* test: replace manual process.env mutation with vi.stubEnv
Enable Vitest's `unstubEnvs` so env stubs are reverted automatically
before every test, and convert the manual `process.env.X = ...` /
`delete process.env.X` pairs in specs to `vi.stubEnv()`.
This removes the hand-written `afterEach` cleanup blocks and the
`const OLD_ENV = process.env` / `process.env = { ...OLD_ENV }`
save-restore dance, which silently leaked state between tests when a
test set a variable without a matching delete.
Left as-is (incompatible with `unstubEnvs`, which unstubs before each
test, not after):
- module top-level env writes (cocoapods, gomod, post-update, pip-compile
and bundler artifacts specs)
- `beforeAll` env writes -- the two in `util/http` moved to `beforeEach`
* test: stop asserting on ambient environment variables
Three specs asserted on values that the surrounding environment can
override, so they only passed on a machine that happened not to set them:
- `util/exec/env`: `getChildProcessEnv()` forwards every entry of
`basicEnvVars`, but the spec only seeded a subset. Any other forwarded
variable that was set ambiently (e.g. a lowercase `http_proxy`) leaked
into the `toEqual` assertion. Clear all of them first.
- `util/git/index`: `process.env.GIT_SSH_COMMAND` deliberately overrides
the default that the two env-inheritance tests assert on. Clear it.
- `logger/pretty-stdout`: the colorized level strings are built at import
time, so the `beforeEach` setting `FORCE_COLOR` could never affect them
-- it was dead code, and the test silently assumed a terminal without
colour support. Derive the expectation the same way the module does.
* test(datasource/nuget): use a throwaway cache dir for nupkg downloads
The `determine source URL from nupkg` tests do not mock the filesystem,
so they really wrote downloaded packages into a fixed `/tmp/cache` path.
That fails wherever the system tmpdir is not writable, and two checkouts
running the suite at once would share the same directory.
Use `tmp.dir({ unsafeCleanup: true })` and clean it up afterwards, which
is the pattern the other specs that touch disk already use.
* feat(lint): add renovate/prefer-stub-env rule
Enforces in specs what the migration to `vi.stubEnv` established:
- assigning to or deleting `process.env.X` inside `beforeEach`, `afterEach`,
`it` or `test` is reported, with a fixer that rewrites it to `vi.stubEnv()`
- module scope, `describe` bodies and `beforeAll` / `afterAll` are left alone:
`unstubEnvs` reverts stubs *before* each test, so a stub created there is
gone by the time the first test runs, and those must keep assigning directly
- replacing the `process.env` object is reported everywhere and never fixed.
`vi.stubEnv()` captures the original object when the worker starts, so after
a reassignment its deletes silently target the old object
The rule found 28 further violations in specs the earlier sweep missed, since
that one was anchored on `delete process.env`. Twelve were auto-fixed; the
rest replaced `process.env` wholesale and are now `clearEnv()` from test/util.
`util/exec/index.spec.ts` opts out with a comment: it asserts on the whole
child env, and `vi.stubEnv()` cannot express that because it maps `PROD`,
`DEV` and `SSR` onto '' instead of deleting them.
feat(lint): add `types-location` rule and enable it for `lib/modules`
Reviewers consistently ask for exported types to be moved into a
colocated `types.ts`. Add a `renovate/types-location` oxlint rule that
reports `export interface` / `export type` declarations outside such a
file, and turn it on across `lib/modules`.
Re-exports (`export type { X }`, `export type *`), file-local types, and
values (enums, classes, functions) are left alone. `schema.ts` files and
`schema/` directories are excluded via config, matching how
`zod-schema-location` is configured — inferred types belong next to the
schema they are derived from.
Relocating the 87 existing violations means:
- new `types.ts` files for the modules that had none
- `terragrunt/common.ts` folded into its `types.ts`, as it held nothing
else
- `RangeBound` in `versioning/gradle/compare.ts` un-exported, since only
a file-local interface referenced it
- `bazel-module/parser/fragments.ts` split from its inferred types, which
now live in `parser/types.ts` and infer through a namespace import so
each schema keeps its merged name
Adds a custom oxlint rule renovate/v8-ignore-no-count that flags the
count form '/* v8 ignore next <N> */'. The count is not honored by
vitest's ast-v8-to-istanbul hint parser, which captures only the
keyword (if/else/next/file); 'next' always exempts exactly the next
AST node. Trailing counts therefore suggest a line range that is never
applied and mislead readers.
The rule is autofixable (drops the count, preserving actual behavior)
and has been applied to all existing occurrences.
To prevent potential SSRF attempts, we can ignore a
misconfigured/malicious `next` link, and log a warning.
For self-hosted users, we can provide an experimental override if they
need to opt back in.
Co-authored-by: Claude Opus 4.8 <jamie.tanna+claude-code@mend.io>
Co-authored-by: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io>
* ci(linter): add renovate/v8-ignore-reason rule
Flag coverage-ignore comments ("v8 ignore") that carry no justification.
Accepted forms: "-- reason" / "--- reason" (recommended) and the legacy
colon form "v8 ignore <kind>[ <count>]: reason", which is already
widespread in the codebase (29 sites). Bare "v8 ignore stop" markers are
exempt because the justification lives on the paired "start" marker. The
"v8 ignore next <N>" count form remains allowed; this rule only requires
a justification.
Registered as "warn" instead of "error": the repo currently has 119
reason-less v8 ignore comments, too many to fix mechanically in one PR,
so existing sites are left untouched and can be fixed incrementally.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci(linter): promote renovate/v8-ignore-reason to error
Record the empirical verdict on the contested "v8 ignore next <N>" count
semantics in the rule doc: verified against vitest 4 with
ast-v8-to-istanbul that the count is not honored - the ignore hint always
exempts exactly the next AST node regardless of N. Counts are therefore
inert and stay allowed; the rule keeps requiring only a justification.
Promoting to error so all remaining reason-less coverage-ignore comments
must be fixed; the burndown follows in subsequent commits.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk
* refactor: add justifications to coverage-ignore comments (batch 1)
Give every reason-less "v8 ignore" comment outside the large platform
index files a site-specific justification explaining why the exempted
branch is untestable or defensive, as now required by the
renovate/v8-ignore-reason rule.
Also guard against a nullish comment.loc in the rule itself; the
merged-in stricter tools typechecking flagged the unguarded access.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk
* refactor: add justifications to coverage-ignore comments (batch 2)
Cover the five large platform index files (github, bitbucket,
bitbucket-server, codecommit, gitlab) with site-specific reasons on
every "v8 ignore" comment, completing the renovate/v8-ignore-reason
burndown. Two meaningless ignore hints inside commented-out CodeCommit
mergePr code are dropped instead of annotated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk
* Apply suggestions from code review
Co-authored-by: Michael Kriese <michael.kriese@gmx.de>
* Update lib/util/regex.ts
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Michael Kriese <michael.kriese@gmx.de>
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
* refactor(oxlint): enable func-style
Enable the built-in func-style rule as ["warn", "declaration"] so that
named functions are written as function declarations instead of arrow or
function expressions assigned to variables, matching the convention in
docs/development/best-practices.md (Functions section). Inline arrow
callbacks are unaffected; prefer-arrow-callback continues to cover the
callback direction.
Severity is "warn" instead of "error" as a fallback: a repo-wide run
found 196 pre-existing violations (70 in spec files, 126 in production
code), exceeding the 80-violation fix threshold for this change, so no
violations were fixed here. Follow-up work can convert the offenders
(largest clusters: lib/modules/versioning/ruby, lib/util/cache/package,
lib/modules/versioning/maven, lib/modules/versioning/gradle) and then
raise the rule to "error". Note oxlint 1.72 also supports an
allowTypeAnnotation option if typed consts should remain arrows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci(linter): enforce func-style at error
* refactor(func-style): convert const-arrow functions to declarations in with-cache spec and ruby versioning
* refactor(func-style): convert const-arrow functions to declarations in maven/gradle modules
* refactor(func-style): convert const-arrow functions to declarations in remaining versioning modules
* refactor(func-style): convert const-arrow functions to declarations in platform modules
Discriminated-union closures (ensureCommentRemoval byTopic/byContent
helpers in github, gitlab, bitbucket, bitbucket-server) needed the
narrowed field passed explicitly as a parameter, since function
declarations are hoisted and lose the outer if-narrowing that inline
arrow-function closures previously relied on.
* refactor(func-style): convert const-arrow functions to declarations across util/manager/datasource modules
One case (logger/renovate-logger.ts logOnceFn) is kept as an arrow
function with a documented oxlint-disable: it is stored on
this.logger.once[level] and relies on lexically-bound this, which a
hoisted function declaration would not preserve.
* refactor(func-style): convert remaining const-arrow test helpers to declarations
get-updated.spec.ts: hoisted expectPipCompilePackageAndLockFile to
module scope (it only closes over the module-level pipCompile mock)
and added a documented oxlint-disable for vitest/expect-expect on its
caller test, since oxlint's expect-expect check does not trace
assertions through an externally-defined helper function.
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* ci(linter): add renovate/no-host-rules-mock rule
Flags vi.mock()/vi.doMock() calls whose module specifier contains
"host-rules" in spec files. Tests should configure real rules via
hostRules.add() instead of mocking the host-rules module.
Enabled at "warn" severity instead of "error": the repo currently has
35 pre-existing violations across 34 spec files, which exceeds the
agreed fix threshold of 12 files. No violation fixes are included;
migrating each spec to real hostRules.add()/clear() setup is a
semantic test rewrite that should be done incrementally, after which
the rule can be promoted to "error".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci(linter): enforce renovate/no-host-rules-mock at error
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(datasource): use real host-rules instead of mocking
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(manager): use real host-rules instead of mocking
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(platform): use real host-rules instead of mocking
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(util,workers): use real host-rules instead of mocking
Also narrow renovate/no-host-rules-mock to the host-rules module itself so that mocking e.g. host-rules-from-env is not flagged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci(linter): mention '~test/host-rules.ts' util in no-host-rules-mock message
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* ci(linter): add renovate/codeblock-in-spec-fixtures rule
Add a custom oxlint rule that requires the codeBlock (or stripIndent)
template helper from common-tags for multi-line string fixtures in spec
files (enabled for **/*.spec.ts and test/** only).
The rule only flags shapes whose value can round-trip through the
common-tags helpers unchanged, since both codeBlock and stripIndent trim
the end result and strip the shortest line indentation:
- only literals spanning multiple source lines are flagged (escaped \n
one-liners are left alone: tagging them is not value-preserving in
general and gains nothing)
- values with leading/trailing whitespace (e.g. fixtures ending in a
newline) are not flagged
- tagged templates, describe/it titles, and inline snapshot arguments
are skipped
All 55 pre-existing violations (22 files) were fixed with a one-off
codemod that re-emits each fixture indented to the statement depth and
verifies value preservation by evaluating codeBlock on the rewritten
template (substitutions kept as opaque placeholders). One site
(pip_requirements/artifacts.spec.ts) mixes line continuations with
escaped newlines and was tagged verbatim instead of re-indented.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci(linter): address review findings
Make getSimpleRequirementsFile in pip-compile/extract.spec.ts
value-preserving again: tagging the whole template with codeBlock
trimmed the trailing blank line when deps is empty (the headerOnly.txt
case), silently changing the fixture from ending in '#\n\n' to '#'.
Tag only the static header and append the deps section via a
single-line template, which reproduces the original string exactly
for both empty and non-empty deps (verified by executing old vs new
in node).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Enable the unicorn/prefer-at rule as error in .oxlintrc.json and fix all
43 pre-existing violations (arr[arr.length - 1] -> arr.at(-1)) via oxlint
autofix. Where .at() widened the type to T | undefined, non-null
assertions were added at sites where surrounding logic guarantees the
element exists (length guards, split results), preserving the original
runtime behavior and typing.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Enable the type-aware typescript/no-unnecessary-type-arguments rule as
error and remove all 44 pre-existing redundant type arguments (type
arguments identical to the type parameter default), e.g.
HttpResponse<string>, ZodError<unknown>,
PackageDependency<Record<string, any>>.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Enable no-else-return (with allowElseIf: false) and unicorn/no-lonely-if
as errors in .oxlintrc.json and fix all 131 pre-existing violations
(91 no-else-return, 40 unicorn/no-lonely-if) across 98 files.
The no-else-return violations were fixed with the oxlint autofix
(iterated to a fixpoint, plus two else-if chains that required
--fix-dangerously) followed by Prettier formatting. One comment dropped
by the autofix in lib/modules/manager/pub/artifacts.ts was restored
manually. The unicorn/no-lonely-if violations were merged by hand,
preserving comments and coverage-ignore hints.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ci(linter): add renovate/no-redundant-mock-reset rule
Vitest sets mockReset: true globally in vitest.config.mts, which resets
all mocks before each test. This makes vi.resetAllMocks() /
vi.clearAllMocks() and per-mock .mockReset() / .mockClear() calls inside
beforeEach/afterEach hooks redundant. Add a custom oxlint rule (enabled
for **/*.spec.ts and test/**) flagging such calls, and remove the 40
redundant calls it found. restoreAllMocks/mockRestore are not flagged
because restoreMocks is not enabled globally.
Two sites keep an inline disable with a reason: nested beforeEach hooks
in filter-checks.spec.ts and onboarding/pr/index.spec.ts intentionally
call mockReset() to discard once-values queued by an outer beforeEach.
Requested by maintainer review feedback (review comment 3052324877).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* ci(linter): add renovate/prefer-zod-nullish rule
Add a custom oxlint rule that flags zod chains calling .optional() and
.nullable() adjacently (in either order) and suggests .nullish(), which
is equivalent (both accept undefined | null in zod v4).
Fix all 26 pre-existing violations across 6 schema files.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci(linter): prefer schema-utils Nullish over zod .nullish()
Rename prefer-zod-nullish to prefer-nullish-util and extend it to flag
.nullish() itself, including the standalone forms z.nullish(x) and
z.optional(z.nullable(x)). Migrate all call sites to Nullish() and
DeepNullish() from schema-utils, which normalize null to undefined in
parsed output. Schemas with more than one nullish position are wrapped
in DeepNullish(); nullish object properties with nullish members use
Nullish(DeepNullish(...)) to keep the optional property type marker.
Spec files are exempt (result.spec.ts intentionally tests
null-preserving parses). Spec assertions expecting null in parsed
output were updated to undefined. Note: docker manifests with an
explicit "mediaType": null are now accepted and treated like an absent
mediaType instead of failing the discriminated union.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(docker): handle 405 maxResults error on auth probe for ECR-compatible proxies
Some private Docker registries that front ECR reject `n=10000` on the tags
URL even during the auth-probe request, returning a 405 with the ECR-style
"Member must have value less than or equal to 1000" error body.
`getAuthHeaders` had no handler for this case and returned null, so the
lookup aborted before the main fetch loop's existing n=1000 retry could fire.
Fix: extract `isECRMaxResultsResponse` from `isECRMaxResultsError` in ecr.ts
and use it in `getAuthHeaders` to detect the 405 pattern. When matched, fall
back to probing the base `/v2/` endpoint for auth, then let the existing
`isECRMaxResultsError` retry in the tag-fetch loop handle the n=1000 fallback.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(docker): address PR review comments on ECR proxy auth probe fix
- Shorten test token to 'abc' to avoid CodeQL secret-detection false positive
- Replace HttpResponse<any> with HttpResponse<unknown> in common.ts and ecr.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(docker): replace HttpResponse<any> with HttpResponse<unknown> in isECRMaxResultsResponse
Cast body to a typed shape inline instead of widening the whole response to any.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(docker): replace toMatchInlineSnapshot with toEqual in ECR auth probe test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(docker): replace getJsonUnchecked with validated getJson
Replace all getJsonUnchecked calls in the Docker datasource with
Zod-validated getJson calls. Add RegistryAuthToken, RegistryTagsList,
and QuayTagsResponse schemas to schema.ts and add unit tests for them.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* do not use getJson if we do not look at the body
* chore: default to false for safety
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: migrate from zod/v3 to zod/v4
This commit contains the logic changes
* chore: migrate imports from zod/v3 to zod/v4
This contains import updates and adds explicit record keys as zod/v4 requires these now
* ci: enforce usage of zod/v4
* fix(config/global): return default values for `GlobalConfig.get`
As noted in #39949, it can be awkward working with in `GlobalConfig.get`, where
we would previously need to specify the default value when calling `.get`,
which could lead to drift with the defaults for a given config option.
To reduce this risk, we can rely on a given `default` for a configuration
option, providing that as a default.
Because this now definitely returns a value that matches the config option, it
now allows us to remove `!` assertions.
* fixup! fix(config/global): return default values for `GlobalConfig.get`
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
* fixup! fix(config/global): return default values for `GlobalConfig.get`
* test(presets): remove impossible test
As it's now not possible to set a value as `undefined` (as
`GlobalConfig` will then use the default), this test no longer validates
anything.
* test(workers/repository): add mock
Now `platform` defaults to `github`, we need to mock out the call to
fetch config.
Co-authored-by: Claude Sonnet 4.6 <jamie.tanna+claude-code@mend.io>
* test: coverage
* test: coverage
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <jamie.tanna+claude-code@mend.io>
* test(noNewURL): add new rule enforcing parseUrl() from 'lib/util/url.ts'
* replace newURL with parseUrl
* remove non-null assertions marks if appropiate
* test: add additional tests
* test: add additional tests and ignore where it makes sense
* implement suggestions
* test: further improve coverage
* apply suggestion