diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index ac932fb1fe1..c745db122b9 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1 +1 @@ -FROM ghcr.io/containerbase/devcontainer:14.6.14 +FROM ghcr.io/containerbase/devcontainer:14.6.15 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6b8a131527c..beffc5b7086 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -366,6 +366,8 @@ jobs: - setup-build runs-on: ubuntu-latest timeout-minutes: 7 + permissions: + security-events: write steps: - name: Checkout code @@ -379,6 +381,11 @@ jobs: with: args: -color -ignore "invalid activity type \"destroyed\" for \"merge_group\" Webhook event. available types are \"checks_requested\"" + - name: Run zizmor + uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2 + with: + version: v1.23.1 + test: needs: [setup, prefetch] diff --git a/docs/usage/reading-list.md b/docs/usage/reading-list.md index 0290f497e04..db319887ba7 100644 --- a/docs/usage/reading-list.md +++ b/docs/usage/reading-list.md @@ -22,8 +22,8 @@ If you're self-hosting or need to update private packages, complete the relevant If you're new to Renovate, you should: - Use the Mend Renovate App, or let someone else host Renovate for you -- Stick with the `config:recommended` preset -- Use the Dependency Dashboard (`config:recommended` enables it automatically) +- Stick with the [`config:recommended`](./presets-config.md#configrecommended) preset +- Use the Dependency Dashboard ([`config:recommended`](./presets-config.md#configrecommended) enables it automatically) - Read the pages in the "Beginners" list - Only create custom Renovate configuration when really needed diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index a87705184a1..43e95964b80 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -659,7 +659,7 @@ const options: Readonly[] = [ description: 'Change this value to override the default Renovate sidecar image.', type: 'string', - default: 'ghcr.io/renovatebot/base-image:13.33.9', + default: 'ghcr.io/renovatebot/base-image:13.33.10', globalOnly: true, deprecationMsg: 'The usage of `binarySource=docker` is deprecated, and will be removed in the future', diff --git a/lib/config/presets/internal/workarounds.preset.ts b/lib/config/presets/internal/workarounds.preset.ts index 12afee3ce43..95c3dd565fb 100644 --- a/lib/config/presets/internal/workarounds.preset.ts +++ b/lib/config/presets/internal/workarounds.preset.ts @@ -165,6 +165,7 @@ export const presets: Record = { 'adoptopenjdk', 'openjdk', 'java', + 'java-jdk', 'java-jre', 'sapmachine', '/^azul/zulu-openjdk/', diff --git a/lib/modules/manager/ant/extract.spec.ts b/lib/modules/manager/ant/extract.spec.ts new file mode 100644 index 00000000000..8ccc5b34e3f --- /dev/null +++ b/lib/modules/manager/ant/extract.spec.ts @@ -0,0 +1,157 @@ +import { codeBlock } from 'common-tags'; +import { fs } from '~test/util.ts'; +import { extractAllPackageFiles, extractPackageFile } from './extract.ts'; + +vi.mock('../../../util/fs/index.ts'); + +describe('modules/manager/ant/extract', () => { + it('extracts inline version dependencies from build.xml', () => { + expect( + extractPackageFile( + codeBlock` + + + + + + `, + 'build.xml', + ), + ).toEqual({ + deps: [ + expect.objectContaining({ + datasource: 'maven', + depName: 'junit:junit', + currentValue: '4.13.2', + depType: 'test', + registryUrls: [], + }), + ], + }); + }); + + it('extracts multiple dependencies', () => { + expect( + extractPackageFile( + codeBlock` + + + + + + + + `, + 'build.xml', + ), + ).toMatchObject({ + deps: [ + expect.objectContaining({ + depName: 'junit:junit', + currentValue: '4.13.2', + depType: 'test', + }), + expect.objectContaining({ + depName: 'org.slf4j:slf4j-api', + currentValue: '1.7.36', + depType: 'compile', + }), + expect.objectContaining({ + depName: 'org.apache.commons:commons-lang3', + currentValue: '3.12.0', + depType: 'runtime', + }), + ], + }); + }); + + it('defaults depType to compile when no scope is set', () => { + expect( + extractPackageFile( + codeBlock` + + + + + + `, + 'build.xml', + ), + ).toEqual({ + deps: [ + expect.objectContaining({ + depName: 'junit:junit', + depType: 'compile', + }), + ], + }); + }); + + it('returns null for invalid XML', () => { + expect(extractPackageFile('<<< not xml >>>', 'build.xml')).toBeNull(); + }); + + it('returns null for build.xml with no dependencies', async () => { + fs.readLocalFile.mockResolvedValue( + '', + ); + + await expect(extractAllPackageFiles({}, ['build.xml'])).resolves.toBeNull(); + }); + + it('ignores dependency nodes without version', () => { + expect( + extractPackageFile( + codeBlock` + + + + + + `, + 'build.xml', + ), + ).toBeNull(); + }); + + it('extracts dependencies with single-quoted attributes', () => { + expect( + extractPackageFile( + "", + 'build.xml', + ), + ).toEqual({ + deps: [ + expect.objectContaining({ + depName: 'junit:junit', + currentValue: '4.13.2', + }), + ], + }); + }); + + it('returns null for unreadable build.xml', async () => { + fs.readLocalFile.mockResolvedValue(null); + + await expect(extractAllPackageFiles({}, ['build.xml'])).resolves.toBeNull(); + }); + + it('does not revisit the same file', async () => { + let readCount = 0; + fs.readLocalFile.mockImplementation(() => { + readCount++; + return Promise.resolve(codeBlock` + + + + + + `); + }); + + const result = await extractAllPackageFiles({}, ['build.xml', 'build.xml']); + + expect(result).toHaveLength(1); + expect(readCount).toBe(1); + }); +}); diff --git a/lib/modules/manager/ant/extract.ts b/lib/modules/manager/ant/extract.ts new file mode 100644 index 00000000000..5c314621a0b --- /dev/null +++ b/lib/modules/manager/ant/extract.ts @@ -0,0 +1,125 @@ +import type { XmlElement } from 'xmldoc'; +import { XmlDocument } from 'xmldoc'; +import { logger } from '../../../logger/index.ts'; +import { readLocalFile } from '../../../util/fs/index.ts'; +import { MavenDatasource } from '../../datasource/maven/index.ts'; +import { isXmlElement } from '../nuget/util.ts'; +import type { + ExtractConfig, + PackageDependency, + PackageFile, + PackageFileContent, +} from '../types.ts'; + +const scopeNames = new Set([ + 'compile', + 'runtime', + 'test', + 'provided', + 'system', +]); + +function getDependencyType(scope: string | undefined): string { + if (scope && scopeNames.has(scope)) { + return scope; + } + return 'compile'; +} + +function collectDependency(node: XmlElement): PackageDependency | null { + const { groupId, artifactId, version, scope } = node.attr; + + if (!version || !groupId || !artifactId) { + return null; + } + + return { + datasource: MavenDatasource.id, + depName: `${groupId}:${artifactId}`, + currentValue: version, + depType: getDependencyType(scope), + registryUrls: [], + }; +} + +function walkNode( + node: XmlElement | XmlDocument, + deps: PackageDependency[], +): void { + for (const child of node.children) { + if (!isXmlElement(child)) { + continue; + } + + if (child.name === 'dependency') { + const dep = collectDependency(child); + if (dep) { + deps.push(dep); + } + } else { + walkNode(child, deps); + } + } +} + +export function extractPackageFile( + content: string, + packageFile: string, +): PackageFileContent | null { + let doc: XmlDocument; + try { + doc = new XmlDocument(content); + } catch { + logger.debug(`ant manager: could not parse XML ${packageFile}`); + return null; + } + + const deps: PackageDependency[] = []; + walkNode(doc, deps); + + if (deps.length === 0) { + return null; + } + + return { deps }; +} + +async function walkXmlFile( + packageFile: string, + visitedFiles: Set, +): Promise { + if (visitedFiles.has(packageFile)) { + return null; + } + visitedFiles.add(packageFile); + + const content = await readLocalFile(packageFile, 'utf8'); + if (!content) { + logger.debug(`ant manager: could not read ${packageFile}`); + return null; + } + + const result = extractPackageFile(content, packageFile); + if (!result) { + return null; + } + + return { packageFile, ...result }; +} + +export async function extractAllPackageFiles( + _config: ExtractConfig, + packageFiles: string[], +): Promise { + const results: PackageFile[] = []; + const visitedFiles = new Set(); + + for (const packageFile of packageFiles) { + const result = await walkXmlFile(packageFile, visitedFiles); + if (result) { + results.push(result); + } + } + + return results.length > 0 ? results : null; +} diff --git a/lib/modules/manager/ant/index.ts b/lib/modules/manager/ant/index.ts new file mode 100644 index 00000000000..697f7efeb17 --- /dev/null +++ b/lib/modules/manager/ant/index.ts @@ -0,0 +1,14 @@ +import type { Category } from '../../../constants/index.ts'; +import { MavenDatasource } from '../../datasource/maven/index.ts'; + +export { extractAllPackageFiles, extractPackageFile } from './extract.ts'; + +export const displayName = 'Apache Ant'; +export const url = 'https://ant.apache.org'; +export const categories: Category[] = ['java']; + +export const defaultConfig = { + managerFilePatterns: ['**/build.xml'], +}; + +export const supportedDatasources = [MavenDatasource.id]; diff --git a/lib/modules/manager/ant/readme.md b/lib/modules/manager/ant/readme.md new file mode 100644 index 00000000000..09fbd672b58 --- /dev/null +++ b/lib/modules/manager/ant/readme.md @@ -0,0 +1,2 @@ +Extracts Apache Ant dependencies from `build.xml` files that use the `maven-resolver-ant-tasks` library. +Dependencies are looked up using the Maven datasource. diff --git a/lib/modules/manager/api.ts b/lib/modules/manager/api.ts index 1343715d32a..78c74496853 100644 --- a/lib/modules/manager/api.ts +++ b/lib/modules/manager/api.ts @@ -1,5 +1,6 @@ import * as ansible from './ansible/index.ts'; import * as ansibleGalaxy from './ansible-galaxy/index.ts'; +import * as ant from './ant/index.ts'; import * as argoCD from './argocd/index.ts'; import * as asdf from './asdf/index.ts'; import * as azurePipelines from './azure-pipelines/index.ts'; @@ -113,6 +114,7 @@ import * as woodpecker from './woodpecker/index.ts'; const api = new Map(); export default api; +api.set('ant', ant); api.set('ansible', ansible); api.set('ansible-galaxy', ansibleGalaxy); api.set('argocd', argoCD); diff --git a/lib/modules/manager/github-actions/community.ts b/lib/modules/manager/github-actions/community.ts index 48162e5c1b2..ec06346f067 100644 --- a/lib/modules/manager/github-actions/community.ts +++ b/lib/modules/manager/github-actions/community.ts @@ -265,6 +265,31 @@ const SetupGolangciLint = z }; }); +const ZizmorcoreZizmorAction = z + .object({ + uses: matchAction('zizmorcore/zizmor-action'), + with: z.object({ version: z.string().optional() }), + }) + .transform(({ with: val }): PackageDependency => { + let skipStage: StageName | undefined; + let skipReason: SkipReason | undefined; + + if (!val.version) { + skipStage = 'extract'; + skipReason = 'unspecified-version'; + } + + return { + datasource: PypiDatasource.id, + depName: 'zizmor', + packageName: 'zizmor', + ...(skipStage && { skipStage }), + ...(skipReason && { skipReason }), + currentValue: val.version, + depType: 'uses-with', + }; + }); + /** * schema here should match the whole step, * there may be some actions use env as arguments version. @@ -282,4 +307,5 @@ export const CommunityActions = z.union([ SetupRuby, SetupHatch, SetupGolangciLint, + ZizmorcoreZizmorAction, ]); diff --git a/lib/modules/manager/github-actions/extract.spec.ts b/lib/modules/manager/github-actions/extract.spec.ts index 6514936154a..48dac7b2acf 100644 --- a/lib/modules/manager/github-actions/extract.spec.ts +++ b/lib/modules/manager/github-actions/extract.spec.ts @@ -1324,6 +1324,56 @@ describe('modules/manager/github-actions/extract', () => { }, ], }, + { + step: { + uses: 'zizmorcore/zizmor-action@v0.5.2', + with: {}, + }, + expected: [ + { + skipStage: 'extract', + skipReason: 'unspecified-version', + datasource: 'pypi', + depName: 'zizmor', + depType: 'uses-with', + packageName: 'zizmor', + }, + ], + }, + { + step: { + uses: 'zizmorcore/zizmor-action@v0.5.2', + with: { + version: 'v1.23.1', + }, + }, + expected: [ + { + currentValue: 'v1.23.1', + datasource: 'pypi', + depName: 'zizmor', + depType: 'uses-with', + packageName: 'zizmor', + }, + ], + }, + { + step: { + uses: 'zizmorcore/zizmor-action@v0.5.2', + with: { + version: '1.23.1', + }, + }, + expected: [ + { + currentValue: '1.23.1', + datasource: 'pypi', + depName: 'zizmor', + depType: 'uses-with', + packageName: 'zizmor', + }, + ], + }, ])('extract from $step.uses', ({ step, expected }) => { const yamlContent = yaml.dump({ jobs: { build: { steps: [step] } } }); diff --git a/lib/modules/manager/gradle/parser.spec.ts b/lib/modules/manager/gradle/parser.spec.ts index ee6f67612ea..10010b44fd5 100644 --- a/lib/modules/manager/gradle/parser.spec.ts +++ b/lib/modules/manager/gradle/parser.spec.ts @@ -382,17 +382,20 @@ describe('modules/manager/gradle/parser', () => { describe('dependencies', () => { describe('simple dependency strings', () => { it.each` - input | output - ${'"foo:bar:1.2.3"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} - ${'"foo:bar:1.2+"'} | ${{ depName: 'foo:bar', currentValue: '1.2+' }} - ${'"foo:bar:1.2.3@zip"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', dataType: 'zip' }} - ${'"foo:bar:1.2.3:docs@jar"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', dataType: 'jar' }} - ${'"foo:bar1:1"'} | ${{ depName: 'foo:bar1', currentValue: '1', managerData: { fileReplacePosition: 10 } }} - ${'"foo:bar:[1.2.3, )"'} | ${{ depName: 'foo:bar', currentValue: '[1.2.3, )', managerData: { fileReplacePosition: 9 } }} - ${'"foo:bar:[1.2.3, 1.2.4)"'} | ${{ depName: 'foo:bar', currentValue: '[1.2.3, 1.2.4)', managerData: { fileReplacePosition: 9 } }} - ${'"foo:bar:[,1.2.4)"'} | ${{ depName: 'foo:bar', currentValue: '[,1.2.4)', managerData: { fileReplacePosition: 9 } }} - ${'"foo:bar:x86@x86"'} | ${{ depName: 'foo:bar', currentValue: 'x86', managerData: { fileReplacePosition: 9 } }} - ${'foo.bar = "foo:bar:1.2.3"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} + input | output + ${'"foo:bar:1.2.3"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} + ${'"foo:bar:1.2+"'} | ${{ depName: 'foo:bar', currentValue: '1.2+' }} + ${'"foo:bar:1.2.3@zip"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', dataType: 'zip' }} + ${'"foo:bar:1.2.3:docs@jar"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', dataType: 'jar' }} + ${'"foo:bar1:1"'} | ${{ depName: 'foo:bar1', currentValue: '1', managerData: { fileReplacePosition: 10 } }} + ${'"foo:bar:[1.2.3, )"'} | ${{ depName: 'foo:bar', currentValue: '[1.2.3, )', managerData: { fileReplacePosition: 9 } }} + ${'"foo:bar:[1.2.3, 1.2.4)"'} | ${{ depName: 'foo:bar', currentValue: '[1.2.3, 1.2.4)', managerData: { fileReplacePosition: 9 } }} + ${'"foo:bar:[,1.2.4)"'} | ${{ depName: 'foo:bar', currentValue: '[,1.2.4)', managerData: { fileReplacePosition: 9 } }} + ${'"foo:bar:[1.2.3, )!!1.2.3"'} | ${{ depName: 'foo:bar', currentValue: '[1.2.3, )!!1.2.3', managerData: { fileReplacePosition: 9 } }} + ${'"foo:bar:[1.2.3, 1.2.4)!!1.2.3"'} | ${{ depName: 'foo:bar', currentValue: '[1.2.3, 1.2.4)!!1.2.3', managerData: { fileReplacePosition: 9 } }} + ${'"foo:bar:[,1.2.4)!!1.2.4"'} | ${{ depName: 'foo:bar', currentValue: '[,1.2.4)!!1.2.4', managerData: { fileReplacePosition: 9 } }} + ${'"foo:bar:x86@x86"'} | ${{ depName: 'foo:bar', currentValue: 'x86', managerData: { fileReplacePosition: 9 } }} + ${'foo.bar = "foo:bar:1.2.3"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} `('$input', ({ input, output }) => { const { deps } = parseGradle(input); expect(deps).toMatchObject([output]); diff --git a/lib/modules/manager/gradle/utils.ts b/lib/modules/manager/gradle/utils.ts index 4309a16b876..de06bdcd439 100644 --- a/lib/modules/manager/gradle/utils.ts +++ b/lib/modules/manager/gradle/utils.ts @@ -12,7 +12,7 @@ const artifactRegex = regEx( '^[a-zA-Z][-_a-zA-Z0-9]*(?:\\.[a-zA-Z0-9][-_a-zA-Z0-9]*?)*$', ); -const versionLikeRegex = regEx('^(?[-_.\\[\\](),a-zA-Z0-9+ ]+)'); +const versionLikeRegex = regEx('^(?[-_.\\[\\](),a-zA-Z0-9+! ]+)'); // Extracts version-like and range-like strings from the beginning of input export function versionLikeSubstring( diff --git a/lib/modules/versioning/gradle/compare.ts b/lib/modules/versioning/gradle/compare.ts index 8b67d04774f..08c408fd5e4 100644 --- a/lib/modules/versioning/gradle/compare.ts +++ b/lib/modules/versioning/gradle/compare.ts @@ -232,6 +232,9 @@ interface MavenBasedRange { rightBound: RangeBound; rightBoundStr: string; rightVal: string | null; + // The existence of preferredVal implies the "strictly" keyword "!!" + // leading up to preferredVal: "!![preferred version]" + preferredVal: string | null; } export function parsePrefixRange(input: string): PrefixRange | null { @@ -256,7 +259,7 @@ export function parsePrefixRange(input: string): PrefixRange | null { } const mavenBasedRangeRegex = regEx( - /^(?[[\](]\s*)(?[-._+a-zA-Z0-9]*?)(?\s*,\s*)(?[-._+a-zA-Z0-9]*?)(?\s*[[\])])$/, + /^(?[[\](]\s*)(?[-._+a-zA-Z0-9]*?)(?\s*,\s*)(?[-._+a-zA-Z0-9]*?)(?\s*[[\])])(?:!!(?[-._+a-zA-Z0-9]+))?$/, ); export function parseMavenBasedRange(input: string): MavenBasedRange | null { @@ -265,47 +268,46 @@ export function parseMavenBasedRange(input: string): MavenBasedRange | null { } const matchGroups = mavenBasedRangeRegex.exec(input)?.groups; - if (matchGroups) { - const { leftBoundStr, separator, rightBoundStr } = matchGroups; - let leftVal: string | null = matchGroups.leftVal; - let rightVal: string | null = matchGroups.rightVal; - if (!leftVal) { - leftVal = null; - } - if (!rightVal) { - rightVal = null; - } - const isVersionLeft = isString(leftVal) && isVersion(leftVal); - const isVersionRight = isString(rightVal) && isVersion(rightVal); - if ( - (leftVal === null || isVersionLeft) && - (rightVal === null || isVersionRight) - ) { - if ( - isVersionLeft && - isVersionRight && - leftVal && - rightVal && - compare(leftVal, rightVal) === 1 - ) { - return null; - } - const leftBound = leftBoundStr.trim() === '[' ? 'inclusive' : 'exclusive'; - const rightBound = - rightBoundStr.trim() === ']' ? 'inclusive' : 'exclusive'; - return { - leftBound, - leftBoundStr, - leftVal, - separator, - rightBound, - rightBoundStr, - rightVal, - }; - } + if (!matchGroups) { + return null; } - return null; + const { leftBoundStr, separator, rightBoundStr } = matchGroups; + const leftVal = matchGroups.leftVal || null; + const rightVal = matchGroups.rightVal || null; + const preferredVal = matchGroups.preferredVal || null; + const isVersionLeft = isString(leftVal) && isVersion(leftVal); + const isVersionRight = isString(rightVal) && isVersion(rightVal); + if ( + (leftVal !== null && !isVersionLeft) || + (rightVal !== null && !isVersionRight) + ) { + return null; + } + + if ( + isVersionLeft && + isVersionRight && + leftVal && + rightVal && + compare(leftVal, rightVal) === 1 + ) { + return null; + } + + const leftBound = leftBoundStr.trim() === '[' ? 'inclusive' : 'exclusive'; + const rightBound = rightBoundStr.trim() === ']' ? 'inclusive' : 'exclusive'; + + return { + leftBound, + leftBoundStr, + leftVal, + separator, + rightBound, + rightBoundStr, + rightVal, + preferredVal, + }; } interface SingleVersionRange { diff --git a/lib/modules/versioning/gradle/index.spec.ts b/lib/modules/versioning/gradle/index.spec.ts index 39726d3e03e..27686e2a1bf 100644 --- a/lib/modules/versioning/gradle/index.spec.ts +++ b/lib/modules/versioning/gradle/index.spec.ts @@ -116,6 +116,7 @@ describe('modules/versioning/gradle/index', () => { ${'[1.2,,1.3]'} ${'[1,[2,3],4]'} ${'[1.3,1.2]'} + ${'[1..3,1.2]'} `('parseMavenBasedRange("$rangeStr") is null', ({ rangeStr }) => { const range = parseMavenBasedRange(rangeStr); expect(range).toBeNull(); @@ -301,46 +302,43 @@ describe('modules/versioning/gradle/index', () => { describe('getNewValue', () => { it.each` - currentValue | rangeStrategy | currentVersion | newVersion | expected - ${'1'} | ${null} | ${null} | ${'1.1'} | ${'1.1'} - ${'[1.2.3,]'} | ${null} | ${null} | ${'1.2.4'} | ${'[1.2.3,]'} - ${'[1.2.3,2)'} | ${null} | ${null} | ${'2.0.0'} | ${'[1.2.3,3)'} - ${'[1.3,1.4)'} | ${null} | ${null} | ${'2.0.0'} | ${'[2.0,3.0)'} - ${'[1.3,1.4)'} | ${null} | ${null} | ${'1.5.1'} | ${'[1.5,1.6)'} - ${'[1,1.4)'} | ${null} | ${null} | ${'1.5.1'} | ${'[1,1.6)'} - ${'[1.3,2)'} | ${null} | ${null} | ${'1.4.0'} | ${'[1.3,2)'} - ${'1.?'} | ${null} | ${null} | ${'2'} | ${'1.?'} - ${'1..'} | ${null} | ${null} | ${'2'} | ${'1..'} - ${'1--'} | ${null} | ${null} | ${'2'} | ${'1--'} - ${'+'} | ${null} | ${null} | ${'1.2.4'} | ${null} - ${'1.+'} | ${null} | ${null} | ${'1.2.4'} | ${'1.+'} - ${'1.+'} | ${null} | ${null} | ${'2.1.2'} | ${'2.+'} - ${'1.+'} | ${null} | ${null} | ${'2'} | ${'2.+'} - ${'1.3.+'} | ${null} | ${null} | ${'1.3.4'} | ${'1.3.+'} - ${'1.3.+'} | ${null} | ${null} | ${'1.5.2'} | ${'1.5.+'} - ${'1.3.+'} | ${null} | ${null} | ${'2'} | ${'2'} - ${'[1.2.3]'} | ${'pin'} | ${'1.2.3'} | ${'1.2.4'} | ${'1.2.4'} - ${'[1.0.0,1.2.3]'} | ${'pin'} | ${'1.0.0'} | ${'1.2.4'} | ${'1.2.4'} - ${'[1.0.0,1.2.23]'} | ${'pin'} | ${'1.0.0'} | ${'1.2.23'} | ${'1.2.23'} - ${'(,1.0]'} | ${'pin'} | ${'0.0.1'} | ${'2.0'} | ${'2.0'} - ${'],1.0]'} | ${'pin'} | ${'0.0.1'} | ${'2.0'} | ${'2.0'} - ${'(,1.0)'} | ${'pin'} | ${'0.1'} | ${'2.0'} | ${'2.0'} - ${'],1.0['} | ${'pin'} | ${'2.0'} | ${'],2.0['} | ${'],2.0['} - ${'[1.0,1.2],[1.3,1.5)'} | ${'pin'} | ${'1.0'} | ${'1.2.4'} | ${'1.2.4'} - ${'[1.0,1.2],[1.3,1.5['} | ${'pin'} | ${'1.0'} | ${'1.2.4'} | ${'1.2.4'} - ${'[1.2.3,)'} | ${'pin'} | ${'1.2.3'} | ${'1.2.4'} | ${'1.2.4'} - ${'[1.2.3,['} | ${'pin'} | ${'1.2.3'} | ${'1.2.4'} | ${'1.2.4'} - ${'[1.2.3]'} | ${'bump'} | ${'1.2.3'} | ${'1.2.4'} | ${'[1.2.4]'} - ${'[1.0.0,1.2.3]'} | ${'bump'} | ${'1.0.0'} | ${'1.2.4'} | ${'[1.0.0,1.2.4]'} - ${'[1.0.0,1.2.23]'} | ${'bump'} | ${'1.0.0'} | ${'1.2.23'} | ${'[1.0.0,1.2.23]'} - ${'(,1.0]'} | ${'bump'} | ${'0.0.1'} | ${'2.0'} | ${'(,2.0]'} - ${'],1.0]'} | ${'bump'} | ${'0.0.1'} | ${'2.0'} | ${'],2.0]'} - ${'(,1.0)'} | ${'bump'} | ${'0.1'} | ${'2.0'} | ${'(,3.0)'} - ${'],1.0['} | ${'bump'} | ${'2.0'} | ${'],2.0['} | ${'],1.0['} - ${'[1.0,1.2],[1.3,1.5)'} | ${'bump'} | ${'1.0'} | ${'1.2.4'} | ${'[1.0,1.2],[1.3,1.5)'} - ${'[1.0,1.2],[1.3,1.5['} | ${'bump'} | ${'1.0'} | ${'1.2.4'} | ${'[1.0,1.2],[1.3,1.5['} - ${'[1.2.3,)'} | ${'bump'} | ${'1.2.3'} | ${'1.2.4'} | ${'[1.2.4,)'} - ${'[1.2.3,['} | ${'bump'} | ${'1.2.3'} | ${'1.2.4'} | ${'[1.2.4,['} + currentValue | rangeStrategy | currentVersion | newVersion | expected + ${'1'} | ${null} | ${null} | ${'1.1'} | ${'1.1'} + ${'[1.2.3,]'} | ${null} | ${null} | ${'1.2.4'} | ${'[1.2.3,]'} + ${'[1.2.3,2)'} | ${null} | ${null} | ${'2.0.0'} | ${'[1.2.3,3)'} + ${'[1.3,1.4)'} | ${null} | ${null} | ${'2.0.0'} | ${'[2.0,3.0)'} + ${'[1.3,1.4)'} | ${null} | ${null} | ${'1.5.1'} | ${'[1.5,1.6)'} + ${'[1,1.4)'} | ${null} | ${null} | ${'1.5.1'} | ${'[1,1.6)'} + ${'[1.3,2)'} | ${null} | ${null} | ${'1.4.0'} | ${'[1.3,2)'} + ${'1.?'} | ${null} | ${null} | ${'2'} | ${'1.?'} + ${'1..'} | ${null} | ${null} | ${'2'} | ${'1..'} + ${'1--'} | ${null} | ${null} | ${'2'} | ${'1--'} + ${'+'} | ${null} | ${null} | ${'1.2.4'} | ${null} + ${'1.+'} | ${null} | ${null} | ${'1.2.4'} | ${'1.+'} + ${'1.+'} | ${null} | ${null} | ${'2.1.2'} | ${'2.+'} + ${'1.+'} | ${null} | ${null} | ${'2'} | ${'2.+'} + ${'1.3.+'} | ${null} | ${null} | ${'1.3.4'} | ${'1.3.+'} + ${'1.3.+'} | ${null} | ${null} | ${'1.5.2'} | ${'1.5.+'} + ${'1.3.+'} | ${null} | ${null} | ${'2'} | ${'2'} + ${'[1.2.3]'} | ${'bump'} | ${'1.2.3'} | ${'1.2.4'} | ${'[1.2.4]'} + ${'[1.0.0,1.2.3]'} | ${'bump'} | ${'1.0.0'} | ${'1.2.4'} | ${'[1.0.0,1.2.4]'} + ${'[1.0.0,1.2.23]'} | ${'bump'} | ${'1.0.0'} | ${'1.2.23'} | ${'[1.0.0,1.2.23]'} + ${'(,1.0]'} | ${'bump'} | ${'0.0.1'} | ${'2.0'} | ${'(,2.0]'} + ${'],1.0]'} | ${'bump'} | ${'0.0.1'} | ${'2.0'} | ${'],2.0]'} + ${'(,1.0)'} | ${'bump'} | ${'0.1'} | ${'2.0'} | ${'(,3.0)'} + ${'],1.0['} | ${'bump'} | ${'2.0'} | ${'],2.0['} | ${'],1.0['} + ${'[1.0,1.2],[1.3,1.5)'} | ${'bump'} | ${'1.0'} | ${'1.2.4'} | ${'[1.0,1.2],[1.3,1.5)'} + ${'[1.0,1.2],[1.3,1.5['} | ${'bump'} | ${'1.0'} | ${'1.2.4'} | ${'[1.0,1.2],[1.3,1.5['} + ${'[1.2.3,)'} | ${'bump'} | ${'1.2.3'} | ${'1.2.4'} | ${'[1.2.4,)'} + ${'[1.2.3,['} | ${'bump'} | ${'1.2.3'} | ${'1.2.4'} | ${'[1.2.4,['} + ${'(,1.0]!!1.0'} | ${'bump'} | ${'0.0.1'} | ${'2.0'} | ${'(,2.0]!!2.0'} + ${'],1.0]!!1.0'} | ${'bump'} | ${'0.0.1'} | ${'2.0'} | ${'],2.0]!!2.0'} + ${'(,1.0)!!1.0'} | ${'bump'} | ${'0.1'} | ${'2.0'} | ${'(,3.0)!!2.0'} + ${'],1.0[!!1.0'} | ${'bump'} | ${'2.0'} | ${'],2.0['} | ${'],1.0[!!1.0'} + ${'[1.0,1.2],[1.3,1.5)!!1.0'} | ${'bump'} | ${'1.0'} | ${'1.2.4'} | ${'[1.0,1.2],[1.3,1.5)!!1.0'} + ${'[1.0,1.2],[1.3,1.5[!!1.0'} | ${'bump'} | ${'1.0'} | ${'1.2.4'} | ${'[1.0,1.2],[1.3,1.5[!!1.0'} + ${'[1.2.3,)!!1.2.3'} | ${'bump'} | ${'1.2.3'} | ${'1.2.4'} | ${'[1.2.4,)!!1.2.4'} + ${'[1.2.3,[!!1.2.3'} | ${'bump'} | ${'1.2.3'} | ${'1.2.4'} | ${'[1.2.4,[!!1.2.4'} `( 'getNewValue($currentValue, $rangeStrategy, $currentVersion, $newVersion, $expected) === $expected', ({ diff --git a/lib/modules/versioning/gradle/index.ts b/lib/modules/versioning/gradle/index.ts index 6de3aa5a6b2..3c01e7b455b 100644 --- a/lib/modules/versioning/gradle/index.ts +++ b/lib/modules/versioning/gradle/index.ts @@ -220,6 +220,35 @@ function getNewValue({ } } + const mavenRange = parseMavenBasedRange(currentValue); + if (mavenRange?.preferredVal) { + const { leftVal, rightVal, preferredVal } = mavenRange; + const baseRange = currentValue.slice( + 0, + currentValue.lastIndexOf(`!!${preferredVal}`), + ); + const newBaseRange = mavenVersion.getNewValue({ + currentValue: baseRange, + rangeStrategy, + newVersion, + }); + // v8 ignore if: the implementation has a non-null return type + if (newBaseRange === null) { + return null; + } + + const preferredIsBoundary = + preferredVal === leftVal || preferredVal === rightVal; + const newParsed = parseMavenBasedRange(newBaseRange); + const preferredStillPresent = + newParsed?.leftVal === preferredVal || + newParsed?.rightVal === preferredVal; + const newPreferredVal = + preferredIsBoundary && !preferredStillPresent ? newVersion : preferredVal; + + return `${newBaseRange}!!${newPreferredVal}`; + } + return mavenVersion.getNewValue({ currentValue, rangeStrategy, newVersion }); } diff --git a/lib/workers/repository/onboarding/pr/__snapshots__/config-description.spec.ts.snap b/lib/workers/repository/onboarding/pr/__snapshots__/config-description.spec.ts.snap index 1cab8241f10..c0e9ad29da8 100644 --- a/lib/workers/repository/onboarding/pr/__snapshots__/config-description.spec.ts.snap +++ b/lib/workers/repository/onboarding/pr/__snapshots__/config-description.spec.ts.snap @@ -1,50 +1,5 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`workers/repository/onboarding/pr/config-description > getConfigDesc() > contains the onboardingConfigFileName if set 1`] = ` -" -### Configuration Summary - -Based on the default config's presets, Renovate will: - - - Start dependency updates only once this onboarding PR is merged - - Run Renovate on following schedule: before 5am - -🔡 Do you want to change how Renovate upgrades your dependencies? Add your custom config to \`.github/renovate.json\` in this branch. Renovate will update the Pull Request description the next time it runs. - ---- -" -`; - -exports[`workers/repository/onboarding/pr/config-description > getConfigDesc() > falls back to "renovate.json" if onboardingConfigFileName is not set 1`] = ` -" -### Configuration Summary - -Based on the default config's presets, Renovate will: - - - Start dependency updates only once this onboarding PR is merged - - Run Renovate on following schedule: before 5am - -🔡 Do you want to change how Renovate upgrades your dependencies? Add your custom config to \`renovate.json\` in this branch. Renovate will update the Pull Request description the next time it runs. - ---- -" -`; - -exports[`workers/repository/onboarding/pr/config-description > getConfigDesc() > falls back to "renovate.json" if onboardingConfigFileName is not valid 1`] = ` -" -### Configuration Summary - -Based on the default config's presets, Renovate will: - - - Start dependency updates only once this onboarding PR is merged - - Run Renovate on following schedule: before 5am - -🔡 Do you want to change how Renovate upgrades your dependencies? Add your custom config to \`renovate.json\` in this branch. Renovate will update the Pull Request description the next time it runs. - ---- -" -`; - exports[`workers/repository/onboarding/pr/config-description > getConfigDesc() > include retry/refresh checkbox message only if onboardingRebaseCheckbox is true 1`] = ` " ### Configuration Summary @@ -54,8 +9,6 @@ Based on the default config's presets, Renovate will: - Start dependency updates only once this onboarding PR is merged - Run Renovate on following schedule: before 5am -🔡 Do you want to change how Renovate upgrades your dependencies? Add your custom config to \`.github/renovate.json\` in this branch and select the Retry/Rebase checkbox below. Renovate will update the Pull Request description the next time it runs. - --- " `; @@ -72,8 +25,6 @@ Based on the default config's presets, Renovate will: - something else - this is Docker-only -🔡 Do you want to change how Renovate upgrades your dependencies? Add your custom config to \`renovate.json\` in this branch. Renovate will update the Pull Request description the next time it runs. - --- " `; diff --git a/lib/workers/repository/onboarding/pr/__snapshots__/index.spec.ts.snap b/lib/workers/repository/onboarding/pr/__snapshots__/index.spec.ts.snap index 2df66344f20..48d56d36cb5 100644 --- a/lib/workers/repository/onboarding/pr/__snapshots__/index.spec.ts.snap +++ b/lib/workers/repository/onboarding/pr/__snapshots__/index.spec.ts.snap @@ -7,6 +7,10 @@ Welcome to [Renovate](https://github.com/renovatebot/renovate)! This is an onboa 🚦 To activate Renovate, merge this Pull Request. To disable Renovate, simply close this Pull Request unmerged. +📚 See our [Reading List](https://docs.renovatebot.com/reading-list/) for relevant documentation you may be interested in reading. + +🔡 Do you want to change how Renovate upgrades your dependencies? Add your custom config to \`renovate.json\` in this branch. Renovate will update the Pull Request description the next time it runs. + --- @@ -39,6 +43,10 @@ Welcome to [Renovate](https://github.com/renovatebot/renovate)! This is an onboa 🚦 To activate Renovate, merge this Pull Request. To disable Renovate, simply close this Pull Request unmerged. +📚 See our [Reading List](https://docs.renovatebot.com/reading-list/) for relevant documentation you may be interested in reading. + +🔡 Do you want to change how Renovate upgrades your dependencies? Add your custom config to \`renovate.json\` in this branch and select the Retry/Rebase checkbox below. Renovate will update the Pull Request description the next time it runs. + --- @@ -76,6 +84,10 @@ Welcome to [Renovate](https://github.com/renovatebot/renovate)! This is an onboa 🚦 To activate Renovate, merge this Pull Request. To disable Renovate, simply close this Pull Request unmerged. +📚 See our [Reading List](https://docs.renovatebot.com/reading-list/) for relevant documentation you may be interested in reading. + +🔡 Do you want to change how Renovate upgrades your dependencies? Add your custom config to \`renovate.json\` in this branch. Renovate will update the Pull Request description the next time it runs. + --- @@ -108,6 +120,10 @@ Welcome to [Renovate](https://github.com/renovatebot/renovate)! This is an onboa 🚦 To activate Renovate, merge this Pull Request. To disable Renovate, simply close this Pull Request unmerged. +📚 See our [Reading List](https://docs.renovatebot.com/reading-list/) for relevant documentation you may be interested in reading. + +🔡 Do you want to change how Renovate upgrades your dependencies? Add your custom config to \`renovate.json\` in this branch and select the Retry/Rebase checkbox below. Renovate will update the Pull Request description the next time it runs. + --- @@ -147,6 +163,10 @@ Welcome to [Renovate](https://github.com/renovatebot/renovate)! This is an onboa 🚦 To activate Renovate, merge this Pull Request. To disable Renovate, simply close this Pull Request unmerged. +📚 See our [Reading List](https://docs.renovatebot.com/reading-list/) for relevant documentation you may be interested in reading. + +🔡 Do you want to change how Renovate upgrades your dependencies? Add your custom config to \`renovate.json\` in this branch. Renovate will update the Pull Request description the next time it runs. + --- @@ -184,6 +204,10 @@ Welcome to [Renovate](https://github.com/renovatebot/renovate)! This is an onboa 🚦 To activate Renovate, merge this Pull Request. To disable Renovate, simply close this Pull Request unmerged. +📚 See our [Reading List](https://docs.renovatebot.com/reading-list/) for relevant documentation you may be interested in reading. + +🔡 Do you want to change how Renovate upgrades your dependencies? Add your custom config to \`renovate.json\` in this branch and select the Retry/Rebase checkbox below. Renovate will update the Pull Request description the next time it runs. + --- diff --git a/lib/workers/repository/onboarding/pr/config-description.spec.ts b/lib/workers/repository/onboarding/pr/config-description.spec.ts index e152622882f..56c312cfd56 100644 --- a/lib/workers/repository/onboarding/pr/config-description.spec.ts +++ b/lib/workers/repository/onboarding/pr/config-description.spec.ts @@ -50,40 +50,11 @@ describe('workers/repository/onboarding/pr/config-description', () => { - Start dependency updates only once this onboarding PR is merged - Run Renovate on following schedule: before 5am - 🔡 Do you want to change how Renovate upgrades your dependencies? Add your custom config to \`renovate.json\` in this branch. Renovate will update the Pull Request description the next time it runs. - --- " `); }); - it('contains the onboardingConfigFileName if set', () => { - delete config.description; - config.schedule = ['before 5am']; - GlobalConfig.set({ onboardingConfigFileName: '.github/renovate.json' }); - const res = getConfigDesc(config); - expect(res).toMatchSnapshot(); - expect(res.indexOf('`.github/renovate.json`')).not.toBe(-1); - expect(res.indexOf('`renovate.json`')).toBe(-1); - }); - - it('falls back to "renovate.json" if onboardingConfigFileName is not set', () => { - delete config.description; - config.schedule = ['before 5am']; - const res = getConfigDesc(config); - expect(res).toMatchSnapshot(); - expect(res.indexOf('`renovate.json`')).not.toBe(-1); - }); - - it('falls back to "renovate.json" if onboardingConfigFileName is not valid', () => { - delete config.description; - config.schedule = ['before 5am']; - GlobalConfig.set({ onboardingConfigFileName: 'foo.bar' }); - const res = getConfigDesc(config); - expect(res).toMatchSnapshot(); - expect(res.indexOf('`renovate.json`')).not.toBe(-1); - }); - it('include retry/refresh checkbox message only if onboardingRebaseCheckbox is true', () => { delete config.description; config.schedule = ['before 5am']; diff --git a/lib/workers/repository/onboarding/pr/config-description.ts b/lib/workers/repository/onboarding/pr/config-description.ts index 65f5e756f18..5774045f0b6 100644 --- a/lib/workers/repository/onboarding/pr/config-description.ts +++ b/lib/workers/repository/onboarding/pr/config-description.ts @@ -2,8 +2,6 @@ import { isArray, isString } from '@sindresorhus/is'; import type { RenovateConfig } from '../../../../config/types.ts'; import { logger } from '../../../../logger/index.ts'; import type { PackageFile } from '../../../../modules/manager/types.ts'; -import { emojify } from '../../../../util/emoji.ts'; -import { getDefaultConfigFileName } from '../common.ts'; export function getScheduleDesc(config: RenovateConfig): string[] { logger.debug('getScheduleDesc()'); @@ -32,8 +30,6 @@ export function getConfigDesc( // TODO: remove unused parameter _packageFiles?: Record, ): string { - // TODO: type (#22198) - const configFile = getDefaultConfigFileName(); logger.debug('getConfigDesc()'); logger.trace({ config }); const descriptionArr = getDescriptionArray(config); @@ -47,15 +43,6 @@ export function getConfigDesc( descriptionArr.forEach((d) => { desc += ` - ${d}\n`; }); - desc += '\n'; - desc += emojify( - `:abcd: Do you want to change how Renovate upgrades your dependencies?`, - ); - desc += ` Add your custom config to \`${configFile}\` in this branch${ - config.onboardingRebaseCheckbox - ? ' and select the Retry/Rebase checkbox below' - : '' - }. Renovate will update the Pull Request description the next time it runs.`; - desc += '\n\n---\n'; + desc += '\n---\n'; return desc; } diff --git a/lib/workers/repository/onboarding/pr/index.spec.ts b/lib/workers/repository/onboarding/pr/index.spec.ts index 52d6a440f1e..b34323091ac 100644 --- a/lib/workers/repository/onboarding/pr/index.spec.ts +++ b/lib/workers/repository/onboarding/pr/index.spec.ts @@ -21,7 +21,7 @@ describe('workers/repository/onboarding/pr/index', () => { let branches: BranchConfig[]; const bodyStruct = { - hash: '6aa71f8cb7b1503b883485c8f5bd564b31923b9c7fa765abe2a7338af40e03b1', + hash: 'ca7d8b2b5477b8db83231a2584c4e0a1748e4c19e26089507ee1447b8eeb6894', }; beforeEach(() => { @@ -213,7 +213,7 @@ describe('workers/repository/onboarding/pr/index', () => { '(onboardingRebaseCheckbox="$onboardingRebaseCheckbox")', async ({ onboardingRebaseCheckbox }) => { const hash = - '30029ee05ed80b34d2f743afda6e78fe20247a1eedaa9ce6a8070045c229ebfa'; // no rebase checkbox PR hash + '16d923d407af84b1d00c4336c5dd88fc3cd0e6695b7e4e13debd02c7b8c4b60d'; // no rebase checkbox PR hash config.onboardingRebaseCheckbox = onboardingRebaseCheckbox; OnboardingState.prUpdateRequested = true; // case 'false' is tested in "breaks early when onboarding" platform.getBranchPr.mockResolvedValue( @@ -475,6 +475,35 @@ describe('workers/repository/onboarding/pr/index', () => { expect(platform.createPr).toHaveBeenCalledTimes(1); }); + describe('the created PR references onboardingConfigFileName', () => { + it('when set', async () => { + GlobalConfig.set({ onboardingConfigFileName: '.github/renovate.json' }); + await ensureOnboardingPr(config, packageFiles, branches); + expect(platform.createPr.mock.calls[0][0].prBody).toContain( + `Add your custom config to \`.github/renovate.json\` in this branch`, + ); + expect(platform.createPr.mock.calls[0][0].prBody).not.toContain( + '`renovate.json`', + ); + }); + + it('when not set, falls back to "renovate.json"', async () => { + GlobalConfig.set({ onboardingConfigFileName: undefined }); + await ensureOnboardingPr(config, packageFiles, branches); + expect(platform.createPr.mock.calls[0][0].prBody).toContain( + `Add your custom config to \`renovate.json\` in this branch`, + ); + }); + + it('when set, but not a valid filename, falls back to "renovate.json"', async () => { + GlobalConfig.set({ onboardingConfigFileName: 'foo.bar' }); + await ensureOnboardingPr(config, packageFiles, branches); + expect(platform.createPr.mock.calls[0][0].prBody).toContain( + `Add your custom config to \`renovate.json\` in this branch`, + ); + }); + }); + it('dryrun of creates PR', async () => { GlobalConfig.set({ dryRun: 'full', diff --git a/lib/workers/repository/onboarding/pr/index.ts b/lib/workers/repository/onboarding/pr/index.ts index f3cfbccd293..de654b56902 100644 --- a/lib/workers/repository/onboarding/pr/index.ts +++ b/lib/workers/repository/onboarding/pr/index.ts @@ -148,6 +148,21 @@ export async function ensureOnboardingPr( : emojify( `:vertical_traffic_light: Renovate will begin keeping your dependencies up-to-date only once you merge or close this Pull Request.\n\n`, ); + + prTemplate += emojify( + `:books: See our [Reading List](https://docs.renovatebot.com/reading-list/) for relevant documentation you may be interested in reading.\n\n`, + ); + + const configFile = getDefaultConfigFileName(); + prTemplate += emojify( + `:abcd: Do you want to change how Renovate upgrades your dependencies?`, + ); + prTemplate += ` Add your custom config to \`${configFile}\` in this branch${ + config.onboardingRebaseCheckbox + ? ' and select the Retry/Rebase checkbox below' + : '' + }. Renovate will update the Pull Request description the next time it runs.`; + prTemplate += '\n\n'; // TODO #22198 prTemplate += emojify( ` diff --git a/package.json b/package.json index 26bb4cc7c1a..0dfc4aeedb4 100644 --- a/package.json +++ b/package.json @@ -232,7 +232,7 @@ "json-stringify-pretty-compact": "4.0.0", "json5": "2.2.3", "jsonata": "2.1.0", - "jsonc-weaver": "0.2.2", + "jsonc-weaver": "0.2.4", "klona": "2.0.6", "luxon": "3.7.2", "markdown-it": "14.1.1", @@ -275,7 +275,7 @@ "optionalDependencies": { "better-sqlite3": "12.8.0", "openpgp": "6.3.0", - "re2": "1.23.3" + "re2": "1.24.0" }, "devDependencies": { "@biomejs/biome": "2.4.6", @@ -298,6 +298,7 @@ "@types/clean-git-ref": "2.0.2", "@types/common-tags": "1.8.4", "@types/eslint-config-prettier": "6.11.3", + "@types/estree": "1.0.8", "@types/fs-extra": "11.0.4", "@types/github-url-from-git": "1.5.3", "@types/global-agent": "3.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e2e7071cee..8ed69c580de 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -246,8 +246,8 @@ importers: specifier: 2.1.0 version: 2.1.0 jsonc-weaver: - specifier: 0.2.2 - version: 0.2.2 + specifier: 0.2.4 + version: 0.2.4 klona: specifier: 2.0.6 version: 2.0.6 @@ -423,6 +423,9 @@ importers: '@types/eslint-config-prettier': specifier: 6.11.3 version: 6.11.3 + '@types/estree': + specifier: 1.0.8 + version: 1.0.8 '@types/fs-extra': specifier: 11.0.4 version: 11.0.4 @@ -620,8 +623,8 @@ importers: specifier: 6.3.0 version: 6.3.0 re2: - specifier: 1.23.3 - version: 1.23.3 + specifier: 1.24.0 + version: 1.24.0 packages: @@ -4308,14 +4311,14 @@ packages: resolution: {integrity: sha512-OCzaRMK8HobtX8fp37uIVmL8CY1IGc/a6gLsDqz3quExFR09/U78HUzWYr7T31UEB6+Eu0/8dkVD5fFDOl9a8w==} engines: {node: '>= 8'} - jsonc-morph@0.3.2: - resolution: {integrity: sha512-FmHfnQ3OMs/+HosrGV8RHwerWXjTaco0hvBhn8+hP0gux06Jylynct4H0P+sjsK3QXRvN5zlq+SJxvXgfu8JfQ==} + jsonc-morph@0.3.3: + resolution: {integrity: sha512-YljoHRXfZacx4utaktqGlz8p9xJJaV12WxciEpf4+g+8jVk0QNmxpxStkXVwjTnJZr9EAK4TT5TzdCO5M2i7rw==} jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} - jsonc-weaver@0.2.2: - resolution: {integrity: sha512-fDO1+8cpEFGlw42kvEqh/HAnXd47Lv19mWQaR30p2k6wOwfL4Dx/B2f8reaQepBc1aupBIOTwWmhThFUgv9zLQ==} + jsonc-weaver@0.2.4: + resolution: {integrity: sha512-aIKK8SOg+TdBdeML4MB++phUEdS7KWZxuaPPjxUSat8Kc/2A+2AoxmAGhvbVajNjqsgSX4dhjfCJq3QlE/NSCg==} jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} @@ -5368,8 +5371,8 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true - re2@1.23.3: - resolution: {integrity: sha512-5jh686rmj/8dYpBo72XYgwzgG8Y9HNDATYZ1x01gqZ6FvXVUP33VZ0+6GLCeavaNywz3OkXBU8iNX7LjiuisPg==} + re2@1.24.0: + resolution: {integrity: sha512-pBm6cMaOb0Yb0kg0Sfw/k4LwDMkPScb/NVd7GrEllDwfsPZstsZIo93A6Nn0wZuWJw3h57GdzkrOk81EofKY/g==} react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -10701,13 +10704,13 @@ snapshots: jsonata@2.1.0: {} - jsonc-morph@0.3.2: {} + jsonc-morph@0.3.3: {} jsonc-parser@3.3.1: {} - jsonc-weaver@0.2.2: + jsonc-weaver@0.2.4: dependencies: - jsonc-morph: 0.3.2 + jsonc-morph: 0.3.3 jsonfile@6.2.0: dependencies: @@ -11946,7 +11949,7 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 - re2@1.23.3: + re2@1.24.0: dependencies: install-artifact-from-github: 1.4.0 nan: 2.26.2 diff --git a/tools/docker/Dockerfile b/tools/docker/Dockerfile index ea24ac0fdf0..b03a82c833a 100644 --- a/tools/docker/Dockerfile +++ b/tools/docker/Dockerfile @@ -5,19 +5,19 @@ ARG BASE_IMAGE_TYPE=slim # -------------------------------------- # slim image # -------------------------------------- -FROM ghcr.io/renovatebot/base-image:13.33.9@sha256:e828902e2a063fa426fec36266df3258a9074015e0626b65acf5aeac917fb365 AS slim-base +FROM ghcr.io/renovatebot/base-image:13.33.10@sha256:e18520da3439e7b4ed56e9f591b356ebf0b3beef0d62ea200f97696191df7f57 AS slim-base # -------------------------------------- # full image # -------------------------------------- -FROM ghcr.io/renovatebot/base-image:13.33.9-full@sha256:66d3ad6a34dc8cd19d58d5cee1e229f8b312469ff325dc0fa8af6bad1c219f8e AS full-base +FROM ghcr.io/renovatebot/base-image:13.33.10-full@sha256:ee12f2e5e560d181f142c62c36a5adb4138917a8aada60e24cc659b233838b0c AS full-base ENV RENOVATE_BINARY_SOURCE=global # -------------------------------------- # build image # -------------------------------------- -FROM --platform=$BUILDPLATFORM ghcr.io/renovatebot/base-image:13.33.9@sha256:e828902e2a063fa426fec36266df3258a9074015e0626b65acf5aeac917fb365 AS build +FROM --platform=$BUILDPLATFORM ghcr.io/renovatebot/base-image:13.33.10@sha256:e18520da3439e7b4ed56e9f591b356ebf0b3beef0d62ea200f97696191df7f57 AS build # We want a specific node version here # renovate: datasource=github-releases packageName=containerbase/node-prebuild versioning=node diff --git a/tools/lint/rules.js b/tools/lint/rules.js index ec95499e029..ae1cf1763e0 100644 --- a/tools/lint/rules.js +++ b/tools/lint/rules.js @@ -1,5 +1,5 @@ -const CWD = process.cwd(); -const TOOLS_IMPORT_PATTERN = /(?:^|\/|\.\.\/)tools\//; +import noToolsImport from './rules/no-tools-import.js'; +import testRootDescribe from './rules/test-root-describe.js'; /** @type {import('eslint').ESLint.Plugin} */ export default { @@ -7,76 +7,7 @@ export default { name: 'renovate', }, rules: { - 'no-tools-import': { - meta: { - type: 'problem', - messages: { - noToolsImport: 'Importing from tools/ is not allowed in lib/', - }, - }, - create(context) { - const filename = context.filename ?? context.physicalFilename ?? ''; - if (!filename.includes('/lib/')) { - return {}; - } - return { - ImportDeclaration(node) { - if (TOOLS_IMPORT_PATTERN.test(node.source.value)) { - context.report({ node: node.source, messageId: 'noToolsImport' }); - } - }, - }; - }, - }, - 'test-root-describe': { - meta: { - fixable: 'code', - }, - create(context) { - const absoluteFileName = context.filename; - if (!absoluteFileName.endsWith('.spec.ts')) { - return {}; - } - const relativeFileName = absoluteFileName - .replace(CWD, '') - .replace(/\\/g, '/') - .replace(/^(?:\/(?:lib|src|test))?\//, ''); - const testName = relativeFileName.replace(/\.spec\.ts$/, ''); - return { - CallExpression(node) { - const { callee } = node; - if ( - callee.type === 'Identifier' && - callee.name === 'describe' && - node.parent.parent.type === 'Program' - ) { - const [descr] = node.arguments; - - if (!descr) { - context.report({ - node, - message: 'Test root describe must have arguments', - }); - return; - } - - const isOkay = - descr.type === 'Literal' && - typeof descr.value === 'string' && - testName === descr.value; - if (!isOkay) { - context.report({ - node: descr, - message: `Test must be described by this string: '${testName}'`, - fix(fixer) { - return fixer.replaceText(descr, `'${testName}'`); - }, - }); - } - } - }, - }; - }, - }, + 'no-tools-import': noToolsImport, + 'test-root-describe': testRootDescribe, }, }; diff --git a/tools/lint/rules/no-tools-import.js b/tools/lint/rules/no-tools-import.js new file mode 100644 index 00000000000..e6d523f1d39 --- /dev/null +++ b/tools/lint/rules/no-tools-import.js @@ -0,0 +1,48 @@ +const TOOLS_IMPORT_PATTERN = /(?:^|\/|\.\.\/)tools\//; + +/** + * @param {import('eslint').Rule.RuleContext} context + * @param {import('estree').Literal} source + */ +function check(context, source) { + if ( + typeof source.value === 'string' && + TOOLS_IMPORT_PATTERN.test(source.value) + ) { + context.report({ node: source, messageId: 'noToolsImport' }); + } +} + +/** @type {import('eslint').Rule.RuleModule} */ +export default { + meta: { + type: 'problem', + messages: { + noToolsImport: 'Importing from tools/ is not allowed in lib/', + }, + }, + create(context) { + const filename = context.filename ?? context.physicalFilename ?? ''; + if (!filename.includes('/lib/')) { + return {}; + } + return { + ImportDeclaration(node) { + check(context, node.source); + }, + ExportNamedDeclaration(node) { + if (node.source) { + check(context, node.source); + } + }, + ExportAllDeclaration(node) { + check(context, node.source); + }, + ImportExpression(node) { + if (node.source.type === 'Literal') { + check(context, node.source); + } + }, + }; + }, +}; diff --git a/tools/lint/rules/test-root-describe.js b/tools/lint/rules/test-root-describe.js new file mode 100644 index 00000000000..3d2f19f9da9 --- /dev/null +++ b/tools/lint/rules/test-root-describe.js @@ -0,0 +1,53 @@ +/** @type {import('eslint').Rule.RuleModule} */ +export default { + meta: { + fixable: 'code', + }, + create(context) { + const absoluteFileName = context.filename; + if (!absoluteFileName.endsWith('.spec.ts')) { + return {}; + } + const relativeFileName = absoluteFileName + .replace(context.cwd, '') + .replace(/\\/g, '/') + .replace(/^(?:\/(?:lib|src|test))?\//, ''); + const testName = relativeFileName.replace(/\.spec\.ts$/, ''); + return { + CallExpression(node) { + const { callee } = node; + if (callee.type !== 'Identifier' || callee.name !== 'describe') { + return; + } + if (node.parent?.parent?.type !== 'Program') { + return; + } + + const [descr] = node.arguments; + if (!descr) { + context.report({ + node, + message: 'Test root describe must have arguments', + }); + return; + } + + if ( + descr.type === 'Literal' && + typeof descr.value === 'string' && + testName === descr.value + ) { + return; + } + + context.report({ + node: descr, + message: `Test must be described by this string: '${testName}'`, + fix(fixer) { + return fixer.replaceText(descr, `'${testName}'`); + }, + }); + }, + }; + }, +};