Files
Matthew Nitschke ac22678812 feat(datasource/npm): integrity and tarball extraction (#44315)
* added support for extracting integrity and tarball from npm datasource

* test(datasource/npm): keep integrity and tarball in packument shape assertions

The packument-trimming tests on main pinned a cached shape that dropped
`dist.tarball`, so they broke once the schema started retaining
`integrity` and `tarball`. Update both expectations to the new shape and
cover `integrity` alongside `tarball`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(workers/lookup): expect integrity and tarball on npm updates

`generateUpdate` copies `downloadUrl` and `newDigest` from the release
onto the update, so every npm lookup in these tests now carries the
tarball URL and, where the packument fixture has one, the integrity
hash. Pin and rollback updates are built without those release fields,
so their expectations are unchanged.

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>
2026-09-11 10:09:59 +00:00

6875 lines
212 KiB
TypeScript

import { codeBlock } from 'common-tags';
import { Fixtures } from '~test/fixtures.ts';
import * as httpMock from '~test/http-mock.ts';
import { fakeSha, logger, partial } from '~test/util.ts';
import * as hostRules from '../../../../../lib/util/host-rules.ts';
import { getConfig } from '../../../../config/defaults.ts';
import { supportedDatasources as presetSupportedDatasources } from '../../../../config/presets/internal/merge-confidence.preset.ts';
import type { AllConfig } from '../../../../config/types.ts';
import { CONFIG_VALIDATION } from '../../../../constants/error-messages.ts';
import { CustomDatasource } from '../../../../modules/datasource/custom/index.ts';
import { DockerDatasource } from '../../../../modules/datasource/docker/index.ts';
import { GitRefsDatasource } from '../../../../modules/datasource/git-refs/index.ts';
import { GithubReleasesDatasource } from '../../../../modules/datasource/github-releases/index.ts';
import { GithubTagsDatasource } from '../../../../modules/datasource/github-tags/index.ts';
import { GoDatasource } from '../../../../modules/datasource/go/index.ts';
import { MavenDatasource } from '../../../../modules/datasource/maven/index.ts';
import { defaultRegistryUrl as npmDefaultRegistryUrl } from '../../../../modules/datasource/npm/common.ts';
import { NpmDatasource } from '../../../../modules/datasource/npm/index.ts';
import { PackagistDatasource } from '../../../../modules/datasource/packagist/index.ts';
import { PypiDatasource } from '../../../../modules/datasource/pypi/index.ts';
import { id as composerVersioningId } from '../../../../modules/versioning/composer/index.ts';
import { id as debianVersioningId } from '../../../../modules/versioning/debian/index.ts';
import { id as dockerVersioningId } from '../../../../modules/versioning/docker/index.ts';
import { id as gitVersioningId } from '../../../../modules/versioning/git/index.ts';
import { id as githubActionsVersioningId } from '../../../../modules/versioning/github-actions/index.ts';
import { id as mavenVersioningId } from '../../../../modules/versioning/maven/index.ts';
import { id as nodeVersioningId } from '../../../../modules/versioning/node/index.ts';
import { id as npmVersioningId } from '../../../../modules/versioning/npm/index.ts';
import { id as pep440VersioningId } from '../../../../modules/versioning/pep440/index.ts';
import { id as poetryVersioningId } from '../../../../modules/versioning/poetry/index.ts';
import { id as semverVersioningId } from '../../../../modules/versioning/semver/index.ts';
import type { HostRule } from '../../../../types/index.ts';
import * as memCache from '../../../../util/cache/memory/index.ts';
import * as McApi from '../../../../util/merge-confidence/index.ts';
import {
initConfig,
resetConfig,
} from '../../../../util/merge-confidence/index.ts';
import { Result } from '../../../../util/result.ts';
import type { Timestamp } from '../../../../util/timestamp.ts';
import * as lookup from './index.ts';
import type { LookupUpdateConfig } from './types.ts';
const qJson = {
...Fixtures.getJson('01.json'),
latestVersion: '1.4.1',
};
const helmetJson = Fixtures.get('02.json');
const coffeelintJson = Fixtures.get('coffeelint.json');
const nextJson = Fixtures.get('next.json');
const typescriptJson = Fixtures.get('typescript.json');
const vueJson = Fixtures.get('vue.json');
const webpackJson = Fixtures.get('webpack.json');
let config: LookupUpdateConfig;
describe('workers/repository/process/lookup/index', () => {
const getGithubReleases = vi.spyOn(
GithubReleasesDatasource.prototype,
'getReleases',
);
const getGithubTags = vi.spyOn(GithubTagsDatasource.prototype, 'getReleases');
const getDockerReleases = vi.spyOn(DockerDatasource.prototype, 'getReleases');
const getMavenReleases = vi.spyOn(MavenDatasource.prototype, 'getReleases');
const postprocessMavenRelease = vi.spyOn(
MavenDatasource.prototype,
'postprocessRelease',
);
const getCustomDatasourceReleases = vi.spyOn(
CustomDatasource.prototype,
'getReleases',
);
const getDockerDigest = vi.spyOn(DockerDatasource.prototype, 'getDigest');
beforeEach(() => {
// TODO: fix types #22198
config = partial<LookupUpdateConfig>(getConfig() as never);
config.manager = 'npm';
config.versioning = npmVersioningId;
config.rangeStrategy = 'replace';
vi.spyOn(GitRefsDatasource.prototype, 'getReleases').mockResolvedValueOnce({
releases: [{ version: 'master' }],
});
vi.spyOn(GitRefsDatasource.prototype, 'getDigest').mockResolvedValueOnce(
'4b825dc642cb6eb9a060e54bf8d69288fbee4904',
);
});
// TODO: fix mocks
afterEach(() => {
httpMock.clear(false);
hostRules.clear();
});
describe('.lookupUpdates()', () => {
it('returns null if invalid currentValue', async () => {
// @ts-expect-error: testing invalid currentValue
config.currentValue = 3;
const { skipReason } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(skipReason).toBe('invalid-value');
});
it('returns null if unknown datasource', async () => {
config.packageName = 'some-dep';
config.datasource = 'does not exist';
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
});
it('handles error result from getPkgReleasesWithResult', async () => {
config.currentValue = '1.0.0';
config.packageName = 'some-dep';
config.datasource = NpmDatasource.id;
config.rollbackPrs = true;
httpMock.scope(npmDefaultRegistryUrl).get('/some-dep').reply(500);
const res = await lookup.lookupUpdates(config);
expect(() => res.unwrapOrThrow()).toThrow('external-host-error');
});
it('returns rollback for pinned version', async () => {
config.currentValue = '0.9.99';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
config.rollbackPrs = true;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'rollback',
newMajor: 0,
newValue: '0.9.7',
newVersion: '0.9.7',
registryUrl: undefined,
updateType: 'rollback',
prBodyNotes: expect.arrayContaining([
expect.stringContaining('The version of `q` in use (`0.9.99`)'),
]),
},
{
bucket: 'major',
isBreaking: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '1.4.1',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'major',
hasAttestation: false,
},
]);
});
it('returns rollback for ranged version', async () => {
config.currentValue = '^0.9.99';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
config.rollbackPrs = true;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'rollback',
newMajor: 0,
newValue: '^0.9.7',
newVersion: '0.9.7',
registryUrl: undefined,
updateType: 'rollback',
prBodyNotes: expect.arrayContaining([
expect.stringContaining('The version of `q` in use (`^0.9.99`)'),
]),
},
]);
});
it('supports minor and major upgrades for tilde ranges', async () => {
config.currentValue = '^0.4.0';
config.rangeStrategy = 'pin';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
isPin: true,
newMajor: 0,
newValue: '0.4.4',
newVersion: '0.4.4',
updateType: 'pin',
},
{
bucket: 'non-major',
isBreaking: true,
isRange: true,
newMajor: 0,
newMinor: 9,
newPatch: 7,
newValue: '^0.9.0',
newVersion: '0.9.7',
downloadUrl: 'https://registry.npmjs.org/q/-/q-0.9.7.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2013-09-04T17:07:22.948Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
{
bucket: 'major',
isBreaking: true,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '^1.0.0',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'major',
hasAttestation: false,
},
]);
});
it('supports lock file updates mixed with regular updates', async () => {
config.currentValue = '^0.4.0';
config.rangeStrategy = 'update-lockfile';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
config.separateMinorPatch = true;
config.lockedVersion = '0.4.0';
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'patch',
isBreaking: true,
isLockfileUpdate: true,
isRange: true,
newMajor: 0,
newMinor: 4,
newPatch: 4,
newValue: '^0.4.0',
newVersion: '0.4.4',
downloadUrl: 'https://registry.npmjs.org/q/-/q-0.4.4.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2011-06-10T17:20:04.719Z' as Timestamp,
updateType: 'patch',
hasAttestation: false,
},
{
bucket: 'minor',
isBreaking: true,
isRange: true,
newMajor: 0,
newMinor: 9,
newPatch: 7,
newValue: '^0.9.0',
newVersion: '0.9.7',
downloadUrl: 'https://registry.npmjs.org/q/-/q-0.9.7.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2013-09-04T17:07:22.948Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
{
bucket: 'major',
isBreaking: true,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '^1.0.0',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'major',
hasAttestation: false,
},
]);
});
it('returns multiple updates if grouping but separateMajorMinor=true', async () => {
config.groupName = 'somegroup';
config.currentValue = '0.4.0';
config.rangeStrategy = 'pin';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: true,
newMajor: 0,
newMinor: 9,
newPatch: 7,
newValue: '0.9.7',
newVersion: '0.9.7',
downloadUrl: 'https://registry.npmjs.org/q/-/q-0.9.7.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'minor',
hasAttestation: false,
},
{
bucket: 'major',
isBreaking: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '1.4.1',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'major',
hasAttestation: false,
},
]);
});
it('returns additional update if grouping but separateMinorPatch=true', async () => {
config.groupName = 'somegroup';
config.currentValue = '0.4.0';
config.rangeStrategy = 'pin';
config.packageName = 'q';
config.separateMinorPatch = true;
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'patch',
isBreaking: true,
newMajor: 0,
newMinor: 4,
newPatch: 4,
newValue: '0.4.4',
newVersion: '0.4.4',
downloadUrl: 'https://registry.npmjs.org/q/-/q-0.4.4.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'patch',
hasAttestation: false,
},
{
bucket: 'minor',
isBreaking: true,
newMajor: 0,
newMinor: 9,
newPatch: 7,
newValue: '0.9.7',
newVersion: '0.9.7',
downloadUrl: 'https://registry.npmjs.org/q/-/q-0.9.7.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'minor',
hasAttestation: false,
},
{
bucket: 'major',
isBreaking: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '1.4.1',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'major',
hasAttestation: false,
},
]);
});
it('returns one update if grouping and separateMajorMinor=false', async () => {
config.groupName = 'somegroup';
config.currentValue = '0.4.0';
config.rangeStrategy = 'pin';
config.separateMajorMinor = false;
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'latest',
isBreaking: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '1.4.1',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'major',
hasAttestation: false,
},
]);
});
it('returns both updates if automerging minor', async () => {
config.minor = { automerge: true };
config.currentValue = '^0.4.0';
config.rangeStrategy = 'pin';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
isPin: true,
newMajor: 0,
newValue: '0.4.4',
newVersion: '0.4.4',
updateType: 'pin',
},
{
bucket: 'non-major',
isBreaking: true,
isRange: true,
newMajor: 0,
newMinor: 9,
newPatch: 7,
newValue: '^0.9.0',
newVersion: '0.9.7',
downloadUrl: 'https://registry.npmjs.org/q/-/q-0.9.7.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2013-09-04T17:07:22.948Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
{
bucket: 'major',
isBreaking: true,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '^1.0.0',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'major',
hasAttestation: false,
},
]);
});
it('enforces allowedVersions', async () => {
config.currentValue = '0.4.0';
config.allowedVersions = '<1';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: true,
newMajor: 0,
newMinor: 9,
newPatch: 7,
newValue: '0.9.7',
newVersion: '0.9.7',
downloadUrl: 'https://registry.npmjs.org/q/-/q-0.9.7.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('enforces allowedVersions with regex', async () => {
config.currentValue = '0.4.0';
config.allowedVersions = '/^0/';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: true,
newMajor: 0,
newMinor: 9,
newPatch: 7,
newValue: '0.9.7',
newVersion: '0.9.7',
downloadUrl: 'https://registry.npmjs.org/q/-/q-0.9.7.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2013-09-04T17:07:22.948Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('enforces allowedVersions with negative regex', async () => {
config.currentValue = '0.4.0';
config.allowedVersions = '!/^1/';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: true,
newMajor: 0,
newMinor: 9,
newPatch: 7,
newValue: '0.9.7',
newVersion: '0.9.7',
downloadUrl: 'https://registry.npmjs.org/q/-/q-0.9.7.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2013-09-04T17:07:22.948Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('falls back to semver syntax allowedVersions', async () => {
config.currentValue = '0.4.0';
config.allowedVersions = '<1';
config.packageName = 'q';
config.versioning = dockerVersioningId; // this doesn't make sense but works for this test
config.datasource = NpmDatasource.id; // this doesn't make sense but works for this test
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
newMajor: 0,
newMinor: 9,
newPatch: 7,
newValue: '0.9.7',
newVersion: '0.9.7',
downloadUrl: 'https://registry.npmjs.org/q/-/q-0.9.7.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2013-09-04T17:07:22.948Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('falls back to pep440 syntax allowedVersions', async () => {
config.currentValue = '0.4.0';
config.allowedVersions = '==0.9.4';
config.packageName = 'q';
config.versioning = poetryVersioningId; // this doesn't make sense but works for this test
config.datasource = NpmDatasource.id; // this doesn't make sense but works for this test
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
newMajor: 0,
newMinor: 9,
newPatch: 4,
newValue: '0.9.4',
newVersion: '0.9.4',
downloadUrl: 'https://registry.npmjs.org/q/-/q-0.9.4.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2013-05-22T20:26:50.888Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('skips invalid allowedVersions', async () => {
config.currentValue = '0.4.0';
config.allowedVersions = 'less than 1';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const res = await lookup.lookupUpdates(config);
expect(() => res.unwrapOrThrow()).toThrow(CONFIG_VALIDATION);
});
it('returns patch update even if separate patches not configured', async () => {
config.currentValue = '0.9.0';
config.rangeStrategy = 'pin';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: true,
newMajor: 0,
newMinor: 9,
newPatch: 7,
newValue: '0.9.7',
newVersion: '0.9.7',
downloadUrl: 'https://registry.npmjs.org/q/-/q-0.9.7.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'patch',
hasAttestation: false,
},
{
bucket: 'major',
isBreaking: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '1.4.1',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'major',
hasAttestation: false,
},
]);
});
it('returns minor update if automerging both patch and minor', async () => {
config.patch = {
automerge: true,
};
config.minor = {
automerge: true,
};
config.currentValue = '0.9.0';
config.rangeStrategy = 'pin';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: true,
newMajor: 0,
newMinor: 9,
newPatch: 7,
newValue: '0.9.7',
newVersion: '0.9.7',
downloadUrl: 'https://registry.npmjs.org/q/-/q-0.9.7.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'patch',
hasAttestation: false,
},
{
bucket: 'major',
isBreaking: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '1.4.1',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'major',
hasAttestation: false,
},
]);
});
it('returns patch update if separateMinorPatch', async () => {
config.separateMinorPatch = true;
config.currentValue = '0.9.0';
config.rangeStrategy = 'pin';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'patch',
isBreaking: true,
newMajor: 0,
newMinor: 9,
newPatch: 7,
newValue: '0.9.7',
newVersion: '0.9.7',
downloadUrl: 'https://registry.npmjs.org/q/-/q-0.9.7.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2013-09-04T17:07:22.948Z' as Timestamp,
updateType: 'patch',
hasAttestation: false,
},
{
bucket: 'major',
isBreaking: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '1.4.1',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'major',
hasAttestation: false,
},
]);
});
it('returns patch minor and major', async () => {
config.separateMinorPatch = true;
config.currentValue = '0.8.0';
config.rangeStrategy = 'pin';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'patch',
isBreaking: true,
newMajor: 0,
newMinor: 8,
newPatch: 12,
newValue: '0.8.12',
newVersion: '0.8.12',
downloadUrl: 'https://registry.npmjs.org/q/-/q-0.8.12.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'patch',
hasAttestation: false,
},
{
bucket: 'minor',
isBreaking: true,
newMajor: 0,
newMinor: 9,
newPatch: 7,
newValue: '0.9.7',
newVersion: '0.9.7',
downloadUrl: 'https://registry.npmjs.org/q/-/q-0.9.7.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'minor',
hasAttestation: false,
},
{
bucket: 'major',
isBreaking: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '1.4.1',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'major',
hasAttestation: false,
},
]);
});
it('disables major release separation (major)', async () => {
config.separateMajorMinor = false;
config.currentValue = '^0.4.0';
config.rangeStrategy = 'pin';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
isPin: true,
newMajor: 0,
newValue: '0.4.4',
newVersion: '0.4.4',
updateType: 'pin',
},
{
bucket: 'latest',
isBreaking: true,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '^1.0.0',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'major',
hasAttestation: false,
},
]);
});
it('disables major release separation (minor)', async () => {
config.separateMajorMinor = false;
config.currentValue = '1.0.0';
config.rangeStrategy = 'pin';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'latest',
isBreaking: false,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '1.4.1',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('uses minimum version for vulnerabilityAlerts', async () => {
config.currentValue = '1.0.0';
config.isVulnerabilityAlert = true;
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
newMajor: 1,
newMinor: 0,
newPatch: 1,
newValue: '1.0.1',
newVersion: '1.0.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.0.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'patch',
hasAttestation: false,
},
]);
});
it('uses highest available version for vulnerabilityAlerts when vulnerabilityFixStrategy=highest', async () => {
config.currentValue = '1.0.0';
config.isVulnerabilityAlert = true;
config.vulnerabilityFixStrategy = 'highest';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '1.4.1',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('uses vulnerabilityFixVersion when a version', async () => {
config.currentValue = '1.0.0';
config.isVulnerabilityAlert = true;
config.vulnerabilityFixVersion = '1.1.0';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
newMajor: 1,
newMinor: 1,
newPatch: 0,
newValue: '1.1.0',
newVersion: '1.1.0',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.1.0.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('takes a later release when vulnerabilityFixVersion does not exist', async () => {
config.currentValue = '1.0.0';
config.isVulnerabilityAlert = true;
config.vulnerabilityFixVersion = '1.0.2';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
newMajor: 1,
newMinor: 1,
newPatch: 0,
newValue: '1.1.0',
newVersion: '1.1.0',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.1.0.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('uses vulnerabilityFixVersion when a range', async () => {
config.currentValue = '1.0.0';
config.isVulnerabilityAlert = true;
config.vulnerabilityFixVersion = '>= 1.1.0';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
newMajor: 1,
newMinor: 1,
newPatch: 0,
newValue: '1.1.0',
newVersion: '1.1.0',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.1.0.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('takes highest available version when using vulnerabilityFixStrategy=highest with vulnerabilityFixVersion', async () => {
config.currentValue = '1.0.0';
config.isVulnerabilityAlert = true;
config.vulnerabilityFixVersion = '1.1.0';
config.vulnerabilityFixStrategy = 'highest';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '1.4.1',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('ignores vulnerabilityFixVersion if not a version', async () => {
config.currentValue = '1.0.0';
config.isVulnerabilityAlert = true;
config.vulnerabilityFixVersion = 'abc';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
newMajor: 1,
newMinor: 0,
newPatch: 1,
newValue: '1.0.1',
newVersion: '1.0.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.0.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'patch',
hasAttestation: false,
},
]);
});
it('returns no results if vulnerabilityFixVersion is too high', async () => {
config.currentValue = '1.0.0';
config.isVulnerabilityAlert = true;
config.vulnerabilityFixVersion = '5.1.0';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
});
it('supports minor and major upgrades for ranged versions', async () => {
config.currentValue = '~0.4.0';
config.rangeStrategy = 'pin';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
isPin: true,
newMajor: 0,
newValue: '0.4.4',
newVersion: '0.4.4',
updateType: 'pin',
},
{
bucket: 'non-major',
isBreaking: true,
isRange: true,
newMajor: 0,
newMinor: 9,
newPatch: 7,
newValue: '~0.9.0',
newVersion: '0.9.7',
downloadUrl: 'https://registry.npmjs.org/q/-/q-0.9.7.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2013-09-04T17:07:22.948Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
{
bucket: 'major',
isBreaking: true,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '~1.4.0',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'major',
hasAttestation: false,
},
]);
});
it('supports for x-range-all for replaceStrategy = pin (with lockfile) abcd', async () => {
config.currentValue = '*';
config.rangeStrategy = 'pin';
config.lockedVersion = '0.4.0';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
isPin: true,
newMajor: 0,
newValue: '0.4.0',
newVersion: '0.4.0',
updateType: 'pin',
},
]);
});
it.each`
strategy
${'widen'}
${'bump'}
${'replace'}
`(
'doesnt offer updates for x-range-all (with lockfile) when replaceStrategy = $strategy',
async ({ strategy }) => {
config.currentValue = 'x';
config.rangeStrategy = strategy;
config.lockedVersion = '0.4.0';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
},
);
it('supports pinning for x-range-all (no lockfile)', async () => {
config.currentValue = '*';
config.rangeStrategy = 'pin';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
isPin: true,
newMajor: 1,
newValue: '1.4.1',
newVersion: '1.4.1',
updateType: 'pin',
},
]);
});
it('covers pinning an unsupported x-range-all value', async () => {
config.currentValue = '';
config.rangeStrategy = 'pin';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
});
it.each`
strategy
${'widen'}
${'bump'}
${'update-lockfile'}
${'replace'}
`(
'doesnt offer updates for x-range-all (no lockfile) when replaceStrategy = $strategy',
async ({ strategy }) => {
config.currentValue = 'X';
config.rangeStrategy = strategy;
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
},
);
it('ignores pinning for ranges when other upgrade exists', async () => {
config.currentValue = '~0.9.0';
config.rangeStrategy = 'pin';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
isPin: true,
newMajor: 0,
newValue: '0.9.7',
newVersion: '0.9.7',
updateType: 'pin',
},
{
bucket: 'major',
hasAttestation: false,
isBreaking: true,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '~1.4.0',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'major',
},
]);
});
it('upgrades minor ranged versions', async () => {
config.currentValue = '~1.0.0';
config.rangeStrategy = 'pin';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
isPin: true,
newMajor: 1,
newValue: '1.0.1',
newVersion: '1.0.1',
updateType: 'pin',
},
{
bucket: 'non-major',
isBreaking: false,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '~1.4.0',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('handles update-lockfile', async () => {
config.currentValue = '^1.2.1';
config.lockedVersion = '1.2.1';
config.rangeStrategy = 'update-lockfile';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
isLockfileUpdate: true,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '^1.2.1',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('handles the in-range-only strategy and updates lockfile within range', async () => {
config.currentValue = '^1.2.1';
config.lockedVersion = '1.2.1';
config.rangeStrategy = 'in-range-only';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
isLockfileUpdate: true,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '^1.2.1',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('handles the in-range-only strategy and discards changes not within range', async () => {
config.currentValue = '~1.2.0';
config.lockedVersion = '1.2.0';
config.rangeStrategy = 'in-range-only';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
isLockfileUpdate: true,
isRange: true,
newMajor: 1,
newMinor: 2,
newPatch: 1,
newValue: '~1.2.0',
newVersion: '1.2.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.2.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'patch',
hasAttestation: false,
},
]);
});
it('handles unconstrainedValue values', async () => {
config.lockedVersion = '1.2.1';
config.rangeStrategy = 'update-lockfile';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
isLockfileUpdate: true,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: undefined,
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('handles unconstrainedValue values with rangeStrategy !== update-lockfile and isVulnerabilityAlert', async () => {
config.lockedVersion = '1.2.1';
config.rangeStrategy = 'bump';
config.packageName = 'q';
config.isVulnerabilityAlert = true;
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toMatchObject([
{
bucket: 'non-major',
isBreaking: false,
isLockfileUpdate: true,
isRange: true,
newMajor: 1,
newMinor: 3,
newPatch: 0,
newValue: undefined,
newVersion: '1.3.0',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.3.0.tgz',
releaseTimestamp: '2015-04-26T16:42:11.311Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
]);
expect(updates[0].newValue).toBeUndefined();
expect(updates[0].updateType).toBe('minor');
});
it('widens minor ranged versions if configured', async () => {
config.currentValue = '~1.3.0';
config.rangeStrategy = 'widen';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '~1.3.0 || ~1.4.0',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('replaces minor complex ranged versions if configured', async () => {
config.currentValue = '~1.2.0 || ~1.3.0';
config.rangeStrategy = 'replace';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '~1.4.0',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('widens major ranged versions if configured', async () => {
config.currentValue = '^2.0.0';
config.rangeStrategy = 'widen';
config.packageName = 'webpack';
config.datasource = NpmDatasource.id;
httpMock
.scope(npmDefaultRegistryUrl)
.get('/webpack')
.reply(200, webpackJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'major',
isBreaking: true,
isRange: true,
newMajor: 3,
newMinor: 8,
newPatch: 1,
newValue: '^2.0.0 || ^3.0.0',
newVersion: '3.8.1',
newDigest:
'sha512-5ZXLWWsMqHKFr5y0N3Eo5IIisxeEeRAajNq4mELb/WELOR7srdbQk2N5XiyNy2A/AgvlR3AmeBCZJW8lHrolbw==',
downloadUrl: 'https://registry.npmjs.org/webpack/-/webpack-3.8.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2017-10-17T15:22:36.646Z' as Timestamp,
updateType: 'major',
hasAttestation: false,
},
]);
});
it('replaces major complex ranged versions if configured', async () => {
config.currentValue = '^1.0.0 || ^2.0.0';
config.rangeStrategy = 'replace';
config.packageName = 'webpack';
config.datasource = NpmDatasource.id;
httpMock
.scope(npmDefaultRegistryUrl)
.get('/webpack')
.reply(200, webpackJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'major',
isBreaking: true,
isRange: true,
newMajor: 3,
newMinor: 8,
newPatch: 1,
newValue: '^3.0.0',
newVersion: '3.8.1',
newDigest:
'sha512-5ZXLWWsMqHKFr5y0N3Eo5IIisxeEeRAajNq4mELb/WELOR7srdbQk2N5XiyNy2A/AgvlR3AmeBCZJW8lHrolbw==',
downloadUrl: 'https://registry.npmjs.org/webpack/-/webpack-3.8.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2017-10-17T15:22:36.646Z' as Timestamp,
updateType: 'major',
hasAttestation: false,
},
]);
});
it('pins minor ranged versions', async () => {
config.currentValue = '^1.0.0';
config.rangeStrategy = 'pin';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
isPin: true,
newMajor: 1,
newValue: '1.4.1',
newVersion: '1.4.1',
updateType: 'pin',
},
]);
});
it('uses the locked version for pinning', async () => {
config.currentValue = '^1.0.0';
config.lockedVersion = '1.0.0';
config.rangeStrategy = 'pin';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
isPin: true,
newMajor: 1,
newValue: '1.0.0',
newVersion: '1.0.0',
updateType: 'pin',
},
]);
});
it('ignores minor ranged versions when not pinning', async () => {
config.rangeStrategy = 'replace';
config.currentValue = '^1.0.0';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
});
it('ignores minor ranged versions when locked', async () => {
config.rangeStrategy = 'replace';
config.currentValue = '^1.0.0';
config.lockedVersion = '1.1.0';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
});
it('upgrades tilde ranges', async () => {
config.rangeStrategy = 'pin';
config.currentValue = '~1.3.0';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
isPin: true,
newMajor: 1,
newValue: '1.3.0',
newVersion: '1.3.0',
updateType: 'pin',
},
{
bucket: 'non-major',
isBreaking: false,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '~1.4.0',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('upgrades .x minor ranges', async () => {
config.currentValue = '1.3.x';
config.rangeStrategy = 'pin';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
isPin: true,
newMajor: 1,
newValue: '1.3.0',
newVersion: '1.3.0',
updateType: 'pin',
},
{
bucket: 'non-major',
isBreaking: false,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '1.4.x',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('upgrades tilde ranges without pinning', async () => {
config.rangeStrategy = 'replace';
config.currentValue = '~1.3.0';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '~1.4.0',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('upgrades .x major ranges without pinning', async () => {
config.rangeStrategy = 'replace';
config.currentValue = '0.x';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'major',
isBreaking: true,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '1.x',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'major',
hasAttestation: false,
},
]);
});
it('upgrades .x minor ranges without pinning', async () => {
config.rangeStrategy = 'replace';
config.currentValue = '1.3.x';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '1.4.x',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('upgrades .x complex minor ranges without pinning', async () => {
config.rangeStrategy = 'widen';
config.currentValue = '1.2.x - 1.3.x';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '1.2.x - 1.4.x',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('upgrades shorthand major ranges without pinning', async () => {
config.rangeStrategy = 'replace';
config.currentValue = '0';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'major',
isBreaking: true,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '1',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'major',
hasAttestation: false,
},
]);
});
it('upgrades shorthand minor ranges without pinning', async () => {
config.rangeStrategy = 'replace';
config.currentValue = '1.3';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '1.4',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('upgrades multiple tilde ranges without pinning', async () => {
config.rangeStrategy = 'replace';
config.currentValue = '~0.7.0';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: true,
isRange: true,
newMajor: 0,
newMinor: 9,
newPatch: 7,
newValue: '~0.9.0',
newVersion: '0.9.7',
downloadUrl: 'https://registry.npmjs.org/q/-/q-0.9.7.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2013-09-04T17:07:22.948Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
{
bucket: 'major',
isBreaking: true,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '~1.4.0',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'major',
hasAttestation: false,
},
]);
});
it('upgrades multiple caret ranges without pinning', async () => {
config.rangeStrategy = 'replace';
config.currentValue = '^0.7.0';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: true,
isRange: true,
newMajor: 0,
newMinor: 9,
newPatch: 7,
newValue: '^0.9.0',
newVersion: '0.9.7',
downloadUrl: 'https://registry.npmjs.org/q/-/q-0.9.7.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2013-09-04T17:07:22.948Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
{
bucket: 'major',
isBreaking: true,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '^1.0.0',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'major',
hasAttestation: false,
},
]);
});
it('supports complex ranges', async () => {
config.rangeStrategy = 'widen';
config.currentValue = '^0.7.0 || ^0.8.0';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: true,
isRange: true,
newMajor: 0,
newMinor: 9,
newPatch: 7,
newValue: '^0.7.0 || ^0.8.0 || ^0.9.0',
newVersion: '0.9.7',
downloadUrl: 'https://registry.npmjs.org/q/-/q-0.9.7.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'minor',
hasAttestation: false,
},
{
bucket: 'major',
isBreaking: true,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '^0.7.0 || ^0.8.0 || ^1.0.0',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'major',
hasAttestation: false,
},
]);
});
it('supports complex major ranges', async () => {
config.rangeStrategy = 'widen';
config.currentValue = '^1.0.0 || ^2.0.0';
config.packageName = 'webpack';
config.datasource = NpmDatasource.id;
httpMock
.scope(npmDefaultRegistryUrl)
.get('/webpack')
.reply(200, webpackJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'major',
isBreaking: true,
isRange: true,
newMajor: 3,
newMinor: 8,
newPatch: 1,
newValue: '^1.0.0 || ^2.0.0 || ^3.0.0',
newVersion: '3.8.1',
newDigest:
'sha512-5ZXLWWsMqHKFr5y0N3Eo5IIisxeEeRAajNq4mELb/WELOR7srdbQk2N5XiyNy2A/AgvlR3AmeBCZJW8lHrolbw==',
downloadUrl: 'https://registry.npmjs.org/webpack/-/webpack-3.8.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2017-10-17T15:22:36.646Z' as Timestamp,
updateType: 'major',
hasAttestation: false,
},
]);
});
it('supports complex major hyphen ranges', async () => {
config.rangeStrategy = 'widen';
config.currentValue = '1.x - 2.x';
config.packageName = 'webpack';
config.datasource = NpmDatasource.id;
httpMock
.scope(npmDefaultRegistryUrl)
.get('/webpack')
.reply(200, webpackJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'major',
isBreaking: true,
isRange: true,
newMajor: 3,
newMinor: 8,
newPatch: 1,
newValue: '1.x - 3.x',
newVersion: '3.8.1',
newDigest:
'sha512-5ZXLWWsMqHKFr5y0N3Eo5IIisxeEeRAajNq4mELb/WELOR7srdbQk2N5XiyNy2A/AgvlR3AmeBCZJW8lHrolbw==',
downloadUrl: 'https://registry.npmjs.org/webpack/-/webpack-3.8.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2017-10-17T15:22:36.646Z' as Timestamp,
updateType: 'major',
hasAttestation: false,
},
]);
});
it('widens .x OR ranges', async () => {
config.rangeStrategy = 'widen';
config.currentValue = '1.x || 2.x';
config.packageName = 'webpack';
config.datasource = NpmDatasource.id;
httpMock
.scope(npmDefaultRegistryUrl)
.get('/webpack')
.reply(200, webpackJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'major',
isBreaking: true,
isRange: true,
newMajor: 3,
newMinor: 8,
newPatch: 1,
newValue: '1.x || 2.x || 3.x',
newVersion: '3.8.1',
newDigest:
'sha512-5ZXLWWsMqHKFr5y0N3Eo5IIisxeEeRAajNq4mELb/WELOR7srdbQk2N5XiyNy2A/AgvlR3AmeBCZJW8lHrolbw==',
downloadUrl: 'https://registry.npmjs.org/webpack/-/webpack-3.8.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2017-10-17T15:22:36.646Z' as Timestamp,
updateType: 'major',
hasAttestation: false,
},
]);
});
it('widens stanndalone major OR ranges', async () => {
config.rangeStrategy = 'widen';
config.currentValue = '1 || 2';
config.packageName = 'webpack';
config.datasource = NpmDatasource.id;
httpMock
.scope(npmDefaultRegistryUrl)
.get('/webpack')
.reply(200, webpackJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'major',
isBreaking: true,
isRange: true,
newMajor: 3,
newMinor: 8,
newPatch: 1,
newValue: '1 || 2 || 3',
newVersion: '3.8.1',
newDigest:
'sha512-5ZXLWWsMqHKFr5y0N3Eo5IIisxeEeRAajNq4mELb/WELOR7srdbQk2N5XiyNy2A/AgvlR3AmeBCZJW8lHrolbw==',
downloadUrl: 'https://registry.npmjs.org/webpack/-/webpack-3.8.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2017-10-17T15:22:36.646Z' as Timestamp,
updateType: 'major',
hasAttestation: false,
},
]);
});
it('supports complex tilde ranges', async () => {
config.rangeStrategy = 'widen';
config.currentValue = '~1.2.0 || ~1.3.0';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '~1.2.0 || ~1.3.0 || ~1.4.0',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('returns nothing for greater than ranges', async () => {
config.rangeStrategy = 'replace';
config.currentValue = '>= 0.7.0';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toHaveLength(0);
});
it('upgrades less than equal ranges without pinning', async () => {
config.rangeStrategy = 'replace';
config.currentValue = '<= 0.7.2';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: true,
isRange: true,
newMajor: 0,
newMinor: 9,
newPatch: 7,
newValue: '<= 0.9.7',
newVersion: '0.9.7',
downloadUrl: 'https://registry.npmjs.org/q/-/q-0.9.7.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2013-09-04T17:07:22.948Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
{
bucket: 'major',
isBreaking: true,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '<= 1.4.1',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'major',
hasAttestation: false,
},
]);
});
it('upgrades less than ranges without pinning', async () => {
config.rangeStrategy = 'replace';
config.currentValue = '< 0.7.2';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: true,
isRange: true,
newMajor: 0,
newMinor: 9,
newPatch: 7,
newValue: '< 0.9.8',
newVersion: '0.9.7',
downloadUrl: 'https://registry.npmjs.org/q/-/q-0.9.7.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2013-09-04T17:07:22.948Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
{
bucket: 'major',
isBreaking: true,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '< 1.4.2',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'major',
hasAttestation: false,
},
]);
});
it('upgrades less than major ranges', async () => {
config.rangeStrategy = 'replace';
config.currentValue = '< 1';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'major',
isBreaking: true,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '< 2',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'major',
hasAttestation: false,
},
]);
});
it('upgrades less than equal minor ranges', async () => {
config.rangeStrategy = 'replace';
config.currentValue = '<= 1.3';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '<= 1.4',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('upgrades equal minor ranges', async () => {
config.rangeStrategy = 'replace';
config.currentValue = '=1.3.1';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '=1.4.1',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('upgrades less than equal major ranges', async () => {
config.rangeStrategy = 'replace';
config.respectLatest = false;
config.currentValue = '<= 1';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'major',
isBreaking: true,
isRange: true,
newMajor: 2,
newMinor: 0,
newPatch: 3,
newValue: '<= 2',
newVersion: '2.0.3',
downloadUrl: 'https://registry.npmjs.org/q/-/q-2.0.3.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-01-31T08:11:47.852Z' as Timestamp,
updateType: 'major',
hasAttestation: false,
},
]);
});
it('upgrades major less than equal ranges', async () => {
config.rangeStrategy = 'replace';
config.currentValue = '<= 1.0.0';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '<= 1.4.1',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('upgrades major less than ranges without pinning', async () => {
config.rangeStrategy = 'replace';
config.currentValue = '< 1.0.0';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'major',
isBreaking: true,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '< 2.0.0',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'major',
hasAttestation: false,
},
]);
});
it('upgrades major greater than less than ranges without pinning', async () => {
config.rangeStrategy = 'widen';
config.currentValue = '>= 0.5.0 < 1.0.0';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'major',
isBreaking: true,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '>= 0.5.0 < 2.0.0',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'major',
hasAttestation: false,
},
]);
});
it('upgrades minor greater than less than ranges without pinning', async () => {
config.rangeStrategy = 'widen';
config.currentValue = '>= 0.5.0 <0.8';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: true,
isRange: true,
newMajor: 0,
newMinor: 9,
newPatch: 7,
newValue: '>= 0.5.0 <0.10',
newVersion: '0.9.7',
downloadUrl: 'https://registry.npmjs.org/q/-/q-0.9.7.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'minor',
hasAttestation: false,
},
{
bucket: 'major',
isBreaking: true,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '>= 0.5.0 <1.5',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'major',
hasAttestation: false,
},
]);
});
it('upgrades minor greater than less than equals ranges without pinning', async () => {
config.rangeStrategy = 'widen';
config.currentValue = '>= 0.5.0 <= 0.8.0';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: true,
isRange: true,
newMajor: 0,
newMinor: 9,
newPatch: 7,
newValue: '>= 0.5.0 <= 0.9.7',
newVersion: '0.9.7',
downloadUrl: 'https://registry.npmjs.org/q/-/q-0.9.7.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'minor',
hasAttestation: false,
},
{
bucket: 'major',
isBreaking: true,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '>= 0.5.0 <= 1.4.1',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'major',
hasAttestation: false,
},
]);
});
it('rejects reverse ordered less than greater than', async () => {
config.rangeStrategy = 'widen';
config.currentValue = '<= 0.8.0 >= 0.5.0';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
});
it('supports > latest versions if configured', async () => {
config.respectLatest = false;
config.currentValue = '1.4.1';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'major',
isBreaking: true,
newMajor: 2,
newMinor: 0,
newPatch: 3,
newValue: '2.0.3',
newVersion: '2.0.3',
downloadUrl: 'https://registry.npmjs.org/q/-/q-2.0.3.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-01-31T08:11:47.852Z' as Timestamp,
updateType: 'major',
hasAttestation: false,
},
]);
});
it('should ignore unstable versions if the current version is stable', async () => {
config.currentValue = '2.5.16';
config.packageName = 'vue';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/vue').reply(200, vueJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toHaveLength(0);
});
it('should ignore unstable versions from datasource', async () => {
config.currentValue = '1.4.4';
config.packageName = 'some/action';
config.datasource = GithubReleasesDatasource.id;
getGithubReleases.mockResolvedValueOnce({
releases: [
{ version: '1.4.4' },
{ version: '2.0.0' },
{ version: '2.1.0', isStable: false },
],
});
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'major',
isBreaking: true,
newMajor: 2,
newMinor: 0,
newPatch: 0,
newValue: '2.0.0',
newVersion: '2.0.0',
updateType: 'major',
hasAttestation: undefined,
},
]);
});
it('should allow unstable versions in same major for node', async () => {
config.currentValue = '20.3.0';
config.packageName = 'node';
config.datasource = GithubTagsDatasource.id;
config.versioning = nodeVersioningId;
getGithubTags.mockResolvedValueOnce({
releases: [
{ version: '20.3.0' },
{ version: '20.3.1' },
{ version: '21.0.0' },
],
});
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
newMajor: 20,
newMinor: 3,
newPatch: 1,
newValue: '20.3.1',
newVersion: '20.3.1',
updateType: 'patch',
hasAttestation: undefined,
},
]);
});
it('should return pendingChecks', async () => {
config.currentValue = '1.4.4';
config.packageName = 'some/action';
config.datasource = GithubReleasesDatasource.id;
config.minimumReleaseAge = '14 days';
config.internalChecksFilter = 'strict';
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const lastWeek = new Date();
lastWeek.setDate(lastWeek.getDate() - 7);
getGithubReleases.mockResolvedValueOnce({
releases: [
{ version: '1.4.4' },
{
version: '1.4.5',
releaseTimestamp: lastWeek.toISOString() as Timestamp,
},
{
version: '1.4.6',
releaseTimestamp: yesterday.toISOString() as Timestamp,
},
],
});
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
newMajor: 1,
newMinor: 4,
newPatch: 6,
newValue: '1.4.6',
newVersion: '1.4.6',
pendingChecks: true,
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'patch',
hasAttestation: undefined,
},
]);
});
it('should return pendingVersions', async () => {
config.currentValue = '1.4.4';
config.packageName = 'some/action';
config.datasource = GithubReleasesDatasource.id;
config.minimumReleaseAge = '3 days';
config.internalChecksFilter = 'strict';
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const lastWeek = new Date();
lastWeek.setDate(lastWeek.getDate() - 7);
getGithubReleases.mockResolvedValueOnce({
releases: [
{ version: '1.4.4' },
{
version: '1.4.5',
releaseTimestamp: lastWeek.toISOString() as Timestamp,
},
{
version: '1.4.6',
releaseTimestamp: yesterday.toISOString() as Timestamp,
},
],
});
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
newMajor: 1,
newMinor: 4,
newPatch: 5,
newValue: '1.4.5',
newVersion: '1.4.5',
pendingVersions: ['1.4.6'],
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'patch',
hasAttestation: undefined,
},
]);
});
it('should allow unstable versions if the ignoreUnstable=false', async () => {
config.currentValue = '2.5.16';
config.ignoreUnstable = false;
config.respectLatest = false;
config.packageName = 'vue';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/vue').reply(200, vueJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: true,
newMajor: 2,
newMinor: 5,
newPatch: 17,
newValue: '2.5.17-beta.0',
newVersion: '2.5.17-beta.0',
newDigest:
'sha512-9BZOxpRe1TaGLS4oXyrUp3BC1dlc93js/yvnHtOovWzrkrjFMm58X+BCHrA/xgMXSM1iyRTEGoxAURyOUaK1dA==',
downloadUrl: 'https://registry.npmjs.org/vue/-/vue-2.5.17-beta.0.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'patch',
hasAttestation: false,
},
]);
});
it('should allow unstable versions if the current version is unstable', async () => {
config.currentValue = '3.1.0-dev.20180731';
config.packageName = 'typescript';
config.datasource = NpmDatasource.id;
httpMock
.scope(npmDefaultRegistryUrl)
.get('/typescript')
.reply(200, typescriptJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: true,
newMajor: 3,
newMinor: 1,
newPatch: 0,
newValue: '3.1.0-dev.20180813',
newVersion: '3.1.0-dev.20180813',
newDigest:
'sha512-3sooaeRS1KvHoZ/hE8v2VhlxojcPvHBVZ5jXi070GscZA9BeZ/sXA21Un5m9xqeXMESrWFt7onto2xZfwj2XQg==',
downloadUrl:
'https://registry.npmjs.org/typescript/-/typescript-3.1.0-dev.20180813.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'patch',
hasAttestation: false,
},
]);
});
it('should not jump unstable versions', async () => {
config.currentValue = '3.0.1-insiders.20180726';
config.packageName = 'typescript';
config.datasource = NpmDatasource.id;
httpMock
.scope(npmDefaultRegistryUrl)
.get('/typescript')
.reply(200, typescriptJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: true,
newMajor: 3,
newMinor: 0,
newPatch: 1,
newValue: '3.0.1',
newVersion: '3.0.1',
newDigest:
'sha512-zQIMOmC+372pC/CCVLqnQ0zSBiY7HHodU7mpQdjiZddek4GMj31I3dUJ7gAs9o65X7mnRma6OokOkc6f9jjfBg==',
downloadUrl:
'https://registry.npmjs.org/typescript/-/typescript-3.0.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'patch',
hasAttestation: false,
},
]);
});
it('should update pinned versions if updatePinnedDependencies=true', async () => {
config.currentValue = '0.0.34';
config.updatePinnedDependencies = true;
config.packageName = '@types/helmet';
config.datasource = NpmDatasource.id;
httpMock
.scope(npmDefaultRegistryUrl)
.get('/@types%2Fhelmet')
.reply(200, helmetJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: true,
newMajor: 0,
newMinor: 0,
newPatch: 35,
newValue: '0.0.35',
newVersion: '0.0.35',
downloadUrl:
'https://registry.npmjs.org/@types/helmet/-/helmet-0.0.35.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'patch',
hasAttestation: false,
},
]);
});
it('should not update pinned versions if updatePinnedDependencies=false', async () => {
config.currentValue = '0.0.34';
config.updatePinnedDependencies = false;
config.packageName = '@types/helmet';
config.datasource = NpmDatasource.id;
httpMock
.scope(npmDefaultRegistryUrl)
.get('/@types%2Fhelmet')
.reply(200, helmetJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
});
it('should follow dist-tag even if newer version exists', async () => {
config.currentValue = '3.0.1-insiders.20180713';
config.packageName = 'typescript';
config.datasource = NpmDatasource.id;
config.followTag = 'insiders';
httpMock
.scope(npmDefaultRegistryUrl)
.get('/typescript')
.reply(200, typescriptJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: true,
newMajor: 3,
newMinor: 0,
newPatch: 1,
newValue: '3.0.1-insiders.20180726',
newVersion: '3.0.1-insiders.20180726',
newDigest:
'sha512-fSY7XOiD1xObVp+vCamB9Hivvvu/SlUu/tzoPz+7xZGk2pqVTRFG7+1a2okxIBVV+C1ePXOR163PIjB3+L4SPA==',
downloadUrl:
'https://registry.npmjs.org/typescript/-/typescript-3.0.1-insiders.20180726.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'patch',
hasAttestation: false,
},
]);
});
it('should roll back to dist-tag if current version is higher', async () => {
config.currentValue = '3.1.0-dev.20180813';
config.packageName = 'typescript';
config.datasource = NpmDatasource.id;
config.followTag = 'insiders';
config.rollbackPrs = true;
httpMock
.scope(npmDefaultRegistryUrl)
.get('/typescript')
.reply(200, typescriptJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'rollback',
newMajor: 3,
newValue: '3.0.1-insiders.20180726',
newVersion: '3.0.1-insiders.20180726',
registryUrl: undefined,
updateType: 'rollback',
prBodyNotes: expect.arrayContaining([
expect.stringContaining(
'The version of `typescript` in use (`3.1.0-dev.20180813`)',
),
]),
},
]);
});
it('should jump unstable versions if followTag', async () => {
config.currentValue = '3.0.0-insiders.20180706';
config.packageName = 'typescript';
config.datasource = NpmDatasource.id;
config.followTag = 'insiders';
httpMock
.scope(npmDefaultRegistryUrl)
.get('/typescript')
.reply(200, typescriptJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: true,
newMajor: 3,
newMinor: 0,
newPatch: 1,
newValue: '3.0.1-insiders.20180726',
newVersion: '3.0.1-insiders.20180726',
newDigest:
'sha512-fSY7XOiD1xObVp+vCamB9Hivvvu/SlUu/tzoPz+7xZGk2pqVTRFG7+1a2okxIBVV+C1ePXOR163PIjB3+L4SPA==',
downloadUrl:
'https://registry.npmjs.org/typescript/-/typescript-3.0.1-insiders.20180726.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'patch',
hasAttestation: false,
},
]);
});
it('should update nothing if current version is dist-tag', async () => {
config.currentValue = '3.0.1-insiders.20180726';
config.packageName = 'typescript';
config.datasource = NpmDatasource.id;
config.followTag = 'insiders';
httpMock
.scope(npmDefaultRegistryUrl)
.get('/typescript')
.reply(200, typescriptJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
});
it('should warn if no version matches dist-tag', async () => {
config.currentValue = '3.0.1-dev.20180726';
config.packageName = 'typescript';
config.datasource = NpmDatasource.id;
config.followTag = 'foo';
httpMock
.scope(npmDefaultRegistryUrl)
.get('/typescript')
.reply(200, typescriptJson);
const { updates, warnings } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
expect(warnings).toEqual([
{
message: "Can't find version with tag foo for npm package typescript",
topic: 'typescript',
},
]);
});
it('should warn if no digest could be found but there is a current digest', async () => {
config.currentValue = 'v1.0.0';
config.currentDigest = 'bla';
config.digestOneAndOnly = true;
config.packageName = 'angular/angular';
config.datasource = GithubTagsDatasource.id;
// Only mock calls once so that the second invocation results in
// no digest being computable.
getGithubReleases.mockResolvedValueOnce({ releases: [] });
getGithubTags.mockResolvedValueOnce({
releases: [
{
version: 'v2.0.0',
gitRef: 'v2.0.0',
releaseTimestamp: '2022-01-01' as Timestamp,
},
],
});
const { updates, warnings } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
expect(warnings).toEqual([
{
message:
'Could not determine new digest for update (github-tags package angular/angular)',
topic: 'angular/angular',
},
]);
});
describe('digest managed externally', () => {
it('does not create a pinDigest update', async () => {
config.currentValue = 'v1.0.0';
config.packageName = 'angular/angular';
config.pinDigests = true;
// for instance, if a lockfile manages the digest instead of the package file
config.digestManagedExternally = true;
config.datasource = GithubTagsDatasource.id;
getGithubTags.mockResolvedValueOnce({
releases: [
{
version: 'v1.0.0',
gitRef: 'v1.0.0',
releaseTimestamp: '2022-01-01' as Timestamp,
},
],
});
const getDigest = vi.spyOn(GithubTagsDatasource.prototype, 'getDigest');
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
expect(getDigest).not.toHaveBeenCalled();
});
it('still refreshes a digest that is already inline', async () => {
config.currentValue = 'v1.0.0';
config.currentDigest = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
config.packageName = 'angular/angular';
config.pinDigests = true;
config.digestManagedExternally = true;
config.datasource = GithubTagsDatasource.id;
getGithubTags.mockResolvedValueOnce({
releases: [
{
version: 'v1.0.0',
gitRef: 'v1.0.0',
releaseTimestamp: '2022-01-01' as Timestamp,
},
],
});
vi.spyOn(
GithubTagsDatasource.prototype,
'getDigest',
).mockResolvedValueOnce('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb');
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toMatchObject([{ updateType: 'digest' }]);
});
});
describe('pinning enabled but no existing digest', () => {
it('should not warn if no new digest could be found', async () => {
config.currentValue = 'v1.0.0';
config.digestOneAndOnly = true;
config.packageName = 'angular/angular';
config.pinDigests = true;
config.datasource = GithubTagsDatasource.id;
// Only mock calls once so that the second invocation results in
// no digest being computable.
getGithubReleases.mockResolvedValueOnce({ releases: [] });
getGithubTags.mockResolvedValueOnce({
releases: [
{
version: 'v2.0.0',
gitRef: 'v2.0.0',
releaseTimestamp: '2022-01-01' as Timestamp,
},
],
});
const { updates, warnings } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
expect(warnings).toBeEmptyArray();
});
});
it('should use registry of update to determine digest', async () => {
config.currentValue = 'v1.0.0';
config.registryUrls = [
'https://github.enterprise.com',
'https://github.com',
];
config.digestOneAndOnly = true;
config.packageName = 'angular/angular';
config.pinDigests = true;
config.datasource = GithubTagsDatasource.id;
getGithubTags.mockRejectedValueOnce(
new Error('Not contained in registry'),
);
getGithubTags.mockResolvedValueOnce({
releases: [
{
version: 'v1.0.0',
gitRef: 'v1.0.0',
releaseTimestamp: '2022-01-01' as Timestamp,
},
],
});
const getGithubTagsDigest = vi
.spyOn(GithubTagsDatasource.prototype, 'getDigest')
.mockResolvedValueOnce('digest1234');
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
isPinDigest: true,
newDigest: 'digest1234',
newValue: 'v1.0.0',
updateType: 'pinDigest',
},
]);
expect(getGithubTagsDigest).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({ registryUrl: 'https://github.com' }),
'v1.0.0',
);
});
it('should treat zero zero tilde ranges as 0.0.x', async () => {
config.rangeStrategy = 'replace';
config.currentValue = '~0.0.34';
config.packageName = '@types/helmet';
config.datasource = NpmDatasource.id;
httpMock
.scope(npmDefaultRegistryUrl)
.get('/@types%2Fhelmet')
.reply(200, helmetJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
});
it('should treat zero zero caret ranges as pinned', async () => {
config.rangeStrategy = 'replace';
config.currentValue = '^0.0.34';
config.packageName = '@types/helmet';
config.datasource = NpmDatasource.id;
httpMock
.scope(npmDefaultRegistryUrl)
.get('/@types%2Fhelmet')
.reply(200, helmetJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: true,
isRange: true,
newMajor: 0,
newMinor: 0,
newPatch: 35,
newValue: '^0.0.35',
newVersion: '0.0.35',
downloadUrl:
'https://registry.npmjs.org/@types/helmet/-/helmet-0.0.35.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2017-04-27T16:59:06.479Z' as Timestamp,
updateType: 'patch',
hasAttestation: false,
},
]);
});
it('should downgrade from missing versions', async () => {
config.currentValue = '1.16.1';
config.packageName = 'coffeelint';
config.datasource = NpmDatasource.id;
config.rollbackPrs = true;
httpMock
.scope(npmDefaultRegistryUrl)
.get('/coffeelint')
.reply(200, coffeelintJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'rollback',
newMajor: 1,
newValue: '1.16.0',
newVersion: '1.16.0',
registryUrl: undefined,
updateType: 'rollback',
prBodyNotes: expect.arrayContaining([
expect.stringContaining(
'The version of `coffeelint` in use (`1.16.1`)',
),
]),
},
]);
});
it('should upgrade to only one major', async () => {
config.currentValue = '1.0.0';
config.packageName = 'webpack';
config.datasource = NpmDatasource.id;
httpMock
.scope(npmDefaultRegistryUrl)
.get('/webpack')
.reply(200, webpackJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
newMajor: 1,
newMinor: 15,
newPatch: 0,
newValue: '1.15.0',
newVersion: '1.15.0',
downloadUrl:
'https://registry.npmjs.org/webpack/-/webpack-1.15.0.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'minor',
hasAttestation: false,
},
{
bucket: 'major',
isBreaking: true,
newMajor: 3,
newMinor: 8,
newPatch: 1,
newValue: '3.8.1',
newVersion: '3.8.1',
newDigest:
'sha512-5ZXLWWsMqHKFr5y0N3Eo5IIisxeEeRAajNq4mELb/WELOR7srdbQk2N5XiyNy2A/AgvlR3AmeBCZJW8lHrolbw==',
downloadUrl: 'https://registry.npmjs.org/webpack/-/webpack-3.8.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'major',
hasAttestation: false,
},
]);
});
it('should upgrade to two majors', async () => {
config.currentValue = '1.0.0';
config.separateMultipleMajor = true;
config.packageName = 'webpack';
config.datasource = NpmDatasource.id;
httpMock
.scope(npmDefaultRegistryUrl)
.get('/webpack')
.reply(200, webpackJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
newMajor: 1,
newMinor: 15,
newPatch: 0,
newValue: '1.15.0',
newVersion: '1.15.0',
downloadUrl:
'https://registry.npmjs.org/webpack/-/webpack-1.15.0.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'minor',
hasAttestation: false,
},
{
bucket: 'v2',
isBreaking: true,
newMajor: 2,
newMinor: 7,
newPatch: 0,
newValue: '2.7.0',
newVersion: '2.7.0',
newDigest:
'sha512-MjAA0ZqO1ba7ZQJRnoCdbM56mmFpipOPUv/vQpwwfSI42p5PVDdoiuK2AL2FwFUVgT859Jr43bFZXRg/LNsqvg==',
downloadUrl: 'https://registry.npmjs.org/webpack/-/webpack-2.7.0.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'major',
hasAttestation: false,
},
{
bucket: 'v3',
isBreaking: true,
newMajor: 3,
newMinor: 8,
newPatch: 1,
newValue: '3.8.1',
newVersion: '3.8.1',
newDigest:
'sha512-5ZXLWWsMqHKFr5y0N3Eo5IIisxeEeRAajNq4mELb/WELOR7srdbQk2N5XiyNy2A/AgvlR3AmeBCZJW8lHrolbw==',
downloadUrl: 'https://registry.npmjs.org/webpack/-/webpack-3.8.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'major',
hasAttestation: false,
},
]);
});
it('should upgrade to 16 minors', async () => {
config.currentValue = '1.0.0';
config.separateMultipleMinor = true;
config.packageName = 'webpack';
config.datasource = NpmDatasource.id;
httpMock
.scope(npmDefaultRegistryUrl)
.get('/webpack')
.reply(200, webpackJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toHaveLength(16);
});
it('does not jump major unstable', async () => {
config.currentValue = '^4.4.0-canary.3';
config.rangeStrategy = 'replace';
config.packageName = 'next';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/next').reply(200, nextJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
});
it('supports in-range caret updates', async () => {
config.rangeStrategy = 'bump';
config.currentValue = '^1.0.0';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
isBump: true,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '^1.4.1',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('supports in-range tilde updates', async () => {
config.rangeStrategy = 'bump';
config.currentValue = '~1.0.0';
config.packageName = 'q';
config.separateMinorPatch = true;
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'patch',
isBreaking: false,
isBump: true,
isRange: true,
newMajor: 1,
newMinor: 0,
newPatch: 1,
newValue: '~1.0.1',
newVersion: '1.0.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.0.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2014-03-11T18:47:17.560Z' as Timestamp,
updateType: 'patch',
hasAttestation: false,
},
{
bucket: 'minor',
isBreaking: false,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '~1.4.1',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('supports in-range tilde patch updates', async () => {
config.rangeStrategy = 'bump';
config.currentValue = '~1.0.0';
config.packageName = 'q';
config.separateMinorPatch = true;
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'patch',
hasAttestation: false,
isBreaking: false,
isBump: true,
isRange: true,
newMajor: 1,
newMinor: 0,
newPatch: 1,
newValue: '~1.0.1',
newVersion: '1.0.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.0.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2014-03-11T18:47:17.560Z' as Timestamp,
updateType: 'patch',
},
{
bucket: 'minor',
hasAttestation: false,
isBreaking: false,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '~1.4.1',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'minor',
},
]);
});
it('supports in-range gte updates', async () => {
config.rangeStrategy = 'bump';
config.currentValue = '>=1.0.0';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
hasAttestation: false,
isBreaking: false,
isBump: true,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '>=1.4.1',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'minor',
},
]);
});
it('supports majorgte updates', async () => {
config.rangeStrategy = 'bump';
config.currentValue = '>=0.9.0';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
config.separateMajorMinor = false;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'latest',
hasAttestation: false,
isBreaking: true,
isBump: true,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '>=1.4.1',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'major',
},
]);
});
it('rejects in-range unsupported operator', async () => {
config.rangeStrategy = 'bump';
config.currentValue = '>1.0.0';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
});
it('rejects non-fully specified in-range updates', async () => {
config.rangeStrategy = 'update-lockfile';
config.currentValue = '1.x';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
});
it('rejects complex range in-range updates', async () => {
config.rangeStrategy = 'bump';
config.currentValue = '^0.9.0 || ^1.0.0';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
});
it('replaces non-range in-range updates', async () => {
config.packageName = 'q';
config.datasource = NpmDatasource.id;
config.packageFile = 'package.json';
config.rangeStrategy = 'bump';
config.currentValue = '1.0.0';
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
hasAttestation: false,
isBreaking: false,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '1.4.1',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'minor',
},
]);
});
it('handles github 404', async () => {
config.packageName = 'foo';
config.datasource = GithubTagsDatasource.id;
config.packageFile = 'package.json';
config.currentValue = '1.0.0';
httpMock.scope('https://pypi.org').get('/pypi/foo/json').reply(404);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
});
it('handles pypi 404', async () => {
config.packageName = 'foo';
config.datasource = PypiDatasource.id;
config.packageFile = 'requirements.txt';
config.currentValue = '1.0.0';
httpMock
.scope('https://api.github.com')
.get('/repos/some/repo/git/refs/tags?per_page=100')
.reply(404);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
});
it('handles packagist', async () => {
config.packageName = 'foo/bar';
config.datasource = PackagistDatasource.id;
config.packageFile = 'composer.json';
config.currentValue = '1.0.0';
config.registryUrls = ['https://repo.packagist.org'];
httpMock
.scope('https://repo.packagist.org')
.get('/packages.json')
.reply(200, { 'metadata-url': '/p2/%package%.json' })
.get('/p2/foo/bar.json')
.reply(404);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
});
it('handles unknown datasource', async () => {
config.packageName = 'foo';
config.datasource = 'typo';
config.packageFile = 'package.json';
config.currentValue = '1.0.0';
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
});
it('handles PEP440', async () => {
config.manager = 'pip_requirements';
config.versioning = pep440VersioningId;
config.manager = 'pip_requirements';
config.versioning = 'pep440';
config.rangeStrategy = 'pin';
config.lockedVersion = '0.9.4';
config.currentValue = '~=0.9';
config.packageName = 'q';
// TODO: we are using npm as source to test pep440 (#9721)
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
isPin: true,
newMajor: 0,
newValue: '==0.9.4',
newVersion: '0.9.4',
updateType: 'pin',
},
{
bucket: 'major',
hasAttestation: false,
isBreaking: true,
isRange: true,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '~=1.4',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2015-05-17T04:25:07.299Z' as Timestamp,
updateType: 'major',
},
]);
});
it('returns complex object', async () => {
config.currentValue = '1.3.0';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(res).toMatchObject({
currentVersion: '1.3.0',
currentVersionTimestamp: '2015-04-26T16:42:11.311Z' as Timestamp,
fixedVersion: '1.3.0',
isSingleVersion: true,
registryUrl: npmDefaultRegistryUrl,
sourceUrl: 'https://github.com/kriskowal/q',
updates: [
{
bucket: 'non-major',
isBreaking: false,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '1.4.1',
newVersion: '1.4.1',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.4.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'minor',
},
],
versioning: 'npm',
warnings: [],
});
});
it('prefers lockedVersion', async () => {
// Contrived test to check that lockedVersion is preferred over currentValue
config.currentValue = '1.3.0';
config.lockedVersion = '1.4.1';
config.rangeStrategy = 'update-lockfile';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(res.currentVersion).toEqual('1.4.1');
expect(res.updates).toHaveLength(0);
});
it('uses lockedVersion timestamp when lockedVersion differs from currentVersion', async () => {
config.currentValue = '^1.0.0';
config.lockedVersion = '1.0.0';
config.rangeStrategy = 'replace';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
httpMock.scope('https://registry.npmjs.org').get('/q').reply(200, qJson);
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
// currentVersion should be the lockedVersion
expect(res.currentVersion).toBe('1.0.0');
// currentVersionTimestamp should be 1.0.0's timestamp, not 1.4.1's
expect(res.currentVersionTimestamp).toBe(
'2014-01-04T17:39:35.181Z' as Timestamp,
);
});
it('ignores deprecated when it is not the latest', async () => {
config.currentValue = '1.3.0';
config.packageName = 'q2';
config.datasource = NpmDatasource.id;
const returnJson = JSON.parse(JSON.stringify(qJson));
returnJson.name = 'q2';
// mark latest minor as deprecated
returnJson.versions['1.4.1'].deprecated = 'true';
// make sure latest release isn't the one deprecated as otherwise every release is deprecated
returnJson['dist-tags'].latest = '2.0.3';
httpMock.scope(npmDefaultRegistryUrl).get('/q2').reply(200, returnJson);
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(res).toMatchObject({
currentVersion: '1.3.0',
currentVersionTimestamp: '2015-04-26T16:42:11.311Z' as Timestamp,
fixedVersion: '1.3.0',
isSingleVersion: true,
registryUrl: npmDefaultRegistryUrl,
sourceUrl: 'https://github.com/kriskowal/q',
updates: [
{
bucket: 'non-major',
isBreaking: false,
newMajor: 1,
newMinor: 4,
newPatch: 0,
newValue: '1.4.0',
newVersion: '1.4.0',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'minor',
},
{
bucket: 'major',
isBreaking: true,
newMajor: 2,
newMinor: 0,
newPatch: 3,
newValue: '2.0.3',
newVersion: '2.0.3',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'major',
},
],
versioning: 'npm',
warnings: [],
});
});
it('treats all versions as deprecated if latest is deprecated', async () => {
config.currentValue = '1.3.0';
config.packageName = 'q3';
config.datasource = NpmDatasource.id;
const returnJson = {
...JSON.parse(JSON.stringify(qJson)),
name: 'q3',
deprecated: true,
repository: { url: null, directory: 'test' },
};
returnJson.versions['1.4.1'].deprecated = 'true';
httpMock.scope(npmDefaultRegistryUrl).get('/q3').reply(200, returnJson);
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(res).toMatchObject({
currentVersion: '1.3.0',
currentVersionTimestamp: '2015-04-26T16:42:11.311Z' as Timestamp,
deprecationMessage: codeBlock`
On registry \`${npmDefaultRegistryUrl}\`, the "latest" version of dependency \`q3\` has the following deprecation notice:
\`true\`
Marking the latest version of an npm package as deprecated results in the entire package being considered deprecated, so contact the package author you think this is a mistake.
`,
fixedVersion: '1.3.0',
isSingleVersion: true,
registryUrl: npmDefaultRegistryUrl,
sourceDirectory: 'test',
sourceUrl: 'https://github.com/kriskowal/q',
updates: [
{
bucket: 'non-major',
isBreaking: false,
newMajor: 1,
newMinor: 4,
newPatch: 1,
newValue: '1.4.1',
newVersion: '1.4.1',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'minor',
},
],
versioning: 'npm',
warnings: [],
});
});
it('skips unsupported values', async () => {
config.currentValue = 'alpine';
config.packageName = 'node';
config.datasource = DockerDatasource.id;
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(res).toEqual({
skipReason: 'invalid-value',
updates: [],
versioning: 'npm',
warnings: [],
});
});
it('skips undefined values', async () => {
config.packageName = 'node';
config.datasource = DockerDatasource.id;
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(res).toEqual({
skipReason: 'invalid-value',
updates: [],
versioning: 'npm',
warnings: [],
});
});
it('handles digest pin', async () => {
config.currentValue = '8.0.0';
config.packageName = 'node';
config.datasource = DockerDatasource.id;
config.pinDigests = true;
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: '8.0.0',
},
{
version: '8.1.0',
},
],
});
getDockerDigest.mockResolvedValueOnce('sha256:abcdef1234567890');
getDockerDigest.mockResolvedValueOnce('sha256:0123456789abcdef');
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(res).toEqual({
currentVersion: '8.0.0',
fixedVersion: '8.0.0',
isSingleVersion: true,
registryUrl: 'https://index.docker.io',
sourceUrl: 'https://github.com/nodejs/node',
updates: [
{
bucket: 'non-major',
isBreaking: false,
newDigest: 'sha256:abcdef1234567890',
newMajor: 8,
newMinor: 1,
newPatch: 0,
newValue: '8.1.0',
newVersion: '8.1.0',
updateType: 'minor',
},
{
isPinDigest: true,
newDigest: 'sha256:0123456789abcdef',
newValue: '8.0.0',
updateType: 'pinDigest',
},
],
versioning: 'npm',
warnings: [],
});
});
it('skips uncompatible versions for 8.1.0', async () => {
config.currentValue = '8.1.0';
config.packageName = 'node';
config.versioning = dockerVersioningId;
config.datasource = DockerDatasource.id;
getDockerReleases.mockResolvedValueOnce({
releases: [
{ version: '8.1.0' },
{ version: '8.1.5' },
{ version: '8.1' },
{ version: '8.2.0' },
{ version: '8.2.5', newDigest: 'abc123' },
{ version: '8.2' },
{ version: '8' },
{ version: '9.0' },
{ version: '9' },
],
});
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(res).toEqual({
currentVersion: '8.1.0',
fixedVersion: '8.1.0',
isSingleVersion: true,
registryUrl: 'https://index.docker.io',
sourceUrl: 'https://github.com/nodejs/node',
updates: [
{
bucket: 'non-major',
isBreaking: false,
newMajor: 8,
newMinor: 2,
newPatch: 5,
newValue: '8.2.5',
newVersion: '8.2.5',
updateType: 'minor',
},
],
versioning: 'docker',
warnings: [],
});
});
it('skips uncompatible versions for 8.1', async () => {
config.currentValue = '8.1';
config.packageName = 'node';
config.versioning = dockerVersioningId;
config.datasource = DockerDatasource.id;
getDockerReleases.mockResolvedValueOnce({
registryUrl: 'https://index.docker.io',
releases: [
{ version: '8.1.0' },
{ version: '8.1.5' },
{ version: '8.1' },
{ version: '8.2.0' },
{ version: '8.2.5' },
{ version: '8.2' },
{ version: '8' },
{ version: '9.0', registryUrl: 'https://other.registry' },
{ version: '9' },
],
});
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(res).toEqual({
currentVersion: '8.1',
fixedVersion: '8.1',
isSingleVersion: true,
registryUrl: 'https://index.docker.io',
sourceUrl: 'https://github.com/nodejs/node',
updates: [
{
bucket: 'non-major',
isBreaking: false,
newMajor: 8,
newMinor: 2,
newPatch: null,
newValue: '8.2',
newVersion: '8.2',
updateType: 'minor',
},
{
bucket: 'major',
isBreaking: true,
newMajor: 9,
newMinor: 0,
newPatch: null,
newValue: '9.0',
newVersion: '9.0',
registryUrl: 'https://other.registry',
updateType: 'major',
},
],
versioning: 'docker',
warnings: [],
});
});
it('skips uncompatible versions for 8', async () => {
config.currentValue = '8';
config.packageName = 'node';
config.versioning = dockerVersioningId;
config.datasource = DockerDatasource.id;
getDockerReleases.mockResolvedValueOnce({
releases: [
{ version: '8.1.0' },
{ version: '8.1.5' },
{ version: '8.1' },
{ version: '8.2.0' },
{ version: '8.2.5' },
{ version: '8.2' },
{ version: '8' },
{ version: '9.0' },
{ version: '9' },
],
});
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(res).toEqual({
currentVersion: '8',
fixedVersion: '8',
isSingleVersion: true,
registryUrl: 'https://index.docker.io',
sourceUrl: 'https://github.com/nodejs/node',
updates: [
{
bucket: 'major',
isBreaking: true,
newMajor: 9,
newMinor: null,
newPatch: null,
newValue: '9',
newVersion: '9',
updateType: 'major',
},
],
versioning: 'docker',
warnings: [],
});
});
it('preserves currentCompatibility after lookup', async () => {
config.currentValue = 'distroless-v1.38.3';
config.packageName = 'envoyproxy/envoy';
config.versioning = semverVersioningId;
config.versionCompatibility = '^(?<compatibility>.*)-(?<version>.*)$';
config.datasource = DockerDatasource.id;
getDockerReleases.mockResolvedValueOnce({
releases: [
{ version: 'distroless-v1.38.3' },
{ version: 'distroless-v1.39.0' },
{ version: 'v1.40.0' },
],
});
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(res).toMatchObject({
currentCompatibility: 'distroless',
currentVersion: 'v1.38.3',
updates: [
{
newValue: 'distroless-v1.39.0',
newVersion: 'v1.39.0',
},
],
});
});
it('applies versionCompatibility for 18.10.0', async () => {
config.currentValue = '18.10.0-alpine';
config.currentDigest = 'aaa111';
config.packageName = 'node';
config.versioning = nodeVersioningId;
config.versionCompatibility = '^(?<version>[^-]+)(?<compatibility>-.*)?$';
config.datasource = DockerDatasource.id;
getDockerReleases.mockResolvedValueOnce({
releases: [
{ version: '18.10.0' },
{ version: '18.18.0' },
{ version: '18.19.0-alpine' },
{ version: '18.20.0' },
],
});
getDockerDigest.mockResolvedValueOnce('bbb222');
getDockerDigest.mockResolvedValueOnce('ccc333');
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(getDockerDigest.mock.calls).toEqual([
[
{
currentDigest: 'aaa111',
currentValue: '18.10.0-alpine',
packageName: 'node',
registryUrl: 'https://index.docker.io',
},
'18.19.0-alpine',
],
[
{
currentDigest: 'aaa111',
currentValue: '18.10.0-alpine',
packageName: 'node',
registryUrl: 'https://index.docker.io',
},
'18.10.0-alpine',
],
]);
expect(res).toEqual({
currentCompatibility: '-alpine',
currentVersion: '18.10.0',
fixedVersion: '18.10.0',
isSingleVersion: true,
registryUrl: 'https://index.docker.io',
sourceUrl: 'https://github.com/nodejs/node',
updates: [
{
bucket: 'non-major',
isBreaking: false,
newDigest: 'bbb222',
newMajor: 18,
newMinor: 19,
newPatch: 0,
newValue: '18.19.0-alpine',
newVersion: '18.19.0',
updateType: 'minor',
},
{
newDigest: 'ccc333',
newValue: '18.10.0-alpine',
updateType: 'digest',
},
],
versioning: 'node',
warnings: [],
});
});
it('applies versionCompatibility for maven', async () => {
config.currentValue = '12.4.2.jre8';
config.packageName = 'com.microsoft.sqlserver:mssql-jdbc';
config.versioning = mavenVersioningId;
config.versionCompatibility =
'^(?<version>.*)(?<compatibility>\\.jre.*)$';
config.datasource = MavenDatasource.id;
getMavenReleases.mockResolvedValueOnce({
releases: [
{ version: '12.4.2.jre8' },
{ version: '12.5.0.jre11' },
{ version: '12.6.1.jre8' },
{ version: '12.6.1.jre11' },
{ version: '12.6.2.jre11' },
],
});
postprocessMavenRelease.mockImplementationOnce((_, x) =>
Promise.resolve(x),
);
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(res).toMatchObject({
currentVersion: '12.4.2',
updates: [
{
bucket: 'non-major',
isBreaking: false,
newValue: '12.6.1.jre8',
newVersion: '12.6.1',
updateType: 'minor',
},
],
versioning: 'maven',
warnings: [],
});
});
it('handles versionCompatibility mismatch', async () => {
config.currentValue = '18.10.0-alpine';
config.packageName = 'node';
config.versioning = nodeVersioningId;
config.versionCompatibility = '^(?<version>[^-]+)-slim$';
config.datasource = DockerDatasource.id;
getDockerReleases.mockResolvedValueOnce({
releases: [
{ version: '18.18.0' },
{ version: '18.19.0-alpine' },
{ version: '18.20.0' },
],
});
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(res).toEqual({
registryUrl: 'https://index.docker.io',
sourceUrl: 'https://github.com/nodejs/node',
updates: [],
versioning: 'node',
warnings: [],
});
});
it('applies versionCompatibility for debian codenames with suffix', async () => {
config.currentValue = 'bullseye-slim';
config.packageName = 'debian';
config.versioning = debianVersioningId;
config.versionCompatibility = '^(?<version>[^-]+)(?<compatibility>-.*)?$';
config.datasource = DockerDatasource.id;
getDockerReleases.mockResolvedValueOnce({
releases: [
{ version: 'bullseye' },
{ version: 'bullseye-slim' },
{ version: 'bookworm' },
{ version: 'bookworm-slim' },
],
});
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(res).toEqual({
currentCompatibility: '-slim',
currentVersion: 'bullseye',
fixedVersion: 'bullseye',
isSingleVersion: true,
registryUrl: 'https://index.docker.io',
updates: [
{
bucket: 'major',
isBreaking: true,
newMajor: 12,
newMinor: null,
newPatch: null,
newValue: 'bookworm-slim',
newVersion: 'bookworm',
updateType: 'major',
},
],
versioning: 'debian',
warnings: [],
});
});
it('handles digest pin for up to date version', async () => {
config.currentValue = '8.1.0';
config.packageName = 'node';
config.datasource = DockerDatasource.id;
config.pinDigests = true;
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: '8.0.0',
},
{
version: '8.1.0',
},
],
});
getDockerDigest.mockResolvedValueOnce('sha256:abcdef1234567890');
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(res).toEqual({
currentVersion: '8.1.0',
fixedVersion: '8.1.0',
registryUrl: 'https://index.docker.io',
sourceUrl: 'https://github.com/nodejs/node',
updates: [
{
isPinDigest: true,
newDigest: 'sha256:abcdef1234567890',
newValue: '8.1.0',
updateType: 'pinDigest',
},
],
versioning: 'npm',
warnings: [],
});
});
it('handles pin for github actions', async () => {
config.currentValue = 'v8';
config.rangeStrategy = 'pin';
config.packageName = 'actions/checkout';
config.versioning = githubActionsVersioningId;
config.datasource = GithubTagsDatasource.id;
getGithubTags.mockResolvedValueOnce({
releases: [{ version: 'v8.0.1' }, { version: 'v8' }],
});
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(res).toEqual({
currentVersion: 'v8.0.1',
registryUrl: 'https://github.com',
updates: [
{
isPin: true,
newMajor: 8,
newValue: 'v8.0.1',
newVersion: 'v8.0.1',
updateType: 'pin',
},
],
versioning: 'github-actions',
warnings: [],
});
});
it('marks a too-new GitHub Actions major-tag digest update as pending when using `internalChecksFilter=strict`', async () => {
config.currentValue = 'v7';
config.currentDigest = fakeSha('current');
config.packageName = 'actions/checkout';
config.versioning = githubActionsVersioningId;
config.datasource = GithubTagsDatasource.id;
config.minimumReleaseAge = '3 days';
config.internalChecksFilter = 'strict';
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
getGithubTags.mockResolvedValueOnce({
releases: [
{ version: 'v7.0.0' },
{
version: 'v7.0.1',
releaseTimestamp: yesterday.toISOString() as Timestamp,
},
],
});
vi.spyOn(
GithubTagsDatasource.prototype,
'getDigest',
).mockResolvedValueOnce(fakeSha('new'));
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
updateType: 'digest',
newValue: 'v7',
newDigest: fakeSha('new'),
pendingChecks: true,
},
]);
});
// when using `rangeStrategy=bump`, this makes `currentVersion` resolve to the oldest matching release (v7.0.0)
// however, when we're performing a digest update, we want to repoint to the newest version available, so have to ignore what `rangeStrategy` is telling us
it('ages a digest update against the newest matching version, not whatever `rangeStrategy` resolves `currentVersion` to', async () => {
config.currentValue = 'v7';
config.currentDigest = fakeSha('current');
config.rangeStrategy = 'bump';
config.packageName = 'actions/checkout';
config.versioning = githubActionsVersioningId;
config.datasource = GithubTagsDatasource.id;
config.minimumReleaseAge = '3 days';
config.internalChecksFilter = 'strict';
const lastWeek = new Date();
lastWeek.setDate(lastWeek.getDate() - 7);
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
getGithubTags.mockResolvedValueOnce({
releases: [
{
version: 'v7.0.0',
releaseTimestamp: lastWeek.toISOString() as Timestamp,
},
{
version: 'v7.0.1',
releaseTimestamp: yesterday.toISOString() as Timestamp,
},
],
});
vi.spyOn(
GithubTagsDatasource.prototype,
'getDigest',
).mockResolvedValueOnce(fakeSha('new'));
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
updateType: 'digest',
newValue: 'v7',
newDigest: fakeSha('new'),
pendingChecks: true,
},
]);
});
// `rangeStrategy=update-lockfile` forces `currentVersion` to `lockedVersion` unconditionally, even though no release actually satisfies the range - so there's no "newest matching version" to age the digest update against.
// With no timestamp available, the safe default (with `minimumReleaseAgeBehaviour=timestamp-required`) is to treat it as pending
it('marks digest updates as pendingChecks when no release satisfies the current range with `rangeStrategy=update-lockfile`', async () => {
config.currentValue = '^2.0.0';
config.lockedVersion = '1.0.0';
config.rangeStrategy = 'update-lockfile';
config.currentDigest = fakeSha('current');
config.packageName = 'actions/checkout';
config.versioning = npmVersioningId;
config.datasource = GithubTagsDatasource.id;
config.minimumReleaseAge = '3 days';
config.internalChecksFilter = 'strict';
getGithubTags.mockResolvedValueOnce({
releases: [{ version: '1.0.0' }],
});
vi.spyOn(
GithubTagsDatasource.prototype,
'getDigest',
).mockResolvedValueOnce(fakeSha('new'));
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
updateType: 'digest',
newValue: '^2.0.0',
newDigest: fakeSha('new'),
pendingChecks: true,
},
]);
});
it('respects a minimumReleaseAge packageRule scoped to matchUpdateTypes=digest', async () => {
config.currentValue = 'v7';
config.currentDigest = fakeSha('current');
config.packageName = 'actions/checkout';
config.versioning = githubActionsVersioningId;
config.datasource = GithubTagsDatasource.id;
config.internalChecksFilter = 'strict';
config.packageRules = [
{ matchUpdateTypes: ['digest'], minimumReleaseAge: '3 days' },
];
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
getGithubTags.mockResolvedValueOnce({
releases: [
{ version: 'v7.0.0' },
{
version: 'v7.0.1',
releaseTimestamp: yesterday.toISOString() as Timestamp,
},
],
});
vi.spyOn(
GithubTagsDatasource.prototype,
'getDigest',
).mockResolvedValueOnce(fakeSha('new'));
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
updateType: 'digest',
newValue: 'v7',
newDigest: fakeSha('new'),
pendingChecks: true,
},
]);
});
it('respects a `digest.minimumReleaseAge` config for digest updates', async () => {
config.currentValue = 'v7';
config.currentDigest = fakeSha('current');
config.packageName = 'actions/checkout';
config.versioning = githubActionsVersioningId;
config.datasource = GithubTagsDatasource.id;
config.internalChecksFilter = 'strict';
config.digest = { minimumReleaseAge: '3 days' };
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
getGithubTags.mockResolvedValueOnce({
releases: [
{ version: 'v7.0.0' },
{
version: 'v7.0.1',
releaseTimestamp: yesterday.toISOString() as Timestamp,
},
],
});
vi.spyOn(
GithubTagsDatasource.prototype,
'getDigest',
).mockResolvedValueOnce(fakeSha('new'));
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
updateType: 'digest',
newValue: 'v7',
newDigest: fakeSha('new'),
pendingChecks: true,
},
]);
});
// Deprecated releases are only dispreferred, not unusable - a digest update repointing to one must still be aged against its timestamp, not held forever for lack of one
it('ages digest updates against a deprecated newest matching version when no non-deprecated version matches', async () => {
config.currentValue = '^1.0.0';
config.currentDigest = fakeSha('current');
config.rangeStrategy = 'replace';
config.packageName = 'actions/checkout';
config.versioning = npmVersioningId;
config.datasource = GithubTagsDatasource.id;
config.minimumReleaseAge = '3 days';
config.internalChecksFilter = 'strict';
const lastWeek = new Date();
lastWeek.setDate(lastWeek.getDate() - 7);
getGithubTags.mockResolvedValueOnce({
releases: [
{
version: '1.0.0',
isDeprecated: true,
releaseTimestamp: lastWeek.toISOString() as Timestamp,
},
],
});
vi.spyOn(
GithubTagsDatasource.prototype,
'getDigest',
).mockResolvedValueOnce(fakeSha('new'));
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
updateType: 'digest',
newValue: '^1.0.0',
newDigest: fakeSha('new'),
},
]);
});
// `followTag` narrows the candidate versions, so the digest update must be aged against the followed tag's version, not the newest release overall
it('ages digest updates against the followed tag version when `followTag` is configured', async () => {
config.currentValue = 'v7';
config.currentDigest = fakeSha('current');
config.followTag = 'stable';
config.packageName = 'actions/checkout';
config.versioning = githubActionsVersioningId;
config.datasource = GithubTagsDatasource.id;
config.minimumReleaseAge = '3 days';
config.internalChecksFilter = 'strict';
const lastWeek = new Date();
lastWeek.setDate(lastWeek.getDate() - 7);
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
getGithubTags.mockResolvedValueOnce({
tags: { stable: 'v7.0.0' },
releases: [
{
version: 'v7.0.0',
releaseTimestamp: lastWeek.toISOString() as Timestamp,
},
{
version: 'v7.0.1',
releaseTimestamp: yesterday.toISOString() as Timestamp,
},
],
});
vi.spyOn(
GithubTagsDatasource.prototype,
'getDigest',
).mockResolvedValueOnce(fakeSha('new'));
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
updateType: 'digest',
newValue: 'v7',
newDigest: fakeSha('new'),
},
]);
});
// The digest didn't change, so no update is proposed - no age check should run, and no timestamp-optional warn should fire
it('does not warn about missing releaseTimestamps when the digest is unchanged', async () => {
config.currentValue = 'v7';
config.currentDigest = fakeSha('current');
config.packageName = 'actions/checkout';
config.versioning = githubActionsVersioningId;
config.datasource = GithubTagsDatasource.id;
config.minimumReleaseAge = '3 days';
config.minimumReleaseAgeBehaviour = 'timestamp-optional';
config.internalChecksFilter = 'strict';
getGithubTags.mockResolvedValueOnce({
releases: [{ version: 'v7.0.0' }, { version: 'v7.0.1' }],
});
vi.spyOn(
GithubTagsDatasource.prototype,
'getDigest',
).mockResolvedValueOnce(fakeSha('current'));
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
expect(logger.logger.once.warn).not.toHaveBeenCalled();
});
// `currentValue` is a range, so unlike `matchUpdateTypes`, which is read from the incoming `config`, matching against `matchCurrentVersion` needs the resolved `currentVersion`
it('respects a minimumReleaseAge packageRule scoped to matchCurrentVersion for digest updates', async () => {
config.currentValue = '^1.0.0';
config.currentDigest = fakeSha('current');
config.packageName = 'some/image';
config.datasource = DockerDatasource.id;
config.internalChecksFilter = 'strict';
config.packageRules = [
{ matchCurrentVersion: '<2.0.0', minimumReleaseAge: '3 days' },
];
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
getDockerReleases.mockResolvedValueOnce({
releases: [
{ version: '1.0.0' },
{
version: '1.1.0',
releaseTimestamp: yesterday.toISOString() as Timestamp,
},
],
});
getDockerDigest.mockResolvedValueOnce(fakeSha('new'));
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
updateType: 'digest',
newValue: '^1.0.0',
newDigest: fakeSha('new'),
pendingChecks: true,
},
]);
});
it('does not mark github actions major-tag digest updates as `pendingChecks` once `minimumReleaseAge` has elapsed', async () => {
config.currentValue = 'v7';
config.currentDigest = fakeSha('current');
config.packageName = 'actions/checkout';
config.versioning = githubActionsVersioningId;
config.datasource = GithubTagsDatasource.id;
config.minimumReleaseAge = '3 days';
config.internalChecksFilter = 'strict';
const lastWeek = new Date();
lastWeek.setDate(lastWeek.getDate() - 7);
getGithubTags.mockResolvedValueOnce({
releases: [
{ version: 'v7.0.0' },
{
version: 'v7.0.1',
releaseTimestamp: lastWeek.toISOString() as Timestamp,
},
],
});
vi.spyOn(
GithubTagsDatasource.prototype,
'getDigest',
).mockResolvedValueOnce(fakeSha('new'));
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
updateType: 'digest',
newValue: 'v7',
newDigest: fakeSha('new'),
},
]);
});
it('marks github actions major-tag digest updates as `pendingChecks` when no releaseTimestamp is available (when `minimumReleaseAgeBehaviour=timestamp-required`)', async () => {
config.currentValue = 'v7';
config.currentDigest = fakeSha('current');
config.packageName = 'actions/checkout';
config.versioning = githubActionsVersioningId;
config.datasource = GithubTagsDatasource.id;
config.minimumReleaseAge = '3 days';
config.minimumReleaseAgeBehaviour = 'timestamp-required';
config.internalChecksFilter = 'strict';
getGithubTags.mockResolvedValueOnce({
releases: [{ version: 'v7.0.0' }, { version: 'v7.0.1' }],
});
vi.spyOn(
GithubTagsDatasource.prototype,
'getDigest',
).mockResolvedValueOnce(fakeSha('new'));
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
updateType: 'digest',
newValue: 'v7',
newDigest: fakeSha('new'),
pendingChecks: true,
},
]);
});
it('does not mark github actions major-tag digest updates as `pendingChecks` when no `releaseTimestamp` is available (when `minimumReleaseAgeBehaviour=timestamp-optional`)', async () => {
config.currentValue = 'v7';
config.currentDigest = fakeSha('current');
config.packageName = 'actions/checkout';
config.versioning = githubActionsVersioningId;
config.datasource = GithubTagsDatasource.id;
config.minimumReleaseAge = '3 days';
config.minimumReleaseAgeBehaviour = 'timestamp-optional';
config.internalChecksFilter = 'strict';
getGithubTags.mockResolvedValueOnce({
releases: [{ version: 'v7.0.0' }, { version: 'v7.0.1' }],
});
vi.spyOn(
GithubTagsDatasource.prototype,
'getDigest',
).mockResolvedValueOnce(fakeSha('new'));
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
updateType: 'digest',
newValue: 'v7',
newDigest: fakeSha('new'),
},
]);
// A digest update passing un-aged is exactly the event a timestamp-optional
// user needs surfaced, so it must warn - just like the version-update path in
// filterInternalChecks().
expect(logger.logger.once.warn).toHaveBeenCalledWith(
"Some release(s) did not have a releaseTimestamp, but as we're running with minimumReleaseAgeBehaviour=timestamp-optional, proceeding. See debug logs for more information",
);
});
it('does not run minimumReleaseAge checks for digest updates under `internalChecksFilter=none`', async () => {
config.currentValue = 'v7';
config.currentDigest = fakeSha('current');
config.packageName = 'actions/checkout';
config.versioning = githubActionsVersioningId;
config.datasource = GithubTagsDatasource.id;
config.minimumReleaseAge = '3 days';
config.minimumReleaseAgeBehaviour = 'timestamp-required';
config.internalChecksFilter = 'none';
// No releaseTimestamp: under `strict`/`timestamp-required` this would be held
// as pending, but `none` opts out of internal checks entirely.
getGithubTags.mockResolvedValueOnce({
releases: [{ version: 'v7.0.0' }, { version: 'v7.0.1' }],
});
vi.spyOn(
GithubTagsDatasource.prototype,
'getDigest',
).mockResolvedValueOnce(fakeSha('new'));
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
updateType: 'digest',
newValue: 'v7',
newDigest: fakeSha('new'),
},
]);
// Short-circuited like filterInternalChecks(): no age check ran, so no
// "no releaseTimestamp to age against" log noise.
expect(logger.logger.once.debug).not.toHaveBeenCalledWith(
expect.anything(),
expect.stringContaining('has no releaseTimestamp to age against'),
);
});
// The `pendingChecks` short-circuit is gated on `internalChecksFilter=strict`: under `flexible` the update proceeds, without setting a pending status
it('does not mark digest updates as `pendingChecks` under `internalChecksFilter=flexible`, even when `minimumReleaseAge` has not elapsed', async () => {
config.currentValue = 'v7';
config.currentDigest = fakeSha('current');
config.packageName = 'actions/checkout';
config.versioning = githubActionsVersioningId;
config.datasource = GithubTagsDatasource.id;
config.minimumReleaseAge = '3 days';
config.internalChecksFilter = 'flexible';
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
getGithubTags.mockResolvedValueOnce({
releases: [
{ version: 'v7.0.0' },
{
version: 'v7.0.1',
releaseTimestamp: yesterday.toISOString() as Timestamp,
},
],
});
vi.spyOn(
GithubTagsDatasource.prototype,
'getDigest',
).mockResolvedValueOnce(fakeSha('new'));
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
updateType: 'digest',
newValue: 'v7',
newDigest: fakeSha('new'),
},
]);
});
it('marks github actions major-tag `pinDigest` updates as `pendingChecks` under `internalChecksFilter=strict`', async () => {
config.currentValue = 'v7';
config.pinDigests = true;
config.packageName = 'actions/checkout';
config.versioning = githubActionsVersioningId;
config.datasource = GithubTagsDatasource.id;
config.minimumReleaseAge = '3 days';
config.internalChecksFilter = 'strict';
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
getGithubTags.mockResolvedValueOnce({
releases: [
{ version: 'v7.0.0' },
{
version: 'v7.0.1',
releaseTimestamp: yesterday.toISOString() as Timestamp,
},
],
});
vi.spyOn(
GithubTagsDatasource.prototype,
'getDigest',
).mockResolvedValueOnce(fakeSha('new'));
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
isPinDigest: true,
updateType: 'pinDigest',
newValue: 'v7',
newDigest: fakeSha('new'),
pendingChecks: true,
},
]);
});
it('does not mark github actions major-tag `pinDigest` updates as `pendingChecks` once `minimumReleaseAge` has elapsed', async () => {
config.currentValue = 'v7';
config.pinDigests = true;
config.packageName = 'actions/checkout';
config.versioning = githubActionsVersioningId;
config.datasource = GithubTagsDatasource.id;
config.minimumReleaseAge = '3 days';
config.internalChecksFilter = 'strict';
const lastWeek = new Date();
lastWeek.setDate(lastWeek.getDate() - 7);
getGithubTags.mockResolvedValueOnce({
releases: [
{ version: 'v7.0.0' },
{
version: 'v7.0.1',
releaseTimestamp: lastWeek.toISOString() as Timestamp,
},
],
});
vi.spyOn(
GithubTagsDatasource.prototype,
'getDigest',
).mockResolvedValueOnce(fakeSha('new'));
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
isPinDigest: true,
updateType: 'pinDigest',
newValue: 'v7',
newDigest: fakeSha('new'),
},
]);
});
// `rangeStrategy=bump` makes `currentVersion` (and thus `res.currentVersionTimestamp`) resolve to the oldest
// matching release (v7.0.0, which already clears `minimumReleaseAge`), but the ref itself has moved on to
// v7.0.1 - which is what actually gets pinned, and hasn't yet cleared `minimumReleaseAge`.
it('ages a `pinDigest` update against the newest matching version, not whatever `rangeStrategy` resolves `currentVersion` to', async () => {
config.currentValue = 'v7';
config.pinDigests = true;
config.rangeStrategy = 'bump';
config.packageName = 'actions/checkout';
config.versioning = githubActionsVersioningId;
config.datasource = GithubTagsDatasource.id;
config.minimumReleaseAge = '3 days';
config.internalChecksFilter = 'strict';
const lastWeek = new Date();
lastWeek.setDate(lastWeek.getDate() - 7);
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
getGithubTags.mockResolvedValueOnce({
releases: [
{
version: 'v7.0.0',
releaseTimestamp: lastWeek.toISOString() as Timestamp,
},
{
version: 'v7.0.1',
releaseTimestamp: yesterday.toISOString() as Timestamp,
},
],
});
vi.spyOn(
GithubTagsDatasource.prototype,
'getDigest',
).mockResolvedValueOnce(fakeSha('new'));
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
isPinDigest: true,
updateType: 'pinDigest',
newValue: 'v7',
newDigest: fakeSha('new'),
pendingChecks: true,
},
]);
});
// Unlike `digest`, an unversioned `currentValue` (e.g. `latest`) is exempt from `minimumReleaseAge` entirely for `pinDigest`:
// pinning a ref that already floats to latest is strictly safer, so holding it would only prolong the un-pinned state.
it('does not hold `pinDigest` updates for unversioned `currentValue`s (e.g. `latest`)', async () => {
config.currentValue = 'alpine';
config.packageName = 'node';
config.datasource = DockerDatasource.id;
config.pinDigests = true;
config.minimumReleaseAge = '3 days';
config.internalChecksFilter = 'strict';
getDockerReleases.mockResolvedValueOnce({
releases: [
{ version: 'alpine' },
{ version: '8.0.0' },
{ version: '8.1.0' },
],
});
getDockerDigest.mockResolvedValueOnce('sha256:abcdef1234567890');
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
isPinDigest: true,
newDigest: 'sha256:abcdef1234567890',
newValue: 'alpine',
updateType: 'pinDigest',
},
]);
// Short-circuited: no age check ran, so no "no releaseTimestamp to age against" log noise.
expect(logger.logger.once.debug).not.toHaveBeenCalledWith(
expect.anything(),
expect.stringContaining('has no releaseTimestamp to age against'),
);
});
it('handles no fitting version and no version in lock file', async () => {
config.currentValue = '~9.5.0';
config.packageName = 'typo3/cms-saltedpasswords';
config.datasource = DockerDatasource.id;
config.versioning = composerVersioningId;
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: '8.0.0',
},
{
version: '8.1.0',
},
],
});
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(res).toEqual({
registryUrl: 'https://index.docker.io',
skipReason: 'invalid-value',
updates: [],
versioning: 'composer',
warnings: [],
});
});
it('handles digest pin for non-version', async () => {
config.currentValue = 'alpine';
config.packageName = 'node';
config.datasource = DockerDatasource.id;
config.pinDigests = true;
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: '8.0.0',
},
{
version: '8.1.0',
},
{
version: 'alpine',
},
],
});
getDockerDigest.mockResolvedValueOnce('sha256:abcdef1234567890');
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(res).toEqual({
updates: [
{
isPinDigest: true,
newDigest: 'sha256:abcdef1234567890',
newValue: 'alpine',
updateType: 'pinDigest',
},
],
versioning: 'npm',
warnings: [],
});
});
it('handles digest lookup failure', async () => {
config.currentValue = 'alpine';
config.packageName = 'node';
config.datasource = DockerDatasource.id;
config.pinDigests = true;
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: '8.0.0',
},
{
version: '8.1.0',
},
{
version: 'alpine',
},
],
});
getDockerDigest.mockResolvedValueOnce(null);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
});
it('handles digest update', async () => {
config.currentValue = '8.0.0';
config.packageName = 'node';
config.datasource = DockerDatasource.id;
config.currentDigest = 'sha256:zzzzzzzzzzzzzzz';
config.pinDigests = true;
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: '8.0.0',
newDigest: 'sha256:0123456789abcdef',
},
{
version: '8.1.0',
},
],
});
getDockerDigest.mockResolvedValueOnce('sha256:abcdef1234567890');
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(res).toEqual({
currentVersion: '8.0.0',
fixedVersion: '8.0.0',
isSingleVersion: true,
registryUrl: 'https://index.docker.io',
sourceUrl: 'https://github.com/nodejs/node',
updates: [
{
bucket: 'non-major',
isBreaking: false,
newDigest: 'sha256:abcdef1234567890',
newMajor: 8,
newMinor: 1,
newPatch: 0,
newValue: '8.1.0',
newVersion: '8.1.0',
updateType: 'minor',
},
{
newDigest: 'sha256:0123456789abcdef',
newValue: '8.0.0',
updateType: 'digest',
},
],
versioning: 'npm',
warnings: [],
});
});
it('handles digest update for custom datasource', async () => {
config.currentValue = '1.0.0';
config.packageName = 'my-package';
config.datasource = CustomDatasource.id;
config.currentDigest = 'zzzzzzzzzzzzzzz';
getCustomDatasourceReleases.mockResolvedValueOnce({
releases: [
{
version: '1.0.0',
newDigest: '0123456789abcdef',
},
],
});
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
newDigest: '0123456789abcdef',
newValue: '1.0.0',
updateType: 'digest',
},
]);
});
it('handles digest update for non-version', async () => {
config.currentValue = 'alpine';
config.packageName = 'node';
config.datasource = DockerDatasource.id;
config.currentDigest = 'sha256:zzzzzzzzzzzzzzz';
config.pinDigests = true;
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: 'alpine',
},
{
version: '8.0.0',
},
{
version: '8.1.0',
},
],
});
getDockerDigest.mockResolvedValueOnce('sha256:abcdef1234567890');
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(res).toEqual({
updates: [
{
newDigest: 'sha256:abcdef1234567890',
newValue: 'alpine',
updateType: 'digest',
},
],
versioning: 'npm',
warnings: [],
});
});
// An unversioned tag like `alpine` has no versioned release to age against. As we're repointing to newer content we apply `minimumReleaseAge`, but as there's no timestamp, this needs to be held
it('marks digest updates for unversioned `currentValue`s as `pendingChecks` under `timestamp-required`', async () => {
config.currentValue = 'alpine';
config.packageName = 'node';
config.datasource = DockerDatasource.id;
config.currentDigest = 'sha256:zzzzzzzzzzzzzzz';
config.pinDigests = true;
config.minimumReleaseAge = '3 days';
config.internalChecksFilter = 'strict';
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: 'alpine',
},
{
version: '8.0.0',
},
{
version: '8.1.0',
},
],
});
getDockerDigest.mockResolvedValueOnce('sha256:abcdef1234567890');
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
newDigest: 'sha256:abcdef1234567890',
newValue: 'alpine',
updateType: 'digest',
pendingChecks: true,
},
]);
});
it('lets digest updates for unversioned `currentValue`s through under `timestamp-optional`', async () => {
config.currentValue = 'alpine';
config.packageName = 'node';
config.datasource = DockerDatasource.id;
config.currentDigest = 'sha256:zzzzzzzzzzzzzzz';
config.pinDigests = true;
config.minimumReleaseAge = '3 days';
config.minimumReleaseAgeBehaviour = 'timestamp-optional';
config.internalChecksFilter = 'strict';
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: 'alpine',
},
{
version: '8.0.0',
},
{
version: '8.1.0',
},
],
});
getDockerDigest.mockResolvedValueOnce('sha256:abcdef1234567890');
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
newDigest: 'sha256:abcdef1234567890',
newValue: 'alpine',
updateType: 'digest',
},
]);
});
// when `config.lockVersion` is set, `res.currentVersion` is implied even for unversioned values (like `alpine`), which means we can't compute a timestamp
it('marks digest updates for unversioned currentValues as pendingChecks when lockedVersion is set', async () => {
config.currentValue = 'alpine';
config.lockedVersion = '8.1.0';
config.packageName = 'node';
config.datasource = DockerDatasource.id;
config.currentDigest = 'sha256:zzzzzzzzzzzzzzz';
config.pinDigests = true;
config.minimumReleaseAge = '3 days';
config.internalChecksFilter = 'strict';
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: 'alpine',
},
{
version: '8.0.0',
},
{
version: '8.1.0',
},
],
});
getDockerDigest.mockResolvedValueOnce('sha256:abcdef1234567890');
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
newDigest: 'sha256:abcdef1234567890',
newValue: 'alpine',
updateType: 'digest',
pendingChecks: true,
},
]);
});
it('handles git submodule update', async () => {
config.packageName = 'some-path';
config.versioning = gitVersioningId;
config.datasource = GitRefsDatasource.id;
config.currentDigest = 'some-digest';
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(res).toEqual({
updates: [
{
newDigest: '4b825dc642cb6eb9a060e54bf8d69288fbee4904',
newValue: undefined,
updateType: 'digest',
},
],
versioning: 'git',
warnings: [],
});
});
it('handles sourceUrl packageRules with version restrictions', async () => {
config.currentValue = '0.9.99';
config.packageName = 'q';
config.datasource = NpmDatasource.id;
config.packageRules = [
{
matchSourceUrls: ['https://github.com/kriskowal/**'],
allowedVersions: '< 1.4.0',
},
];
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(res).toEqual({
currentVersion: '0.9.99',
fixedVersion: '0.9.99',
isSingleVersion: true,
registryUrl: npmDefaultRegistryUrl,
sourceUrl: 'https://github.com/kriskowal/q',
updates: [
{
bucket: 'major',
hasAttestation: false,
isBreaking: true,
newMajor: 1,
newMinor: 3,
newPatch: 0,
newValue: '1.3.0',
newVersion: '1.3.0',
downloadUrl: 'https://registry.npmjs.org/q/-/q-1.3.0.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: expect.any(String),
updateType: 'major',
},
],
versioning: 'npm',
warnings: [],
});
});
it('handles current age packageRules with version restrictions', async () => {
config.packageName = 'openjdk';
config.currentValue = '17.0.0';
config.datasource = DockerDatasource.id;
config.versioning = dockerVersioningId;
// This config is normally set when packageRules are applied
config.packageRules = [
{
matchCurrentAge: '> 1 day',
allowedVersions: '< 19.0.0',
},
];
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: '17.0.0',
// a day old release
releaseTimestamp: new Date(
Date.now() - 25 * 60 * 60 * 1000,
).toISOString() as Timestamp,
},
{
version: '18.0.0',
},
{
version: '19.0.0',
},
],
});
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'major',
isBreaking: true,
newMajor: 18,
newMinor: 0,
newPatch: 0,
newValue: '18.0.0',
newVersion: '18.0.0',
updateType: 'major',
},
]);
});
it('does not apply package rules for matchCurrentAge if packageRules doesn not have a current age matcher', async () => {
config.packageName = 'openjdk';
config.currentValue = '17.0.0';
config.datasource = DockerDatasource.id;
config.versioning = dockerVersioningId;
// This config is normally set when packageRules are applied
config.packageRules = [
{
matchDepNames: ['openjdk'],
allowedVersions: '< 19.0.0',
},
];
const releaseTimestamp = new Date(
Date.now() - 25 * 60 * 60 * 1000,
).toISOString() as Timestamp;
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: '17.0.0',
// a day old release
releaseTimestamp,
},
{
version: '18.0.0',
},
{
version: '19.0.0',
},
],
});
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(res).toEqual({
currentVersion: '17.0.0',
currentVersionAgeInDays: 1,
currentVersionTimestamp: releaseTimestamp,
fixedVersion: '17.0.0',
isSingleVersion: true,
registryUrl: 'https://index.docker.io',
updates: [
{
bucket: 'major',
isBreaking: true,
newMajor: 19,
newMinor: 0,
newPatch: 0,
newValue: '19.0.0',
newVersion: '19.0.0',
updateType: 'major',
},
],
versioning: 'docker',
warnings: [],
});
});
it('does not apply package rules for matchCurrentAge if the releaseTimestamp for current version is missing', async () => {
config.packageName = 'openjdk';
config.currentValue = '17.0.0';
config.datasource = DockerDatasource.id;
config.versioning = dockerVersioningId;
// This config is normally set when packageRules are applied
config.packageRules = [
{
matchCurrentAge: '> 1 day',
allowedVersions: '< 19.0.0',
},
];
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: '17.0.0',
},
{
version: '18.0.0',
},
{
version: '19.0.0',
},
],
});
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(res).toEqual({
currentVersion: '17.0.0',
fixedVersion: '17.0.0',
isSingleVersion: true,
registryUrl: 'https://index.docker.io',
updates: [
{
bucket: 'major',
isBreaking: true,
newMajor: 19,
newMinor: 0,
newPatch: 0,
newValue: '19.0.0',
newVersion: '19.0.0',
updateType: 'major',
},
],
versioning: 'docker',
warnings: [],
currentVersionTimestamp: undefined,
});
});
it('handles replacements - name only without pinDigests enabled', async () => {
config.packageName = 'openjdk';
config.currentValue = '17.0.0';
config.datasource = DockerDatasource.id;
config.versioning = dockerVersioningId;
// This config is normally set when packageRules are applied
config.replacementName = 'eclipse-temurin';
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: '17.0.0',
},
{
version: '18.0.0',
},
],
});
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'major',
isBreaking: true,
newMajor: 18,
newMinor: 0,
newPatch: 0,
newValue: '18.0.0',
newVersion: '18.0.0',
updateType: 'major',
},
{
newName: 'eclipse-temurin',
newValue: '17.0.0',
newVersion: undefined,
updateType: 'replacement',
},
]);
});
it('handles replacements - name only with pinDigests enabled', async () => {
config.packageName = 'openjdk';
config.currentValue = '17.0.0';
config.pinDigests = true;
config.datasource = DockerDatasource.id;
config.versioning = dockerVersioningId;
// This config is normally set when packageRules are applied
config.replacementName = 'eclipse-temurin';
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: '17.0.0',
},
{
version: '18.0.0',
},
],
});
getDockerDigest.mockResolvedValueOnce('sha256:abcdef1234567890');
getDockerDigest.mockResolvedValueOnce('sha256:0123456789abcdef');
getDockerDigest.mockResolvedValueOnce('sha256:pin0987654321');
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'major',
isBreaking: true,
newDigest: 'sha256:abcdef1234567890',
newMajor: 18,
newMinor: 0,
newPatch: 0,
newValue: '18.0.0',
newVersion: '18.0.0',
updateType: 'major',
},
{
newDigest: 'sha256:0123456789abcdef',
newName: 'eclipse-temurin',
newValue: '17.0.0',
newVersion: undefined,
updateType: 'replacement',
},
{
isPinDigest: true,
newDigest: 'sha256:pin0987654321',
newValue: '17.0.0',
newVersion: undefined,
updateType: 'pinDigest',
},
]);
});
it('handles replacements - name only no version/tag', async () => {
config.packageName = 'openjdk';
config.currentValue = undefined;
config.datasource = DockerDatasource.id;
config.versioning = dockerVersioningId;
// This config is normally set when packageRules are applied
config.replacementName = 'eclipse-temurin';
getDockerDigest.mockResolvedValueOnce('sha256:abcdef1234567890');
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
updateType: 'replacement',
newName: 'eclipse-temurin',
newValue: undefined,
},
]);
});
it('handles replacements - Digest configured and validating getDigest funtion call', async () => {
config.packageName = 'openjdk';
config.currentDigest = 'sha256:fedcba0987654321';
config.currentValue = '17.0.0';
config.datasource = DockerDatasource.id;
config.versioning = dockerVersioningId;
config.replacementName = 'eclipse-temurin';
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: '17.0.0',
newDigest: 'sha256:fedcba0987654321',
},
{
version: '17.0.1',
newDigest: 'sha256:abcdef1234567890',
},
],
lookupName: 'openjdk',
});
getDockerDigest.mockResolvedValueOnce('sha256:pin0987654321');
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
newDigest: 'sha256:abcdef1234567890',
newMajor: 17,
newMinor: 0,
newPatch: 1,
newValue: '17.0.1',
newVersion: '17.0.1',
updateType: 'patch',
},
{
newDigest: 'sha256:pin0987654321',
newName: 'eclipse-temurin',
newValue: '17.0.0',
newVersion: undefined,
updateType: 'replacement',
},
]);
expect(getDockerDigest).toHaveBeenNthCalledWith(
1,
{
currentDigest: undefined,
currentValue: '17.0.0',
lookupName: undefined,
packageName: 'eclipse-temurin',
registryUrl: 'https://index.docker.io',
},
'17.0.0',
);
});
it('handles replacements - Digest configured with replacementNameTemplate and validating getDigest function call', async () => {
config.packageName = 'mirror-some-org/library/openjdk';
config.currentDigest = 'sha256:fedcba0987654321';
config.currentValue = '17.0.0';
config.datasource = DockerDatasource.id;
config.versioning = dockerVersioningId;
config.replacementNameTemplate = `{{{replace 'mirror-some-org/' 'new.registry.io/' packageName}}}`;
config.replacementVersion = '19.0.0';
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: '17.0.0',
},
{
version: '17.0.1',
},
],
lookupName: 'mirror-some-org/library/openjdk',
});
getDockerDigest.mockResolvedValueOnce('sha256:abcdef1234567890');
getDockerDigest.mockResolvedValueOnce('sha256:fedcba0987654321');
getDockerDigest.mockResolvedValueOnce('sha256:pin0987654321');
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
newDigest: 'sha256:abcdef1234567890',
newMajor: 17,
newMinor: 0,
newPatch: 1,
newValue: '17.0.1',
newVersion: '17.0.1',
updateType: 'patch',
},
{
newDigest: 'sha256:fedcba0987654321',
newName: 'new.registry.io/library/openjdk',
newValue: '19.0.0',
newVersion: undefined,
updateType: 'replacement',
},
{
newDigest: 'sha256:pin0987654321',
newValue: '17.0.0',
newVersion: undefined,
updateType: 'digest',
},
]);
expect(getDockerDigest).toHaveBeenNthCalledWith(
1,
{
currentDigest: 'sha256:fedcba0987654321',
currentValue: '17.0.0',
lookupName: 'mirror-some-org/library/openjdk',
packageName: 'mirror-some-org/library/openjdk',
registryUrl: 'https://index.docker.io',
},
'17.0.1',
);
expect(getDockerDigest).toHaveBeenNthCalledWith(
2,
{
currentDigest: undefined,
currentValue: '17.0.0',
lookupName: undefined,
packageName: 'new.registry.io/library/openjdk',
registryUrl: 'https://index.docker.io',
},
'19.0.0',
);
expect(getDockerDigest).toHaveBeenNthCalledWith(
3,
{
currentDigest: 'sha256:fedcba0987654321',
currentValue: '17.0.0',
lookupName: 'mirror-some-org/library/openjdk',
packageName: 'mirror-some-org/library/openjdk',
registryUrl: 'https://index.docker.io',
},
'17.0.0',
);
});
it('handles replacements - skips if package and replacement names match', async () => {
config.packageName = 'openjdk';
config.currentValue = undefined;
config.datasource = DockerDatasource.id;
config.replacementName = 'openjdk';
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
});
it('handles replacements - name and version', async () => {
config.currentValue = '1.4.1';
config.packageName = 'q';
config.replacementName = 'r';
config.replacementVersion = '2.0.0';
config.datasource = NpmDatasource.id;
httpMock.scope(npmDefaultRegistryUrl).get('/q').reply(200, qJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
updateType: 'replacement',
newName: 'r',
newValue: '2.0.0',
},
]);
});
it('handles replacements - can template replacement name without a replacement version', async () => {
config.packageName = 'mirror.some.org/library/openjdk';
config.currentValue = '17.0.0';
config.replacementNameTemplate = `{{{replace 'mirror.some.org/' 'new.registry.io/' packageName}}}`;
config.datasource = DockerDatasource.id;
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: '17.0.0',
},
{
version: '18.0.0',
},
],
});
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'major',
isBreaking: true,
newMajor: 18,
newMinor: 0,
newPatch: 0,
newValue: '18.0.0',
newVersion: '18.0.0',
updateType: 'major',
},
{
newName: 'new.registry.io/library/openjdk',
newValue: '17.0.0',
updateType: 'replacement',
},
]);
});
it('handles replacements - can template replacement name with a replacement version', async () => {
config.packageName = 'mirror.some.org/library/openjdk';
config.currentValue = '17.0.0';
config.replacementNameTemplate = `{{{replace 'mirror.some.org/' 'new.registry.io/' packageName}}}`;
config.replacementVersion = '18.0.0';
config.datasource = DockerDatasource.id;
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: '17.0.0',
},
{
version: '18.0.0',
},
],
});
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'major',
isBreaking: true,
newMajor: 18,
newMinor: 0,
newPatch: 0,
newValue: '18.0.0',
newVersion: '18.0.0',
updateType: 'major',
},
{
newName: 'new.registry.io/library/openjdk',
newValue: '18.0.0',
updateType: 'replacement',
},
]);
});
it('handles replacements - replacementName takes precedence over replacementNameTemplate', async () => {
config.packageName = 'mirror.some.org/library/openjdk';
config.currentValue = '17.0.0';
config.replacementNameTemplate = `{{{replace 'mirror.some.org/' 'new.registry.io/' packageName}}}`;
config.replacementName = 'eclipse-temurin';
config.datasource = DockerDatasource.id;
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: '17.0.0',
},
{
version: '18.0.0',
},
],
});
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'major',
isBreaking: true,
newMajor: 18,
newMinor: 0,
newPatch: 0,
newValue: '18.0.0',
newVersion: '18.0.0',
updateType: 'major',
},
{
newName: 'eclipse-temurin',
newValue: '17.0.0',
updateType: 'replacement',
},
]);
});
it('handles replacements - can template replacement version without a replacement name', async () => {
config.packageName = 'mirror_some_org/library/openjdk';
config.currentValue = '17.0.0';
config.replacementVersionTemplate = `{{ lookup (split currentValue '.') 0 }}`;
config.datasource = DockerDatasource.id;
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: '17.0.0',
},
{
version: '18.0.0',
},
],
});
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'major',
isBreaking: true,
newMajor: 18,
newMinor: 0,
newPatch: 0,
newValue: '18.0.0',
newVersion: '18.0.0',
updateType: 'major',
},
{
newName: 'mirror_some_org/library/openjdk',
newValue: '17',
updateType: 'replacement',
},
]);
});
it('handles replacements - can template replacement version with a replacement name', async () => {
config.packageName = 'mirror_some_org/library/openjdk';
config.currentValue = '17.0.0';
config.replacementName = 'eclipse-temurin';
config.replacementVersionTemplate = `{{ lookup (split currentValue '.') 0 }}`;
config.datasource = DockerDatasource.id;
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: '17.0.0',
},
{
version: '18.0.0',
},
],
});
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'major',
isBreaking: true,
newMajor: 18,
newMinor: 0,
newPatch: 0,
newValue: '18.0.0',
newVersion: '18.0.0',
updateType: 'major',
},
{
newName: 'eclipse-temurin',
newValue: '17',
updateType: 'replacement',
},
]);
});
it('handles replacements - can template replacement version with a template replacement name', async () => {
config.packageName = 'mirror_some_org/library/openjdk';
config.currentValue = '17.0.0';
config.replacementNameTemplate = `{{{replace 'mirror_some_org/' 'new.registry.io/' packageName}}}`;
config.replacementVersionTemplate = `{{ lookup (split currentValue '.') 0 }}`;
config.datasource = DockerDatasource.id;
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: '17.0.0',
},
{
version: '18.0.0',
},
],
});
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'major',
isBreaking: true,
newMajor: 18,
newMinor: 0,
newPatch: 0,
newValue: '18.0.0',
newVersion: '18.0.0',
updateType: 'major',
},
{
newName: 'new.registry.io/library/openjdk',
newValue: '17',
updateType: 'replacement',
},
]);
});
it('handles replacements - replacementVersion takes precedence over replacementVersionTemplate', async () => {
config.packageName = 'mirror_some_org/library/openjdk';
config.currentValue = '17.0.0';
config.replacementVersionTemplate = `{{ lookup (split currentValue '.') 0 }}`;
config.replacementVersion = '18.0.0';
config.datasource = DockerDatasource.id;
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: '17.0.0',
},
{
version: '18.0.0',
},
],
});
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'major',
isBreaking: true,
newMajor: 18,
newMinor: 0,
newPatch: 0,
newValue: '18.0.0',
newVersion: '18.0.0',
updateType: 'major',
},
{
newName: 'mirror_some_org/library/openjdk',
newValue: '18.0.0',
updateType: 'replacement',
},
]);
});
it('handles replacements - can perform replacement even for invalid versioning', async () => {
config.packageName = 'adoptopenjdk/openjdk11';
config.currentValue = 'alpine-jre';
config.replacementName = 'eclipse-temurin';
config.replacementVersion = '17.0.0-jre-alpine';
config.datasource = DockerDatasource.id;
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: 'alpine-jre',
},
],
});
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
updateType: 'replacement',
newName: 'eclipse-temurin',
newValue: '17.0.0-jre-alpine',
},
]);
});
it('handles replacements - from datasource', async () => {
config.currentValue = '2.0.0';
config.packageName = 'org.example:foo';
config.datasource = MavenDatasource.id;
getMavenReleases.mockResolvedValueOnce({
releases: [{ version: '2.0.0' }],
replacementName: 'foo:bar',
replacementVersion: '2.0.0',
});
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
updateType: 'replacement',
newName: 'foo:bar',
newValue: '2.0.0',
},
]);
});
it('rollback for invalid version to last stable version', async () => {
config.currentValue = '2.5.17';
config.packageName = 'vue';
config.datasource = NpmDatasource.id;
config.rollbackPrs = true;
config.ignoreUnstable = true;
httpMock.scope(npmDefaultRegistryUrl).get('/vue').reply(200, vueJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: `rollback`,
newMajor: 2,
newValue: `2.5.16`,
newVersion: `2.5.16`,
updateType: `rollback`,
prBodyNotes: expect.arrayContaining([
expect.stringContaining('The version of `vue` in use (`2.5.17`)'),
]),
},
]);
});
describe('handles merge confidence', () => {
const defaultApiBaseUrl = 'https://developer.mend.io/';
const mcConfig: AllConfig = {
mergeConfidenceEndpoint: defaultApiBaseUrl,
mergeConfidenceDatasources: presetSupportedDatasources,
};
const getMergeConfidenceSpy = vi.spyOn(McApi, 'getMergeConfidenceLevel');
const hostRule: HostRule = {
hostType: 'merge-confidence',
token: 'some-token',
};
beforeEach(() => {
hostRules.add(hostRule);
initConfig(mcConfig);
memCache.reset();
});
afterEach(() => {
resetConfig();
});
it('gets a merge confidence level for a given update when corresponding packageRule is in use', async () => {
getMergeConfidenceSpy.mockRestore();
const datasource = NpmDatasource.id;
const packageName = 'webpack';
const newVersion = '3.8.1';
const currentValue = '3.7.0';
config.packageRules = [{ matchConfidence: ['high'] }];
config.currentValue = currentValue;
config.packageName = packageName;
config.datasource = datasource;
httpMock
.scope(npmDefaultRegistryUrl)
.get('/webpack')
.reply(200, webpackJson);
httpMock
.scope(defaultApiBaseUrl)
.get(
`/api/mc/json/${datasource}/${packageName}/${currentValue}/${newVersion}`,
)
.reply(200, { confidence: 'high' });
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
mergeConfidenceLevel: 'high',
newMajor: 3,
newMinor: 8,
newPatch: 1,
newValue: '3.8.1',
newVersion: '3.8.1',
newDigest:
'sha512-5ZXLWWsMqHKFr5y0N3Eo5IIisxeEeRAajNq4mELb/WELOR7srdbQk2N5XiyNy2A/AgvlR3AmeBCZJW8lHrolbw==',
downloadUrl:
'https://registry.npmjs.org/webpack/-/webpack-3.8.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2017-10-17T15:22:36.646Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('does not get a merge confidence level when no packageRule is set', async () => {
config.currentValue = '3.7.0';
config.packageName = 'webpack';
config.datasource = NpmDatasource.id;
httpMock
.scope(npmDefaultRegistryUrl)
.get('/webpack')
.reply(200, webpackJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(getMergeConfidenceSpy).toHaveBeenCalledTimes(0);
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: false,
newMajor: 3,
newMinor: 8,
newPatch: 1,
newValue: '3.8.1',
newVersion: '3.8.1',
newDigest:
'sha512-5ZXLWWsMqHKFr5y0N3Eo5IIisxeEeRAajNq4mELb/WELOR7srdbQk2N5XiyNy2A/AgvlR3AmeBCZJW8lHrolbw==',
downloadUrl:
'https://registry.npmjs.org/webpack/-/webpack-3.8.1.tgz',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2017-10-17T15:22:36.646Z' as Timestamp,
updateType: 'minor',
hasAttestation: false,
},
]);
});
it('does not set merge confidence value when API is not in use', async () => {
const datasource = NpmDatasource.id;
config.packageRules = [{ matchConfidence: ['high'] }];
config.currentValue = '3.7.0';
config.packageName = 'webpack';
config.datasource = datasource;
hostRules.clear(); // reset merge confidence
initConfig(mcConfig);
httpMock
.scope(npmDefaultRegistryUrl)
.get('/webpack')
.reply(200, webpackJson);
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toBeEmptyArray();
});
});
it('detects gomod updates and uses updateType=digest when appropriate', async () => {
config.manager = 'gomod';
config.datasource = GoDatasource.id;
config.currentValue = 'v0.0.0-20240506185236-b8a5c65736ae';
config.currentDigest = 'b8a5c65736ae';
config.packageName = 'google.golang.org/genproto/googleapis/rpc';
config.digestOneAndOnly = true;
httpMock
.scope(
'https://proxy.golang.org/google.golang.org/genproto/googleapis/rpc',
)
.get('/@v/list')
.reply(200, '')
.get('/v2/@v/list')
.reply(404)
.get('/@latest')
.reply(200, { Version: 'v0.0.0-20240509183442-62759503f434' });
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: true,
newDigest: '62759503f434',
newMajor: 0,
newMinor: 0,
newPatch: 0,
newValue: 'v0.0.0-20240509183442-62759503f434',
newVersion: 'v0.0.0-20240509183442-62759503f434',
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: '2024-05-09T18:34:42.000Z' as Timestamp,
updateType: 'digest',
hasAttestation: undefined,
},
]);
});
// gomod pseudo-version updates are relabelled to `updateType=digest` after filterInternalChecks()
// has already age-checked them under the version-derived updateType, so digest-scoped rules must be re-applied
it('applies digest-scoped minimumReleaseAge packageRules to gomod pseudo-version updates', async () => {
config.manager = 'gomod';
config.datasource = GoDatasource.id;
config.currentValue = 'v0.0.0-20240506185236-b8a5c65736ae';
config.currentDigest = 'b8a5c65736ae';
config.packageName = 'google.golang.org/genproto/googleapis/rpc';
config.digestOneAndOnly = true;
config.internalChecksFilter = 'strict';
config.packageRules = [
{ matchUpdateTypes: ['digest'], minimumReleaseAge: '3 days' },
];
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
yesterday.setMilliseconds(0);
const pseudoTimestamp = yesterday
.toISOString()
.replace(/[-:T]/g, '')
.slice(0, 14);
const newVersion = `v0.0.0-${pseudoTimestamp}-62759503f434`;
httpMock
.scope(
'https://proxy.golang.org/google.golang.org/genproto/googleapis/rpc',
)
.get('/@v/list')
.reply(200, '')
.get('/v2/@v/list')
.reply(404)
.get('/@latest')
.reply(200, { Version: newVersion });
const { updates } = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(updates).toEqual([
{
bucket: 'non-major',
isBreaking: true,
newDigest: '62759503f434',
newMajor: 0,
newMinor: 0,
newPatch: 0,
newValue: newVersion,
newVersion,
newVersionAgeInDays: expect.any(Number),
releaseTimestamp: yesterday.toISOString() as Timestamp,
updateType: 'digest',
hasAttestation: undefined,
pendingChecks: true,
},
]);
});
it('handles changelog with content', async () => {
config.currentValue = '8.0.0';
config.packageName = 'node';
config.datasource = DockerDatasource.id;
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: '8.0.0',
},
{
changelogContent: 'testContent',
changelogUrl: 'http://testChangelogUrl',
version: '8.1.0',
},
],
});
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(res).toEqual({
changelogContent: 'testContent',
changelogUrl: 'http://testChangelogUrl',
currentVersion: '8.0.0',
fixedVersion: '8.0.0',
isSingleVersion: true,
registryUrl: 'https://index.docker.io',
sourceUrl: 'https://github.com/nodejs/node',
updates: [
{
bucket: 'non-major',
isBreaking: false,
newMajor: 8,
newMinor: 1,
newPatch: 0,
newValue: '8.1.0',
newVersion: '8.1.0',
updateType: 'minor',
hasAttestation: undefined,
},
],
versioning: 'npm',
warnings: [],
});
});
it('handles changelog with content for ranges', async () => {
config.currentValue = '[8.0,8.1)';
config.packageName = 'node';
config.datasource = DockerDatasource.id;
config.versioning = 'maven';
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: '8.0.0',
},
{
changelogContent: 'testContent',
changelogUrl: 'http://testChangelogUrl',
version: '8.1.0',
},
],
});
const res = await Result.wrap(
lookup.lookupUpdates(config),
).unwrapOrThrow();
expect(res).toEqual({
changelogContent: 'testContent',
changelogUrl: 'http://testChangelogUrl',
currentVersion: '8.0.0',
isSingleVersion: false,
registryUrl: 'https://index.docker.io',
sourceUrl: 'https://github.com/nodejs/node',
updates: [
{
bucket: 'non-major',
isBreaking: false,
isRange: true,
newMajor: 8,
newMinor: 1,
newPatch: 0,
newValue: '[8.0,8.2)',
newVersion: '8.1.0',
updateType: 'minor',
hasAttestation: undefined,
},
],
versioning: 'maven',
warnings: [],
});
});
});
});