diff --git a/lib/config/__snapshots__/validation.spec.ts.snap b/lib/config/__snapshots__/validation.spec.ts.snap index 39e78b3c564..db643cdbf9a 100644 --- a/lib/config/__snapshots__/validation.spec.ts.snap +++ b/lib/config/__snapshots__/validation.spec.ts.snap @@ -155,19 +155,6 @@ exports[`config/validation > validateConfig(config) > errors if manager objects ] `; -exports[`config/validation > validateConfig(config) > errors if managerFilePatterns has wrong parent 1`] = ` -[ - { - "message": "managerFilePatterns should only be configured within one of "ansible or ansible-galaxy or argocd or asdf or azure-pipelines or batect or batect-wrapper or bazel or bazel-module or bazelisk or bicep or bitbucket-pipelines or bitrise or buildkite or buildpacks or bun or bun-version or bundler or cake or cargo or cdnurl or circleci or cloudbuild or cocoapods or composer or conan or copier or cpanfile or crossplane or crow or deps-edn or devbox or devcontainer or docker-compose or dockerfile or droneci or fleet or flux or fvm or git-submodules or github-actions or gitlabci or gitlabci-include or glasskube or gleam or gomod or gradle or gradle-wrapper or haskell-cabal or helm-requirements or helm-values or helmfile or helmsman or helmv3 or hermit or homebrew or html or jenkins or jsonnet-bundler or kotlin-script or kubernetes or kustomize or leiningen or maven or maven-wrapper or meteor or mint or mise or mix or nix or nodenv or npm or nuget or nvm or ocb or osgi or pep621 or pep723 or pip-compile or pip_requirements or pip_setup or pipenv or pixi or poetry or pre-commit or pub or puppet or pyenv or renovate-config-presets or ruby-version or runtime-version or sbt or scalafmt or setup-cfg or sveltos or swift or tekton or terraform or terraform-version or terragrunt or terragrunt-version or tflint-plugin or travis or unity3d or velaci or vendir or woodpecker or customManagers" objects. Was found in .", - "topic": "managerFilePatterns", - }, - { - "message": "managerFilePatterns should only be configured within one of "ansible or ansible-galaxy or argocd or asdf or azure-pipelines or batect or batect-wrapper or bazel or bazel-module or bazelisk or bicep or bitbucket-pipelines or bitrise or buildkite or buildpacks or bun or bun-version or bundler or cake or cargo or cdnurl or circleci or cloudbuild or cocoapods or composer or conan or copier or cpanfile or crossplane or crow or deps-edn or devbox or devcontainer or docker-compose or dockerfile or droneci or fleet or flux or fvm or git-submodules or github-actions or gitlabci or gitlabci-include or glasskube or gleam or gomod or gradle or gradle-wrapper or haskell-cabal or helm-requirements or helm-values or helmfile or helmsman or helmv3 or hermit or homebrew or html or jenkins or jsonnet-bundler or kotlin-script or kubernetes or kustomize or leiningen or maven or maven-wrapper or meteor or mint or mise or mix or nix or nodenv or npm or nuget or nvm or ocb or osgi or pep621 or pep723 or pip-compile or pip_requirements or pip_setup or pipenv or pixi or poetry or pre-commit or pub or puppet or pyenv or renovate-config-presets or ruby-version or runtime-version or sbt or scalafmt or setup-cfg or sveltos or swift or tekton or terraform or terraform-version or terragrunt or terragrunt-version or tflint-plugin or travis or unity3d or velaci or vendir or woodpecker or customManagers" objects. Was found in minor", - "topic": "npm.minor.managerFilePatterns", - }, -] -`; - exports[`config/validation > validateConfig(config) > ignore packageRule nesting validation for presets 1`] = `[]`; exports[`config/validation > validateConfig(config) > included managers of the wrong type 1`] = ` @@ -242,7 +229,7 @@ exports[`config/validation > validateConfig(config) > validates regEx for each m exports[`config/validation > validateConfig(config) > warns if hostType has the wrong parent 1`] = ` [ { - "message": "hostType should only be configured within one of "hostRules" objects. Was found in .", + "message": ""hostType" can't be used in ".". Allowed objects: hostRules.", "topic": "hostType", }, ] diff --git a/lib/config/validation.spec.ts b/lib/config/validation.spec.ts index a01925d4580..6eb5ee9053d 100644 --- a/lib/config/validation.spec.ts +++ b/lib/config/validation.spec.ts @@ -1,12 +1,9 @@ -import { getManagerList } from '../modules/manager'; import { configFileNames } from './app-strings'; import { GlobalConfig } from './global'; import type { RenovateConfig } from './types'; import * as configValidation from './validation'; import { partial } from '~test/util'; -const managerList = getManagerList().sort(); - describe('config/validation', () => { describe('validateConfig(config)', () => { it('returns deprecation warnings', async () => { @@ -1098,7 +1095,20 @@ describe('config/validation', () => { expect(errors).toHaveLength(0); expect(warnings).toHaveLength(2); - expect(warnings).toMatchSnapshot(); + expect(warnings).toEqual([ + { + topic: 'managerFilePatterns', + message: expect.toStartWith( + `"managerFilePatterns" can't be used in ".". Allowed objects: `, + ), + }, + { + topic: 'npm.minor.managerFilePatterns', + message: expect.toStartWith( + `"managerFilePatterns" can't be used in "minor". Allowed objects: `, + ), + }, + ]); }); it('errors if manager objects are nested', async () => { @@ -1763,7 +1773,9 @@ describe('config/validation', () => { }, { topic: 'managerFilePatterns', - message: `managerFilePatterns should only be configured within one of "${managerList.join(' or ')} or customManagers" objects. Was found in .`, + message: expect.toStartWith( + `"managerFilePatterns" can't be used in ".". Allowed objects: `, + ), }, ]); }); @@ -1786,7 +1798,9 @@ describe('config/validation', () => { expect(warnings).toEqual([ { topic: 'managerFilePatterns', - message: `managerFilePatterns should only be configured within one of "${managerList.join(' or ')} or customManagers" objects. Was found in .`, + message: expect.toStartWith( + `"managerFilePatterns" can't be used in ".". Allowed objects: `, + ), }, ]); }); diff --git a/lib/config/validation.ts b/lib/config/validation.ts index 8437bfcc330..d84462611a5 100644 --- a/lib/config/validation.ts +++ b/lib/config/validation.ts @@ -250,9 +250,8 @@ export async function validateConfig( !optionParents[key].includes(parentName as AllowedParents) ) { // TODO: types (#22198) - const message = `${key} should only be configured within one of "${optionParents[ - key - ]?.join(' or ')}" objects. Was found in ${parentName}`; + const options = optionParents[key]?.sort().join(', '); + const message = `"${key}" can't be used in "${parentName}". Allowed objects: ${options}.`; warnings.push({ topic: `${parentPath ? `${parentPath}.` : ''}${key}`, message, diff --git a/lib/modules/datasource/npm/__snapshots__/index.spec.ts.snap b/lib/modules/datasource/npm/__snapshots__/index.spec.ts.snap index c4526e97b83..0ddf0af9ae6 100644 --- a/lib/modules/datasource/npm/__snapshots__/index.spec.ts.snap +++ b/lib/modules/datasource/npm/__snapshots__/index.spec.ts.snap @@ -6,6 +6,7 @@ exports[`modules/datasource/npm/index > should fetch package info from custom re "registryUrl": "https://npm.mycustomregistry.com", "releases": [ { + "attestation": false, "dependencies": undefined, "devDependencies": undefined, "gitRef": undefined, @@ -13,6 +14,7 @@ exports[`modules/datasource/npm/index > should fetch package info from custom re "version": "0.0.1", }, { + "attestation": false, "dependencies": undefined, "devDependencies": undefined, "gitRef": undefined, @@ -33,6 +35,7 @@ exports[`modules/datasource/npm/index > should fetch package info from npm 1`] = "registryUrl": "https://registry.npmjs.org", "releases": [ { + "attestation": false, "dependencies": undefined, "devDependencies": undefined, "gitRef": undefined, @@ -40,6 +43,7 @@ exports[`modules/datasource/npm/index > should fetch package info from npm 1`] = "version": "0.0.1", }, { + "attestation": false, "dependencies": undefined, "devDependencies": undefined, "gitRef": undefined, @@ -61,6 +65,7 @@ exports[`modules/datasource/npm/index > should handle foobar 1`] = ` "registryUrl": "https://registry.npmjs.org", "releases": [ { + "attestation": false, "dependencies": undefined, "devDependencies": undefined, "gitRef": undefined, @@ -68,6 +73,7 @@ exports[`modules/datasource/npm/index > should handle foobar 1`] = ` "version": "0.0.1", }, { + "attestation": false, "dependencies": undefined, "devDependencies": undefined, "gitRef": undefined, @@ -89,6 +95,7 @@ exports[`modules/datasource/npm/index > should handle no time 1`] = ` "registryUrl": "https://registry.npmjs.org", "releases": [ { + "attestation": false, "dependencies": undefined, "devDependencies": undefined, "gitRef": undefined, @@ -96,6 +103,7 @@ exports[`modules/datasource/npm/index > should handle no time 1`] = ` "version": "0.0.1", }, { + "attestation": false, "dependencies": undefined, "devDependencies": undefined, "gitRef": undefined, @@ -116,6 +124,7 @@ exports[`modules/datasource/npm/index > should not send an authorization header "registryUrl": "https://registry.npmjs.org", "releases": [ { + "attestation": false, "dependencies": undefined, "devDependencies": undefined, "gitRef": undefined, @@ -123,6 +132,7 @@ exports[`modules/datasource/npm/index > should not send an authorization header "version": "0.0.1", }, { + "attestation": false, "dependencies": undefined, "devDependencies": undefined, "gitRef": undefined, @@ -144,6 +154,7 @@ exports[`modules/datasource/npm/index > should parse repo url (string) 1`] = ` "registryUrl": "https://registry.npmjs.org", "releases": [ { + "attestation": false, "dependencies": undefined, "devDependencies": undefined, "gitRef": undefined, @@ -164,6 +175,7 @@ exports[`modules/datasource/npm/index > should parse repo url 1`] = ` "registryUrl": "https://registry.npmjs.org", "releases": [ { + "attestation": false, "dependencies": undefined, "devDependencies": undefined, "gitRef": undefined, @@ -184,6 +196,7 @@ exports[`modules/datasource/npm/index > should replace any environment variable "registryUrl": "https://registry.from-env.com", "releases": [ { + "attestation": false, "dependencies": undefined, "devDependencies": undefined, "gitRef": undefined, @@ -191,6 +204,7 @@ exports[`modules/datasource/npm/index > should replace any environment variable "version": "0.0.1", }, { + "attestation": false, "dependencies": undefined, "devDependencies": undefined, "gitRef": undefined, @@ -217,6 +231,7 @@ Marking the latest version of an npm package as deprecated results in the entire "registryUrl": "https://registry.npmjs.org", "releases": [ { + "attestation": false, "dependencies": undefined, "devDependencies": undefined, "gitRef": undefined, @@ -225,6 +240,7 @@ Marking the latest version of an npm package as deprecated results in the entire "version": "0.0.1", }, { + "attestation": false, "dependencies": undefined, "devDependencies": undefined, "gitRef": undefined, @@ -254,6 +270,7 @@ exports[`modules/datasource/npm/index > should send an authorization header if p "registryUrl": "https://registry.npmjs.org", "releases": [ { + "attestation": false, "dependencies": undefined, "devDependencies": undefined, "gitRef": undefined, @@ -261,6 +278,7 @@ exports[`modules/datasource/npm/index > should send an authorization header if p "version": "0.0.1", }, { + "attestation": false, "dependencies": undefined, "devDependencies": undefined, "gitRef": undefined, @@ -282,6 +300,7 @@ exports[`modules/datasource/npm/index > should use default registry if missing f "registryUrl": "https://registry.npmjs.org", "releases": [ { + "attestation": false, "dependencies": undefined, "devDependencies": undefined, "gitRef": undefined, @@ -289,6 +308,7 @@ exports[`modules/datasource/npm/index > should use default registry if missing f "version": "0.0.1", }, { + "attestation": false, "dependencies": undefined, "devDependencies": undefined, "gitRef": undefined, @@ -310,6 +330,7 @@ exports[`modules/datasource/npm/index > should use host rules by baseUrl if prov "registryUrl": "https://npm.mycustomregistry.com/_packaging/mycustomregistry/npm/registry", "releases": [ { + "attestation": false, "dependencies": undefined, "devDependencies": undefined, "gitRef": undefined, @@ -317,6 +338,7 @@ exports[`modules/datasource/npm/index > should use host rules by baseUrl if prov "version": "0.0.1", }, { + "attestation": false, "dependencies": undefined, "devDependencies": undefined, "gitRef": undefined, @@ -338,6 +360,7 @@ exports[`modules/datasource/npm/index > should use host rules by hostName if pro "registryUrl": "https://npm.mycustomregistry.com", "releases": [ { + "attestation": false, "dependencies": undefined, "devDependencies": undefined, "gitRef": undefined, @@ -345,6 +368,7 @@ exports[`modules/datasource/npm/index > should use host rules by hostName if pro "version": "0.0.1", }, { + "attestation": false, "dependencies": undefined, "devDependencies": undefined, "gitRef": undefined, diff --git a/lib/modules/datasource/npm/get.spec.ts b/lib/modules/datasource/npm/get.spec.ts index 8dc7ba8fdca..540e4c51a4a 100644 --- a/lib/modules/datasource/npm/get.spec.ts +++ b/lib/modules/datasource/npm/get.spec.ts @@ -596,7 +596,15 @@ describe('modules/datasource/npm/get', () => { expect(dep).toEqual({ registryUrl: 'https://example.com', - releases: [{ version: '1.0.0' }], + releases: [ + { + attestation: false, + dependencies: undefined, + devDependencies: undefined, + gitRef: undefined, + version: '1.0.0', + }, + ], sourceDirectory: 'packages/foo', sourceUrl: 'https://github.com/octocat/Hello-World/tree/master/packages/test', @@ -622,7 +630,15 @@ describe('modules/datasource/npm/get', () => { expect(dep).toEqual({ registryUrl: 'https://example.com', - releases: [{ version: '1.0.0' }], + releases: [ + { + attestation: false, + dependencies: undefined, + devDependencies: undefined, + gitRef: undefined, + version: '1.0.0', + }, + ], sourceDirectory: 'packages/foo', sourceUrl: 'https://github.com/octocat/Hello-World/tree/master/packages/test', @@ -648,7 +664,15 @@ describe('modules/datasource/npm/get', () => { expect(dep).toEqual({ registryUrl: 'https://example.com', - releases: [{ version: '1.0.0' }], + releases: [ + { + attestation: false, + dependencies: undefined, + devDependencies: undefined, + gitRef: undefined, + version: '1.0.0', + }, + ], sourceDirectory: 'packages/foo', sourceUrl: 'https://github.com/octocat/Hello-World/tree/master/packages/test', diff --git a/lib/modules/datasource/npm/get.ts b/lib/modules/datasource/npm/get.ts index be7f5d3bdde..7e474f1d88c 100644 --- a/lib/modules/datasource/npm/get.ts +++ b/lib/modules/datasource/npm/get.ts @@ -1,4 +1,4 @@ -import is from '@sindresorhus/is'; +import is, { isString } from '@sindresorhus/is'; import { z } from 'zod'; import { HOST_DISABLED } from '../../../constants/error-messages'; import { logger } from '../../../logger'; @@ -138,6 +138,7 @@ export async function getDependency( gitRef: res.versions?.[version].gitHead, dependencies: res.versions?.[version].dependencies, devDependencies: res.versions?.[version].devDependencies, + attestation: isString(res.versions?.[version].dist?.attestations?.url), }; const releaseTimestamp = asTimestamp(res.time?.[version]); if (releaseTimestamp) { diff --git a/lib/modules/datasource/npm/index.spec.ts b/lib/modules/datasource/npm/index.spec.ts index 3de47a006ac..21f3f30c651 100644 --- a/lib/modules/datasource/npm/index.spec.ts +++ b/lib/modules/datasource/npm/index.spec.ts @@ -140,6 +140,58 @@ describe('modules/datasource/npm/index', () => { expect(res?.deprecationMessage).toMatchSnapshot(); }); + it('should return attestation', async () => { + const deprecatedPackage = { + name: 'foobar', + versions: { + '0.0.1': { + foo: 1, + dist: { + attestations: { + url: 'https://registry.npmjs.org/-/npm/v1/attestations/foobar@0.0.1', + }, + }, + }, + '0.0.2': { + foo: 2, + dist: { + attestations: { + url: 'https://registry.npmjs.org/-/npm/v1/attestations/foobar@0.0.2', + }, + }, + }, + }, + repository: { + type: 'git', + url: 'git://github.com/renovateapp/dummy.git', + }, + 'dist-tags': { + latest: '0.0.2', + }, + time: { + '0.0.1': '2018-05-06T07:21:53+02:00', + '0.0.2': '2018-05-07T07:21:53+02:00', + }, + }; + httpMock + .scope('https://registry.npmjs.org') + .get('/foobar') + .reply(200, deprecatedPackage); + const res = await getPkgReleases({ datasource, packageName: 'foobar' }); + expect(res).toMatchObject({ + releases: [ + { + version: '0.0.1', + attestation: true, + }, + { + version: '0.0.2', + attestation: true, + }, + ], + }); + }); + it('should handle foobar', async () => { httpMock .scope('https://registry.npmjs.org') diff --git a/lib/modules/datasource/npm/types.ts b/lib/modules/datasource/npm/types.ts index d07b93bf15e..038134c38d5 100644 --- a/lib/modules/datasource/npm/types.ts +++ b/lib/modules/datasource/npm/types.ts @@ -6,6 +6,14 @@ export interface NpmrcRules { packageRules: PackageRule[]; } +export interface NpmAttestations { + url?: string; +} + +export interface NpmDistribution { + attestations?: NpmAttestations; +} + export interface NpmResponseVersion { repository?: { url: string; @@ -17,6 +25,7 @@ export interface NpmResponseVersion { dependencies?: Record; devDependencies?: Record; engines?: Record; + dist?: NpmDistribution; } export interface NpmResponse { diff --git a/lib/modules/datasource/types.ts b/lib/modules/datasource/types.ts index 1e081197553..3e6d1abc5f4 100644 --- a/lib/modules/datasource/types.ts +++ b/lib/modules/datasource/types.ts @@ -75,6 +75,7 @@ export interface Release { sourceDirectory?: string; currentAge?: string; isLatest?: boolean; + attestation?: boolean; } export interface ReleaseResult { diff --git a/lib/workers/global/config/parse/index.spec.ts b/lib/workers/global/config/parse/index.spec.ts index 91a33f132c9..9350a2bb4e9 100644 --- a/lib/workers/global/config/parse/index.spec.ts +++ b/lib/workers/global/config/parse/index.spec.ts @@ -337,5 +337,16 @@ describe('workers/global/config/parse/index', () => { 'customManagers:azurePipelinesVersions', ]); }); + + it('adds extends from fileConfig only', async () => { + fileConfigParser.getConfig.mockResolvedValueOnce({ + extends: [':pinDigests'], + }); + const parsedConfig = await configParser.parseConfigs( + defaultEnv, + defaultArgv, + ); + expect(parsedConfig.extends).toMatchObject([':pinDigests']); + }); }); }); diff --git a/lib/workers/global/config/parse/index.ts b/lib/workers/global/config/parse/index.ts index ffa3d83050f..7f7fe91a2a6 100644 --- a/lib/workers/global/config/parse/index.ts +++ b/lib/workers/global/config/parse/index.ts @@ -50,7 +50,10 @@ export async function parseConfigs( let config: AllConfig = mergeChildConfig(fileConfig, additionalFileConfig); // merge extends from file config and additional file config - if (is.nonEmptyArray(fileConfig.extends)) { + if ( + is.nonEmptyArray(fileConfig.extends) && + is.nonEmptyArray(additionalFileConfig.extends) + ) { config.extends = [...fileConfig.extends, ...(config.extends ?? [])]; } config = mergeChildConfig(config, envConfig); diff --git a/lib/workers/repository/update/pr/index.spec.ts b/lib/workers/repository/update/pr/index.spec.ts index 2e3ad352f53..0177dce437e 100644 --- a/lib/workers/repository/update/pr/index.spec.ts +++ b/lib/workers/repository/update/pr/index.spec.ts @@ -973,6 +973,87 @@ describe('workers/repository/update/pr/index', () => { }); }); + describe('Warnings', () => { + describe('Attestations', () => { + describe('when attestation is not removed', () => { + describe.each([ + [true, true], + [false, true], + [false, false], + ])( + 'current attestation %s, new attestation %s', + (currentAttestation, newAttestation) => { + const dummyUpgrade = partial({ + branchName: sourceBranch, + depType: 'foo', + depName: 'bar', + manager: 'npm', + currentVersion: '1.2.3', + newVersion: '2.3.4', + releases: [ + { version: '1.2.3', attestation: currentAttestation }, + { version: '2.3.4', attestation: newAttestation }, + ], + }); + + it('does not warn the user', async () => { + platform.createPr.mockResolvedValueOnce(pr); + + const res = await ensurePr({ + ...config, + upgrades: [dummyUpgrade], + }); + + expect(res).toEqual({ type: 'with-pr', pr }); + const [[bodyConfig]] = prBody.getPrBody.mock.calls; + expect(bodyConfig.upgrades[0].prBodyNotes).toBeUndefined(); + }); + }, + ); + }); + describe('when attestation is removed', () => { + const dummyUpgrade = partial({ + branchName: sourceBranch, + depType: 'foo', + depName: 'bar', + manager: 'npm', + currentVersion: '1.2.3', + newVersion: '2.3.4', + releases: [ + { version: '1.2.3', attestation: true }, + { version: '2.3.4', attestation: false }, + ], + }); + + it('warns the user', async () => { + platform.createPr.mockResolvedValueOnce(pr); + + const res = await ensurePr({ + ...config, + upgrades: [dummyUpgrade], + }); + + expect(res).toEqual({ type: 'with-pr', pr }); + const [[bodyConfig]] = prBody.getPrBody.mock.calls; + expect(bodyConfig).toMatchObject({ + upgrades: [ + { + prBodyNotes: [ + `> :exclamation: **Warning** +> +> bar 1.2.3 was released with an attestation, but 2.3.4 has no attestation. +> Verify that release 2.3.4 was published by the expected author. + +`, + ], + }, + ], + }); + }); + }); + }); + }); + describe('prCache', () => { const existingPr: Pr = { ...pr, diff --git a/lib/workers/repository/update/pr/index.ts b/lib/workers/repository/update/pr/index.ts index ae04c104b41..f36baa937a2 100644 --- a/lib/workers/repository/update/pr/index.ts +++ b/lib/workers/repository/update/pr/index.ts @@ -117,6 +117,38 @@ function hasNotIgnoredReviewers(pr: Pr, config: BranchConfig): boolean { return is.nonEmptyArray(pr.reviewers); } +function addPullRequestNoteIfAttestationHasBeenLost( + upgrade: BranchUpgradeConfig, +): void { + const { packageName, depName, currentVersion, newVersion } = upgrade; + const name = packageName ?? depName; + + const currentRelease = upgrade.releases?.find( + (release) => release.version === currentVersion, + ); + const newRelease = upgrade.releases?.find( + (release) => release.version === newVersion, + ); + + if ( + currentRelease && + newRelease && + currentRelease.attestation === true && + newRelease.attestation !== true + ) { + upgrade.prBodyNotes ??= []; + upgrade.prBodyNotes.push( + [ + '> :exclamation: **Warning**', + '>', + `> ${name} ${currentVersion} was released with an attestation, but ${newVersion} has no attestation.`, + `> Verify that release ${newVersion} was published by the expected author.`, + '\n', + ].join('\n'), + ); + } +} + // Ensures that PR exists with matching title/body export async function ensurePr( prConfig: BranchConfig, @@ -290,8 +322,7 @@ export async function ensurePr( } } else if (logJSON.error === 'MissingGithubToken') { upgrade.prBodyNotes ??= []; - upgrade.prBodyNotes = [ - ...upgrade.prBodyNotes, + upgrade.prBodyNotes.push( [ '> :exclamation: **Important**', '> ', @@ -299,9 +330,12 @@ export async function ensurePr( '> If you are self-hosted, please see [this instruction](https://github.com/renovatebot/renovate/blob/master/docs/usage/examples/self-hosting.md#githubcom-token-for-release-notes).', '\n', ].join('\n'), - ]; + ); } } + + addPullRequestNoteIfAttestationHasBeenLost(upgrade); + config.upgrades.push(upgrade); }