diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ff52204e6e3a68..6daf80c6940f4a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -19,7 +19,8 @@ "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "orta.vscode-jest", - "EditorConfig.editorconfig" + "editorconfig.editorconfig", + "github.vscode-github-actions" ] } }, diff --git a/.github/label-actions.yml b/.github/label-actions.yml index e0f3205b11fd67..5e85dbe2729290 100644 --- a/.github/label-actions.yml +++ b/.github/label-actions.yml @@ -23,12 +23,12 @@ The Renovate team -'logs:problem': +'needs-logs': comment: > Hi there, - We found a problem with the logs. + This issue or discussion is missing some logs, making it difficult or impossible to help you. Depending on which situation applies follow one, some or all of these instructions. @@ -178,6 +178,26 @@ The Renovate team +'pr:discussion-first': + comment: > + **Please create a GitHub Discussion before continuing with this PR.** + + + Thank you for your PR, but we need to discuss the requirements and implementation first. + This PR will be closed, but you can reopen it after the discussion has been resolved. + + + Thanks, The Renovate team + close: true + +'needs-details': + comment: > + Hi there, + + + This discussion is missing some details, making it difficult or impossible to help you. + Please try again to provide more details. + 'needs-discussion': unlabel: - 'type:bug' @@ -199,9 +219,9 @@ lock: true lock-reason: 'resolved' -'needs-code-formatting': +'format-code-comments': comment: > - Hi, please format your code or logs so they're readable. + Hi, please format any copy/pasted code or logs so they're readable. You can find a Markdown code formatting guide [here](https://www.markdownguide.org/basic-syntax/#code) as well as some GitHub-specific information formatting code blocks [here](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks). diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 6e8e5835f18a35..c8efa2c88f5165 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -30,7 +30,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@f3feb00acb00f31a6f60280e6ace9ca31d91c76a # v2.3.2 + uses: github/codeql-action/init@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3 with: languages: javascript @@ -40,7 +40,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@f3feb00acb00f31a6f60280e6ace9ca31d91c76a # v2.3.2 + uses: github/codeql-action/autobuild@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -54,4 +54,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@f3feb00acb00f31a6f60280e6ace9ca31d91c76a # v2.3.2 + uses: github/codeql-action/analyze@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3 diff --git a/.vscode/extensions.json b/.vscode/extensions.json index e569c62d68ba77..a45d022d1c1ebc 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,6 +3,7 @@ "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "orta.vscode-jest", - "EditorConfig.editorconfig" + "editorconfig.editorconfig", + "github.vscode-github-actions" ] } diff --git a/docs/usage/.pages b/docs/usage/.pages index e6e2117b871bdd..f5d466abd10b82 100644 --- a/docs/usage/.pages +++ b/docs/usage/.pages @@ -6,6 +6,7 @@ nav: - 'Self-hosted': 'self-hosted-configuration.md' - 'Repository': 'configuration-options.md' - 'Presets': 'config-presets.md' + - 'Validation': 'config-validation.md' - ... | key-concepts - ... | modules - Language Support: diff --git a/docs/usage/config-validation.md b/docs/usage/config-validation.md new file mode 100644 index 00000000000000..7871cc5367d072 --- /dev/null +++ b/docs/usage/config-validation.md @@ -0,0 +1,22 @@ +--- +title: Config Validation +description: How to validate Renovate's configuration. +--- + +# Config Validation + +All [`renovate` distributions](getting-started/running.md#available-distributions) contain a standalone validator program (`renovate-config-validator`) that can be used to validate Renovate's configuration. + +The validator program checks files passed as CLI arguments. +If no argument is given, all [default locations](configuration-options.md) (if files exist) and the `RENOVATE_CONFIG_FILE` environment variable are checked. + +```console +$ npm install --global renovate +added 750 packages, and audited 751 packages in 51s +$ renovate-config-validator +INFO: Validating renovate.json +INFO: Config validated successfully +``` + +You can configure a [pre-commit](https://pre-commit.com) hook to validate your configuration automatically. +Please check out the [`renovatebot/pre-commit-hooks` repository](https://github.com/renovatebot/pre-commit-hooks) for more information. diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index 82061dc96c13d4..2ba14ca9de3e8e 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -448,6 +448,11 @@ Check out the default value for `commitMessage` to understand how this field is This is used to alter `commitMessage` and `prTitle` without needing to copy/paste the whole string. The "extra" is usually an identifier of the new version, e.g. "to v1.3.2" or "to tag 9.2". +## commitMessageLowerCase + +With `semanticCommits` pr- and commit-titles will by default (`"auto"`) be converted to all-lowercase. +Set this to `"never"` to leave the titles untouched, allowing uppercase characters in semantic commit titles. + ## commitMessagePrefix This is used to alter `commitMessage` and `prTitle` without needing to copy/paste the whole string. @@ -2853,14 +2858,14 @@ Typically you shouldn't need to modify this setting. Use `regexManagers` entries to configure the `regex` manager in Renovate. -You can define custom managers for cases such as: +You can define custom managers to handle: - Proprietary file formats or conventions - Popular file formats not yet supported as a manager by Renovate The custom manager concept is based on using Regular Expression named capture groups. -You must have a named capture group matching (e.g. `(?.*)`) _or_ configure it's corresponding template (e.g. `depNameTemplate`) for these fields: +You must have a named capture group matching (e.g. `(?.*)`) _or_ configure its corresponding template (e.g. `depNameTemplate`) for these fields: - `datasource` - `depName` @@ -2875,6 +2880,10 @@ If the `versioning` field is missing, then Renovate defaults to using `semver` v For more details and examples, see our [documentation for the `regex` manager](/modules/manager/regex/). For template fields, use the triple brace `{{{ }}}` notation to avoid Handlebars escaping any special characters. + +!!! tip + Look at our [Regex Manager Presets](https://docs.renovatebot.com/presets-regexManagers/), they may have what you need. + ### matchStrings `matchStrings` should each be a valid regular expression, optionally with named capture groups. @@ -3136,6 +3145,7 @@ This feature works with the following managers: - [`kubernetes`](/modules/manager/kubernetes) - [`ansible`](/modules/manager/ansible) - [`droneci`](/modules/manager/droneci) +- [`terraform`](/modules/manager/terraform) ## registryUrls diff --git a/docs/usage/configuration-templates.md b/docs/usage/configuration-templates.md index d20c501df41b9e..ae35c8b6a8194a 100644 --- a/docs/usage/configuration-templates.md +++ b/docs/usage/configuration-templates.md @@ -14,8 +14,8 @@ If you change the `branchPrefix` while you have ignored some upgrades (closed PR `branchName` default value is `{{{branchPrefix}}}{{{additionalBranchPrefix}}}{{{branchTopic}}}`. -The most common branch name you will see looks like this: `renovate/react-17.x`. -In this example, the `branchPrefix` is the default `renovate/`, `additionalBranchPrefix` is empty, and `branchTopic` is `react-17.x`. +The most common branch name you will see looks like this: `renovate/react-18.x`. +In this example, the `branchPrefix` is the default `renovate/`, `additionalBranchPrefix` is empty, and `branchTopic` is `react-18.x`. Most users will be happy with the default `branchPrefix` of `renovate/`, but you can change this if you don't like the default. Say you don't want the forward slashes, in that case you would use `renovate-` as your `branchPrefix`. @@ -47,7 +47,7 @@ You may want to edit this. If you think your new `commitMessageTopic` is helpful for others, please [open a PR](https://github.com/renovatebot/renovate/pulls). `commitMessageExtra` refers to the version being updated to. -e.g. `to v17` for a major upgrade, or `to v17.0.2` for a patch update. +e.g. `to v18` for a major upgrade, or `to v18.0.2` for a patch update. It can be empty in some cases, like if the action/topic doesn't change a package version, e.g. `Pin Docker digests`. `commitMessageSuffix` defaults to empty but is currently used in two cases: diff --git a/docs/usage/docker.md b/docs/usage/docker.md index 11b5aa17944e9d..be4ba4f94da90b 100644 --- a/docs/usage/docker.md +++ b/docs/usage/docker.md @@ -388,7 +388,7 @@ To get access to the token a custom Renovate Docker image is needed that include The Dockerfile to create such an image can look like this: ```Dockerfile -FROM renovate/renovate:35.66.3 +FROM renovate/renovate:35.82.0 # Include the "Docker tip" which you can find here https://cloud.google.com/sdk/docs/install # under "Installation" for "Debian/Ubuntu" RUN ... diff --git a/docs/usage/getting-started/installing-onboarding.md b/docs/usage/getting-started/installing-onboarding.md index f681575fcedea2..7b3275c2adde70 100644 --- a/docs/usage/getting-started/installing-onboarding.md +++ b/docs/usage/getting-started/installing-onboarding.md @@ -133,15 +133,9 @@ If you want to make config edits directly, follow these steps: 1. Create a new Git branch to work on 1. Install or update the `renovate` package globally (`npm i -g renovate` or `yarn global add renovate`) to get the `renovate-config-validator` program 1. Edit your Renovate configuration file -1. Validate your config by running `renovate-config-validator` +1. [Validate your config](../config-validation.md) 1. If the improved config passes the validation, merge the branch into your mainline branch -The validator program checks files passed as CLI arguments. -If no argument is given, all [default locations](../configuration-options.md) (if files exist) and the `RENOVATE_CONFIG_FILE` environment variable are checked. - -You can configure a [pre-commit](https://pre-commit.com) hook to validate your configuration automatically. -Please check out the [`renovatebot/pre-commit-hooks` repository](https://github.com/renovatebot/pre-commit-hooks) for more information. - ### Nuke config and re-onboard Perhaps you really liked the interactive onboarding PR and want to use it again. diff --git a/docs/usage/troubleshooting.md b/docs/usage/troubleshooting.md index 3ad9a414486f11..75d1ee1bb00777 100644 --- a/docs/usage/troubleshooting.md +++ b/docs/usage/troubleshooting.md @@ -64,3 +64,8 @@ If none of these steps have helped you, then create a new discussion post to get Please locate the relevant parts of the logs as described earlier before asking for help or posting a bug report. Do not expect the Renovate maintainers to read through the full logs when trying to help you, as that takes a lot of time on our part. If later it turns out that the full logs are necessary, you will be asked for them then. + +## Validating configuration changes + +Sometimes you will have to change your Renovate configuration to solve a problem. +The [`renovate-config-validator` program](config-validation.md) helps validate such configuration changes without commiting them to your repository. diff --git a/lib/config/index.spec.ts b/lib/config/index.spec.ts index 41deb82830f776..001ea8c8b53488 100644 --- a/lib/config/index.spec.ts +++ b/lib/config/index.spec.ts @@ -1,4 +1,5 @@ import { getConfig } from './defaults'; +import { filterConfig, getManagerConfig, mergeChildConfig } from './index'; jest.mock('../modules/datasource/npm'); try { @@ -11,7 +12,7 @@ const defaultConfig = getConfig(); describe('config/index', () => { describe('mergeChildConfig(parentConfig, childConfig)', () => { - it('merges', async () => { + it('merges', () => { const parentConfig = { ...defaultConfig }; const childConfig = { foo: 'bar', @@ -20,15 +21,14 @@ describe('config/index', () => { schedule: ['on monday'], }, }; - const configParser = await import('./index'); - const config = configParser.mergeChildConfig(parentConfig, childConfig); + const config = mergeChildConfig(parentConfig, childConfig); expect(config.foo).toBe('bar'); expect(config.rangeStrategy).toBe('replace'); expect(config.lockFileMaintenance.schedule).toEqual(['on monday']); expect(config.lockFileMaintenance).toMatchSnapshot(); }); - it('merges packageRules', async () => { + it('merges packageRules', () => { const parentConfig = { ...defaultConfig }; Object.assign(parentConfig, { packageRules: [{ a: 1 }, { a: 2 }], @@ -36,14 +36,13 @@ describe('config/index', () => { const childConfig = { packageRules: [{ a: 3 }, { a: 4 }], }; - const configParser = await import('./index'); - const config = configParser.mergeChildConfig(parentConfig, childConfig); + const config = mergeChildConfig(parentConfig, childConfig); expect(config.packageRules.map((rule) => rule.a)).toMatchObject([ 1, 2, 3, 4, ]); }); - it('merges constraints', async () => { + it('merges constraints', () => { const parentConfig = { ...defaultConfig }; Object.assign(parentConfig, { constraints: { @@ -56,8 +55,7 @@ describe('config/index', () => { node: '<15', }, }; - const configParser = await import('./index'); - const config = configParser.mergeChildConfig(parentConfig, childConfig); + const config = mergeChildConfig(parentConfig, childConfig); expect(config.constraints).toMatchSnapshot(); expect(config.constraints.node).toBe('<15'); }); @@ -75,39 +73,48 @@ describe('config/index', () => { expect(config.packageRules).toHaveLength(2); }); - it('handles null child packageRules', async () => { + it('handles null child packageRules', () => { const parentConfig = { ...defaultConfig }; parentConfig.packageRules = [{ a: 3 }, { a: 4 }]; - const configParser = await import('./index'); - const config = configParser.mergeChildConfig(parentConfig, {}); + const config = mergeChildConfig(parentConfig, {}); expect(config.packageRules).toHaveLength(2); }); - it('handles undefined childConfig', async () => { + it('handles undefined childConfig', () => { const parentConfig = { ...defaultConfig }; - const configParser = await import('./index'); - const config = configParser.mergeChildConfig(parentConfig, undefined); + const config = mergeChildConfig(parentConfig, undefined); expect(config).toMatchObject(parentConfig); }); - it('getManagerConfig()', async () => { + it('getManagerConfig()', () => { const parentConfig = { ...defaultConfig }; - const configParser = await import('./index'); - const config = configParser.getManagerConfig(parentConfig, 'npm'); + const config = getManagerConfig(parentConfig, 'npm'); expect(config).toContainEntries([ ['fileMatch', ['(^|/)package\\.json$']], ['rollbackPrs', true], ]); - expect( - configParser.getManagerConfig(parentConfig, 'html') - ).toContainEntries([['fileMatch', ['\\.html?$']]]); + expect(getManagerConfig(parentConfig, 'html')).toContainEntries([ + ['fileMatch', ['\\.html?$']], + ]); }); - it('filterConfig()', async () => { + it('filterConfig()', () => { const parentConfig = { ...defaultConfig }; - const configParser = await import('./index'); - const config = configParser.filterConfig(parentConfig, 'pr'); + const config = filterConfig(parentConfig, 'pr'); expect(config).toBeObject(); }); + + it('highest vulnerabilitySeverity maintained when config is vulnerability alert', () => { + const parentConfig = { ...defaultConfig }; + Object.assign(parentConfig, { + isVulnerabilityAlert: true, + vulnerabilitySeverity: 'HIGH', + }); + const childConfig = { + vulnerabilitySeverity: 'CRITICAL', + }; + const config = mergeChildConfig(parentConfig, childConfig); + expect(config.vulnerabilitySeverity).toBe('CRITICAL'); + }); }); }); diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index a020d9dae72157..0e05c187ba51cf 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -1549,6 +1549,13 @@ const options: RenovateOptions[] = [ type: 'string', default: 'deps', }, + { + name: 'commitMessageLowerCase', + description: 'Lowercase PR- and commit titles.', + type: 'string', + allowedValues: ['auto', 'never'], + default: 'auto', + }, // PR Behaviour { name: 'rollbackPrs', diff --git a/lib/config/presets/internal/regex-managers.ts b/lib/config/presets/internal/regex-managers.ts index afaf9f19156832..c4574bcc09ac44 100644 --- a/lib/config/presets/internal/regex-managers.ts +++ b/lib/config/presets/internal/regex-managers.ts @@ -7,7 +7,10 @@ export const presets: Record = { description: 'Update `_VERSION` variables in Dockerfiles.', regexManagers: [ { - fileMatch: ['(^|/|\\.)Dockerfile$', '(^|/)Dockerfile[^/]*$'], + fileMatch: [ + '(^|/|\\.)([Dd]ocker|[Cc]ontainer)file$', + '(^|/)([Dd]ocker|[Cc]ontainer)file[^/]*$', + ], matchStrings: [ '# renovate: datasource=(?[a-z-]+?) depName=(?[^\\s]+?)(?: (lookupName|packageName)=(?[^\\s]+?))?(?: versioning=(?[^\\s]+?))?(?: registryUrl=(?[^\\s]+?))?\\s(?:ENV|ARG) .+?_VERSION[ =]"?(?.+?)"?\\s', ], @@ -27,7 +30,7 @@ export const presets: Record = { ], }, helmChartYamlAppVersions: { - description: 'Update `appVersion` value in Helm chart Chart.yaml.', + description: 'Update `appVersion` value in Helm chart `Chart.yaml`.', regexManagers: [ { datasourceTemplate: 'docker', diff --git a/lib/config/presets/local/index.ts b/lib/config/presets/local/index.ts index ac8e2a69703049..6f99390a6c676d 100644 --- a/lib/config/presets/local/index.ts +++ b/lib/config/presets/local/index.ts @@ -24,6 +24,7 @@ const resolvers = { gitea, github, gitlab, + local: null, } satisfies Record; export function getPreset({ diff --git a/lib/config/types.ts b/lib/config/types.ts index 0df00b27a9a688..ad5908b00e24b9 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -78,6 +78,7 @@ export interface RenovateSharedConfig { automergeSchedule?: string[]; semanticCommits?: 'auto' | 'enabled' | 'disabled'; semanticCommitScope?: string | null; + commitMessageLowerCase?: 'auto' | 'never'; semanticCommitType?: string; suppressNotifications?: string[]; timezone?: string; @@ -251,6 +252,7 @@ export interface RenovateConfig warnings?: ValidationMessage[]; vulnerabilityAlerts?: RenovateSharedConfig; osvVulnerabilityAlerts?: boolean; + vulnerabilitySeverity?: string; regexManagers?: RegExManager[]; fetchReleaseNotes?: boolean; @@ -309,6 +311,7 @@ export interface PackageRule UpdateConfig, Record { description?: string | string[]; + isVulnerabilityAlert?: boolean; matchFiles?: string[]; matchPaths?: string[]; matchLanguages?: string[]; @@ -333,6 +336,7 @@ export interface PackageRule matchUpdateTypes?: UpdateType[]; matchConfidence?: MergeConfidence[]; registryUrls?: string[] | null; + vulnerabilitySeverity?: string; } export interface ValidationMessage { diff --git a/lib/config/utils.ts b/lib/config/utils.ts index 106d9e168f1453..140d5f0df030a6 100644 --- a/lib/config/utils.ts +++ b/lib/config/utils.ts @@ -1,4 +1,5 @@ import { logger } from '../logger'; +import { getHighestVulnerabilitySeverity } from '../util/vulnerability/utils'; import * as options from './options'; import type { RenovateConfig } from './types'; @@ -13,6 +14,15 @@ export function mergeChildConfig< const parentConfig = structuredClone(parent); const childConfig = structuredClone(child); const config: Record = { ...parentConfig, ...childConfig }; + + // Ensure highest severity survives parent / child merge + if (config?.isVulnerabilityAlert) { + config.vulnerabilitySeverity = getHighestVulnerabilitySeverity( + parent, + child + ); + } + for (const option of options.getOptions()) { if ( option.mergeable && @@ -20,6 +30,7 @@ export function mergeChildConfig< parentConfig[option.name] ) { logger.trace(`mergeable option: ${option.name}`); + if (option.name === 'constraints') { config[option.name] = { ...parentConfig[option.name], diff --git a/lib/constants/platforms.ts b/lib/constants/platforms.ts index f3a1e71ac8b7ba..049155bd3ba43c 100644 --- a/lib/constants/platforms.ts +++ b/lib/constants/platforms.ts @@ -5,7 +5,8 @@ export type PlatformId = | 'bitbucket-server' | 'gitea' | 'github' - | 'gitlab'; + | 'gitlab' + | 'local'; export const GITHUB_API_USING_HOST_TYPES = [ 'github', diff --git a/lib/logger/index.spec.ts b/lib/logger/index.spec.ts index ccf5403a29f189..c8422d469e2be6 100644 --- a/lib/logger/index.spec.ts +++ b/lib/logger/index.spec.ts @@ -1,4 +1,6 @@ -import _fs from 'fs-extra'; +import type { WriteStream } from 'node:fs'; +import fs from 'fs-extra'; +import { partial } from '../../test/util'; import { add } from '../util/host-rules'; import { addSecretForSanitizing as addSecret } from '../util/sanitize'; import { @@ -16,9 +18,6 @@ import { jest.unmock('.'); -jest.mock('fs-extra'); -const fs: any = _fs; - describe('logger/index', () => { it('inits', () => { expect(logger).toBeDefined(); @@ -97,12 +96,15 @@ describe('logger/index', () => { it('supports file-based logging', () => { let chunk = ''; - fs.createWriteStream.mockReturnValueOnce({ - writable: true, - write(x: string) { - chunk = x; - }, - }); + jest.spyOn(fs, 'createWriteStream').mockReturnValueOnce( + partial({ + writable: true, + write(x: string): boolean { + chunk = x; + return true; + }, + }) + ); addStream({ name: 'logfile', @@ -117,12 +119,15 @@ describe('logger/index', () => { it('handles cycles', () => { let logged: Record = {}; - fs.createWriteStream.mockReturnValueOnce({ - writable: true, - write(x: string) { - logged = JSON.parse(x); - }, - }); + jest.spyOn(fs, 'createWriteStream').mockReturnValueOnce( + partial({ + writable: true, + write(x: string): boolean { + logged = JSON.parse(x); + return true; + }, + }) + ); addStream({ name: 'logfile', @@ -142,12 +147,15 @@ describe('logger/index', () => { it('sanitizes secrets', () => { let logged: Record = {}; - fs.createWriteStream.mockReturnValueOnce({ - writable: true, - write(x: string) { - logged = JSON.parse(x); - }, - }); + jest.spyOn(fs, 'createWriteStream').mockReturnValueOnce( + partial({ + writable: true, + write(x: string): boolean { + logged = JSON.parse(x); + return true; + }, + }) + ); addStream({ name: 'logfile', diff --git a/lib/modules/datasource/github-tags/index.spec.ts b/lib/modules/datasource/github-tags/index.spec.ts index 72ae3f11f2421b..cf276d32b32c12 100644 --- a/lib/modules/datasource/github-tags/index.spec.ts +++ b/lib/modules/datasource/github-tags/index.spec.ts @@ -1,5 +1,6 @@ import { getPkgReleases } from '..'; import * as httpMock from '../../../../test/http-mock'; +import { partial } from '../../../../test/util'; import * as githubGraphql from '../../../util/github/graphql'; import type { GithubTagItem } from '../../../util/github/graphql/types'; import * as hostRules from '../../../util/host-rules'; @@ -77,11 +78,11 @@ describe('modules/datasource/github-tags/index', () => { releaseTimestamp: '2021-01-01', hash: '123', }, - { + partial({ version: 'v2.0.0', gitRef: 'v2.0.0', releaseTimestamp: '2022-01-01', - } as GithubTagItem, + }), ]); const res = await github.getDigest({ packageName }, 'v2.0.0'); expect(res).toBeNull(); diff --git a/lib/modules/datasource/pypi/index.ts b/lib/modules/datasource/pypi/index.ts index 44acf798b116db..6757b39e594391 100644 --- a/lib/modules/datasource/pypi/index.ts +++ b/lib/modules/datasource/pypi/index.ts @@ -21,9 +21,9 @@ export class PypiDatasource extends Datasource { override readonly customRegistrySupport = true; - override readonly defaultRegistryUrls = [ - process.env.PIP_INDEX_URL ?? 'https://pypi.org/pypi/', - ]; + static readonly defaultURL = + process.env.PIP_INDEX_URL ?? 'https://pypi.org/pypi/'; + override readonly defaultRegistryUrls = [PypiDatasource.defaultURL]; override readonly defaultVersioning = pep440.id; diff --git a/lib/modules/datasource/rubygems/index.spec.ts b/lib/modules/datasource/rubygems/index.spec.ts index 0ab48107c352c4..3f62ca70cb9014 100644 --- a/lib/modules/datasource/rubygems/index.spec.ts +++ b/lib/modules/datasource/rubygems/index.spec.ts @@ -13,16 +13,6 @@ const emptyMarshalArray = Buffer.from([4, 8, 91, 0]); describe('modules/datasource/rubygems/index', () => { describe('getReleases', () => { - const params = { - versioning: rubyVersioning.id, - datasource: RubyGemsDatasource.id, - packageName: 'rails', - registryUrls: [ - 'https://thirdparty.com', - 'https://firstparty.com/basepath/', - ], - }; - beforeEach(() => { memCache.clear(); jest.resetAllMocks(); @@ -42,26 +32,34 @@ describe('modules/datasource/rubygems/index', () => { .scope('https://thirdparty.com') .get('/api/v1/gems/rails.json') .reply(200); - expect(await getPkgReleases(params)).toBeNull(); + expect( + await getPkgReleases({ + versioning: rubyVersioning.id, + datasource: RubyGemsDatasource.id, + packageName: 'rails', + registryUrls: [ + 'https://thirdparty.com', + 'https://firstparty.com/basepath/', + ], + }) + ).toBeNull(); }); it('returns null for rubygems.org package miss', async () => { - const newparams = { ...params }; - newparams.registryUrls = []; httpMock .scope('https://rubygems.org') .get('/versions') .reply(404, rubygemsOrgVersions); - const res = await getPkgReleases(newparams); + const res = await getPkgReleases({ + versioning: rubyVersioning.id, + datasource: RubyGemsDatasource.id, + packageName: 'rails', + registryUrls: [], + }); expect(res).toBeNull(); }); it('returns null for an error without "not_supported" reason', async () => { - const newparams = { - ...params, - registryUrls: [], - }; - const versionsdataSourceSpy = jest .spyOn(VersionsDatasource.prototype, 'syncVersions') .mockImplementationOnce(() => { @@ -69,7 +67,12 @@ describe('modules/datasource/rubygems/index', () => { }); try { - const res = await getPkgReleases(newparams); + const res = await getPkgReleases({ + versioning: rubyVersioning.id, + datasource: RubyGemsDatasource.id, + packageName: 'rails', + registryUrls: [], + }); expect(res).toBeNull(); } finally { versionsdataSourceSpy.mockRestore(); @@ -77,16 +80,16 @@ describe('modules/datasource/rubygems/index', () => { }); it('returns a dep for rubygems.org package hit', async () => { - const newparams = { - ...params, - packageName: '1pass', - registryUrls: [], - }; httpMock .scope('https://rubygems.org') .get('/versions') .reply(200, rubygemsOrgVersions); - const res = await getPkgReleases(newparams); + const res = await getPkgReleases({ + versioning: rubyVersioning.id, + datasource: RubyGemsDatasource.id, + packageName: '1pass', + registryUrls: [], + }); expect(res).not.toBeNull(); expect(res?.releases).toHaveLength(2); expect(res).toMatchSnapshot(); @@ -104,16 +107,16 @@ describe('modules/datasource/rubygems/index', () => { --- sidekiq-ent 0.7.10,1.0.0,1.0.1,1.2.4,2.0.0,2.1.2 4c0f62a49b15b4775b7fb6824ec34d45 `; - const newparams = { - ...params, - packageName: 'sidekiq-ent', - registryUrls: ['https://enterprise.contribsys.com'], - }; httpMock .scope('https://enterprise.contribsys.com') .get('/versions') .reply(200, contribsysComVersions); - const res = await getPkgReleases(newparams); + const res = await getPkgReleases({ + versioning: rubyVersioning.id, + datasource: RubyGemsDatasource.id, + packageName: 'sidekiq-ent', + registryUrls: ['https://enterprise.contribsys.com'], + }); expect(res).not.toBeNull(); expect(res?.releases).toHaveLength(6); expect(res).toMatchObject({ @@ -143,13 +146,16 @@ describe('modules/datasource/rubygems/index', () => { expect( await getPkgReleases({ - ...params, + versioning: rubyVersioning.id, + datasource: RubyGemsDatasource.id, + packageName: 'rails', registryUrls: [], }) ).toBeNull(); const res = await getPkgReleases({ - ...params, + versioning: rubyVersioning.id, + datasource: RubyGemsDatasource.id, packageName: '1pass', registryUrls: [], }); @@ -168,7 +174,15 @@ describe('modules/datasource/rubygems/index', () => { .get('/api/v1/versions/rails.json') .reply(200, railsVersions); - const res = await getPkgReleases(params); + const res = await getPkgReleases({ + versioning: rubyVersioning.id, + datasource: RubyGemsDatasource.id, + packageName: 'rails', + registryUrls: [ + 'https://thirdparty.com', + 'https://firstparty.com/basepath/', + ], + }); expect(res?.releases).toHaveLength(339); expect(res).toMatchSnapshot(); }); @@ -189,7 +203,15 @@ describe('modules/datasource/rubygems/index', () => { .get('/basepath/api/v1/versions/rails.json') .reply(200, railsVersions); - const res = await getPkgReleases(params); + const res = await getPkgReleases({ + versioning: rubyVersioning.id, + datasource: RubyGemsDatasource.id, + packageName: 'rails', + registryUrls: [ + 'https://thirdparty.com', + 'https://firstparty.com/basepath/', + ], + }); expect(res?.releases).toHaveLength(339); expect(res).toMatchSnapshot(); }); @@ -207,7 +229,17 @@ describe('modules/datasource/rubygems/index', () => { .reply(404) .get('/basepath/api/v1/gems/rails.json') .reply(200); - expect(await getPkgReleases(params)).toBeNull(); + expect( + await getPkgReleases({ + versioning: rubyVersioning.id, + datasource: RubyGemsDatasource.id, + packageName: 'rails', + registryUrls: [ + 'https://thirdparty.com', + 'https://firstparty.com/basepath/', + ], + }) + ).toBeNull(); }); it('falls back to info when version request fails', async () => { @@ -219,7 +251,15 @@ describe('modules/datasource/rubygems/index', () => { .reply(200, railsInfo) .get('/api/v1/versions/rails.json') .reply(400, {}); - const res = await getPkgReleases(params); + const res = await getPkgReleases({ + versioning: rubyVersioning.id, + datasource: RubyGemsDatasource.id, + packageName: 'rails', + registryUrls: [ + 'https://thirdparty.com', + 'https://firstparty.com/basepath/', + ], + }); expect(res?.releases).toHaveLength(1); expect(res?.releases[0].version).toBe(railsInfo.version); }); @@ -239,7 +279,17 @@ describe('modules/datasource/rubygems/index', () => { .reply(404) .get('/api/v1/gems/rails.json') .reply(500); - expect(await getPkgReleases(params)).toBeNull(); + expect( + await getPkgReleases({ + versioning: rubyVersioning.id, + datasource: RubyGemsDatasource.id, + packageName: 'rails', + registryUrls: [ + 'https://thirdparty.com', + 'https://firstparty.com/basepath/', + ], + }) + ).toBeNull(); }); it('falls back to dependencies api', async () => { @@ -252,32 +302,48 @@ describe('modules/datasource/rubygems/index', () => { .get('/api/v1/dependencies?gems=rails') .reply(200, railsDependencies); - const res = await getPkgReleases(params); + const res = await getPkgReleases({ + versioning: rubyVersioning.id, + datasource: RubyGemsDatasource.id, + packageName: 'rails', + registryUrls: [ + 'https://thirdparty.com', + 'https://firstparty.com/basepath/', + ], + }); expect(res?.releases).toHaveLength(339); }); it('returns null for GitHub Packages package miss', async () => { - const newparams = { ...params }; - newparams.registryUrls = ['https://rubygems.pkg.github.com/example']; httpMock .scope('https://rubygems.pkg.github.com/example') .get('/versions') .reply(404) .get('/api/v1/dependencies?gems=rails') .reply(200, emptyMarshalArray); - expect(await getPkgReleases(newparams)).toBeNull(); + expect( + await getPkgReleases({ + versioning: rubyVersioning.id, + datasource: RubyGemsDatasource.id, + packageName: 'rails', + registryUrls: ['https://rubygems.pkg.github.com/example'], + }) + ).toBeNull(); }); it('returns a dep for GitHub Packages package hit', async () => { - const newparams = { ...params }; - newparams.registryUrls = ['https://rubygems.pkg.github.com/example']; httpMock .scope('https://rubygems.pkg.github.com/example') .get('/versions') .reply(404) .get('/api/v1/dependencies?gems=rails') .reply(200, railsDependencies); - const res = await getPkgReleases(newparams); + const res = await getPkgReleases({ + versioning: rubyVersioning.id, + datasource: RubyGemsDatasource.id, + packageName: 'rails', + registryUrls: ['https://rubygems.pkg.github.com/example'], + }); expect(res?.releases).toHaveLength(339); expect(res).toMatchSnapshot(); }); diff --git a/lib/modules/manager/api.ts b/lib/modules/manager/api.ts index 41dc108d9d1d8c..fcd8aca94e6e2a 100644 --- a/lib/modules/manager/api.ts +++ b/lib/modules/manager/api.ts @@ -58,6 +58,7 @@ import * as npm from './npm'; import * as nuget from './nuget'; import * as nvm from './nvm'; import * as osgi from './osgi'; +import * as pep621 from './pep621'; import * as pipCompile from './pip-compile'; import * as pip_requirements from './pip_requirements'; import * as pip_setup from './pip_setup'; @@ -146,6 +147,7 @@ api.set('npm', npm); api.set('nuget', nuget); api.set('nvm', nvm); api.set('osgi', osgi); +api.set('pep621', pep621); api.set('pip-compile', pipCompile); api.set('pip_requirements', pip_requirements); api.set('pip_setup', pip_setup); diff --git a/lib/modules/manager/asdf/extract.spec.ts b/lib/modules/manager/asdf/extract.spec.ts index bc77ae464bd7a4..4b9e67bf232326 100644 --- a/lib/modules/manager/asdf/extract.spec.ts +++ b/lib/modules/manager/asdf/extract.spec.ts @@ -53,6 +53,7 @@ dart 2.19.3 deno 1.26.2 direnv 2.32.1 dprint 0.32.2 +ecspresso 2.1.0 elixir 1.14.1 elm 0.19.1 erlang 25.1.2 @@ -89,6 +90,9 @@ terraform 1.3.3 terragrunt 0.43.2 trivy 0.33.0 zig 0.9.1 +maestro 1.24.0 +detekt 1.21.0 +ktlint 0.48.1 dummy 1.2.3 ` ); @@ -159,6 +163,13 @@ dummy 1.2.3 packageName: 'dprint/dprint', depName: 'dprint', }, + { + currentValue: '2.1.0', + datasource: 'github-releases', + packageName: 'kayac/ecspresso', + depName: 'ecspresso', + extractVersion: '^v(?\\S+)', + }, { currentValue: '1.14.1', datasource: 'hexpm-bob', @@ -398,6 +409,26 @@ dummy 1.2.3 packageName: 'ziglang/zig', depName: 'zig', }, + { + currentValue: '1.24.0', + datasource: 'github-releases', + packageName: 'mobile-dev-inc/maestro', + depName: 'maestro', + extractVersion: '^cli-(?\\S+)', + }, + { + currentValue: '1.21.0', + datasource: 'github-releases', + packageName: 'detekt/detekt', + depName: 'detekt', + extractVersion: '^v(?\\S+)', + }, + { + currentValue: '0.48.1', + datasource: 'github-releases', + packageName: 'pinterest/ktlint', + depName: 'ktlint', + }, { depName: 'dummy', skipReason: 'unsupported-datasource', diff --git a/lib/modules/manager/asdf/upgradeable-tooling.ts b/lib/modules/manager/asdf/upgradeable-tooling.ts index d076f0913e22ca..921d1274af0276 100644 --- a/lib/modules/manager/asdf/upgradeable-tooling.ts +++ b/lib/modules/manager/asdf/upgradeable-tooling.ts @@ -112,6 +112,14 @@ export const upgradeableTooling: Record = { packageName: 'dprint/dprint', }, }, + ecspresso: { + asdfPluginUrl: 'https://github.com/kayac/asdf-ecspresso', + config: { + datasource: GithubReleasesDatasource.id, + packageName: 'kayac/ecspresso', + extractVersion: '^v(?\\S+)', + }, + }, elixir: { asdfPluginUrl: 'https://github.com/asdf-vm/asdf-elixir', config: { @@ -414,4 +422,27 @@ export const upgradeableTooling: Record = { packageName: 'ziglang/zig', }, }, + maestro: { + asdfPluginUrl: 'https://github.com/dotanuki-labs/asdf-maestro', + config: { + datasource: GithubReleasesDatasource.id, + packageName: 'mobile-dev-inc/maestro', + extractVersion: '^cli-(?\\S+)', + }, + }, + detekt: { + asdfPluginUrl: 'https://github.com/dotanuki-labs/asdf-detekt', + config: { + datasource: GithubReleasesDatasource.id, + packageName: 'detekt/detekt', + extractVersion: '^v(?\\S+)', + }, + }, + ktlint: { + asdfPluginUrl: 'https://github.com/asdf-community/asdf-ktlint', + config: { + datasource: GithubReleasesDatasource.id, + packageName: 'pinterest/ktlint', + }, + }, }; diff --git a/lib/modules/manager/azure-pipelines/readme.md b/lib/modules/manager/azure-pipelines/readme.md index 7903346f2ba24e..aee00bea3e63ae 100644 --- a/lib/modules/manager/azure-pipelines/readme.md +++ b/lib/modules/manager/azure-pipelines/readme.md @@ -58,7 +58,7 @@ stages: Read the [resources block][resources-docs] and the [tasks block][tasks-docs] Azure Pipelines documentation for more information. -Files that are processed by the manager includes: +The `azure-pipelines` manager can process these files: - `.azure-pipelines/**/*.yaml` - `.azure-pipelines.yaml` @@ -70,5 +70,10 @@ Files that are processed by the manager includes: - `azure-pipeline.yaml` - `azure-pipeline.yml` + +!!! warning + Renovate can't update (root) container-element in containers jobs, see [issue #21987](https://github.com/renovatebot/renovate/issues/21987). + Renovate can't read Azure repositories defined in resource blocks, see [issue #15028](https://github.com/renovatebot/renovate/issues/15028). + [resources-docs]: https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema/resources?view=azure-pipelines [tasks-docs]: https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema/steps-task?view=azure-pipelines diff --git a/lib/modules/manager/bundler/artifacts.spec.ts b/lib/modules/manager/bundler/artifacts.spec.ts index a2eab121cdc71f..841f8ecc899d2d 100644 --- a/lib/modules/manager/bundler/artifacts.spec.ts +++ b/lib/modules/manager/bundler/artifacts.spec.ts @@ -23,7 +23,6 @@ import { updateArtifacts } from '.'; const datasource = mocked(_datasource); const bundlerHostRules = mocked(_bundlerHostRules); -jest.mock('fs-extra'); jest.mock('../../../util/exec/env'); jest.mock('../../datasource'); jest.mock('../../../util/fs'); diff --git a/lib/modules/manager/composer/artifacts.ts b/lib/modules/manager/composer/artifacts.ts index cc6acbbf266ff0..a902bf827db023 100644 --- a/lib/modules/manager/composer/artifacts.ts +++ b/lib/modules/manager/composer/artifacts.ts @@ -1,5 +1,6 @@ import is from '@sindresorhus/is'; import { quote } from 'shlex'; +import { z } from 'zod'; import { SYSTEM_INSUFFICIENT_DISK_SPACE, TEMPORARY_ERROR, @@ -18,10 +19,12 @@ import { import { getRepoStatus } from '../../../util/git'; import * as hostRules from '../../../util/host-rules'; import { regEx } from '../../../util/regex'; +import { Json } from '../../../util/schema-utils'; import { GitTagsDatasource } from '../../datasource/git-tags'; import { PackagistDatasource } from '../../datasource/packagist'; import type { UpdateArtifact, UpdateArtifactsResult } from '../types'; -import type { AuthJson, ComposerLock } from './types'; +import { Lockfile, PackageFile } from './schema'; +import type { AuthJson } from './types'; import { extractConstraints, findGithubToken, @@ -105,10 +108,19 @@ export async function updateArtifacts({ }: UpdateArtifact): Promise { logger.debug(`composer.updateArtifacts(${packageFileName})`); + const file = Json.pipe(PackageFile).parse(newPackageFileContent); + const lockFileName = packageFileName.replace(regEx(/\.json$/), '.lock'); - const existingLockFileContent = await readLocalFile(lockFileName, 'utf8'); - if (!existingLockFileContent) { - logger.debug('No composer.lock found'); + const lockfile = await z + .string() + .transform((f) => readLocalFile(f, 'utf8')) + .pipe(Json) + .pipe(Lockfile) + .nullable() + .catch(null) + .parseAsync(lockFileName); + if (!lockfile) { + logger.debug('Composer: unable to read lockfile'); return null; } @@ -118,12 +130,8 @@ export async function updateArtifacts({ try { await writeLocalFile(packageFileName, newPackageFileContent); - const existingLockFile: ComposerLock = JSON.parse(existingLockFileContent); const constraints = { - ...extractConstraints( - JSON.parse(newPackageFileContent), - existingLockFile - ), + ...extractConstraints(file, lockfile), ...config.constraints, }; @@ -150,7 +158,7 @@ export async function updateArtifacts({ const commands: string[] = []; // Determine whether install is required before update - if (requireComposerDependencyInstallation(existingLockFile)) { + if (requireComposerDependencyInstallation(lockfile)) { const preCmd = 'composer'; const preArgs = 'install' + getComposerArguments(config, composerToolConstraint); diff --git a/lib/modules/manager/composer/extract.spec.ts b/lib/modules/manager/composer/extract.spec.ts index 617db0e7245fe2..9c7d71a4cfd14b 100644 --- a/lib/modules/manager/composer/extract.spec.ts +++ b/lib/modules/manager/composer/extract.spec.ts @@ -279,7 +279,7 @@ describe('modules/manager/composer/extract', () => { }); it('extracts dependencies with lock file', async () => { - fs.readLocalFile.mockResolvedValue('some content'); + fs.readLocalFile.mockResolvedValue('{}'); const res = await extractPackageFile(requirements1, packageFile); expect(res).toMatchSnapshot(); expect(res?.deps).toHaveLength(33); diff --git a/lib/modules/manager/composer/extract.ts b/lib/modules/manager/composer/extract.ts index ae40abec469249..22eb4bdae316d4 100644 --- a/lib/modules/manager/composer/extract.ts +++ b/lib/modules/manager/composer/extract.ts @@ -1,215 +1,15 @@ -import is from '@sindresorhus/is'; import { logger } from '../../../logger'; -import { readLocalFile } from '../../../util/fs'; -import { regEx } from '../../../util/regex'; -import { GitTagsDatasource } from '../../datasource/git-tags'; -import { GithubTagsDatasource } from '../../datasource/github-tags'; -import { PackagistDatasource } from '../../datasource/packagist'; -import { api as semverComposer } from '../../versioning/composer'; -import type { PackageDependency, PackageFileContent } from '../types'; -import type { - ComposerConfig, - ComposerLock, - ComposerManagerData, - ComposerRepositories, - Repo, -} from './types'; - -/** - * The regUrl is expected to be a base URL. GitLab composer repository installation guide specifies - * to use a base URL containing packages.json. Composer still works in this scenario by determining - * whether to add / remove packages.json from the URL. - * - * See https://github.com/composer/composer/blob/750a92b4b7aecda0e5b2f9b963f1cb1421900675/src/Composer/Repository/ComposerRepository.php#L815 - */ -function transformRegUrl(url: string): string { - return url.replace(regEx(/(\/packages\.json)$/), ''); -} - -/** - * Parse the repositories field from a composer.json - * - * Entries with type vcs or git will be added to repositories, - * other entries will be added to registryUrls - */ -function parseRepositories( - repoJson: ComposerRepositories, - repositories: Record, - registryUrls: string[] -): void { - try { - let packagist = true; - Object.entries(repoJson).forEach(([key, repo]) => { - if (is.object(repo)) { - const name = is.array(repoJson) ? repo.name : key; - - switch (repo.type) { - case 'vcs': - case 'git': - case 'path': - repositories[name!] = repo; - break; - case 'composer': - registryUrls.push(transformRegUrl(repo.url)); - break; - case 'package': - logger.debug( - { url: repo.url }, - 'type package is not supported yet' - ); - } - if (repo.packagist === false || repo['packagist.org'] === false) { - packagist = false; - } - } // istanbul ignore else: invalid repo - else if (['packagist', 'packagist.org'].includes(key) && repo === false) { - packagist = false; - } - }); - if (packagist) { - registryUrls.push('https://packagist.org'); - } else { - logger.debug('Disabling packagist.org'); - } - } catch (e) /* istanbul ignore next */ { - logger.debug( - { repositories: repoJson }, - 'Error parsing composer.json repositories config' - ); - } -} +import type { PackageFileContent } from '../types'; +import { ComposerExtract } from './schema'; export async function extractPackageFile( content: string, fileName: string ): Promise { - logger.trace(`composer.extractPackageFile(${fileName})`); - let composerJson: ComposerConfig; - try { - composerJson = JSON.parse(content); - } catch (err) { - logger.debug(`Invalid JSON in ${fileName}`); + const res = await ComposerExtract.safeParseAsync({ content, fileName }); + if (!res.success) { + logger.debug({ fileName, err: res.error }, 'Composer: extract failed'); return null; } - const repositories: Record = {}; - const registryUrls: string[] = []; - const res: PackageFileContent = { deps: [] }; - - // handle lockfile - const lockfilePath = fileName.replace(regEx(/\.json$/), '.lock'); - const lockContents = await readLocalFile(lockfilePath, 'utf8'); - let lockParsed: ComposerLock | undefined; - if (lockContents) { - logger.debug(`Found composer lock file ${fileName}`); - res.lockFiles = [lockfilePath]; - try { - lockParsed = JSON.parse(lockContents) as ComposerLock; - } catch (err) /* istanbul ignore next */ { - logger.warn({ err }, 'Error processing composer.lock'); - } - } - - // handle composer.json repositories - if (composerJson.repositories) { - parseRepositories(composerJson.repositories, repositories, registryUrls); - } - - const deps: PackageDependency[] = []; - const depTypes: ('require' | 'require-dev')[] = ['require', 'require-dev']; - for (const depType of depTypes) { - if (composerJson[depType]) { - try { - for (const [depName, version] of Object.entries( - composerJson[depType]! - )) { - const currentValue = version.trim(); - if (depName === 'php') { - deps.push({ - depType, - depName, - currentValue, - datasource: GithubTagsDatasource.id, - packageName: 'php/php-src', - extractVersion: '^php-(?.*)$', - }); - } else { - // Default datasource and packageName - let datasource = PackagistDatasource.id; - let packageName = depName; - - // Check custom repositories by type - if (repositories[depName]) { - switch (repositories[depName].type) { - case 'vcs': - case 'git': - datasource = GitTagsDatasource.id; - packageName = repositories[depName].url; - break; - case 'path': - deps.push({ - depType, - depName, - currentValue, - skipReason: 'path-dependency', - }); - continue; - } - } - const dep: PackageDependency = { - depType, - depName, - currentValue, - datasource, - }; - if (depName !== packageName) { - dep.packageName = packageName; - } - if (!depName.includes('/')) { - dep.skipReason = 'unsupported'; - } - if (lockParsed) { - const lockField = - depType === 'require' - ? 'packages' - : /* istanbul ignore next */ 'packages-dev'; - const lockedDep = lockParsed[lockField]?.find( - (item) => item.name === dep.depName - ); - if (lockedDep && semverComposer.isVersion(lockedDep.version)) { - dep.lockedVersion = lockedDep.version.replace(regEx(/^v/i), ''); - } - } - if ( - !dep.skipReason && - (!repositories[depName] || - repositories[depName].type === 'composer') && - registryUrls.length !== 0 - ) { - dep.registryUrls = registryUrls; - } - deps.push(dep); - } - } - } catch (err) /* istanbul ignore next */ { - logger.debug({ fileName, depType, err }, 'Error parsing composer.json'); - return null; - } - } - } - if (!deps.length) { - return null; - } - res.deps = deps; - if (is.string(composerJson.type)) { - const managerData: ComposerManagerData = { - composerJsonType: composerJson.type, - }; - res.managerData = managerData; - } - - if (composerJson.require?.php) { - res.extractedConstraints = { php: composerJson.require.php }; - } - - return res; + return res.data; } diff --git a/lib/modules/manager/composer/schema.spec.ts b/lib/modules/manager/composer/schema.spec.ts new file mode 100644 index 00000000000000..d105b70690e926 --- /dev/null +++ b/lib/modules/manager/composer/schema.spec.ts @@ -0,0 +1,115 @@ +import { Repos, ReposArray, ReposRecord } from './schema'; + +describe('modules/manager/composer/schema', () => { + describe('ReposRecord', () => { + it('parses default values', () => { + expect(ReposRecord.parse({})).toEqual([]); + }); + + it('parses repositories', () => { + expect( + ReposRecord.parse({ + wpackagist: { type: 'composer', url: 'https://wpackagist.org' }, + someGit: { type: 'vcs', url: 'https://some-vcs.com' }, + somePath: { type: 'path', url: '/some/path' }, + packagist: false, + 'packagist.org': false, + foo: 'bar', + }) + ).toEqual([ + { type: 'composer', url: 'https://wpackagist.org' }, + { name: 'someGit', type: 'git', url: 'https://some-vcs.com' }, + { name: 'somePath', type: 'path', url: '/some/path' }, + { type: 'disable-packagist' }, + { type: 'disable-packagist' }, + ]); + }); + }); + + describe('ReposArray', () => { + it('parses default values', () => { + expect(ReposArray.parse([])).toEqual([]); + }); + + it('parses repositories', () => { + expect( + ReposArray.parse([ + { + type: 'composer', + url: 'https://wpackagist.org', + }, + { name: 'someGit', type: 'vcs', url: 'https://some-vcs.com' }, + { name: 'somePath', type: 'path', url: '/some/path' }, + { packagist: false }, + { 'packagist.org': false }, + { foo: 'bar' }, + ]) + ).toEqual([ + { type: 'composer', url: 'https://wpackagist.org' }, + { name: 'someGit', type: 'git', url: 'https://some-vcs.com' }, + { name: 'somePath', type: 'path', url: '/some/path' }, + { type: 'disable-packagist' }, + { type: 'disable-packagist' }, + ]); + }); + }); + + describe('Repos', () => { + it('parses default values', () => { + expect(Repos.parse(null)).toEqual({ + pathRepos: {}, + gitRepos: {}, + registryUrls: null, + }); + }); + + it('parses repositories', () => { + expect( + Repos.parse([ + { + name: 'wpackagist', + type: 'composer', + url: 'https://wpackagist.org', + }, + { name: 'someGit', type: 'vcs', url: 'https://some-vcs.com' }, + { name: 'somePath', type: 'path', url: '/some/path' }, + ]) + ).toEqual({ + pathRepos: { + somePath: { name: 'somePath', type: 'path', url: '/some/path' }, + }, + registryUrls: ['https://wpackagist.org', 'https://packagist.org'], + gitRepos: { + someGit: { + name: 'someGit', + type: 'git', + url: 'https://some-vcs.com', + }, + }, + }); + }); + + it(`parses repositories with packagist disabled`, () => { + expect( + Repos.parse({ + wpackagist: { type: 'composer', url: 'https://wpackagist.org' }, + someGit: { type: 'vcs', url: 'https://some-vcs.com' }, + somePath: { type: 'path', url: '/some/path' }, + packagist: false, + }) + ).toEqual({ + pathRepos: { + somePath: { name: 'somePath', type: 'path', url: '/some/path' }, + }, + registryUrls: ['https://wpackagist.org'], + gitRepos: { + someGit: { + name: 'someGit', + type: 'git', + url: 'https://some-vcs.com', + }, + }, + }); + }); + }); +}); diff --git a/lib/modules/manager/composer/schema.ts b/lib/modules/manager/composer/schema.ts new file mode 100644 index 00000000000000..5d0ac8e7e4f817 --- /dev/null +++ b/lib/modules/manager/composer/schema.ts @@ -0,0 +1,344 @@ +import { z } from 'zod'; +import { logger } from '../../../logger'; +import { readLocalFile } from '../../../util/fs'; +import { regEx } from '../../../util/regex'; +import { Json, LooseArray, LooseRecord } from '../../../util/schema-utils'; +import { GitTagsDatasource } from '../../datasource/git-tags'; +import { GithubTagsDatasource } from '../../datasource/github-tags'; +import { PackagistDatasource } from '../../datasource/packagist'; +import { api as semverComposer } from '../../versioning/composer'; +import type { PackageDependency, PackageFileContent } from '../types'; +import type { ComposerManagerData } from './types'; + +export const ComposerRepo = z.object({ + type: z.literal('composer'), + /** + * The regUrl is expected to be a base URL. GitLab composer repository installation guide specifies + * to use a base URL containing packages.json. Composer still works in this scenario by determining + * whether to add / remove packages.json from the URL. + * + * See https://github.com/composer/composer/blob/750a92b4b7aecda0e5b2f9b963f1cb1421900675/src/Composer/Repository/ComposerRepository.php#L815 + */ + url: z.string().transform((url) => url.replace(/\/packages\.json$/, '')), +}); +export type ComposerRepo = z.infer; + +export const GitRepo = z.object({ + type: z.enum(['vcs', 'git']).transform(() => 'git' as const), + url: z.string(), + name: z.string().optional(), +}); +export type GitRepo = z.infer; + +export const PathRepo = z.object({ + type: z.literal('path'), + url: z.string(), + name: z.string().optional(), +}); +export type PathRepo = z.infer; + +export const PackageRepo = z.object({ + type: z.literal('package'), +}); + +export const Repo = z.discriminatedUnion('type', [ + ComposerRepo, + GitRepo, + PathRepo, + PackageRepo, +]); +export type Repo = z.infer; + +export const NamedRepo = z.discriminatedUnion('type', [ + ComposerRepo, + GitRepo.extend({ name: z.string() }), + PathRepo.extend({ name: z.string() }), + PackageRepo, +]); +export type NamedRepo = z.infer; + +const DisablePackagist = z.object({ type: z.literal('disable-packagist') }); +export type DisablePackagist = z.infer; + +export const ReposRecord = LooseRecord(z.union([Repo, z.literal(false)]), { + onError: ({ error: err }) => { + logger.debug({ err }, 'Composer: error parsing repositories object'); + }, +}).transform((repos) => { + const result: (NamedRepo | DisablePackagist)[] = []; + for (const [name, repo] of Object.entries(repos)) { + if (repo === false) { + if (name === 'packagist' || name === 'packagist.org') { + result.push({ type: 'disable-packagist' }); + } + continue; + } + + if (repo.type === 'path' || repo.type === 'git') { + result.push({ name, ...repo }); + continue; + } + + if (repo.type === 'composer') { + result.push(repo); + continue; + } + } + + return result; +}); +export type ReposRecord = z.infer; + +export const ReposArray = LooseArray( + z.union([ + Repo, + z + .union([ + z.object({ packagist: z.literal(false) }), + z.object({ 'packagist.org': z.literal(false) }), + ]) + .transform((): DisablePackagist => ({ type: 'disable-packagist' })), + ]), + { + onError: ({ error: err }) => { + logger.debug({ err }, 'Composer: error parsing repositories array'); + }, + } +).transform((repos) => { + const result: (NamedRepo | DisablePackagist)[] = []; + for (let idx = 0; idx < repos.length; idx++) { + const repo = repos[idx]; + if (repo.type === 'path' || repo.type === 'git') { + result.push({ name: `__${idx}`, ...repo }); + } else { + result.push(repo); + } + } + return result; +}); +export type ReposArray = z.infer; + +export const Repos = z + .union([ReposRecord, ReposArray]) + .default([]) // Prevents warnings for packages without repositories field + .catch(({ error: err }) => { + logger.debug({ err }, 'Composer: invalid "repositories" field'); + return []; + }) + .transform((repos) => { + let packagist = true; + const repoUrls: string[] = []; + const gitRepos: Record = {}; + const pathRepos: Record = {}; + + for (const repo of repos) { + if (repo.type === 'composer') { + repoUrls.push(repo.url); + } else if (repo.type === 'git') { + gitRepos[repo.name] = repo; + } else if (repo.type === 'path') { + pathRepos[repo.name] = repo; + } else if (repo.type === 'disable-packagist') { + packagist = false; + } + } + + if (packagist && repoUrls.length) { + repoUrls.push('https://packagist.org'); + } + const registryUrls = repoUrls.length ? repoUrls : null; + + return { registryUrls, gitRepos, pathRepos }; + }); +export type Repos = z.infer; + +const RequireDefs = LooseRecord(z.string().transform((x) => x.trim())).catch( + {} +); + +export const PackageFile = z + .object({ + type: z.string().optional(), + config: z + .object({ + platform: z.object({ + php: z.string(), + }), + }) + .nullable() + .catch(null), + repositories: Repos, + require: RequireDefs, + 'require-dev': RequireDefs, + }) + .transform( + ({ + type: composerJsonType, + config, + repositories, + require, + 'require-dev': requireDev, + }) => ({ + composerJsonType, + config, + repositories, + require, + requireDev, + }) + ); +export type PackageFile = z.infer; + +const LockedPackage = z.object({ + name: z.string(), + version: z.string(), +}); +type LockedPackage = z.infer; + +export const Lockfile = z + .object({ + 'plugin-api-version': z.string().optional(), + packages: LooseArray(LockedPackage).catch([]), + 'packages-dev': LooseArray(LockedPackage).catch([]), + }) + .transform( + ({ + 'plugin-api-version': pluginApiVersion, + packages, + 'packages-dev': packagesDev, + }) => ({ pluginApiVersion, packages, packagesDev }) + ); +export type Lockfile = z.infer; + +export const ComposerExtract = z + .object({ + content: z.string(), + fileName: z.string(), + }) + .transform(({ content, fileName }) => { + const lockfileName = fileName.replace(/\.json$/, '.lock'); + return { + file: content, + lockfileName, + lockfile: lockfileName, + }; + }) + .pipe( + z.object({ + file: Json.pipe(PackageFile), + lockfileName: z.string(), + lockfile: z + .string() + .transform((lockfileName) => readLocalFile(lockfileName, 'utf8')) + .pipe( + z.union([ + z.null(), + z + .string() + .pipe(Json) + .pipe(Lockfile) + .nullable() + .catch(({ error: err }) => { + logger.debug({ err }, 'Composer: lockfile parsing error'); + return null; + }), + ]) + ), + }) + ) + .transform(({ file, lockfile, lockfileName }) => { + const { composerJsonType, require, requireDev } = file; + const { registryUrls, gitRepos, pathRepos } = file.repositories; + + const deps: PackageDependency[] = []; + + const profiles = [ + { + depType: 'require', + req: require, + locked: lockfile?.packages ?? [], + }, + { + depType: 'require-dev', + req: requireDev, + locked: lockfile?.packagesDev ?? [], + }, + ]; + + for (const { depType, req, locked } of profiles) { + for (const [depName, currentValue] of Object.entries(req)) { + if (depName === 'php') { + deps.push({ + depType, + depName, + currentValue, + datasource: GithubTagsDatasource.id, + packageName: 'php/php-src', + extractVersion: '^php-(?.*)$', + }); + continue; + } + + if (pathRepos[depName]) { + deps.push({ + depType, + depName, + currentValue, + skipReason: 'path-dependency', + }); + continue; + } + + const dep: PackageDependency = { + depType, + depName, + currentValue, + }; + + if (!depName.includes('/')) { + dep.skipReason = 'unsupported'; + } + + const lockedDep = locked.find((item) => item.name === depName); + if (lockedDep && semverComposer.isVersion(lockedDep.version)) { + dep.lockedVersion = lockedDep.version.replace(regEx(/^v/i), ''); + } + + const gitRepo = gitRepos[depName]; + if (gitRepo) { + dep.datasource = GitTagsDatasource.id; + dep.packageName = gitRepo.url; + deps.push(dep); + continue; + } + + dep.datasource = PackagistDatasource.id; + + if (registryUrls) { + dep.registryUrls = registryUrls; + } + + deps.push(dep); + } + } + + if (!deps.length) { + return null; + } + + const res: PackageFileContent = { deps }; + + if (composerJsonType) { + res.managerData = { composerJsonType }; + } + + if (require.php) { + res.extractedConstraints = { php: require.php }; + } + + if (lockfile) { + res.lockFiles = [lockfileName]; + } + + return res; + }); +export type ComposerExtract = z.infer; diff --git a/lib/modules/manager/composer/types.ts b/lib/modules/manager/composer/types.ts index c46f294d0cf861..491dc7eac3f78c 100644 --- a/lib/modules/manager/composer/types.ts +++ b/lib/modules/manager/composer/types.ts @@ -1,49 +1,3 @@ -// istanbul ignore file: types only -export interface Repo { - name?: string; - type: 'composer' | 'git' | 'package' | 'path' | 'vcs'; - packagist?: boolean; - 'packagist.org'?: boolean; - url: string; -} -export type ComposerRepositories = Record | Repo[]; - -export interface ComposerConfig { - type?: string; - /** - * Setting a fixed PHP version (e.g. {"php": "7.0.3"}) will let you fake the - * platform version so that you can emulate a production env or define your - * target platform in the config. - * See https://getcomposer.org/doc/06-config.md#platform - */ - config?: { - platform?: { - php?: string; - }; - }; - /** - * A repositories field can be an array of Repo objects or an object of repoName: Repo - * Also it can be a boolean (usually false) to disable packagist. - * (Yes this can be confusing, as it is also not properly documented in the composer docs) - * See https://getcomposer.org/doc/05-repositories.md#disabling-packagist-org - */ - repositories?: ComposerRepositories; - - require?: Record; - 'require-dev'?: Record; -} - -export interface ComposerLockPackage { - name: string; - version: string; -} - -export interface ComposerLock { - 'plugin-api-version'?: string; - packages?: ComposerLockPackage[]; - 'packages-dev'?: ComposerLockPackage[]; -} - export interface ComposerManagerData { composerJsonType?: string; } diff --git a/lib/modules/manager/composer/update-locked.ts b/lib/modules/manager/composer/update-locked.ts index fcfaa89d2f11c9..469ac1a81df64b 100644 --- a/lib/modules/manager/composer/update-locked.ts +++ b/lib/modules/manager/composer/update-locked.ts @@ -1,7 +1,8 @@ import { logger } from '../../../logger'; +import { Json } from '../../../util/schema-utils'; import { api as composer } from '../../versioning/composer'; import type { UpdateLockedConfig, UpdateLockedResult } from '../types'; -import type { ComposerLock } from './types'; +import { Lockfile } from './schema'; export function updateLockedDependency( config: UpdateLockedConfig @@ -12,12 +13,11 @@ export function updateLockedDependency( `composer.updateLockedDependency: ${depName}@${currentVersion} -> ${newVersion} [${lockFile}]` ); try { - const locked = JSON.parse(lockFileContent!) as ComposerLock; + const lockfile = Json.pipe(Lockfile).parse(lockFileContent); if ( - locked.packages?.find( - (entry) => - entry.name === depName && - composer.equals(entry.version || '', newVersion) + lockfile?.packages.find( + ({ name, version }) => + name === depName && composer.equals(version, newVersion) ) ) { return { status: 'already-updated' }; diff --git a/lib/modules/manager/composer/utils.spec.ts b/lib/modules/manager/composer/utils.spec.ts index 481df517e6f260..c9e6117fccfd5b 100644 --- a/lib/modules/manager/composer/utils.spec.ts +++ b/lib/modules/manager/composer/utils.spec.ts @@ -1,6 +1,7 @@ import { GlobalConfig } from '../../../config/global'; import * as hostRules from '../../../util/host-rules'; import { GitTagsDatasource } from '../../datasource/git-tags'; +import { Lockfile, PackageFile } from './schema'; import { extractConstraints, findGithubToken, @@ -21,114 +22,121 @@ describe('modules/manager/composer/utils', () => { describe('extractConstraints', () => { it('returns from require', () => { - expect( - extractConstraints( - { require: { php: '>=5.3.2', 'composer/composer': '1.1.0' } }, - {} - ) - ).toEqual({ php: '>=5.3.2', composer: '1.1.0' }); + const file = PackageFile.parse({ + require: { php: '>=5.3.2', 'composer/composer': '1.1.0' }, + }); + const lockfile = Lockfile.parse({}); + expect(extractConstraints(file, lockfile)).toEqual({ + php: '>=5.3.2', + composer: '1.1.0', + }); }); it('returns platform php version', () => { - expect( - extractConstraints( - { - config: { platform: { php: '7.4.27' } }, - require: { php: '~7.4 || ~8.0' }, - }, - {} - ) - ).toEqual({ composer: '1.*', php: '<=7.4.27' }); + const file = PackageFile.parse({ + config: { platform: { php: '7.4.27' } }, + require: { php: '~7.4 || ~8.0' }, + }); + const lockfile = Lockfile.parse({}); + expect(extractConstraints(file, lockfile)).toEqual({ + composer: '1.*', + php: '<=7.4.27', + }); }); it('returns platform 0 minor php version', () => { - expect( - extractConstraints( - { - config: { platform: { php: '7.0.5' } }, - require: { php: '^7.0 || ~8.0' }, - }, - {} - ) - ).toEqual({ composer: '1.*', php: '<=7.0.5' }); + const file = PackageFile.parse({ + config: { platform: { php: '7.0.5' } }, + require: { php: '^7.0 || ~8.0' }, + }); + const lockfile = Lockfile.parse({}); + expect(extractConstraints(file, lockfile)).toEqual({ + composer: '1.*', + php: '<=7.0.5', + }); }); it('returns platform 0 patch php version', () => { - expect( - extractConstraints( - { - config: { platform: { php: '7.4.0' } }, - require: { php: '^7.0 || ~8.0' }, - }, - {} - ) - ).toEqual({ composer: '1.*', php: '<=7.4.0' }); + const file = PackageFile.parse({ + config: { platform: { php: '7.4.0' } }, + require: { php: '^7.0 || ~8.0' }, + }); + const lockfile = Lockfile.parse({}); + expect(extractConstraints(file, lockfile)).toEqual({ + composer: '1.*', + php: '<=7.4.0', + }); }); it('returns platform lowest minor php version', () => { - expect( - extractConstraints( - { - config: { platform: { php: '7' } }, - require: { php: '^7.0 || ~8.0' }, - }, - {} - ) - ).toEqual({ composer: '1.*', php: '<=7.0.0' }); + const file = PackageFile.parse({ + config: { platform: { php: '7' } }, + require: { php: '^7.0 || ~8.0' }, + }); + const lockfile = Lockfile.parse({}); + expect(extractConstraints(file, lockfile)).toEqual({ + composer: '1.*', + php: '<=7.0.0', + }); }); it('returns platform lowest patch php version', () => { - expect( - extractConstraints( - { - config: { platform: { php: '7.4' } }, - require: { php: '~7.4 || ~8.0' }, - }, - {} - ) - ).toEqual({ composer: '1.*', php: '<=7.4.0' }); + const file = PackageFile.parse({ + config: { platform: { php: '7.4' } }, + require: { php: '~7.4 || ~8.0' }, + }); + const lockfile = Lockfile.parse({}); + expect(extractConstraints(file, lockfile)).toEqual({ + composer: '1.*', + php: '<=7.4.0', + }); }); it('returns from require-dev', () => { - expect( - extractConstraints( - { 'require-dev': { 'composer/composer': '1.1.0' } }, - {} - ) - ).toEqual({ composer: '1.1.0' }); + const file = PackageFile.parse({ + 'require-dev': { 'composer/composer': '1.1.0' }, + }); + const lockfile = Lockfile.parse({}); + expect(extractConstraints(file, lockfile)).toEqual({ composer: '1.1.0' }); }); it('returns from composer platform require', () => { - expect( - extractConstraints({ require: { php: '^8.1', composer: '2.2.0' } }, {}) - ).toEqual({ php: '^8.1', composer: '2.2.0' }); + const file = PackageFile.parse({ + require: { php: '^8.1', composer: '2.2.0' }, + }); + const lockfile = Lockfile.parse({}); + expect(extractConstraints(file, lockfile)).toEqual({ + php: '^8.1', + composer: '2.2.0', + }); }); it('returns from composer platform require-dev', () => { - expect( - extractConstraints({ 'require-dev': { composer: '^2.2' } }, {}) - ).toEqual({ composer: '^2.2' }); + const file = PackageFile.parse({ 'require-dev': { composer: '^2.2' } }); + const lockfile = Lockfile.parse({}); + expect(extractConstraints(file, lockfile)).toEqual({ composer: '^2.2' }); }); it('returns from composer-runtime-api', () => { - expect( - extractConstraints( - { require: { 'composer-runtime-api': '^1.1.0' } }, - {} - ) - ).toEqual({ composer: '^1.1' }); + const file = PackageFile.parse({ + require: { 'composer-runtime-api': '^1.1.0' }, + }); + const lockfile = Lockfile.parse({}); + expect(extractConstraints(file, lockfile)).toEqual({ composer: '^1.1' }); }); it('returns from plugin-api-version', () => { - expect(extractConstraints({}, { 'plugin-api-version': '1.1.0' })).toEqual( - { - composer: '^1.1', - } - ); + const file = PackageFile.parse({}); + const lockfile = Lockfile.parse({ 'plugin-api-version': '1.1.0' }); + expect(extractConstraints(file, lockfile)).toEqual({ + composer: '^1.1', + }); }); it('fallback to 1.*', () => { - expect(extractConstraints({}, {})).toEqual({ composer: '1.*' }); + const file = PackageFile.parse({}); + const lockfile = Lockfile.parse({}); + expect(extractConstraints(file, lockfile)).toEqual({ composer: '1.*' }); }); }); @@ -276,27 +284,24 @@ describe('modules/manager/composer/utils', () => { describe('requireComposerDependencyInstallation', () => { it('returns true when symfony/flex has been installed', () => { - expect( - requireComposerDependencyInstallation({ - packages: [{ name: 'symfony/flex', version: '1.17.1' }], - }) - ).toBeTrue(); + const lockfile = Lockfile.parse({ + packages: [{ name: 'symfony/flex', version: '1.17.1' }], + }); + expect(requireComposerDependencyInstallation(lockfile)).toBeTrue(); }); it('returns true when symfony/flex has been installed as dev dependency', () => { - expect( - requireComposerDependencyInstallation({ - 'packages-dev': [{ name: 'symfony/flex', version: '1.17.1' }], - }) - ).toBeTrue(); + const lockfile = Lockfile.parse({ + 'packages-dev': [{ name: 'symfony/flex', version: '1.17.1' }], + }); + expect(requireComposerDependencyInstallation(lockfile)).toBeTrue(); }); it('returns false when symfony/flex has not been installed', () => { - expect( - requireComposerDependencyInstallation({ - packages: [{ name: 'symfony/console', version: '5.4.0' }], - }) - ).toBeFalse(); + const lockfile = Lockfile.parse({ + packages: [{ name: 'symfony/console', version: '5.4.0' }], + }); + expect(requireComposerDependencyInstallation(lockfile)).toBeFalse(); }); }); diff --git a/lib/modules/manager/composer/utils.ts b/lib/modules/manager/composer/utils.ts index 8342798971adb7..5a73be6c9120ac 100644 --- a/lib/modules/manager/composer/utils.ts +++ b/lib/modules/manager/composer/utils.ts @@ -7,7 +7,7 @@ import type { HostRuleSearchResult } from '../../../types'; import type { ToolConstraint } from '../../../util/exec/types'; import { api, id as composerVersioningId } from '../../versioning/composer'; import type { UpdateArtifactsConfig } from '../types'; -import type { ComposerConfig, ComposerLock } from './types'; +import type { Lockfile, PackageFile } from './schema'; export { composerVersioningId }; @@ -59,53 +59,55 @@ export function getPhpConstraint( return null; } -export function requireComposerDependencyInstallation( - lock: ComposerLock -): boolean { +export function requireComposerDependencyInstallation({ + packages, + packagesDev, +}: Lockfile): boolean { return ( - lock.packages?.some((p) => depRequireInstall.has(p.name)) === true || - lock['packages-dev']?.some((p) => depRequireInstall.has(p.name)) === true + packages.some((p) => depRequireInstall.has(p.name)) === true || + packagesDev.some((p) => depRequireInstall.has(p.name)) === true ); } export function extractConstraints( - composerJson: ComposerConfig, - lockParsed: ComposerLock + { config, require, requireDev }: PackageFile, + { pluginApiVersion }: Lockfile ): Record { const res: Record = { composer: '1.*' }; // extract php - if (composerJson.config?.platform?.php) { - const major = api.getMajor(composerJson.config.platform.php); - const minor = api.getMinor(composerJson.config.platform.php) ?? 0; - const patch = api.getPatch(composerJson.config.platform.php) ?? 0; + const phpVersion = config?.platform.php; + if (phpVersion) { + const major = api.getMajor(phpVersion); + const minor = api.getMinor(phpVersion) ?? 0; + const patch = api.getPatch(phpVersion) ?? 0; res.php = `<=${major}.${minor}.${patch}`; - } else if (composerJson.require?.php) { - res.php = composerJson.require.php; + } else if (require.php) { + res.php = require.php; } // extract direct composer dependency - if (composerJson.require?.['composer/composer']) { - res.composer = composerJson.require?.['composer/composer']; - } else if (composerJson['require-dev']?.['composer/composer']) { - res.composer = composerJson['require-dev']?.['composer/composer']; + if (require['composer/composer']) { + res.composer = require['composer/composer']; + } else if (requireDev['composer/composer']) { + res.composer = requireDev['composer/composer']; } // composer platform package - else if (composerJson.require?.['composer']) { - res.composer = composerJson.require?.['composer']; - } else if (composerJson['require-dev']?.['composer']) { - res.composer = composerJson['require-dev']?.['composer']; + else if (require['composer']) { + res.composer = require['composer']; + } else if (requireDev['composer']) { + res.composer = requireDev['composer']; } // check last used composer version - else if (lockParsed?.['plugin-api-version']) { - const major = api.getMajor(lockParsed?.['plugin-api-version']); - const minor = api.getMinor(lockParsed?.['plugin-api-version']); + else if (pluginApiVersion) { + const major = api.getMajor(pluginApiVersion); + const minor = api.getMinor(pluginApiVersion); res.composer = `^${major}.${minor}`; } // check composer api dependency - else if (composerJson.require?.['composer-runtime-api']) { - const major = api.getMajor(composerJson.require?.['composer-runtime-api']); - const minor = api.getMinor(composerJson.require?.['composer-runtime-api']); + else if (require['composer-runtime-api']) { + const major = api.getMajor(require['composer-runtime-api']); + const minor = api.getMinor(require['composer-runtime-api']); res.composer = `^${major}.${minor}`; } return res; diff --git a/lib/modules/manager/dockerfile/extract.ts b/lib/modules/manager/dockerfile/extract.ts index 45a6fc56cf4e51..f62f281bf28198 100644 --- a/lib/modules/manager/dockerfile/extract.ts +++ b/lib/modules/manager/dockerfile/extract.ts @@ -182,7 +182,7 @@ export function getDep( ...getDep(`${value}/${groups.depName}`), replaceString: currentFrom, }; - dep.autoReplaceStringTemplate = getAutoReplaceTemplate(dep)!; + dep.autoReplaceStringTemplate = getAutoReplaceTemplate(dep); return dep; } } diff --git a/lib/modules/manager/gomod/artifacts.spec.ts b/lib/modules/manager/gomod/artifacts.spec.ts index b7370430da197d..aee0645b62011a 100644 --- a/lib/modules/manager/gomod/artifacts.spec.ts +++ b/lib/modules/manager/gomod/artifacts.spec.ts @@ -1362,9 +1362,11 @@ describe('modules/manager/gomod/artifacts', () => { fs.readLocalFile.mockResolvedValueOnce('Current go.sum'); fs.readLocalFile.mockResolvedValueOnce(null); // vendor modules filename const execSnapshots = mockExecAll(); - git.getRepoStatus.mockResolvedValueOnce({ - modified: ['go.sum'], - } as StatusResult); + git.getRepoStatus.mockResolvedValueOnce( + partial({ + modified: ['go.sum'], + }) + ); fs.readLocalFile .mockResolvedValueOnce('New go.sum') .mockResolvedValueOnce('New go.mod'); @@ -1405,9 +1407,11 @@ describe('modules/manager/gomod/artifacts', () => { fs.readLocalFile.mockResolvedValueOnce('Current go.sum'); fs.readLocalFile.mockResolvedValueOnce(null); // vendor modules filename const execSnapshots = mockExecAll(); - git.getRepoStatus.mockResolvedValueOnce({ - modified: ['go.sum'], - } as StatusResult); + git.getRepoStatus.mockResolvedValueOnce( + partial({ + modified: ['go.sum'], + }) + ); fs.readLocalFile .mockResolvedValueOnce('New go.sum') .mockResolvedValueOnce('New go.mod'); diff --git a/lib/modules/manager/gradle/__fixtures__/1/libs.versions.toml b/lib/modules/manager/gradle/__fixtures__/1/libs.versions.toml index fccce61156ba5a..8aac96c8ed1b95 100644 --- a/lib/modules/manager/gradle/__fixtures__/1/libs.versions.toml +++ b/lib/modules/manager/gradle/__fixtures__/1/libs.versions.toml @@ -17,5 +17,5 @@ kotest = [ "kotest-runner-junit5", "kotest-assertions-core-jvm" ] [plugins] detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } -publish-on-central = { id = "org.danilopianini.publish-on-central", version.ref = "publish-on-central" } +publish-on-central = { id = "org.danilopianini.publish-on-central", version.ref = "publish.on.central" } grgit = { id = "org.ajoberstar.grgit", version.unknown = "this will fail" } diff --git a/lib/modules/manager/gradle/__fixtures__/3/libs.versions.toml b/lib/modules/manager/gradle/__fixtures__/3/libs.versions.toml index bddeb5947dfe1d..7188d1a6c6b443 100644 --- a/lib/modules/manager/gradle/__fixtures__/3/libs.versions.toml +++ b/lib/modules/manager/gradle/__fixtures__/3/libs.versions.toml @@ -7,4 +7,4 @@ junit = "1.4.9" [libraries] junit-legacy = { module = "junit:junit", version.ref = "junit" } -mocha-junit = { module = "mocha-junit:mocha-junit", version.ref = "mocha-junit-reporter" } +mocha-junit = { module = "mocha-junit:mocha-junit", version.ref = "mocha.junit.reporter" } diff --git a/lib/modules/manager/gradle/artifacts.spec.ts b/lib/modules/manager/gradle/artifacts.spec.ts index d42d22dc55c691..087952f444900b 100644 --- a/lib/modules/manager/gradle/artifacts.spec.ts +++ b/lib/modules/manager/gradle/artifacts.spec.ts @@ -12,6 +12,7 @@ import { logger, mockedFunction, partial, + scm, } from '../../../../test/util'; import { GlobalConfig } from '../../../config/global'; import type { RepoGlobalConfig } from '../../../config/types'; @@ -61,7 +62,7 @@ describe('modules/manager/gradle/artifacts', () => { }); fs.findUpLocal.mockResolvedValue('gradlew'); - git.getFileList.mockResolvedValue([ + scm.getFileList.mockResolvedValue([ 'gradlew', 'build.gradle', 'gradle.lockfile', @@ -92,7 +93,7 @@ describe('modules/manager/gradle/artifacts', () => { it('aborts if no lockfile is found', async () => { const execSnapshots = mockExecAll(); - git.getFileList.mockResolvedValue(['build.gradle', 'settings.gradle']); + scm.getFileList.mockResolvedValue(['build.gradle', 'settings.gradle']); expect( await updateArtifacts({ diff --git a/lib/modules/manager/gradle/artifacts.ts b/lib/modules/manager/gradle/artifacts.ts index 671aa24a628fc7..68efbcb4206c86 100644 --- a/lib/modules/manager/gradle/artifacts.ts +++ b/lib/modules/manager/gradle/artifacts.ts @@ -6,8 +6,9 @@ import { logger } from '../../../logger'; import { exec } from '../../../util/exec'; import type { ExecOptions } from '../../../util/exec/types'; import { findUpLocal, readLocalFile, writeLocalFile } from '../../../util/fs'; -import { getFileList, getFiles, getRepoStatus } from '../../../util/git'; +import { getFiles, getRepoStatus } from '../../../util/git'; import { regEx } from '../../../util/regex'; +import { scm } from '../../platform/scm'; import { extraEnv, extractGradleVersion, @@ -94,7 +95,7 @@ export async function updateArtifacts({ }: UpdateArtifact): Promise { logger.debug(`gradle.updateArtifacts(${packageFileName})`); - const fileList = await getFileList(); + const fileList = await scm.getFileList(); const lockFiles = fileList.filter((file) => isLockFile(file)); if (!lockFiles.length) { logger.debug('No Gradle dependency lockfiles found - skipping update'); diff --git a/lib/modules/manager/gradle/extract.spec.ts b/lib/modules/manager/gradle/extract.spec.ts index e5f314711d3bd6..94c9a4f1b18865 100644 --- a/lib/modules/manager/gradle/extract.spec.ts +++ b/lib/modules/manager/gradle/extract.spec.ts @@ -120,6 +120,126 @@ describe('modules/manager/gradle/extract', () => { ]); }); + it('resolves cross-file Kotlin objects', async () => { + const fsMock = { + 'buildSrc/src/main/kotlin/Deps.kt': codeBlock` + object Libraries { + const val jacksonAnnotations = "com.fasterxml.jackson.core:jackson-annotations:\${Versions.jackson}" + const val rxjava: String = "io.reactivex.rxjava2:rxjava:" + Versions.rxjava + const val jCache = "javax.cache:cache-api:1.1.0" + private const val shadowVersion = "7.1.2" + + object Kotlin { + const val version = GradleDeps.Kotlin.version + const val stdlibJdk = "org.jetbrains.kotlin:kotlin-stdlib:$version" + } + + object Android { + object Tools { + private const val version = "4.1.2" + const val buildGradle = "com.android.tools.build:gradle:$version" + } + } + + val modulePlugins = mapOf( + "shadow" to shadowVersion + ) + + object Test { + private const val version = "1.3.0-rc01" + const val core = "androidx.test:core:\${Test.version}" + + object Espresso { + private const val version = "3.3.0-rc01" + const val espressoCore = "androidx.test.espresso:espresso-core:$version" + } + + object Androidx { + const val coreKtx = "androidx.test:core-ktx:$version" + } + } + } + `, + 'buildSrc/src/main/kotlin/GradleDeps.kt': codeBlock` + object GradleDeps { + object Kotlin { + const val version = "1.8.10" + } + } + `, + 'buildSrc/src/main/kotlin/Versions.kt': codeBlock` + object Versions { + const val jackson = "2.9.10" + const val rxjava: String = "1.2.3" + } + `, + }; + mockFs(fsMock); + + const res = await extractAllPackageFiles( + partial(), + Object.keys(fsMock) + ); + + expect(res).toMatchObject([ + { + packageFile: 'buildSrc/src/main/kotlin/Deps.kt', + deps: [ + { + depName: 'javax.cache:cache-api', + currentValue: '1.1.0', + groupName: 'Libraries.jCache', + }, + { + depName: 'com.android.tools.build:gradle', + currentValue: '4.1.2', + groupName: 'Libraries.Android.Tools.version', + }, + { + depName: 'androidx.test:core', + currentValue: '1.3.0-rc01', + groupName: 'Libraries.Test.version', + }, + { + depName: 'androidx.test.espresso:espresso-core', + currentValue: '3.3.0-rc01', + groupName: 'Libraries.Test.Espresso.version', + }, + { + depName: 'androidx.test:core-ktx', + currentValue: '1.3.0-rc01', + groupName: 'Libraries.Test.version', + }, + ], + }, + { + packageFile: 'buildSrc/src/main/kotlin/GradleDeps.kt', + deps: [ + { + depName: 'org.jetbrains.kotlin:kotlin-stdlib', + currentValue: '1.8.10', + groupName: 'GradleDeps.Kotlin.version', + }, + ], + }, + { + packageFile: 'buildSrc/src/main/kotlin/Versions.kt', + deps: [ + { + depName: 'com.fasterxml.jackson.core:jackson-annotations', + currentValue: '2.9.10', + groupName: 'Versions.jackson', + }, + { + depName: 'io.reactivex.rxjava2:rxjava', + currentValue: '1.2.3', + groupName: 'Versions.rxjava', + }, + ], + }, + ]); + }); + it('inherits gradle variables', async () => { const fsMock = { 'gradle.properties': 'foo=1.0.0', diff --git a/lib/modules/manager/gradle/extract.ts b/lib/modules/manager/gradle/extract.ts index e0a76398b747fa..a75d6a9098b430 100644 --- a/lib/modules/manager/gradle/extract.ts +++ b/lib/modules/manager/gradle/extract.ts @@ -9,7 +9,7 @@ import { parseGcv, usesGcv, } from './extract/consistent-versions-plugin'; -import { parseGradle, parseProps } from './parser'; +import { parseGradle, parseKotlinSource, parseProps } from './parser'; import { REGISTRY_URLS } from './parser/common'; import type { GradleManagerData, @@ -19,6 +19,7 @@ import type { import { getVars, isGradleScriptFile, + isKotlinSourceFile, isPropsFile, isTOMLFile, reorderFiles, @@ -94,6 +95,15 @@ async function parsePackageFiles( ) { const deps = parseGcv(packageFile, fileContents); extractedDeps.push(...deps); + } else if (isKotlinSourceFile(packageFile)) { + const vars = getVars(varRegistry, packageFileDir); + const { vars: gradleVars, deps } = parseKotlinSource( + content, + vars, + packageFile + ); + updateVars(varRegistry, '/', gradleVars); + extractedDeps.push(...deps); } else if (isGradleScriptFile(packageFile)) { const vars = getVars(varRegistry, packageFileDir); const { @@ -123,11 +133,14 @@ export async function extractAllPackageFiles( const packageFilesByName: Record = {}; const packageRegistries: PackageRegistry[] = []; const extractedDeps: PackageDependency[] = []; - const gradleFiles = reorderFiles(packageFiles); + const kotlinSourceFiles = packageFiles.filter(isKotlinSourceFile); + const gradleFiles = reorderFiles( + packageFiles.filter((e) => !kotlinSourceFiles.includes(e)) + ); await parsePackageFiles( config, - gradleFiles, + [...kotlinSourceFiles, ...kotlinSourceFiles, ...gradleFiles], extractedDeps, packageFilesByName, packageRegistries @@ -161,9 +174,10 @@ export async function extractAllPackageFiles( dep.registryUrls = getRegistryUrlsForDep(packageRegistries, dep); if (!dep.depType) { - dep.depType = key.startsWith('buildSrc') - ? 'devDependencies' - : 'dependencies'; + dep.depType = + key.startsWith('buildSrc') && !kotlinSourceFiles.length + ? 'devDependencies' + : 'dependencies'; } } diff --git a/lib/modules/manager/gradle/extract/catalog.ts b/lib/modules/manager/gradle/extract/catalog.ts index 49b21ccceea7bc..16483dcb895d30 100644 --- a/lib/modules/manager/gradle/extract/catalog.ts +++ b/lib/modules/manager/gradle/extract/catalog.ts @@ -55,6 +55,10 @@ function isVersionPointer( return hasKey('ref', obj); } +function normalizeVersionPointer(versionPointer: string): string { + return versionPointer.replace(regEx(/[._]/g), '-'); +} + interface VersionExtract { currentValue?: string; fileReplacePosition?: number; @@ -79,12 +83,13 @@ function extractVersion({ versionSubContent: string; }): VersionExtract { if (isVersionPointer(version)) { + const parsedVersion = normalizeVersionPointer(version.ref); // everything else is ignored return extractLiteralVersion({ - version: versions[version.ref], + version: versions[parsedVersion], depStartIndex: versionStartIndex, depSubContent: versionSubContent, - sectionKey: version.ref, + sectionKey: parsedVersion, }); } else { return extractLiteralVersion({ @@ -205,7 +210,7 @@ function extractDependency({ }; } const versionRef = isVersionPointer(descriptor.version) - ? descriptor.version.ref + ? normalizeVersionPointer(descriptor.version.ref) : null; if (isArtifactDescriptor(descriptor)) { const { group, name } = descriptor; @@ -284,7 +289,7 @@ export function parseCatalog( dependency.skipReason = skipReason; } if (isVersionPointer(version) && dependency.commitMessageTopic) { - dependency.groupName = version.ref; + dependency.groupName = normalizeVersionPointer(version.ref); delete dependency.commitMessageTopic; } diff --git a/lib/modules/manager/gradle/index.ts b/lib/modules/manager/gradle/index.ts index ad83ff7f60ba5d..82419fa6f81868 100644 --- a/lib/modules/manager/gradle/index.ts +++ b/lib/modules/manager/gradle/index.ts @@ -14,6 +14,7 @@ export const defaultConfig = { '\\.gradle(\\.kts)?$', '(^|/)gradle\\.properties$', '(^|/)gradle/.+\\.toml$', + '(^|/)buildSrc/.+\\.kt$', '\\.versions\\.toml$', // The two below is for gradle-consistent-versions plugin `(^|/)versions.props$`, diff --git a/lib/modules/manager/gradle/parser.spec.ts b/lib/modules/manager/gradle/parser.spec.ts index 2462903b8fe3d5..fe948df16dd442 100644 --- a/lib/modules/manager/gradle/parser.spec.ts +++ b/lib/modules/manager/gradle/parser.spec.ts @@ -2,7 +2,7 @@ import is from '@sindresorhus/is'; import { codeBlock } from 'common-tags'; import { Fixtures } from '../../../../test/fixtures'; import { fs, logger } from '../../../../test/util'; -import { parseGradle, parseProps } from './parser'; +import { parseGradle, parseKotlinSource, parseProps } from './parser'; import { GRADLE_PLUGINS, REGISTRY_URLS } from './parser/common'; jest.mock('../../../util/fs'); @@ -363,6 +363,7 @@ describe('modules/manager/gradle/parser', () => { ${'baz = "1.2.3"'} | ${'"foo:bar:${ext.baz}"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', groupName: 'baz' }} ${'baz = "1.2.3"'} | ${'"foo:bar:${project.ext[\'baz\']}"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', groupName: 'baz' }} ${'a = "foo"; b = "bar"; c="1.2.3"'} | ${'"${a}:${b}:${property("c")}"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', groupName: 'c' }} + ${'a = "foo"; b = "bar"; c="1.2.3"'} | ${'"${a}:${b}:${properties["c"]}"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', groupName: 'c' }} `('$def | $str', ({ def, str, output }) => { const { deps } = parseGradle([def, str].join('\n')); expect(deps).toMatchObject([output].filter(is.truthy)); @@ -929,4 +930,107 @@ describe('modules/manager/gradle/parser', () => { expect(deps).toMatchObject([output].filter(is.truthy)); }); }); + + describe('Kotlin object notation', () => { + it('simple objects', () => { + const input = codeBlock` + object Versions { + const val baz = "1.2.3" + } + + object Libraries { + val deps = mapOf("api" to "org.slf4j:slf4j-api:\${Versions.baz}") + val dep: String = "foo:bar:" + Versions.baz + } + `; + + const res = parseKotlinSource(input); + expect(res).toMatchObject({ + vars: { + 'Versions.baz': { + key: 'Versions.baz', + value: '1.2.3', + }, + }, + deps: [ + { + depName: 'org.slf4j:slf4j-api', + groupName: 'Versions.baz', + currentValue: '1.2.3', + }, + { + depName: 'foo:bar', + groupName: 'Versions.baz', + currentValue: '1.2.3', + }, + ], + }); + }); + + it('nested objects', () => { + const input = codeBlock` + object Deps { + const val kotlinVersion = "1.5.31" + + object Kotlin { + val stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:\${Deps.kotlinVersion}" + } + + object Test { + private const val version = "1.3.0-rc01" + const val core = "androidx.test:core:\${Deps.Test.version}" + + object Espresso { + private const val version = "3.3.0-rc01" + const val espressoCore = "androidx.test.espresso:espresso-core:$version" + } + + object Androidx { + const val coreKtx = "androidx.test:core-ktx:$version" + } + } + } + `; + + const res = parseKotlinSource(input); + expect(res).toMatchObject({ + vars: { + 'Deps.kotlinVersion': { + key: 'Deps.kotlinVersion', + value: '1.5.31', + }, + 'Deps.Test.version': { + key: 'Deps.Test.version', + value: '1.3.0-rc01', + }, + 'Deps.Test.Espresso.version': { + key: 'Deps.Test.Espresso.version', + value: '3.3.0-rc01', + }, + }, + deps: [ + { + depName: 'org.jetbrains.kotlin:kotlin-stdlib-jdk7', + currentValue: '1.5.31', + groupName: 'Deps.kotlinVersion', + }, + { + depName: 'androidx.test:core', + currentValue: '1.3.0-rc01', + groupName: 'Deps.Test.version', + }, + { + depName: 'androidx.test.espresso:espresso-core', + currentValue: '3.3.0-rc01', + groupName: 'Deps.Test.Espresso.version', + }, + { + depName: 'androidx.test:core-ktx', + currentValue: '1.3.0-rc01', + groupName: 'Deps.Test.version', + }, + ], + }); + }); + }); }); diff --git a/lib/modules/manager/gradle/parser.ts b/lib/modules/manager/gradle/parser.ts index 1d4d3914b989eb..a3824ed8a2d0e7 100644 --- a/lib/modules/manager/gradle/parser.ts +++ b/lib/modules/manager/gradle/parser.ts @@ -5,6 +5,7 @@ import { qApplyFrom } from './parser/apply-from'; import { qAssignments } from './parser/assignments'; import { qDependencies, qLongFormDep } from './parser/dependencies'; import { setParseGradleFunc } from './parser/handlers'; +import { qKotlinMultiObjectVarAssignment } from './parser/objects'; import { qPlugins } from './parser/plugins'; import { qRegistryUrls } from './parser/registry-urls'; import { qVersionCatalogs } from './parser/version-catalogs'; @@ -77,6 +78,34 @@ export function parseGradle( return { deps, urls, vars }; } +export function parseKotlinSource( + input: string, + initVars: PackageVariables = {}, + packageFile = '' +): { vars: PackageVariables; deps: PackageDependency[] } { + let vars: PackageVariables = { ...initVars }; + const deps: PackageDependency[] = []; + + const query = q.tree({ + type: 'root-tree', + maxDepth: 1, + search: qKotlinMultiObjectVarAssignment, + }); + + const parsedResult = groovy.query(input, query, { + ...ctx, + packageFile, + globalVars: vars, + }); + + if (parsedResult) { + deps.push(...parsedResult.deps); + vars = { ...vars, ...parsedResult.globalVars }; + } + + return { deps, vars }; +} + const propWord = '[a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*'; const propRegex = regEx( `^(?\\s*(?${propWord})\\s*[= :]\\s*['"]?)(?[^\\s'"]+)['"]?\\s*$` diff --git a/lib/modules/manager/gradle/parser/assignments.ts b/lib/modules/manager/gradle/parser/assignments.ts index 91da408fb475c8..424be4895e8c80 100644 --- a/lib/modules/manager/gradle/parser/assignments.ts +++ b/lib/modules/manager/gradle/parser/assignments.ts @@ -138,7 +138,7 @@ const qKotlinMapOfExpr = ( ); // val versions = mapOf("foo1" to "bar1", "foo2" to "bar2", "foo3" to "bar3") -const qKotlinMultiMapOfVarAssignment = qVariableAssignmentIdentifier +export const qKotlinMultiMapOfVarAssignment = qVariableAssignmentIdentifier .op('=') .sym('mapOf') .tree({ diff --git a/lib/modules/manager/gradle/parser/common.spec.ts b/lib/modules/manager/gradle/parser/common.spec.ts index 8c377a1345911c..6915f7339d4b3a 100644 --- a/lib/modules/manager/gradle/parser/common.spec.ts +++ b/lib/modules/manager/gradle/parser/common.spec.ts @@ -94,7 +94,14 @@ describe('modules/manager/gradle/parser/common', () => { }); it('stripReservedPrefixFromKeyTokens', () => { - const tokenValues = ['rootProject', 'project', 'ext', 'extra', 'foo']; + const tokenValues = [ + 'rootProject', + 'project', + 'ext', + 'extra', + 'properties', + 'foo', + ]; ctx.varTokens.push( ...tokenValues.map((value) => partial({ value })) @@ -114,11 +121,23 @@ describe('modules/manager/gradle/parser/common', () => { }); it('findVariable', () => { + ctx.tmpNestingDepth = [token, token]; ctx.globalVars = { foo: { key: 'foo', value: 'bar' }, + 'test.foo': { key: 'test.foo', value: 'bar2' }, + 'test.test.foo3': { key: 'test.test.foo3', value: 'bar3' }, }; expect(findVariable('unknown-global-var', ctx)).toBeUndefined(); + expect(findVariable('foo3', ctx)).toStrictEqual( + ctx.globalVars['test.test.foo3'] + ); + expect(findVariable('test.foo', ctx)).toStrictEqual( + ctx.globalVars['test.foo'] + ); + expect(findVariable('foo', ctx)).toStrictEqual(ctx.globalVars['test.foo']); + + ctx.tmpNestingDepth = []; expect(findVariable('foo', ctx)).toStrictEqual(ctx.globalVars['foo']); }); diff --git a/lib/modules/manager/gradle/parser/common.ts b/lib/modules/manager/gradle/parser/common.ts index e72d582ea0ac8f..6f92589544e793 100644 --- a/lib/modules/manager/gradle/parser/common.ts +++ b/lib/modules/manager/gradle/parser/common.ts @@ -82,7 +82,13 @@ export function cleanupTempVars(ctx: Ctx): Ctx { } export function stripReservedPrefixFromKeyTokens(ctx: Ctx): Ctx { - const unwantedPrefixes = ['ext', 'extra', 'project', 'rootProject']; + const unwantedPrefixes = [ + 'ext', + 'extra', + 'project', + 'rootProject', + 'properties', + ]; while ( ctx.varTokens.length > 1 && // ensures there will be always at least one token ctx.varTokens[0] && @@ -110,6 +116,18 @@ export function findVariable( ctx: Ctx, variables: PackageVariables = ctx.globalVars ): VariableData | undefined { + if (ctx.tmpNestingDepth.length) { + const prefixParts = ctx.tmpNestingDepth.map((token) => token.value); + for (let idx = ctx.tmpNestingDepth.length; idx > 0; idx -= 1) { + const prefix = prefixParts.slice(0, idx).join('.'); + const identifier = `${prefix}.${name}`; + + if (variables[identifier]) { + return variables[identifier]; + } + } + } + return variables[name]; } diff --git a/lib/modules/manager/gradle/parser/objects.ts b/lib/modules/manager/gradle/parser/objects.ts new file mode 100644 index 00000000000000..b1d0f0c862e358 --- /dev/null +++ b/lib/modules/manager/gradle/parser/objects.ts @@ -0,0 +1,54 @@ +import { parser, query as q } from 'good-enough-parser'; +import type { Ctx } from '../types'; +import { qKotlinMultiMapOfVarAssignment } from './assignments'; +import { + cleanupTempVars, + coalesceVariable, + increaseNestingDepth, + prependNestingDepth, + qValueMatcher, + qVariableAssignmentIdentifier, + reduceNestingDepth, + storeInTokenMap, + storeVarToken, +} from './common'; +import { handleAssignment } from './handlers'; + +const qKotlinSingleObjectVarAssignment = q.alt( + // val dep = mapOf("qux" to "foo:bar:\${Versions.baz}") + qKotlinMultiMapOfVarAssignment, + // val dep: String = "foo:bar:" + Versions.baz + qVariableAssignmentIdentifier + .opt(q.op(':').sym('String')) + .op('=') + .handler(prependNestingDepth) + .handler(coalesceVariable) + .handler((ctx) => storeInTokenMap(ctx, 'keyToken')) + .join(qValueMatcher) + .handler((ctx) => storeInTokenMap(ctx, 'valToken')) + .handler(handleAssignment) + .handler(cleanupTempVars) +); + +// object foo { ... } +const qKotlinMultiObjectExpr = ( + search: q.QueryBuilder +): q.QueryBuilder => + q.alt( + q.sym('object').sym(storeVarToken).tree({ + type: 'wrapped-tree', + maxDepth: 1, + startsWith: '{', + endsWith: '}', + preHandler: increaseNestingDepth, + search, + postHandler: reduceNestingDepth, + }), + qKotlinSingleObjectVarAssignment + ); + +export const qKotlinMultiObjectVarAssignment = qKotlinMultiObjectExpr( + qKotlinMultiObjectExpr( + qKotlinMultiObjectExpr(qKotlinSingleObjectVarAssignment) + ) +).handler(cleanupTempVars); diff --git a/lib/modules/manager/gradle/utils.ts b/lib/modules/manager/gradle/utils.ts index bed8720663dc5f..06b8476d8d5572 100644 --- a/lib/modules/manager/gradle/utils.ts +++ b/lib/modules/manager/gradle/utils.ts @@ -113,6 +113,11 @@ export function isPropsFile(path: string): boolean { return filename === 'gradle.properties'; } +export function isKotlinSourceFile(path: string): boolean { + const filename = upath.basename(path).toLowerCase(); + return filename.endsWith('.kt'); +} + export function isTOMLFile(path: string): boolean { const filename = upath.basename(path).toLowerCase(); return filename.endsWith('.toml'); diff --git a/lib/modules/manager/npm/extract/__snapshots__/index.spec.ts.snap b/lib/modules/manager/npm/extract/__snapshots__/index.spec.ts.snap index 668bbac7b8a309..06e92a81a48d4c 100644 --- a/lib/modules/manager/npm/extract/__snapshots__/index.spec.ts.snap +++ b/lib/modules/manager/npm/extract/__snapshots__/index.spec.ts.snap @@ -129,6 +129,7 @@ exports[`modules/manager/npm/extract/index .extractPackageFile() extracts engine "extractedConstraints": { "node": ">= 8.9.2", "npm": "^8.0.0", + "pnpm": "^1.2.0", "vscode": ">=1.49.3", "yarn": "disabled", }, diff --git a/lib/modules/manager/npm/extract/index.spec.ts b/lib/modules/manager/npm/extract/index.spec.ts index 1932f6fd8525ab..6013a242400032 100644 --- a/lib/modules/manager/npm/extract/index.spec.ts +++ b/lib/modules/manager/npm/extract/index.spec.ts @@ -418,6 +418,7 @@ describe('modules/manager/npm/extract/index', () => { extractedConstraints: { node: '>= 8.9.2', npm: '^8.0.0', + pnpm: '^1.2.0', vscode: '>=1.49.3', yarn: 'disabled', }, diff --git a/lib/modules/manager/npm/extract/index.ts b/lib/modules/manager/npm/extract/index.ts index 05803936be9f3e..4af1aefdcaa7cb 100644 --- a/lib/modules/manager/npm/extract/index.ts +++ b/lib/modules/manager/npm/extract/index.ts @@ -233,6 +233,7 @@ export async function extractPackageFile( } else if (depName === 'pnpm') { dep.datasource = NpmDatasource.id; dep.commitMessageTopic = 'pnpm'; + extractedConstraints.pnpm = dep.currentValue; } else if (depName === 'vscode') { dep.datasource = GithubTagsDatasource.id; dep.packageName = 'microsoft/vscode'; diff --git a/lib/modules/manager/nuget/artifacts.spec.ts b/lib/modules/manager/nuget/artifacts.spec.ts index b6ae31d6cc5749..5520b23a89ea87 100644 --- a/lib/modules/manager/nuget/artifacts.spec.ts +++ b/lib/modules/manager/nuget/artifacts.spec.ts @@ -1,6 +1,6 @@ import { join } from 'upath'; import { envMock, mockExecAll } from '../../../../test/exec-util'; -import { env, fs, git, mocked } from '../../../../test/util'; +import { env, fs, git, mocked, scm } from '../../../../test/util'; import { GlobalConfig } from '../../../config/global'; import type { RepoGlobalConfig } from '../../../config/types'; import * as docker from '../../../util/exec/docker'; @@ -40,7 +40,7 @@ describe('modules/manager/nuget/artifacts', () => { getDefaultRegistries.mockReturnValue([]); env.getChildProcessEnv.mockReturnValue(envMock.basic); fs.privateCacheDir.mockImplementation(realFs.privateCacheDir); - git.getFileList.mockResolvedValueOnce([]); + scm.getFileList.mockResolvedValueOnce([]); GlobalConfig.set(adminConfig); docker.resetPrefetchedImages(); }); diff --git a/lib/modules/manager/nuget/package-tree.spec.ts b/lib/modules/manager/nuget/package-tree.spec.ts index a5541829293479..3b9e99e04c860e 100644 --- a/lib/modules/manager/nuget/package-tree.spec.ts +++ b/lib/modules/manager/nuget/package-tree.spec.ts @@ -1,7 +1,7 @@ import { fs as memfs } from 'memfs'; import upath from 'upath'; import { Fixtures } from '../../../../test/fixtures'; -import { git } from '../../../../test/util'; +import { scm } from '../../../../test/util'; import { GlobalConfig } from '../../../config/global'; import type { RepoGlobalConfig } from '../../../config/types'; import { getDependentPackageFiles } from './package-tree'; @@ -27,7 +27,7 @@ describe('modules/manager/nuget/package-tree', () => { }); it('returns self for single project', async () => { - git.getFileList.mockResolvedValue(['single.csproj']); + scm.getFileList.mockResolvedValue(['single.csproj']); Fixtures.mock({ '/tmp/repo/single.csproj': Fixtures.get( 'single-project-file/single.csproj' @@ -40,7 +40,7 @@ describe('modules/manager/nuget/package-tree', () => { }); it('returns self for two projects with no references', async () => { - git.getFileList.mockResolvedValue(['one.csproj', 'two.csproj']); + scm.getFileList.mockResolvedValue(['one.csproj', 'two.csproj']); Fixtures.mock({ '/tmp/repo/one.csproj': Fixtures.get('two-no-reference/one.csproj'), '/tmp/repo/two.csproj': Fixtures.get('two-no-reference/two.csproj'), @@ -55,7 +55,7 @@ describe('modules/manager/nuget/package-tree', () => { }); it('returns projects for two projects with one reference', async () => { - git.getFileList.mockResolvedValue(['one/one.csproj', 'two/two.csproj']); + scm.getFileList.mockResolvedValue(['one/one.csproj', 'two/two.csproj']); Fixtures.mock({ '/tmp/repo/one/one.csproj': Fixtures.get( 'two-one-reference/one/one.csproj' @@ -72,7 +72,7 @@ describe('modules/manager/nuget/package-tree', () => { }); it('returns project for two projects with one reference and central versions', async () => { - git.getFileList.mockResolvedValue(['one/one.csproj', 'two/two.csproj']); + scm.getFileList.mockResolvedValue(['one/one.csproj', 'two/two.csproj']); Fixtures.mock({ '/tmp/repo/one/one.csproj': Fixtures.get( 'two-one-reference-with-central-versions/one/one.csproj' @@ -94,7 +94,7 @@ describe('modules/manager/nuget/package-tree', () => { }); it('returns projects for three projects with two linear references', async () => { - git.getFileList.mockResolvedValue([ + scm.getFileList.mockResolvedValue([ 'one/one.csproj', 'two/two.csproj', 'three/three.csproj', @@ -128,7 +128,7 @@ describe('modules/manager/nuget/package-tree', () => { }); it('returns projects for three projects with two tree-like references', async () => { - git.getFileList.mockResolvedValue([ + scm.getFileList.mockResolvedValue([ 'one/one.csproj', 'two/two.csproj', 'three/three.csproj', @@ -160,7 +160,7 @@ describe('modules/manager/nuget/package-tree', () => { }); it('throws error on circular reference', async () => { - git.getFileList.mockResolvedValue(['one/one.csproj', 'two/two.csproj']); + scm.getFileList.mockResolvedValue(['one/one.csproj', 'two/two.csproj']); Fixtures.mock({ '/tmp/repo/one/one.csproj': Fixtures.get( 'circular-reference/one/one.csproj' @@ -176,7 +176,7 @@ describe('modules/manager/nuget/package-tree', () => { }); it('skips on invalid xml file', async () => { - git.getFileList.mockResolvedValue(['foo/bar.csproj']); + scm.getFileList.mockResolvedValue(['foo/bar.csproj']); Fixtures.mock({ '/tmp/repo/foo/bar.csproj': ' { - const allFiles = await getFileList(); + const allFiles = await scm.getFileList(); const filteredPackageFiles = allFiles.filter( minimatch.filter('*.{cs,vb,fs}proj', { matchBase: true, nocase: true }) ); diff --git a/lib/modules/manager/pep621/__fixtures__/pyproject_pdm_sources.toml b/lib/modules/manager/pep621/__fixtures__/pyproject_pdm_sources.toml new file mode 100644 index 00000000000000..a09c836f392278 --- /dev/null +++ b/lib/modules/manager/pep621/__fixtures__/pyproject_pdm_sources.toml @@ -0,0 +1,33 @@ +[project] +name = "pdm" +dynamic = ["version"] +requires-python = ">=3.7" +license = {text = "MIT"} +dependencies = [ + "blinker", + "packaging>=20.9,!=22.0", +] +readme = "README.md" + +[project.optional-dependencies] +pytest = [ + "pytest>12", +] + +[tool.pdm.dev-dependencies] +test = [ + "pytest-rerunfailures>=10.2", +] +tox = [ + "tox-pdm>=0.5", +] + +[[tool.pdm.source]] +url = "https://private-site.org/pypi/simple" +verify_ssl = true +name = "internal" + +[[tool.pdm.source]] +url = "https://private.pypi.org/simple" +verify_ssl = true +name = "pypi" diff --git a/lib/modules/manager/pep621/__fixtures__/pyproject_with_pdm.toml b/lib/modules/manager/pep621/__fixtures__/pyproject_with_pdm.toml new file mode 100644 index 00000000000000..915cf6063ca561 --- /dev/null +++ b/lib/modules/manager/pep621/__fixtures__/pyproject_with_pdm.toml @@ -0,0 +1,37 @@ +[project] +name = "pdm" +dynamic = ["version"] +requires-python = ">=3.7" +license = {text = "MIT"} +dependencies = [ + "blinker", + "packaging>=20.9,!=22.0", + "rich>=12.3.0", + "virtualenv==20.0.0", + "pyproject-hooks", + "unearth>=0.9.0", + "tomlkit>=0.11.1,<1", + "installer<0.8,>=0.7", + "cachecontrol[filecache]>=0.12.11", + "tomli>=1.1.0; python_version < \"3.11\"", + "typing-extensions; python_version < \"3.8\"", + "importlib-metadata>=3.6; python_version < \"3.10\"", +] +readme = "README.md" + +[project.optional-dependencies] +pytest = [ + "pytest>12", + "pytest-mock", +] + +[tool.pdm.dev-dependencies] +test = [ + "pdm[pytest]", + "pytest-rerunfailures>=10.2", +] +tox = [ + "tox", + "tox-pdm>=0.5", + "", # fail to parse +] diff --git a/lib/modules/manager/pep621/extract.spec.ts b/lib/modules/manager/pep621/extract.spec.ts new file mode 100644 index 00000000000000..75e1ad6e2745bc --- /dev/null +++ b/lib/modules/manager/pep621/extract.spec.ts @@ -0,0 +1,267 @@ +import { codeBlock } from 'common-tags'; +import { Fixtures } from '../../../../test/fixtures'; +import { extractPackageFile } from './extract'; + +const pdmPyProject = Fixtures.get('pyproject_with_pdm.toml'); +const pdmSourcesPyProject = Fixtures.get('pyproject_pdm_sources.toml'); + +describe('modules/manager/pep621/extract', () => { + describe('extractPackageFile()', () => { + it('should return null for empty content', function () { + const result = extractPackageFile('', 'pyproject.toml'); + expect(result).toBeNull(); + }); + + it('should return null for invalid toml', function () { + const result = extractPackageFile( + codeBlock` + [project] + name = + `, + 'pyproject.toml' + ); + expect(result).toBeNull(); + }); + + it('should return dependencies for valid content', function () { + const result = extractPackageFile(pdmPyProject, 'pyproject.toml'); + + const dependencies = result?.deps.filter( + (dep) => dep.depType === 'project.dependencies' + ); + expect(dependencies).toEqual([ + { + packageName: 'blinker', + depName: 'blinker', + datasource: 'pypi', + depType: 'project.dependencies', + skipReason: 'any-version', + }, + { + packageName: 'packaging', + depName: 'packaging', + datasource: 'pypi', + depType: 'project.dependencies', + currentValue: '>=20.9,!=22.0', + }, + { + packageName: 'rich', + depName: 'rich', + datasource: 'pypi', + depType: 'project.dependencies', + currentValue: '>=12.3.0', + }, + { + packageName: 'virtualenv', + depName: 'virtualenv', + datasource: 'pypi', + depType: 'project.dependencies', + currentValue: '==20.0.0', + }, + { + packageName: 'pyproject-hooks', + depName: 'pyproject-hooks', + datasource: 'pypi', + depType: 'project.dependencies', + skipReason: 'any-version', + }, + { + packageName: 'unearth', + depName: 'unearth', + datasource: 'pypi', + depType: 'project.dependencies', + currentValue: '>=0.9.0', + }, + { + packageName: 'tomlkit', + depName: 'tomlkit', + datasource: 'pypi', + depType: 'project.dependencies', + currentValue: '>=0.11.1,<1', + }, + { + packageName: 'installer', + depName: 'installer', + datasource: 'pypi', + depType: 'project.dependencies', + currentValue: '<0.8,>=0.7', + }, + { + packageName: 'cachecontrol', + depName: 'cachecontrol', + datasource: 'pypi', + depType: 'project.dependencies', + currentValue: '>=0.12.11', + }, + { + packageName: 'tomli', + depName: 'tomli', + datasource: 'pypi', + depType: 'project.dependencies', + currentValue: '>=1.1.0', + }, + { + packageName: 'typing-extensions', + depName: 'typing-extensions', + datasource: 'pypi', + depType: 'project.dependencies', + skipReason: 'any-version', + }, + { + packageName: 'importlib-metadata', + depName: 'importlib-metadata', + datasource: 'pypi', + depType: 'project.dependencies', + currentValue: '>=3.6', + }, + ]); + + const optionalDependencies = result?.deps.filter( + (dep) => dep.depType === 'project.optional-dependencies' + ); + expect(optionalDependencies).toEqual([ + { + packageName: 'pytest', + datasource: 'pypi', + depType: 'project.optional-dependencies', + currentValue: '>12', + depName: 'pytest/pytest', + }, + { + packageName: 'pytest-mock', + datasource: 'pypi', + depType: 'project.optional-dependencies', + skipReason: 'any-version', + depName: 'pytest/pytest-mock', + }, + ]); + + const pdmDevDependencies = result?.deps.filter( + (dep) => dep.depType === 'tool.pdm.dev-dependencies' + ); + expect(pdmDevDependencies).toEqual([ + { + packageName: 'pdm', + datasource: 'pypi', + depType: 'tool.pdm.dev-dependencies', + skipReason: 'any-version', + depName: 'test/pdm', + }, + { + packageName: 'pytest-rerunfailures', + datasource: 'pypi', + depType: 'tool.pdm.dev-dependencies', + currentValue: '>=10.2', + depName: 'test/pytest-rerunfailures', + }, + { + packageName: 'tox', + datasource: 'pypi', + depType: 'tool.pdm.dev-dependencies', + skipReason: 'any-version', + depName: 'tox/tox', + }, + { + packageName: 'tox-pdm', + datasource: 'pypi', + depType: 'tool.pdm.dev-dependencies', + currentValue: '>=0.5', + depName: 'tox/tox-pdm', + }, + ]); + }); + + it('should return dependencies with overwritten pypi registryUrl', function () { + const result = extractPackageFile(pdmSourcesPyProject, 'pyproject.toml'); + + expect(result?.deps).toEqual([ + { + packageName: 'blinker', + depName: 'blinker', + datasource: 'pypi', + depType: 'project.dependencies', + skipReason: 'any-version', + registryUrls: [ + 'https://private-site.org/pypi/simple', + 'https://private.pypi.org/simple', + ], + }, + { + packageName: 'packaging', + depName: 'packaging', + datasource: 'pypi', + depType: 'project.dependencies', + currentValue: '>=20.9,!=22.0', + registryUrls: [ + 'https://private-site.org/pypi/simple', + 'https://private.pypi.org/simple', + ], + }, + { + packageName: 'pytest', + datasource: 'pypi', + depType: 'project.optional-dependencies', + currentValue: '>12', + depName: 'pytest/pytest', + registryUrls: [ + 'https://private-site.org/pypi/simple', + 'https://private.pypi.org/simple', + ], + }, + { + packageName: 'pytest-rerunfailures', + datasource: 'pypi', + depType: 'tool.pdm.dev-dependencies', + currentValue: '>=10.2', + depName: 'test/pytest-rerunfailures', + registryUrls: [ + 'https://private-site.org/pypi/simple', + 'https://private.pypi.org/simple', + ], + }, + { + packageName: 'tox-pdm', + datasource: 'pypi', + depType: 'tool.pdm.dev-dependencies', + currentValue: '>=0.5', + depName: 'tox/tox-pdm', + registryUrls: [ + 'https://private-site.org/pypi/simple', + 'https://private.pypi.org/simple', + ], + }, + ]); + }); + + it('should return dependencies with original pypi registryUrl', function () { + const result = extractPackageFile( + codeBlock` + [project] + dependencies = [ + "packaging>=20.9,!=22.0", + ] + + [[tool.pdm.source]] + url = "https://private-site.org/pypi/simple" + verify_ssl = true + name = "internal" + `, + 'pyproject.toml' + ); + + expect(result?.deps).toEqual([ + { + packageName: 'packaging', + depName: 'packaging', + datasource: 'pypi', + depType: 'project.dependencies', + currentValue: '>=20.9,!=22.0', + registryUrls: [ + 'https://pypi.org/pypi/', + 'https://private-site.org/pypi/simple', + ], + }, + ]); + }); + }); +}); diff --git a/lib/modules/manager/pep621/extract.ts b/lib/modules/manager/pep621/extract.ts new file mode 100644 index 00000000000000..09064595188470 --- /dev/null +++ b/lib/modules/manager/pep621/extract.ts @@ -0,0 +1,51 @@ +import toml from '@iarna/toml'; +import { logger } from '../../../logger'; +import type { + ExtractConfig, + PackageDependency, + PackageFileContent, +} from '../types'; +import { processors } from './processors'; +import { PyProject, PyProjectSchema } from './schema'; +import { parseDependencyGroupRecord, parseDependencyList } from './utils'; + +export function extractPackageFile( + content: string, + fileName: string, + config?: ExtractConfig +): PackageFileContent | null { + logger.trace({ fileName }, 'pep621.extractPackageFile'); + + const deps: PackageDependency[] = []; + + let def: PyProject; + try { + const jsonMap = toml.parse(content); + def = PyProjectSchema.parse(jsonMap); + } catch (err) { + logger.warn( + { fileName, err }, + `Failed to parse and validate pyproject file` + ); + return null; + } + + // pyProject standard definitions + deps.push( + ...parseDependencyList('project.dependencies', def.project?.dependencies) + ); + deps.push( + ...parseDependencyGroupRecord( + 'project.optional-dependencies', + def.project?.['optional-dependencies'] + ) + ); + + // process specific tool sets + let processedDeps = deps; + for (const processor of processors) { + processedDeps = processor.process(def, processedDeps); + } + + return processedDeps.length ? { deps: processedDeps } : null; +} diff --git a/lib/modules/manager/pep621/index.ts b/lib/modules/manager/pep621/index.ts new file mode 100644 index 00000000000000..9b1adb54893e40 --- /dev/null +++ b/lib/modules/manager/pep621/index.ts @@ -0,0 +1,8 @@ +import { PypiDatasource } from '../../datasource/pypi'; +export { extractPackageFile } from './extract'; + +export const supportedDatasources = [PypiDatasource.id]; + +export const defaultConfig = { + fileMatch: ['(^|/)pyproject\\.toml$'], +}; diff --git a/lib/modules/manager/pep621/processors/index.ts b/lib/modules/manager/pep621/processors/index.ts new file mode 100644 index 00000000000000..ba80de12ccbe20 --- /dev/null +++ b/lib/modules/manager/pep621/processors/index.ts @@ -0,0 +1,3 @@ +import { PdmProcessor } from './pdm'; + +export const processors = [new PdmProcessor()]; diff --git a/lib/modules/manager/pep621/processors/pdm.ts b/lib/modules/manager/pep621/processors/pdm.ts new file mode 100644 index 00000000000000..e362694b55a745 --- /dev/null +++ b/lib/modules/manager/pep621/processors/pdm.ts @@ -0,0 +1,42 @@ +import is from '@sindresorhus/is'; +import { PypiDatasource } from '../../../datasource/pypi'; +import type { PackageDependency } from '../../types'; +import type { PyProject } from '../schema'; +import { parseDependencyGroupRecord } from '../utils'; +import type { PyProjectProcessor } from './types'; + +export class PdmProcessor implements PyProjectProcessor { + process(project: PyProject, deps: PackageDependency[]): PackageDependency[] { + const pdm = project.tool?.pdm; + if (is.nullOrUndefined(pdm)) { + return deps; + } + + deps.push( + ...parseDependencyGroupRecord( + 'tool.pdm.dev-dependencies', + pdm['dev-dependencies'] + ) + ); + + const pdmSource = pdm.source; + if (is.nullOrUndefined(pdmSource)) { + return deps; + } + + // add pypi default url, if there is no source declared with the name `pypi`. https://daobook.github.io/pdm/pyproject/tool-pdm/#specify-other-sources-for-finding-packages + const containsPyPiUrl = pdmSource.some((value) => value.name === 'pypi'); + const registryUrls: string[] = []; + if (!containsPyPiUrl) { + registryUrls.push(PypiDatasource.defaultURL); + } + for (const source of pdmSource) { + registryUrls.push(source.url); + } + for (const dep of deps) { + dep.registryUrls = registryUrls; + } + + return deps; + } +} diff --git a/lib/modules/manager/pep621/processors/types.ts b/lib/modules/manager/pep621/processors/types.ts new file mode 100644 index 00000000000000..dbe645396e76c2 --- /dev/null +++ b/lib/modules/manager/pep621/processors/types.ts @@ -0,0 +1,12 @@ +import type { PackageDependency } from '../../types'; +import type { PyProject } from '../schema'; + +export interface PyProjectProcessor { + /** + * Extracts additional dependencies and/or modifies existing ones based on the tool configuration. + * If no relevant section for the processor exists, then it should return the received dependencies unmodified. + * @param project PyProject object + * @param deps List of already extracted/processed dependencies + */ + process(project: PyProject, deps: PackageDependency[]): PackageDependency[]; +} diff --git a/lib/modules/manager/pep621/readme.md b/lib/modules/manager/pep621/readme.md new file mode 100644 index 00000000000000..96fcde1007a12a --- /dev/null +++ b/lib/modules/manager/pep621/readme.md @@ -0,0 +1,11 @@ +This manager supports updating dependencies inside `pyproject.toml` files. + +In addition to standard dependencies, these toolsets are also supported: + +- `pdm` + +Available `depType`s: + +- `project.dependencies` +- `project.optional-dependencies` +- `tool.pdm.dev-dependencies` diff --git a/lib/modules/manager/pep621/schema.ts b/lib/modules/manager/pep621/schema.ts new file mode 100644 index 00000000000000..04dcc6609ce98c --- /dev/null +++ b/lib/modules/manager/pep621/schema.ts @@ -0,0 +1,35 @@ +import { z } from 'zod'; + +export type PyProject = z.infer; + +const DependencyListSchema = z.array(z.string()).optional(); +const DependencyRecordSchema = z + .record(z.string(), z.array(z.string())) + .optional(); + +export const PyProjectSchema = z.object({ + project: z + .object({ + dependencies: DependencyListSchema, + 'optional-dependencies': DependencyRecordSchema, + }) + .optional(), + tool: z + .object({ + pdm: z + .object({ + 'dev-dependencies': DependencyRecordSchema, + source: z + .array( + z.object({ + url: z.string(), + name: z.string(), + verify_ssl: z.boolean().optional(), + }) + ) + .optional(), + }) + .optional(), + }) + .optional(), +}); diff --git a/lib/modules/manager/pep621/types.ts b/lib/modules/manager/pep621/types.ts new file mode 100644 index 00000000000000..1e68aa19818a84 --- /dev/null +++ b/lib/modules/manager/pep621/types.ts @@ -0,0 +1,6 @@ +export interface Pep508ParseResult { + packageName: string; + currentValue?: string; + extras?: string[]; + marker?: string; +} diff --git a/lib/modules/manager/pep621/utils.spec.ts b/lib/modules/manager/pep621/utils.spec.ts new file mode 100644 index 00000000000000..6029a7b8ec8a51 --- /dev/null +++ b/lib/modules/manager/pep621/utils.spec.ts @@ -0,0 +1,39 @@ +import is from '@sindresorhus/is'; +import { parsePEP508 } from './utils'; + +describe('modules/manager/pep621/utils', () => { + describe('parsePEP508()', () => { + it.each` + value | success | packageName | currentValue | extras | marker + ${''} | ${false} | ${undefined} | ${undefined} | ${undefined} | ${undefined} + ${undefined} | ${false} | ${undefined} | ${undefined} | ${undefined} | ${undefined} + ${null} | ${false} | ${undefined} | ${undefined} | ${undefined} | ${undefined} + ${'blinker'} | ${true} | ${'blinker'} | ${undefined} | ${undefined} | ${undefined} + ${'packaging==20.0.0'} | ${true} | ${'packaging'} | ${'==20.0.0'} | ${undefined} | ${undefined} + ${'packaging>=20.9,!=22.0'} | ${true} | ${'packaging'} | ${'>=20.9,!=22.0'} | ${undefined} | ${undefined} + ${'cachecontrol[filecache]>=0.12.11'} | ${true} | ${'cachecontrol'} | ${'>=0.12.11'} | ${['filecache']} | ${undefined} + ${'tomli>=1.1.0; python_version < "3.11"'} | ${true} | ${'tomli'} | ${'>=1.1.0'} | ${undefined} | ${'python_version < "3.11"'} + ${'typing-extensions; python_version < "3.8"'} | ${true} | ${'typing-extensions'} | ${undefined} | ${undefined} | ${'python_version < "3.8"'} + ${'typing-extensions[test-feature]; python_version < "3.8"'} | ${true} | ${'typing-extensions'} | ${undefined} | ${['test-feature']} | ${'python_version < "3.8"'} + `( + '(parse $value"', + ({ value, success, packageName, currentValue, extras, marker }) => { + const result = parsePEP508(value); + + const expected = is.truthy(success) + ? clear({ packageName, currentValue, extras, marker }) + : null; + expect(result).toEqual(expected); + } + ); + }); +}); + +function clear(a: any) { + Object.keys(a).forEach((key) => { + if (a[key] === undefined) { + delete a[key]; + } + }); + return a; +} diff --git a/lib/modules/manager/pep621/utils.ts b/lib/modules/manager/pep621/utils.ts new file mode 100644 index 00000000000000..8741c28dd46a21 --- /dev/null +++ b/lib/modules/manager/pep621/utils.ts @@ -0,0 +1,101 @@ +import is from '@sindresorhus/is'; +import { logger } from '../../../logger'; +import { regEx } from '../../../util/regex'; +import { PypiDatasource } from '../../datasource/pypi'; +import type { PackageDependency } from '../types'; +import type { Pep508ParseResult } from './types'; + +const pep508Regex = regEx( + /^(?[A-Z0-9._-]+)\s*(\[(?[A-Z0-9,._-]+)\])?\s*(?[^;]+)?(;\s*(?.*))?/i +); + +export function parsePEP508( + value: string | null | undefined +): Pep508ParseResult | null { + if (is.nullOrUndefined(value)) { + return null; + } + + const regExpExec = pep508Regex.exec(value); + if ( + is.nullOrUndefined(regExpExec) || + is.nullOrUndefined(regExpExec?.groups) + ) { + logger.trace(`Pep508 could not be extracted`); + return null; + } + + const result: Pep508ParseResult = { + packageName: regExpExec.groups.packageName, + }; + if (is.nonEmptyString(regExpExec.groups.currentValue)) { + result.currentValue = regExpExec.groups.currentValue; + } + if (is.nonEmptyString(regExpExec.groups.marker)) { + result.marker = regExpExec.groups.marker; + } + if (is.nonEmptyString(regExpExec.groups.extras)) { + result.extras = regExpExec.groups.extras.split(','); + } + + return result; +} + +export function pep508ToPackageDependency( + depType: string, + value: string +): PackageDependency | null { + const parsed = parsePEP508(value); + if (is.nullOrUndefined(parsed)) { + return null; + } + + const dep: PackageDependency = { + packageName: parsed.packageName, + depName: parsed.packageName, + datasource: PypiDatasource.id, + depType, + }; + + if (is.nullOrUndefined(parsed.currentValue)) { + dep.skipReason = 'any-version'; + } else { + dep.currentValue = parsed.currentValue; + } + return dep; +} + +export function parseDependencyGroupRecord( + depType: string, + records: Record | null | undefined +): PackageDependency[] { + if (is.nullOrUndefined(records)) { + return []; + } + + const deps: PackageDependency[] = []; + for (const [groupName, pep508Strings] of Object.entries(records)) { + for (const dep of parseDependencyList(depType, pep508Strings)) { + deps.push({ ...dep, depName: `${groupName}/${dep.packageName!}` }); + } + } + return deps; +} + +export function parseDependencyList( + depType: string, + list: string[] | null | undefined +): PackageDependency[] { + if (is.nullOrUndefined(list)) { + return []; + } + + const deps: PackageDependency[] = []; + for (const element of list) { + const dep = pep508ToPackageDependency(depType, element); + if (is.truthy(dep)) { + deps.push(dep); + } + } + return deps; +} diff --git a/lib/modules/manager/poetry/__fixtures__/poetry12.lock b/lib/modules/manager/poetry/__fixtures__/poetry12.lock new file mode 100644 index 00000000000000..0bd5addc4f251e --- /dev/null +++ b/lib/modules/manager/poetry/__fixtures__/poetry12.lock @@ -0,0 +1,482 @@ +[[package]] +name = "attrs" +version = "22.2.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=3.6" + +[metadata] +lock-version = "1.1" +python-versions = ">=3.8,<4.0" +content-hash = "8387ae7c79d1e9c796e33b5c4506682418457e8b0b55908ce811bb876dcb40fd" + +[metadata.files] +attrs = [ + {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, + {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, +] +certifi = [ + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, +] +cfgv = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] +chardet = [ + {file = "chardet-5.1.0-py3-none-any.whl", hash = "sha256:362777fb014af596ad31334fde1e8c327dfdb076e1960d1694662d46a6917ab9"}, + {file = "chardet-5.1.0.tar.gz", hash = "sha256:0d62712b956bc154f85fb0a266e2a3c5913c2967e00348701b32411d6def31e5"}, +] +charset-normalizer = [ + {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, + {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, +] +click = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] +colorama = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] +coverage = [ + {file = "coverage-7.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c90e73bdecb7b0d1cea65a08cb41e9d672ac6d7995603d6465ed4914b98b9ad7"}, + {file = "coverage-7.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e2926b8abedf750c2ecf5035c07515770944acf02e1c46ab08f6348d24c5f94d"}, + {file = "coverage-7.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57b77b9099f172804e695a40ebaa374f79e4fb8b92f3e167f66facbf92e8e7f5"}, + {file = "coverage-7.2.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:efe1c0adad110bf0ad7fb59f833880e489a61e39d699d37249bdf42f80590169"}, + {file = "coverage-7.2.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2199988e0bc8325d941b209f4fd1c6fa007024b1442c5576f1a32ca2e48941e6"}, + {file = "coverage-7.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:81f63e0fb74effd5be736cfe07d710307cc0a3ccb8f4741f7f053c057615a137"}, + {file = "coverage-7.2.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:186e0fc9cf497365036d51d4d2ab76113fb74f729bd25da0975daab2e107fd90"}, + {file = "coverage-7.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:420f94a35e3e00a2b43ad5740f935358e24478354ce41c99407cddd283be00d2"}, + {file = "coverage-7.2.2-cp310-cp310-win32.whl", hash = "sha256:38004671848b5745bb05d4d621526fca30cee164db42a1f185615f39dc997292"}, + {file = "coverage-7.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:0ce383d5f56d0729d2dd40e53fe3afeb8f2237244b0975e1427bfb2cf0d32bab"}, + {file = "coverage-7.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3eb55b7b26389dd4f8ae911ba9bc8c027411163839dea4c8b8be54c4ee9ae10b"}, + {file = "coverage-7.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d2b96123a453a2d7f3995ddb9f28d01fd112319a7a4d5ca99796a7ff43f02af5"}, + {file = "coverage-7.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:299bc75cb2a41e6741b5e470b8c9fb78d931edbd0cd009c58e5c84de57c06731"}, + {file = "coverage-7.2.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e1df45c23d4230e3d56d04414f9057eba501f78db60d4eeecfcb940501b08fd"}, + {file = "coverage-7.2.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:006ed5582e9cbc8115d2e22d6d2144a0725db542f654d9d4fda86793832f873d"}, + {file = "coverage-7.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d683d230b5774816e7d784d7ed8444f2a40e7a450e5720d58af593cb0b94a212"}, + {file = "coverage-7.2.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8efb48fa743d1c1a65ee8787b5b552681610f06c40a40b7ef94a5b517d885c54"}, + {file = "coverage-7.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4c752d5264053a7cf2fe81c9e14f8a4fb261370a7bb344c2a011836a96fb3f57"}, + {file = "coverage-7.2.2-cp311-cp311-win32.whl", hash = "sha256:55272f33da9a5d7cccd3774aeca7a01e500a614eaea2a77091e9be000ecd401d"}, + {file = "coverage-7.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:92ebc1619650409da324d001b3a36f14f63644c7f0a588e331f3b0f67491f512"}, + {file = "coverage-7.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5afdad4cc4cc199fdf3e18088812edcf8f4c5a3c8e6cb69127513ad4cb7471a9"}, + {file = "coverage-7.2.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0484d9dd1e6f481b24070c87561c8d7151bdd8b044c93ac99faafd01f695c78e"}, + {file = "coverage-7.2.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d530191aa9c66ab4f190be8ac8cc7cfd8f4f3217da379606f3dd4e3d83feba69"}, + {file = "coverage-7.2.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac0f522c3b6109c4b764ffec71bf04ebc0523e926ca7cbe6c5ac88f84faced0"}, + {file = "coverage-7.2.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ba279aae162b20444881fc3ed4e4f934c1cf8620f3dab3b531480cf602c76b7f"}, + {file = "coverage-7.2.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:53d0fd4c17175aded9c633e319360d41a1f3c6e352ba94edcb0fa5167e2bad67"}, + {file = "coverage-7.2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c99cb7c26a3039a8a4ee3ca1efdde471e61b4837108847fb7d5be7789ed8fd9"}, + {file = "coverage-7.2.2-cp37-cp37m-win32.whl", hash = "sha256:5cc0783844c84af2522e3a99b9b761a979a3ef10fb87fc4048d1ee174e18a7d8"}, + {file = "coverage-7.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:817295f06eacdc8623dc4df7d8b49cea65925030d4e1e2a7c7218380c0072c25"}, + {file = "coverage-7.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6146910231ece63facfc5984234ad1b06a36cecc9fd0c028e59ac7c9b18c38c6"}, + {file = "coverage-7.2.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:387fb46cb8e53ba7304d80aadca5dca84a2fbf6fe3faf6951d8cf2d46485d1e5"}, + {file = "coverage-7.2.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:046936ab032a2810dcaafd39cc4ef6dd295df1a7cbead08fe996d4765fca9fe4"}, + {file = "coverage-7.2.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e627dee428a176ffb13697a2c4318d3f60b2ccdde3acdc9b3f304206ec130ccd"}, + {file = "coverage-7.2.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fa54fb483decc45f94011898727802309a109d89446a3c76387d016057d2c84"}, + {file = "coverage-7.2.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3668291b50b69a0c1ef9f462c7df2c235da3c4073f49543b01e7eb1dee7dd540"}, + {file = "coverage-7.2.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7c20b731211261dc9739bbe080c579a1835b0c2d9b274e5fcd903c3a7821cf88"}, + {file = "coverage-7.2.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5764e1f7471cb8f64b8cda0554f3d4c4085ae4b417bfeab236799863703e5de2"}, + {file = "coverage-7.2.2-cp38-cp38-win32.whl", hash = "sha256:4f01911c010122f49a3e9bdc730eccc66f9b72bd410a3a9d3cb8448bb50d65d3"}, + {file = "coverage-7.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:c448b5c9e3df5448a362208b8d4b9ed85305528313fca1b479f14f9fe0d873b8"}, + {file = "coverage-7.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bfe7085783cda55e53510482fa7b5efc761fad1abe4d653b32710eb548ebdd2d"}, + {file = "coverage-7.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9d22e94e6dc86de981b1b684b342bec5e331401599ce652900ec59db52940005"}, + {file = "coverage-7.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:507e4720791977934bba016101579b8c500fb21c5fa3cd4cf256477331ddd988"}, + {file = "coverage-7.2.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc4803779f0e4b06a2361f666e76f5c2e3715e8e379889d02251ec911befd149"}, + {file = "coverage-7.2.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db8c2c5ace167fd25ab5dd732714c51d4633f58bac21fb0ff63b0349f62755a8"}, + {file = "coverage-7.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4f68ee32d7c4164f1e2c8797535a6d0a3733355f5861e0f667e37df2d4b07140"}, + {file = "coverage-7.2.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d52f0a114b6a58305b11a5cdecd42b2e7f1ec77eb20e2b33969d702feafdd016"}, + {file = "coverage-7.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:797aad79e7b6182cb49c08cc5d2f7aa7b2128133b0926060d0a8889ac43843be"}, + {file = "coverage-7.2.2-cp39-cp39-win32.whl", hash = "sha256:db45eec1dfccdadb179b0f9ca616872c6f700d23945ecc8f21bb105d74b1c5fc"}, + {file = "coverage-7.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:8dbe2647bf58d2c5a6c5bcc685f23b5f371909a5624e9f5cd51436d6a9f6c6ef"}, + {file = "coverage-7.2.2-pp37.pp38.pp39-none-any.whl", hash = "sha256:872d6ce1f5be73f05bea4df498c140b9e7ee5418bfa2cc8204e7f9b817caa968"}, + {file = "coverage-7.2.2.tar.gz", hash = "sha256:36dd42da34fe94ed98c39887b86db9d06777b1c8f860520e21126a75507024f2"}, +] +deptry = [ + {file = "deptry-0.6.6-py3-none-any.whl", hash = "sha256:38c03085899339a43374ba51c845d6d1e3d3d2d003d8270d85f0c67f19775e36"}, + {file = "deptry-0.6.6.tar.gz", hash = "sha256:b968b459929ede5428d695c801f39d03c058de7fa7f855585ca8620a1ec3ae62"}, +] +distlib = [ + {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, + {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, +] +exceptiongroup = [ + {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"}, + {file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"}, +] +filelock = [ + {file = "filelock-3.10.0-py3-none-any.whl", hash = "sha256:e90b34656470756edf8b19656785c5fea73afa1953f3e1b0d645cef11cab3182"}, + {file = "filelock-3.10.0.tar.gz", hash = "sha256:3199fd0d3faea8b911be52b663dfccceb84c95949dd13179aa21436d1a79c4ce"}, +] +ghp-import = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] +griffe = [ + {file = "griffe-0.25.5-py3-none-any.whl", hash = "sha256:1fb9edff48e66d4873014a2ebf21aca5f271d0006a4c937826e3cf592ffb3706"}, + {file = "griffe-0.25.5.tar.gz", hash = "sha256:11ea3403ef0560a1cbcf7f302eb5d21cf4c1d8ed3f8a16a75aa9f6f458caf3f1"}, +] +identify = [ + {file = "identify-2.5.21-py2.py3-none-any.whl", hash = "sha256:69edcaffa8e91ae0f77d397af60f148b6b45a8044b2cc6d99cafa5b04793ff00"}, + {file = "identify-2.5.21.tar.gz", hash = "sha256:7671a05ef9cfaf8ff63b15d45a91a1147a03aaccb2976d4e9bd047cbbc508471"}, +] +idna = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] +importlib-metadata = [ + {file = "importlib_metadata-6.0.0-py3-none-any.whl", hash = "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad"}, + {file = "importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"}, +] +iniconfig = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] +jinja2 = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] +markdown = [ + {file = "Markdown-3.3.7-py3-none-any.whl", hash = "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"}, + {file = "Markdown-3.3.7.tar.gz", hash = "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874"}, +] +markupsafe = [ + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, + {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, +] +mergedeep = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] +mkdocs = [ + {file = "mkdocs-1.4.2-py3-none-any.whl", hash = "sha256:c8856a832c1e56702577023cd64cc5f84948280c1c0fcc6af4cd39006ea6aa8c"}, + {file = "mkdocs-1.4.2.tar.gz", hash = "sha256:8947af423a6d0facf41ea1195b8e1e8c85ad94ac95ae307fe11232e0424b11c5"}, +] +mkdocs-autorefs = [ + {file = "mkdocs-autorefs-0.4.1.tar.gz", hash = "sha256:70748a7bd025f9ecd6d6feeba8ba63f8e891a1af55f48e366d6d6e78493aba84"}, + {file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"}, +] +mkdocs-material = [ + {file = "mkdocs_material-8.5.11-py3-none-any.whl", hash = "sha256:c907b4b052240a5778074a30a78f31a1f8ff82d7012356dc26898b97559f082e"}, + {file = "mkdocs_material-8.5.11.tar.gz", hash = "sha256:b0ea0513fd8cab323e8a825d6692ea07fa83e917bb5db042e523afecc7064ab7"}, +] +mkdocs-material-extensions = [ + {file = "mkdocs_material_extensions-1.1.1-py3-none-any.whl", hash = "sha256:e41d9f38e4798b6617ad98ca8f7f1157b1e4385ac1459ca1e4ea219b556df945"}, + {file = "mkdocs_material_extensions-1.1.1.tar.gz", hash = "sha256:9c003da71e2cc2493d910237448c672e00cefc800d3d6ae93d2fc69979e3bd93"}, +] +mkdocstrings = [ + {file = "mkdocstrings-0.19.1-py3-none-any.whl", hash = "sha256:32a38d88f67f65b264184ea71290f9332db750d189dea4200cbbe408d304c261"}, + {file = "mkdocstrings-0.19.1.tar.gz", hash = "sha256:d1037cacb4b522c1e8c164ed5d00d724a82e49dcee0af80db8fb67b384faeef9"}, +] +mkdocstrings-python = [ + {file = "mkdocstrings-python-0.8.3.tar.gz", hash = "sha256:9ae473f6dc599339b09eee17e4d2b05d6ac0ec29860f3fc9b7512d940fc61adf"}, + {file = "mkdocstrings_python-0.8.3-py3-none-any.whl", hash = "sha256:4e6e1cd6f37a785de0946ced6eb846eb2f5d891ac1cc2c7b832943d3529087a7"}, +] +mypy = [ + {file = "mypy-0.981-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4bc460e43b7785f78862dab78674e62ec3cd523485baecfdf81a555ed29ecfa0"}, + {file = "mypy-0.981-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:756fad8b263b3ba39e4e204ee53042671b660c36c9017412b43af210ddee7b08"}, + {file = "mypy-0.981-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a16a0145d6d7d00fbede2da3a3096dcc9ecea091adfa8da48fa6a7b75d35562d"}, + {file = "mypy-0.981-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce65f70b14a21fdac84c294cde75e6dbdabbcff22975335e20827b3b94bdbf49"}, + {file = "mypy-0.981-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e35d764784b42c3e256848fb8ed1d4292c9fc0098413adb28d84974c095b279"}, + {file = "mypy-0.981-cp310-cp310-win_amd64.whl", hash = "sha256:e53773073c864d5f5cec7f3fc72fbbcef65410cde8cc18d4f7242dea60dac52e"}, + {file = "mypy-0.981-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6ee196b1d10b8b215e835f438e06965d7a480f6fe016eddbc285f13955cca659"}, + {file = "mypy-0.981-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ad21d4c9d3673726cf986ea1d0c9fb66905258709550ddf7944c8f885f208be"}, + {file = "mypy-0.981-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d1debb09043e1f5ee845fa1e96d180e89115b30e47c5d3ce53bc967bab53f62d"}, + {file = "mypy-0.981-cp37-cp37m-win_amd64.whl", hash = "sha256:9f362470a3480165c4c6151786b5379351b790d56952005be18bdbdd4c7ce0ae"}, + {file = "mypy-0.981-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c9e0efb95ed6ca1654951bd5ec2f3fa91b295d78bf6527e026529d4aaa1e0c30"}, + {file = "mypy-0.981-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e178eaffc3c5cd211a87965c8c0df6da91ed7d258b5fc72b8e047c3771317ddb"}, + {file = "mypy-0.981-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:06e1eac8d99bd404ed8dd34ca29673c4346e76dd8e612ea507763dccd7e13c7a"}, + {file = "mypy-0.981-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa38f82f53e1e7beb45557ff167c177802ba7b387ad017eab1663d567017c8ee"}, + {file = "mypy-0.981-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:64e1f6af81c003f85f0dfed52db632817dabb51b65c0318ffbf5ff51995bbb08"}, + {file = "mypy-0.981-cp38-cp38-win_amd64.whl", hash = "sha256:e1acf62a8c4f7c092462c738aa2c2489e275ed386320c10b2e9bff31f6f7e8d6"}, + {file = "mypy-0.981-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b6ede64e52257931315826fdbfc6ea878d89a965580d1a65638ef77cb551f56d"}, + {file = "mypy-0.981-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eb3978b191b9fa0488524bb4ffedf2c573340e8c2b4206fc191d44c7093abfb7"}, + {file = "mypy-0.981-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77f8fcf7b4b3cc0c74fb33ae54a4cd00bb854d65645c48beccf65fa10b17882c"}, + {file = "mypy-0.981-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f64d2ce043a209a297df322eb4054dfbaa9de9e8738291706eaafda81ab2b362"}, + {file = "mypy-0.981-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2ee3dbc53d4df7e6e3b1c68ac6a971d3a4fb2852bf10a05fda228721dd44fae1"}, + {file = "mypy-0.981-cp39-cp39-win_amd64.whl", hash = "sha256:8e8e49aa9cc23aa4c926dc200ce32959d3501c4905147a66ce032f05cb5ecb92"}, + {file = "mypy-0.981-py3-none-any.whl", hash = "sha256:794f385653e2b749387a42afb1e14c2135e18daeb027e0d97162e4b7031210f8"}, + {file = "mypy-0.981.tar.gz", hash = "sha256:ad77c13037d3402fbeffda07d51e3f228ba078d1c7096a73759c9419ea031bf4"}, +] +mypy-extensions = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] +nodeenv = [ + {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, + {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, +] +packaging = [ + {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, + {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, +] +platformdirs = [ + {file = "platformdirs-3.1.1-py3-none-any.whl", hash = "sha256:e5986afb596e4bb5bde29a79ac9061aa955b94fca2399b7aaac4090860920dd8"}, + {file = "platformdirs-3.1.1.tar.gz", hash = "sha256:024996549ee88ec1a9aa99ff7f8fc819bb59e2c3477b410d90a16d32d6e707aa"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +pre-commit = [ + {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, + {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pygments = [ + {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, + {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, +] +pymdown-extensions = [ + {file = "pymdown_extensions-9.10-py3-none-any.whl", hash = "sha256:31eaa76ce6f96aabfcea98787c2fff2c5c0611b20a53a94213970cfbf05f02b8"}, + {file = "pymdown_extensions-9.10.tar.gz", hash = "sha256:562c38eee4ce3f101ce631b804bfc2177a8a76c7e4dc908871fb6741a90257a7"}, +] +pytest = [ + {file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"}, + {file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"}, +] +pytest-cov = [ + {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, + {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] +pyyaml = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] +pyyaml-env-tag = [ + {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, + {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, +] +requests = [ + {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, + {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, +] +setuptools = [ + {file = "setuptools-67.6.0-py3-none-any.whl", hash = "sha256:b78aaa36f6b90a074c1fa651168723acbf45d14cb1196b6f02c0fd07f17623b2"}, + {file = "setuptools-67.6.0.tar.gz", hash = "sha256:2ee892cd5f29f3373097f5a814697e397cf3ce313616df0af11231e2ad118077"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +tomli = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] +tox = [ + {file = "tox-3.28.0-py2.py3-none-any.whl", hash = "sha256:57b5ab7e8bb3074edc3c0c0b4b192a4f3799d3723b2c5b76f1fa9f2d40316eea"}, + {file = "tox-3.28.0.tar.gz", hash = "sha256:d0d28f3fe6d6d7195c27f8b054c3e99d5451952b54abdae673b71609a581f640"}, +] +typing-extensions = [ + {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, + {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, +] +urllib3 = [ + {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"}, + {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"}, +] +virtualenv = [ + {file = "virtualenv-20.21.0-py3-none-any.whl", hash = "sha256:31712f8f2a17bd06234fa97fdf19609e789dd4e3e4bf108c3da71d710651adbc"}, + {file = "virtualenv-20.21.0.tar.gz", hash = "sha256:f50e3e60f990a0757c9b68333c9fdaa72d7188caa417f96af9e52407831a3b68"}, +] +watchdog = [ + {file = "watchdog-2.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1f1200d4ec53b88bf04ab636f9133cb703eb19768a39351cee649de21a33697"}, + {file = "watchdog-2.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:564e7739abd4bd348aeafbf71cc006b6c0ccda3160c7053c4a53b67d14091d42"}, + {file = "watchdog-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:95ad708a9454050a46f741ba5e2f3468655ea22da1114e4c40b8cbdaca572565"}, + {file = "watchdog-2.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a073c91a6ef0dda488087669586768195c3080c66866144880f03445ca23ef16"}, + {file = "watchdog-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa8b028750b43e80eea9946d01925168eeadb488dfdef1d82be4b1e28067f375"}, + {file = "watchdog-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:964fd236cd443933268ae49b59706569c8b741073dbfd7ca705492bae9d39aab"}, + {file = "watchdog-2.3.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:91fd146d723392b3e6eb1ac21f122fcce149a194a2ba0a82c5e4d0ee29cd954c"}, + {file = "watchdog-2.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:efe3252137392a471a2174d721e1037a0e6a5da7beb72a021e662b7000a9903f"}, + {file = "watchdog-2.3.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:85bf2263290591b7c5fa01140601b64c831be88084de41efbcba6ea289874f44"}, + {file = "watchdog-2.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8f2df370cd8e4e18499dd0bfdef476431bcc396108b97195d9448d90924e3131"}, + {file = "watchdog-2.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ea5d86d1bcf4a9d24610aa2f6f25492f441960cf04aed2bd9a97db439b643a7b"}, + {file = "watchdog-2.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6f5d0f7eac86807275eba40b577c671b306f6f335ba63a5c5a348da151aba0fc"}, + {file = "watchdog-2.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b848c71ef2b15d0ef02f69da8cc120d335cec0ed82a3fa7779e27a5a8527225"}, + {file = "watchdog-2.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0d9878be36d2b9271e3abaa6f4f051b363ff54dbbe7e7df1af3c920e4311ee43"}, + {file = "watchdog-2.3.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4cd61f98cb37143206818cb1786d2438626aa78d682a8f2ecee239055a9771d5"}, + {file = "watchdog-2.3.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3d2dbcf1acd96e7a9c9aefed201c47c8e311075105d94ce5e899f118155709fd"}, + {file = "watchdog-2.3.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:03f342a9432fe08107defbe8e405a2cb922c5d00c4c6c168c68b633c64ce6190"}, + {file = "watchdog-2.3.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7a596f9415a378d0339681efc08d2249e48975daae391d58f2e22a3673b977cf"}, + {file = "watchdog-2.3.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:0e1dd6d449267cc7d6935d7fe27ee0426af6ee16578eed93bacb1be9ff824d2d"}, + {file = "watchdog-2.3.1-py3-none-manylinux2014_i686.whl", hash = "sha256:7a1876f660e32027a1a46f8a0fa5747ad4fcf86cb451860eae61a26e102c8c79"}, + {file = "watchdog-2.3.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:2caf77ae137935c1466f8cefd4a3aec7017b6969f425d086e6a528241cba7256"}, + {file = "watchdog-2.3.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:53f3e95081280898d9e4fc51c5c69017715929e4eea1ab45801d5e903dd518ad"}, + {file = "watchdog-2.3.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:9da7acb9af7e4a272089bd2af0171d23e0d6271385c51d4d9bde91fe918c53ed"}, + {file = "watchdog-2.3.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:8a4d484e846dcd75e96b96d80d80445302621be40e293bfdf34a631cab3b33dc"}, + {file = "watchdog-2.3.1-py3-none-win32.whl", hash = "sha256:a74155398434937ac2780fd257c045954de5b11b5c52fc844e2199ce3eecf4cf"}, + {file = "watchdog-2.3.1-py3-none-win_amd64.whl", hash = "sha256:5defe4f0918a2a1a4afbe4dbb967f743ac3a93d546ea4674567806375b024adb"}, + {file = "watchdog-2.3.1-py3-none-win_ia64.whl", hash = "sha256:4109cccf214b7e3462e8403ab1e5b17b302ecce6c103eb2fc3afa534a7f27b96"}, + {file = "watchdog-2.3.1.tar.gz", hash = "sha256:d9f9ed26ed22a9d331820a8432c3680707ea8b54121ddcc9dc7d9f2ceeb36906"}, +] +zipp = [ + {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, + {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, +] diff --git a/lib/modules/manager/poetry/__fixtures__/poetry142.lock b/lib/modules/manager/poetry/__fixtures__/poetry142.lock new file mode 100644 index 00000000000000..a0bee059f58a89 --- /dev/null +++ b/lib/modules/manager/poetry/__fixtures__/poetry142.lock @@ -0,0 +1,18 @@ +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. + +[[package]] +name = "attrs" +version = "22.2.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, + {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.7" +content-hash = "9aaf819933593fe3f84ff37db4ed316090da9b491e66fd5108b2b33f32f683c3" diff --git a/lib/modules/manager/poetry/artifacts.spec.ts b/lib/modules/manager/poetry/artifacts.spec.ts index 733d5ad9870bdc..67cc4098f841dd 100644 --- a/lib/modules/manager/poetry/artifacts.spec.ts +++ b/lib/modules/manager/poetry/artifacts.spec.ts @@ -8,6 +8,7 @@ import * as docker from '../../../util/exec/docker'; import * as _hostRules from '../../../util/host-rules'; import * as _datasource from '../../datasource'; import type { UpdateArtifactsConfig } from '../types'; +import { getPoetryRequirement } from './artifacts'; import { updateArtifacts } from '.'; const pyproject1toml = Fixtures.get('pyproject.1.toml'); @@ -32,419 +33,440 @@ const adminConfig: RepoGlobalConfig = { const config: UpdateArtifactsConfig = {}; describe('modules/manager/poetry/artifacts', () => { - beforeEach(() => { - jest.resetAllMocks(); - env.getChildProcessEnv.mockReturnValue(envMock.basic); - GlobalConfig.set(adminConfig); - docker.resetPrefetchedImages(); - }); + describe('getPoetryRequirement', () => { + const poetry12lock = Fixtures.get('poetry12.lock'); + const poetry142lock = Fixtures.get('poetry142.lock'); - it('returns null if no poetry.lock found', async () => { - const execSnapshots = mockExecAll(); - const updatedDeps = [{ depName: 'dep1' }]; - expect( - await updateArtifacts({ - packageFileName: 'pyproject.toml', - updatedDeps, - newPackageFileContent: '', - config, - }) - ).toBeNull(); - expect(execSnapshots).toEqual([]); - }); + it('detects poetry from first line of poetry.lock', () => { + const pyprojectContent = ''; + expect(getPoetryRequirement(pyprojectContent, poetry142lock)).toBe( + '1.4.2' + ); + }); - it('returns null if updatedDeps is empty', async () => { - const execSnapshots = mockExecAll(); - expect( - await updateArtifacts({ - packageFileName: 'pyproject.toml', - updatedDeps: [], - newPackageFileContent: '', - config, - }) - ).toBeNull(); - expect(execSnapshots).toEqual([]); + it('detects poetry from metadata', () => { + const pyprojectContent = ''; + expect(getPoetryRequirement(pyprojectContent, poetry12lock)).toBe( + '<1.3.0' + ); + }); }); - it('returns null if unchanged', async () => { - const execSnapshots = mockExecAll(); - fs.ensureCacheDir.mockResolvedValueOnce('/tmp/renovate/cache/others/pip'); - fs.readLocalFile.mockResolvedValueOnce('Current poetry.lock'); - fs.readLocalFile.mockResolvedValueOnce('Current poetry.lock'); - const updatedDeps = [{ depName: 'dep1' }]; - expect( - await updateArtifacts({ - packageFileName: 'pyproject.toml', - updatedDeps, - newPackageFileContent: '', - config, - }) - ).toBeNull(); - expect(execSnapshots).toMatchObject([ - { - cmd: 'poetry update --lock --no-interaction dep1', - options: { - cwd: '/tmp/github/some/repo', - env: { PIP_CACHE_DIR: '/tmp/renovate/cache/others/pip' }, - }, - }, - ]); - }); + describe('updateArtifacts', () => { + beforeEach(() => { + jest.resetAllMocks(); + env.getChildProcessEnv.mockReturnValue(envMock.basic); + GlobalConfig.set(adminConfig); + docker.resetPrefetchedImages(); + }); - it('returns updated poetry.lock', async () => { - const execSnapshots = mockExecAll(); - // poetry.lock - fs.getSiblingFileName.mockReturnValueOnce('poetry.lock'); - fs.readLocalFile.mockResolvedValueOnce('[metadata]\n'); - fs.readLocalFile.mockResolvedValueOnce('New poetry.lock'); - const updatedDeps = [{ depName: 'dep1' }]; - expect( - await updateArtifacts({ - packageFileName: 'pyproject.toml', - updatedDeps, - newPackageFileContent: '{}', - config, - }) - ).toEqual([ - { - file: { - type: 'addition', - path: 'poetry.lock', - contents: 'New poetry.lock', + it('returns null if no poetry.lock found', async () => { + const execSnapshots = mockExecAll(); + const updatedDeps = [{ depName: 'dep1' }]; + expect( + await updateArtifacts({ + packageFileName: 'pyproject.toml', + updatedDeps, + newPackageFileContent: '', + config, + }) + ).toBeNull(); + expect(execSnapshots).toEqual([]); + }); + + it('returns null if updatedDeps is empty', async () => { + const execSnapshots = mockExecAll(); + expect( + await updateArtifacts({ + packageFileName: 'pyproject.toml', + updatedDeps: [], + newPackageFileContent: '', + config, + }) + ).toBeNull(); + expect(execSnapshots).toEqual([]); + }); + + it('returns null if unchanged', async () => { + const execSnapshots = mockExecAll(); + fs.ensureCacheDir.mockResolvedValueOnce('/tmp/renovate/cache/others/pip'); + fs.readLocalFile.mockResolvedValueOnce('Current poetry.lock'); + fs.readLocalFile.mockResolvedValueOnce('Current poetry.lock'); + const updatedDeps = [{ depName: 'dep1' }]; + expect( + await updateArtifacts({ + packageFileName: 'pyproject.toml', + updatedDeps, + newPackageFileContent: '', + config, + }) + ).toBeNull(); + expect(execSnapshots).toMatchObject([ + { + cmd: 'poetry update --lock --no-interaction dep1', + options: { + cwd: '/tmp/github/some/repo', + env: { PIP_CACHE_DIR: '/tmp/renovate/cache/others/pip' }, + }, }, - }, - ]); - expect(execSnapshots).toMatchObject([ - { cmd: 'poetry update --lock --no-interaction dep1' }, - ]); - }); + ]); + }); - it('passes private credential environment vars', async () => { - // poetry.lock - fs.getSiblingFileName.mockReturnValueOnce('poetry.lock'); - fs.readLocalFile.mockResolvedValueOnce(null); - // pyproject.lock - fs.getSiblingFileName.mockReturnValueOnce('pyproject.lock'); - fs.readLocalFile.mockResolvedValueOnce('[metadata]\n'); - const execSnapshots = mockExecAll(); - fs.readLocalFile.mockResolvedValueOnce('New poetry.lock'); - hostRules.find.mockReturnValueOnce({ - username: 'usernameOne', - password: 'passwordOne', + it('returns updated poetry.lock', async () => { + const execSnapshots = mockExecAll(); + // poetry.lock + fs.getSiblingFileName.mockReturnValueOnce('poetry.lock'); + fs.readLocalFile.mockResolvedValueOnce('[metadata]\n'); + fs.readLocalFile.mockResolvedValueOnce('New poetry.lock'); + const updatedDeps = [{ depName: 'dep1' }]; + expect( + await updateArtifacts({ + packageFileName: 'pyproject.toml', + updatedDeps, + newPackageFileContent: '{}', + config, + }) + ).toEqual([ + { + file: { + type: 'addition', + path: 'poetry.lock', + contents: 'New poetry.lock', + }, + }, + ]); + expect(execSnapshots).toMatchObject([ + { cmd: 'poetry update --lock --no-interaction dep1' }, + ]); }); - hostRules.find.mockReturnValueOnce({ username: 'usernameTwo' }); - hostRules.find.mockReturnValueOnce({}); - hostRules.find.mockReturnValueOnce({ password: 'passwordFour' }); - const updatedDeps = [{ depName: 'dep1' }]; - expect( - await updateArtifacts({ - packageFileName: 'pyproject.toml', - updatedDeps, - newPackageFileContent: pyproject10toml, - config, - }) - ).toEqual([ - { - file: { - type: 'addition', - path: 'pyproject.lock', - contents: 'New poetry.lock', + + it('passes private credential environment vars', async () => { + // poetry.lock + fs.getSiblingFileName.mockReturnValueOnce('poetry.lock'); + fs.readLocalFile.mockResolvedValueOnce(null); + // pyproject.lock + fs.getSiblingFileName.mockReturnValueOnce('pyproject.lock'); + fs.readLocalFile.mockResolvedValueOnce('[metadata]\n'); + const execSnapshots = mockExecAll(); + fs.readLocalFile.mockResolvedValueOnce('New poetry.lock'); + hostRules.find.mockReturnValueOnce({ + username: 'usernameOne', + password: 'passwordOne', + }); + hostRules.find.mockReturnValueOnce({ username: 'usernameTwo' }); + hostRules.find.mockReturnValueOnce({}); + hostRules.find.mockReturnValueOnce({ password: 'passwordFour' }); + const updatedDeps = [{ depName: 'dep1' }]; + expect( + await updateArtifacts({ + packageFileName: 'pyproject.toml', + updatedDeps, + newPackageFileContent: pyproject10toml, + config, + }) + ).toEqual([ + { + file: { + type: 'addition', + path: 'pyproject.lock', + contents: 'New poetry.lock', + }, }, - }, - ]); - expect(hostRules.find.mock.calls).toHaveLength(4); - expect(execSnapshots).toMatchObject([ - { cmd: 'poetry update --lock --no-interaction dep1' }, - ]); - }); + ]); + expect(hostRules.find.mock.calls).toHaveLength(4); + expect(execSnapshots).toMatchObject([ + { cmd: 'poetry update --lock --no-interaction dep1' }, + ]); + }); - it('prioritizes pypi-scoped credentials', async () => { - const execSnapshots = mockExecAll(); - // poetry.lock - fs.getSiblingFileName.mockReturnValueOnce('poetry.lock'); - fs.readLocalFile.mockResolvedValueOnce(null); - // pyproject.lock - fs.getSiblingFileName.mockReturnValueOnce('pyproject.lock'); - fs.readLocalFile.mockResolvedValueOnce('[metadata]\n'); - fs.readLocalFile.mockResolvedValueOnce('New poetry.lock'); - hostRules.find.mockImplementation((search) => ({ - password: - search.hostType === 'pypi' ? 'scoped-password' : 'unscoped-password', - })); - const updatedDeps = [{ depName: 'dep1' }]; - expect( - await updateArtifacts({ - packageFileName: 'pyproject.toml', - updatedDeps, - newPackageFileContent: ` + it('prioritizes pypi-scoped credentials', async () => { + const execSnapshots = mockExecAll(); + // poetry.lock + fs.getSiblingFileName.mockReturnValueOnce('poetry.lock'); + fs.readLocalFile.mockResolvedValueOnce(null); + // pyproject.lock + fs.getSiblingFileName.mockReturnValueOnce('pyproject.lock'); + fs.readLocalFile.mockResolvedValueOnce('[metadata]\n'); + fs.readLocalFile.mockResolvedValueOnce('New poetry.lock'); + hostRules.find.mockImplementation((search) => ({ + password: + search.hostType === 'pypi' ? 'scoped-password' : 'unscoped-password', + })); + const updatedDeps = [{ depName: 'dep1' }]; + expect( + await updateArtifacts({ + packageFileName: 'pyproject.toml', + updatedDeps, + newPackageFileContent: ` [[tool.poetry.source]] name = "one" url = "some.url" `, - config, - }) - ).toEqual([ - { - file: { - type: 'addition', - path: 'pyproject.lock', - contents: 'New poetry.lock', + config, + }) + ).toEqual([ + { + file: { + type: 'addition', + path: 'pyproject.lock', + contents: 'New poetry.lock', + }, }, - }, - ]); - expect(execSnapshots).toMatchObject([ - { cmd: 'poetry update --lock --no-interaction dep1' }, - ]); - }); + ]); + expect(execSnapshots).toMatchObject([ + { cmd: 'poetry update --lock --no-interaction dep1' }, + ]); + }); - it('returns updated pyproject.lock', async () => { - const execSnapshots = mockExecAll(); - // poetry.lock - fs.getSiblingFileName.mockReturnValueOnce('poetry.lock'); - fs.readLocalFile.mockResolvedValueOnce(null); - // pyproject.lock - fs.getSiblingFileName.mockReturnValueOnce('pyproject.lock'); - fs.readLocalFile.mockResolvedValueOnce('[metadata]\n'); - fs.readLocalFile.mockResolvedValueOnce('New poetry.lock'); - const updatedDeps = [{ depName: 'dep1' }]; - expect( - await updateArtifacts({ - packageFileName: 'pyproject.toml', - updatedDeps, - newPackageFileContent: '{}', - config, - }) - ).toEqual([ - { - file: { - type: 'addition', - path: 'pyproject.lock', - contents: 'New poetry.lock', + it('returns updated pyproject.lock', async () => { + const execSnapshots = mockExecAll(); + // poetry.lock + fs.getSiblingFileName.mockReturnValueOnce('poetry.lock'); + fs.readLocalFile.mockResolvedValueOnce(null); + // pyproject.lock + fs.getSiblingFileName.mockReturnValueOnce('pyproject.lock'); + fs.readLocalFile.mockResolvedValueOnce('[metadata]\n'); + fs.readLocalFile.mockResolvedValueOnce('New poetry.lock'); + const updatedDeps = [{ depName: 'dep1' }]; + expect( + await updateArtifacts({ + packageFileName: 'pyproject.toml', + updatedDeps, + newPackageFileContent: '{}', + config, + }) + ).toEqual([ + { + file: { + type: 'addition', + path: 'pyproject.lock', + contents: 'New poetry.lock', + }, }, - }, - ]); - expect(execSnapshots).toMatchObject([ - { cmd: 'poetry update --lock --no-interaction dep1' }, - ]); - }); - - it('returns updated poetry.lock using docker', async () => { - GlobalConfig.set({ ...adminConfig, binarySource: 'docker' }); - const execSnapshots = mockExecAll(); - fs.ensureCacheDir.mockResolvedValueOnce('/tmp/renovate/cache/others/pip'); - // poetry.lock - fs.getSiblingFileName.mockReturnValueOnce('poetry.lock'); - fs.readLocalFile.mockResolvedValueOnce('[metadata]\n'); - fs.readLocalFile.mockResolvedValueOnce('New poetry.lock'); - // python - datasource.getPkgReleases.mockResolvedValueOnce({ - releases: [{ version: '2.7.5' }, { version: '3.4.2' }], - }); - // poetry - datasource.getPkgReleases.mockResolvedValueOnce({ - releases: [{ version: '1.2.0' }], + ]); + expect(execSnapshots).toMatchObject([ + { cmd: 'poetry update --lock --no-interaction dep1' }, + ]); }); - const updatedDeps = [{ depName: 'dep1' }]; - expect( - await updateArtifacts({ - packageFileName: 'pyproject.toml', - updatedDeps, - newPackageFileContent: pyproject1toml, - config: { - ...config, - constraints: { - python: '~2.7 || ^3.4', + + it('returns updated poetry.lock using docker', async () => { + GlobalConfig.set({ ...adminConfig, binarySource: 'docker' }); + const execSnapshots = mockExecAll(); + fs.ensureCacheDir.mockResolvedValueOnce('/tmp/renovate/cache/others/pip'); + // poetry.lock + fs.getSiblingFileName.mockReturnValueOnce('poetry.lock'); + fs.readLocalFile.mockResolvedValueOnce('[metadata]\n'); + fs.readLocalFile.mockResolvedValueOnce('New poetry.lock'); + // python + datasource.getPkgReleases.mockResolvedValueOnce({ + releases: [{ version: '2.7.5' }, { version: '3.4.2' }], + }); + // poetry + datasource.getPkgReleases.mockResolvedValueOnce({ + releases: [{ version: '1.2.0' }], + }); + const updatedDeps = [{ depName: 'dep1' }]; + expect( + await updateArtifacts({ + packageFileName: 'pyproject.toml', + updatedDeps, + newPackageFileContent: pyproject1toml, + config: { + ...config, + constraints: { + python: '~2.7 || ^3.4', + }, + }, + }) + ).toEqual([ + { + file: { + type: 'addition', + path: 'poetry.lock', + contents: 'New poetry.lock', }, }, - }) - ).toEqual([ - { - file: { - type: 'addition', - path: 'poetry.lock', - contents: 'New poetry.lock', + ]); + expect(execSnapshots).toMatchObject([ + { cmd: 'docker pull containerbase/sidecar' }, + { cmd: 'docker ps --filter name=renovate_sidecar -aq' }, + { + cmd: + 'docker run --rm --name=renovate_sidecar --label=renovate_child ' + + '-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' + + '-v "/tmp/cache":"/tmp/cache" ' + + '-e PIP_CACHE_DIR ' + + '-e BUILDPACK_CACHE_DIR ' + + '-e CONTAINERBASE_CACHE_DIR ' + + '-w "/tmp/github/some/repo" ' + + 'containerbase/sidecar ' + + 'bash -l -c "' + + 'install-tool python 3.4.2 ' + + '&& ' + + 'install-tool poetry 1.2.0 ' + + '&& ' + + 'poetry update --lock --no-interaction dep1' + + '"', }, - }, - ]); - expect(execSnapshots).toMatchObject([ - { cmd: 'docker pull containerbase/sidecar' }, - { cmd: 'docker ps --filter name=renovate_sidecar -aq' }, - { - cmd: - 'docker run --rm --name=renovate_sidecar --label=renovate_child ' + - '-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' + - '-v "/tmp/cache":"/tmp/cache" ' + - '-e PIP_CACHE_DIR ' + - '-e BUILDPACK_CACHE_DIR ' + - '-e CONTAINERBASE_CACHE_DIR ' + - '-w "/tmp/github/some/repo" ' + - 'containerbase/sidecar ' + - 'bash -l -c "' + - 'install-tool python 3.4.2 ' + - '&& ' + - 'install-tool poetry 1.2.0 ' + - '&& ' + - 'poetry update --lock --no-interaction dep1' + - '"', - }, - ]); - }); + ]); + }); - it('returns updated poetry.lock using docker (constraints)', async () => { - GlobalConfig.set({ ...adminConfig, binarySource: 'docker' }); - const execSnapshots = mockExecAll(); + it('returns updated poetry.lock using docker (constraints)', async () => { + GlobalConfig.set({ ...adminConfig, binarySource: 'docker' }); + const execSnapshots = mockExecAll(); - fs.ensureCacheDir.mockResolvedValueOnce('/tmp/renovate/cache/others/pip'); - // poetry.lock - fs.getSiblingFileName.mockReturnValueOnce('poetry.lock'); - fs.readLocalFile.mockResolvedValueOnce( - '[metadata]\npython-versions = "~2.7 || ^3.4"' - ); - fs.readLocalFile.mockResolvedValueOnce('New poetry.lock'); - // python - datasource.getPkgReleases.mockResolvedValueOnce({ - releases: [{ version: '2.7.5' }, { version: '3.3.2' }], - }); - // poetry - datasource.getPkgReleases.mockResolvedValueOnce({ - releases: [{ version: '1.0.0' }, { version: '1.2.0' }], - }); - const updatedDeps = [{ depName: 'dep1' }]; - expect( - await updateArtifacts({ - packageFileName: 'pyproject.toml', - updatedDeps, - newPackageFileContent: pyproject1toml, - config: { - ...config, - constraints: {}, + fs.ensureCacheDir.mockResolvedValueOnce('/tmp/renovate/cache/others/pip'); + // poetry.lock + fs.getSiblingFileName.mockReturnValueOnce('poetry.lock'); + fs.readLocalFile.mockResolvedValueOnce( + '[metadata]\npython-versions = "~2.7 || ^3.4"' + ); + fs.readLocalFile.mockResolvedValueOnce('New poetry.lock'); + // python + datasource.getPkgReleases.mockResolvedValueOnce({ + releases: [{ version: '2.7.5' }, { version: '3.3.2' }], + }); + // poetry + datasource.getPkgReleases.mockResolvedValueOnce({ + releases: [{ version: '1.0.0' }, { version: '1.2.0' }], + }); + const updatedDeps = [{ depName: 'dep1' }]; + expect( + await updateArtifacts({ + packageFileName: 'pyproject.toml', + updatedDeps, + newPackageFileContent: pyproject1toml, + config: { + ...config, + constraints: {}, + }, + }) + ).toEqual([ + { + file: { + type: 'addition', + path: 'poetry.lock', + contents: 'New poetry.lock', + }, }, - }) - ).toEqual([ - { - file: { - type: 'addition', - path: 'poetry.lock', - contents: 'New poetry.lock', + ]); + expect(execSnapshots).toMatchObject([ + { cmd: 'docker pull containerbase/sidecar' }, + { cmd: 'docker ps --filter name=renovate_sidecar -aq' }, + { + cmd: + 'docker run --rm --name=renovate_sidecar --label=renovate_child ' + + '-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' + + '-v "/tmp/cache":"/tmp/cache" ' + + '-e PIP_CACHE_DIR ' + + '-e BUILDPACK_CACHE_DIR ' + + '-e CONTAINERBASE_CACHE_DIR ' + + '-w "/tmp/github/some/repo" ' + + 'containerbase/sidecar ' + + 'bash -l -c "' + + 'install-tool python 2.7.5 ' + + '&& ' + + 'install-tool poetry 1.2.0 ' + + '&& ' + + 'poetry update --lock --no-interaction dep1' + + '"', }, - }, - ]); - expect(execSnapshots).toMatchObject([ - { cmd: 'docker pull containerbase/sidecar' }, - { cmd: 'docker ps --filter name=renovate_sidecar -aq' }, - { - cmd: - 'docker run --rm --name=renovate_sidecar --label=renovate_child ' + - '-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' + - '-v "/tmp/cache":"/tmp/cache" ' + - '-e PIP_CACHE_DIR ' + - '-e BUILDPACK_CACHE_DIR ' + - '-e CONTAINERBASE_CACHE_DIR ' + - '-w "/tmp/github/some/repo" ' + - 'containerbase/sidecar ' + - 'bash -l -c "' + - 'install-tool python 2.7.5 ' + - '&& ' + - 'install-tool poetry 1.2.0 ' + - '&& ' + - 'poetry update --lock --no-interaction dep1' + - '"', - }, - ]); - }); - - it('returns updated poetry.lock using install mode', async () => { - GlobalConfig.set({ ...adminConfig, binarySource: 'install' }); - const execSnapshots = mockExecAll(); - // poetry.lock - fs.getSiblingFileName.mockReturnValueOnce('poetry.lock'); - fs.readLocalFile.mockResolvedValueOnce( - '[metadata]\npython-versions = "~2.7 || ^3.4"' - ); - fs.readLocalFile.mockResolvedValueOnce('New poetry.lock'); - // python - datasource.getPkgReleases.mockResolvedValueOnce({ - releases: [{ version: '2.7.5' }, { version: '3.3.2' }], + ]); }); - // poetry - datasource.getPkgReleases.mockResolvedValueOnce({ - releases: [{ version: '1.2.0' }], - }); - const updatedDeps = [{ depName: 'dep1' }]; - expect( - await updateArtifacts({ - packageFileName: 'pyproject.toml', - updatedDeps, - newPackageFileContent: pyproject1toml, - config: { - ...config, - constraints: {}, - }, - }) - ).toEqual([ - { - file: { - type: 'addition', - path: 'poetry.lock', - contents: 'New poetry.lock', + + it('returns updated poetry.lock using install mode', async () => { + GlobalConfig.set({ ...adminConfig, binarySource: 'install' }); + const execSnapshots = mockExecAll(); + // poetry.lock + fs.getSiblingFileName.mockReturnValueOnce('poetry.lock'); + fs.readLocalFile.mockResolvedValueOnce( + '[metadata]\npython-versions = "~2.7 || ^3.4"' + ); + fs.readLocalFile.mockResolvedValueOnce('New poetry.lock'); + // python + datasource.getPkgReleases.mockResolvedValueOnce({ + releases: [{ version: '2.7.5' }, { version: '3.3.2' }], + }); + // poetry + datasource.getPkgReleases.mockResolvedValueOnce({ + releases: [{ version: '1.2.0' }], + }); + const updatedDeps = [{ depName: 'dep1' }]; + expect( + await updateArtifacts({ + packageFileName: 'pyproject.toml', + updatedDeps, + newPackageFileContent: pyproject1toml, + config: { + ...config, + constraints: {}, + }, + }) + ).toEqual([ + { + file: { + type: 'addition', + path: 'poetry.lock', + contents: 'New poetry.lock', + }, }, - }, - ]); + ]); - expect(execSnapshots).toMatchObject([ - { cmd: 'install-tool python 2.7.5' }, - { cmd: 'install-tool poetry 1.2.0' }, - { cmd: 'poetry update --lock --no-interaction dep1' }, - ]); - }); + expect(execSnapshots).toMatchObject([ + { cmd: 'install-tool python 2.7.5' }, + { cmd: 'install-tool poetry 1.2.0' }, + { cmd: 'poetry update --lock --no-interaction dep1' }, + ]); + }); - it('catches errors', async () => { - const execSnapshots = mockExecAll(); - // poetry.lock - fs.getSiblingFileName.mockReturnValueOnce('poetry.lock'); - fs.readLocalFile.mockResolvedValueOnce('Current poetry.lock'); - fs.writeLocalFile.mockImplementationOnce(() => { - throw new Error('not found'); + it('catches errors', async () => { + const execSnapshots = mockExecAll(); + // poetry.lock + fs.getSiblingFileName.mockReturnValueOnce('poetry.lock'); + fs.readLocalFile.mockResolvedValueOnce('Current poetry.lock'); + fs.writeLocalFile.mockImplementationOnce(() => { + throw new Error('not found'); + }); + const updatedDeps = [{ depName: 'dep1' }]; + expect( + await updateArtifacts({ + packageFileName: 'pyproject.toml', + updatedDeps, + newPackageFileContent: '{}', + config, + }) + ).toMatchObject([{ artifactError: { lockFile: 'poetry.lock' } }]); + expect(execSnapshots).toMatchObject([]); }); - const updatedDeps = [{ depName: 'dep1' }]; - expect( - await updateArtifacts({ - packageFileName: 'pyproject.toml', - updatedDeps, - newPackageFileContent: '{}', - config, - }) - ).toMatchObject([{ artifactError: { lockFile: 'poetry.lock' } }]); - expect(execSnapshots).toMatchObject([]); - }); - it('returns updated poetry.lock when doing lockfile maintenance', async () => { - const execSnapshots = mockExecAll(); - // poetry.lock - fs.getSiblingFileName.mockReturnValueOnce('poetry.lock'); - fs.readLocalFile.mockResolvedValueOnce('Old poetry.lock'); - fs.readLocalFile.mockResolvedValueOnce('New poetry.lock'); - expect( - await updateArtifacts({ - packageFileName: 'pyproject.toml', - updatedDeps: [], - newPackageFileContent: '{}', - config: { - ...config, - updateType: 'lockFileMaintenance', - }, - }) - ).toEqual([ - { - file: { - contents: 'New poetry.lock', - path: 'poetry.lock', - type: 'addition', + it('returns updated poetry.lock when doing lockfile maintenance', async () => { + const execSnapshots = mockExecAll(); + // poetry.lock + fs.getSiblingFileName.mockReturnValueOnce('poetry.lock'); + fs.readLocalFile.mockResolvedValueOnce('Old poetry.lock'); + fs.readLocalFile.mockResolvedValueOnce('New poetry.lock'); + expect( + await updateArtifacts({ + packageFileName: 'pyproject.toml', + updatedDeps: [], + newPackageFileContent: '{}', + config: { + ...config, + updateType: 'lockFileMaintenance', + }, + }) + ).toEqual([ + { + file: { + contents: 'New poetry.lock', + path: 'poetry.lock', + type: 'addition', + }, }, - }, - ]); - expect(execSnapshots).toMatchObject([ - { cmd: 'poetry update --lock --no-interaction' }, - ]); + ]); + expect(execSnapshots).toMatchObject([ + { cmd: 'poetry update --lock --no-interaction' }, + ]); + }); }); }); diff --git a/lib/modules/manager/poetry/artifacts.ts b/lib/modules/manager/poetry/artifacts.ts index f0248841a781e9..0bcb3c721830a3 100644 --- a/lib/modules/manager/poetry/artifacts.ts +++ b/lib/modules/manager/poetry/artifacts.ts @@ -17,24 +17,12 @@ import { find } from '../../../util/host-rules'; import { regEx } from '../../../util/regex'; import { PypiDatasource } from '../../datasource/pypi'; import { dependencyPattern } from '../pip_requirements/extract'; -import type { - UpdateArtifact, - UpdateArtifactsConfig, - UpdateArtifactsResult, -} from '../types'; +import type { UpdateArtifact, UpdateArtifactsResult } from '../types'; import type { PoetryFile, PoetryLock, PoetrySource } from './types'; export function getPythonConstraint( - existingLockFileContent: string, - config: UpdateArtifactsConfig + existingLockFileContent: string ): string | undefined | null { - const { constraints = {} } = config; - const { python } = constraints; - - if (python) { - logger.debug('Using python constraint from config'); - return python; - } try { const data = parse(existingLockFileContent) as PoetryLock; if (is.string(data?.metadata?.['python-versions'])) { @@ -48,7 +36,35 @@ export function getPythonConstraint( const pkgValRegex = regEx(`^${dependencyPattern}$`); -export function getPoetryRequirement(pyProjectContent: string): string | null { +const poetryConstraint: Record = { + '1.0': '<1.1.0', + '1.1': '<1.3.0', + '2.0': '>=1.3.0', +}; + +export function getPoetryRequirement( + pyProjectContent: string, + existingLockFileContent: string +): undefined | string | null { + // Read Poetry version from first line of poetry.lock + const firstLine = existingLockFileContent.split('\n')[0]; + const poetryVersionMatch = firstLine.match(/by Poetry ([\d\\.]+)/); + if (poetryVersionMatch?.[1]) { + logger.debug('Using poetry version from poetry.lock header'); + return poetryVersionMatch[1]; + } + try { + const data = parse(existingLockFileContent) as PoetryLock; + const lockVersion = data?.metadata?.['lock-version']; + if (is.string(lockVersion)) { + if (poetryConstraint[lockVersion]) { + logger.debug('Using poetry version from poetry.lock metadata'); + return poetryConstraint[lockVersion]; + } + } + } catch (err) { + // Do nothing + } try { const pyproject: PoetryFile = parse(pyProjectContent); // https://python-poetry.org/docs/pyproject/#poetry-and-pep-517 @@ -173,12 +189,12 @@ export async function updateArtifacts({ .join(' ')}` ); } - const pythonConstraint = getPythonConstraint( - existingLockFileContent, - config - ); + const pythonConstraint = + config?.constraints?.python ?? + getPythonConstraint(existingLockFileContent); const poetryConstraint = - config.constraints?.poetry ?? getPoetryRequirement(newPackageFileContent); + config.constraints?.poetry ?? + getPoetryRequirement(newPackageFileContent, existingLockFileContent); const extraEnv = { ...getSourceCredentialVars(newPackageFileContent, packageFileName), PIP_CACHE_DIR: await ensureCacheDir('pip'), diff --git a/lib/modules/manager/poetry/types.ts b/lib/modules/manager/poetry/types.ts index 6fa510ad515f9c..0c2eaa792548a4 100644 --- a/lib/modules/manager/poetry/types.ts +++ b/lib/modules/manager/poetry/types.ts @@ -40,6 +40,7 @@ export interface PoetryLockSection { export interface PoetryLock { metadata?: { + 'lock-version'?: string; 'python-versions'?: string; }; package?: PoetryLockSection[]; diff --git a/lib/modules/manager/regex/utils.spec.ts b/lib/modules/manager/regex/utils.spec.ts new file mode 100644 index 00000000000000..8e40e3a5ff69db --- /dev/null +++ b/lib/modules/manager/regex/utils.spec.ts @@ -0,0 +1,14 @@ +import { regEx } from '../../../util/regex'; +import * as utils from './utils'; + +describe('modules/manager/regex/utils', () => { + it('does not crash for lazy regex', () => { + const lazyMatch = regEx('(?.*?)', 'g'); + expect( + utils.regexMatchAll( + lazyMatch, + '1f699d2bfc99bbbe4c1ed5bb8fc21e6911d69c6e\n' + ) + ).toBeArray(); + }); +}); diff --git a/lib/modules/manager/regex/utils.ts b/lib/modules/manager/regex/utils.ts index 8f9a2743fcd219..ca5470b9f0d37f 100644 --- a/lib/modules/manager/regex/utils.ts +++ b/lib/modules/manager/regex/utils.ts @@ -85,12 +85,18 @@ export function regexMatchAll( ): RegExpMatchArray[] { const matches: RegExpMatchArray[] = []; let matchResult: RegExpMatchArray | null; + let iterations = 0; + const maxIterations = 10000; do { matchResult = regex.exec(content); if (matchResult) { matches.push(matchResult); } - } while (matchResult); + iterations += 1; + } while (matchResult && iterations < maxIterations); + if (iterations === maxIterations) { + logger.warn('Max iterations reached for matchStrings'); + } return matches; } diff --git a/lib/modules/manager/terraform/__fixtures__/docker.tf b/lib/modules/manager/terraform/__fixtures__/docker.tf index de04d0cd91d607..4490a11bff41ea 100644 --- a/lib/modules/manager/terraform/__fixtures__/docker.tf +++ b/lib/modules/manager/terraform/__fixtures__/docker.tf @@ -12,6 +12,10 @@ resource "docker_image" "ignore_variable" { pull_triggers = ["${data.docker_registry_image.ubuntu.sha256_digest}"] } +resource "docker_image" "proxy" { + name = "hub.proxy.test/bitnami/nginx:1.24.0" +} + # docker_container resources # https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs/resources/container diff --git a/lib/modules/manager/terraform/__fixtures__/helm.tf b/lib/modules/manager/terraform/__fixtures__/helm.tf index 6cae1674acbcaf..bef91bd3c646a3 100644 --- a/lib/modules/manager/terraform/__fixtures__/helm.tf +++ b/lib/modules/manager/terraform/__fixtures__/helm.tf @@ -53,3 +53,11 @@ resource "helm_release" "karpenter_oci_repo" { chart = "karpenter" version = "v0.22.1" } + +## chart in OCI registry +resource "helm_release" "proxy_oci_repo" { + name = "kube-prometheus" + repository = "oci://hub.proxy.test/bitnamicharts" + chart = "kube-prometheus" + version = "8.9.1" +} diff --git a/lib/modules/manager/terraform/base.ts b/lib/modules/manager/terraform/base.ts index 274052535b94aa..4947f14f8ddb8d 100644 --- a/lib/modules/manager/terraform/base.ts +++ b/lib/modules/manager/terraform/base.ts @@ -1,7 +1,7 @@ import is from '@sindresorhus/is'; import { regEx } from '../../../util/regex'; import { TerraformProviderDatasource } from '../../datasource/terraform-provider'; -import type { PackageDependency } from '../types'; +import type { ExtractConfig, PackageDependency } from '../types'; import type { TerraformDefinitionFile } from './hcl/types'; import type { ProviderLock } from './lockfile/types'; import { getLockedVersion, massageProviderLookupName } from './util'; @@ -20,7 +20,8 @@ export abstract class DependencyExtractor { */ abstract extract( hclRoot: TerraformDefinitionFile, - locks: ProviderLock[] + locks: ProviderLock[], + config: ExtractConfig ): PackageDependency[]; } diff --git a/lib/modules/manager/terraform/extract.spec.ts b/lib/modules/manager/terraform/extract.spec.ts index 4c5d53346d459f..c4e8246a9adc16 100644 --- a/lib/modules/manager/terraform/extract.spec.ts +++ b/lib/modules/manager/terraform/extract.spec.ts @@ -7,18 +7,18 @@ import type { RepoGlobalConfig } from '../../../config/types'; import * as hashicorp from '../../versioning/hashicorp'; import { extractPackageFile } from '.'; -const modules = Fixtures?.get('modules.tf'); -const bitbucketModules = Fixtures?.get('bitbucketModules.tf'); -const azureDevOpsModules = Fixtures?.get('azureDevOpsModules.tf'); -const providers = Fixtures?.get('providers.tf'); -const docker = Fixtures?.get('docker.tf'); -const kubernetes = Fixtures?.get('kubernetes.tf'); +const modules = Fixtures.get('modules.tf'); +const bitbucketModules = Fixtures.get('bitbucketModules.tf'); +const azureDevOpsModules = Fixtures.get('azureDevOpsModules.tf'); +const providers = Fixtures.get('providers.tf'); +const docker = Fixtures.get('docker.tf'); +const kubernetes = Fixtures.get('kubernetes.tf'); -const helm = Fixtures?.get('helm.tf'); -const lockedVersion = Fixtures?.get('lockedVersion.tf'); -const lockedVersionLockfile = Fixtures?.get('rangeStrategy.hcl'); -const terraformBlock = Fixtures?.get('terraformBlock.tf'); -const tfeWorkspaceBlock = Fixtures?.get('tfeWorkspace.tf'); +const helm = Fixtures.get('helm.tf'); +const lockedVersion = Fixtures.get('lockedVersion.tf'); +const lockedVersionLockfile = Fixtures.get('rangeStrategy.hcl'); +const terraformBlock = Fixtures.get('terraformBlock.tf'); +const tfeWorkspaceBlock = Fixtures.get('tfeWorkspace.tf'); const adminConfig: RepoGlobalConfig = { // `join` fixes Windows CI @@ -401,18 +401,19 @@ describe('modules/manager/terraform/extract', () => { }); it('extracts docker resources', async () => { - const res = await extractPackageFile(docker, 'docker.tf', {}); - expect(res?.deps).toHaveLength(6); + const res = await extractPackageFile(docker, 'docker.tf', { + registryAliases: { 'hub.proxy.test': 'index.docker.io' }, + }); + expect(res?.deps).toHaveLength(7); expect(res?.deps.filter((dep) => dep.skipReason)).toHaveLength(3); - expect(res?.deps).toIncludeAllPartialMembers([ + expect(res?.deps).toMatchObject([ { autoReplaceStringTemplate: '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', - currentValue: '1.7.8', datasource: 'docker', - depName: 'nginx', depType: 'docker_image', - replaceString: 'nginx:1.7.8', + replaceString: '${data.docker_registry_image.ubuntu.name}', + skipReason: 'contains-variable', }, { depType: 'docker_image', @@ -421,10 +422,20 @@ describe('modules/manager/terraform/extract', () => { { autoReplaceStringTemplate: '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', + currentValue: '1.7.8', datasource: 'docker', + depName: 'nginx', depType: 'docker_image', - replaceString: '${data.docker_registry_image.ubuntu.name}', - skipReason: 'contains-variable', + replaceString: 'nginx:1.7.8', + }, + { + autoReplaceStringTemplate: + 'hub.proxy.test/bitnami/nginx:{{#if newValue}}{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', + currentValue: '1.24.0', + datasource: 'docker', + depName: 'index.docker.io/bitnami/nginx', + depType: 'docker_image', + replaceString: 'hub.proxy.test/bitnami/nginx:1.24.0', }, { autoReplaceStringTemplate: @@ -574,35 +585,17 @@ describe('modules/manager/terraform/extract', () => { }); it('extract helm releases', async () => { - const res = await extractPackageFile(helm, 'helm.tf', {}); - expect(res?.deps).toHaveLength(8); + const res = await extractPackageFile(helm, 'helm.tf', { + registryAliases: { 'hub.proxy.test': 'index.docker.io' }, + }); + expect(res?.deps).toHaveLength(9); expect(res?.deps.filter((dep) => dep.skipReason)).toHaveLength(2); - expect(res?.deps).toIncludeAllPartialMembers([ - { - currentValue: '1.0.1', - datasource: 'helm', - depName: 'redis', - depType: 'helm_release', - registryUrls: ['https://charts.helm.sh/stable'], - }, - { - datasource: 'helm', - depName: 'redis', - depType: 'helm_release', - registryUrls: ['https://charts.helm.sh/stable'], - }, - { - datasource: 'helm', - depName: './charts/example', - depType: 'helm_release', - skipReason: 'local-chart', - }, + expect(res?.deps).toMatchObject([ { currentValue: '4.0.1', datasource: 'helm', depName: undefined, depType: 'helm_release', - registryUrls: ['https://charts.helm.sh/stable'], skipReason: 'invalid-name', }, { @@ -629,7 +622,33 @@ describe('modules/manager/terraform/extract', () => { datasource: 'docker', depName: 'karpenter', depType: 'helm_release', - registryUrls: ['https://public.ecr.aws/karpenter'], + packageName: 'public.ecr.aws/karpenter/karpenter', + }, + { + datasource: 'helm', + depName: './charts/example', + depType: 'helm_release', + skipReason: 'local-chart', + }, + { + currentValue: '8.9.1', + datasource: 'docker', + depName: 'kube-prometheus', + depType: 'helm_release', + packageName: 'index.docker.io/bitnamicharts/kube-prometheus', + }, + { + currentValue: '1.0.1', + datasource: 'helm', + depName: 'redis', + depType: 'helm_release', + registryUrls: ['https://charts.helm.sh/stable'], + }, + { + datasource: 'helm', + depName: 'redis', + depType: 'helm_release', + registryUrls: ['https://charts.helm.sh/stable'], }, ]); }); diff --git a/lib/modules/manager/terraform/extract.ts b/lib/modules/manager/terraform/extract.ts index 605f4d364b41ba..ef6ac343891362 100644 --- a/lib/modules/manager/terraform/extract.ts +++ b/lib/modules/manager/terraform/extract.ts @@ -37,7 +37,7 @@ export async function extractPackageFile( ); const dependencies = []; - const hclMap = hcl.parseHCL(content); + const hclMap = await hcl.parseHCL(content, fileName); if (is.nullOrUndefined(hclMap)) { logger.trace({ fileName }, 'failed to parse HCL file'); return null; @@ -46,7 +46,7 @@ export async function extractPackageFile( const locks = await extractLocksForPackageFile(fileName); for (const extractor of passedExtractors) { - const deps = extractor.extract(hclMap, locks); + const deps = extractor.extract(hclMap, locks, config); dependencies.push(...deps); } diff --git a/lib/modules/manager/terraform/extractors/resources/generic-docker-image-ref.spec.ts b/lib/modules/manager/terraform/extractors/resources/generic-docker-image-ref.spec.ts index dc8e78c5e1f8a8..6d48a30c6a27d2 100644 --- a/lib/modules/manager/terraform/extractors/resources/generic-docker-image-ref.spec.ts +++ b/lib/modules/manager/terraform/extractors/resources/generic-docker-image-ref.spec.ts @@ -4,7 +4,7 @@ describe('modules/manager/terraform/extractors/resources/generic-docker-image-re const extractor = new GenericDockerImageRefExtractor(); it('return empty array if no resource is found', () => { - const res = extractor.extract({}); + const res = extractor.extract({}, [], {}); expect(res).toBeArrayOfSize(0); }); }); diff --git a/lib/modules/manager/terraform/extractors/resources/generic-docker-image-ref.ts b/lib/modules/manager/terraform/extractors/resources/generic-docker-image-ref.ts index eb3f4c97d92628..7e89f12fd8200e 100644 --- a/lib/modules/manager/terraform/extractors/resources/generic-docker-image-ref.ts +++ b/lib/modules/manager/terraform/extractors/resources/generic-docker-image-ref.ts @@ -1,8 +1,9 @@ import is from '@sindresorhus/is'; import { getDep } from '../../../dockerfile/extract'; -import type { PackageDependency } from '../../../types'; +import type { ExtractConfig, PackageDependency } from '../../../types'; import { DependencyExtractor } from '../../base'; import type { TerraformDefinitionFile } from '../../hcl/types'; +import type { ProviderLock } from '../../lockfile/types'; import { generic_image_resource } from './utils'; export class GenericDockerImageRefExtractor extends DependencyExtractor { @@ -10,7 +11,11 @@ export class GenericDockerImageRefExtractor extends DependencyExtractor { return generic_image_resource.map((value) => `"${value.type}"`); } - extract(hclMap: TerraformDefinitionFile): PackageDependency[] { + extract( + hclMap: TerraformDefinitionFile, + _locks: ProviderLock[], + config: ExtractConfig + ): PackageDependency[] { const resourceTypMap = hclMap.resource; if (is.nullOrUndefined(resourceTypMap)) { return []; @@ -28,7 +33,9 @@ export class GenericDockerImageRefExtractor extends DependencyExtractor { // loop over instances of a resource type for (const instance of Object.values(resourceInstancesMap).flat()) { - dependencies.push(...this.walkPath({ depType: type }, instance, path)); + dependencies.push( + ...this.walkPath({ depType: type }, instance, path, config) + ); } } return dependencies; @@ -45,7 +52,8 @@ export class GenericDockerImageRefExtractor extends DependencyExtractor { private walkPath( abstractDep: PackageDependency, parentElement: unknown, - leftPath: string[] + leftPath: string[], + config: ExtractConfig ): PackageDependency[] { const dependencies: PackageDependency[] = []; // if there are no path elements left, we have reached the end of the path @@ -59,7 +67,7 @@ export class GenericDockerImageRefExtractor extends DependencyExtractor { }, ]; } - const test = getDep(parentElement); + const test = getDep(parentElement, true, config.registryAliases); const dep: PackageDependency = { ...abstractDep, ...test, @@ -87,11 +95,11 @@ export class GenericDockerImageRefExtractor extends DependencyExtractor { if (is.array(element)) { for (const arrayElement of element) { dependencies.push( - ...this.walkPath(abstractDep, arrayElement, leftPath.slice(1)) + ...this.walkPath(abstractDep, arrayElement, leftPath.slice(1), config) ); } return dependencies; } - return this.walkPath(abstractDep, element, leftPath.slice(1)); + return this.walkPath(abstractDep, element, leftPath.slice(1), config); } } diff --git a/lib/modules/manager/terraform/extractors/resources/helm-release.spec.ts b/lib/modules/manager/terraform/extractors/resources/helm-release.spec.ts index 84f2451c8030bb..7c89c540df6adb 100644 --- a/lib/modules/manager/terraform/extractors/resources/helm-release.spec.ts +++ b/lib/modules/manager/terraform/extractors/resources/helm-release.spec.ts @@ -4,7 +4,7 @@ describe('modules/manager/terraform/extractors/resources/helm-release', () => { const extractor = new HelmReleaseExtractor(); it('return empty array if no resource is found', () => { - const res = extractor.extract({}); + const res = extractor.extract({}, [], {}); expect(res).toBeArrayOfSize(0); }); }); diff --git a/lib/modules/manager/terraform/extractors/resources/helm-release.ts b/lib/modules/manager/terraform/extractors/resources/helm-release.ts index 87d6375a1bbef7..4e2ec94052ff35 100644 --- a/lib/modules/manager/terraform/extractors/resources/helm-release.ts +++ b/lib/modules/manager/terraform/extractors/resources/helm-release.ts @@ -1,11 +1,13 @@ import is from '@sindresorhus/is'; import { logger } from '../../../../../logger'; -import { DockerDatasource } from '../../../../datasource/docker'; +import { joinUrlParts } from '../../../../../util/url'; import { HelmDatasource } from '../../../../datasource/helm'; +import { getDep } from '../../../dockerfile/extract'; import { isOCIRegistry } from '../../../helmv3/utils'; -import type { PackageDependency } from '../../../types'; +import type { ExtractConfig, PackageDependency } from '../../../types'; import { DependencyExtractor } from '../../base'; import type { TerraformDefinitionFile } from '../../hcl/types'; +import type { ProviderLock } from '../../lockfile/types'; import { checkIfStringIsPath } from '../../util'; export class HelmReleaseExtractor extends DependencyExtractor { @@ -13,7 +15,11 @@ export class HelmReleaseExtractor extends DependencyExtractor { return [`"helm_release"`]; } - override extract(hclMap: TerraformDefinitionFile): PackageDependency[] { + override extract( + hclMap: TerraformDefinitionFile, + _locks: ProviderLock[], + config: ExtractConfig + ): PackageDependency[] { const dependencies = []; const helmReleases = hclMap?.resource?.helm_release; @@ -37,30 +43,48 @@ export class HelmReleaseExtractor extends DependencyExtractor { depName: helmRelease.chart, datasource: HelmDatasource.id, }; - if (is.nonEmptyString(helmRelease.repository)) { - if (isOCIRegistry(helmRelease.repository)) { - // For oci repos, we remove the oci:// and use the docker datasource - dep.registryUrls = [ - helmRelease.repository.replace('oci://', 'https://'), - ]; - dep.datasource = DockerDatasource.id; - } else { - dep.registryUrls = [helmRelease.repository]; - } - } - if (!helmRelease.chart) { + + dependencies.push(dep); + + if (!is.nonEmptyString(helmRelease.chart)) { dep.skipReason = 'invalid-name'; } else if (isOCIRegistry(helmRelease.chart)) { // For oci charts, we remove the oci:// and use the docker datasource dep.depName = helmRelease.chart.replace('oci://', ''); - dep.datasource = DockerDatasource.id; + this.processOCI(dep.depName, config, dep); } else if (checkIfStringIsPath(helmRelease.chart)) { dep.skipReason = 'local-chart'; + } else if (is.nonEmptyString(helmRelease.repository)) { + if (isOCIRegistry(helmRelease.repository)) { + // For oci charts, we remove the oci:// and use the docker datasource + this.processOCI( + joinUrlParts( + helmRelease.repository.replace('oci://', ''), + helmRelease.chart + ), + config, + dep + ); + } else { + dep.registryUrls = [helmRelease.repository]; + } } - - dependencies.push(dep); } return dependencies; } + + private processOCI( + depName: string, + config: ExtractConfig, + dep: PackageDependency + ): void { + const { depName: packageName, datasource } = getDep( + depName, + false, + config.registryAliases + ); + dep.packageName = packageName; + dep.datasource = datasource; + } } diff --git a/lib/modules/manager/terraform/hcl/index.spec.ts b/lib/modules/manager/terraform/hcl/index.spec.ts index bd9be234275687..70cf21a38a79a6 100644 --- a/lib/modules/manager/terraform/hcl/index.spec.ts +++ b/lib/modules/manager/terraform/hcl/index.spec.ts @@ -8,8 +8,8 @@ const lockedVersion = Fixtures.get('lockedVersion.tf'); describe('modules/manager/terraform/hcl/index', () => { describe('parseHCL()', () => { - it('should return flat modules', () => { - const res = parseHCL(modulesTF); + it('should return flat modules', async () => { + const res = await parseHCL(modulesTF, 'file.tf'); expect(res?.module).toBeDefined(); expect(Object.keys(res!.module!)).toBeArrayOfSize(6); expect(res).toMatchObject({ @@ -50,8 +50,8 @@ describe('modules/manager/terraform/hcl/index', () => { }); }); - it('should return nested terraform block', () => { - const res = parseHCL(lockedVersion); + it('should return nested terraform block', async () => { + const res = await parseHCL(lockedVersion, 'file.tf'); expect(res).toMatchObject({ terraform: [ { @@ -67,8 +67,8 @@ describe('modules/manager/terraform/hcl/index', () => { }); }); - it('should return resource blocks', () => { - const res = parseHCL(resourcesTF); + it('should return resource blocks', async () => { + const res = await parseHCL(resourcesTF, 'file.tf'); expect(res).toMatchObject({ resource: { docker_container: { diff --git a/lib/modules/manager/terraform/hcl/index.ts b/lib/modules/manager/terraform/hcl/index.ts index 61803c995dda4d..8691daf3801a5f 100644 --- a/lib/modules/manager/terraform/hcl/index.ts +++ b/lib/modules/manager/terraform/hcl/index.ts @@ -1,10 +1,12 @@ -import * as hcl_parser from 'hcl2-parser'; - +import { parse } from '@cdktf/hcl2json'; import type { TerraformDefinitionFile } from './types'; -export function parseHCL(content: string): TerraformDefinitionFile | null { +export async function parseHCL( + content: string, + fileName: string +): Promise { try { - return hcl_parser.parseToObject(content)[0]; + return await parse(fileName, content); } catch (err) /* istanbul ignore next */ { return null; } diff --git a/lib/modules/manager/types.ts b/lib/modules/manager/types.ts index 6e26451238eff8..dfb040005aaee0 100644 --- a/lib/modules/manager/types.ts +++ b/lib/modules/manager/types.ts @@ -174,6 +174,7 @@ export interface Upgrade> extends PackageDependency { isLockFileMaintenance?: boolean; isRemediation?: boolean; isVulnerabilityAlert?: boolean; + vulnerabilitySeverity?: string; registryUrls?: string[] | null; currentVersion?: string; replaceString?: string; diff --git a/lib/modules/platform/api.ts b/lib/modules/platform/api.ts index bb67f2c39432a4..973f6b75d39db6 100644 --- a/lib/modules/platform/api.ts +++ b/lib/modules/platform/api.ts @@ -6,6 +6,7 @@ import * as codecommit from './codecommit'; import * as gitea from './gitea'; import * as github from './github'; import * as gitlab from './gitlab'; +import * as local from './local'; import type { Platform } from './types'; const api = new Map(); @@ -18,3 +19,4 @@ api.set(codecommit.id, codecommit); api.set(gitea.id, gitea); api.set(github.id, github); api.set(gitlab.id, gitlab); +api.set(local.id, local); diff --git a/lib/modules/platform/azure/__snapshots__/index.spec.ts.snap b/lib/modules/platform/azure/__snapshots__/index.spec.ts.snap index 21858fbbb4b3cc..3be9ce9411245c 100644 --- a/lib/modules/platform/azure/__snapshots__/index.spec.ts.snap +++ b/lib/modules/platform/azure/__snapshots__/index.spec.ts.snap @@ -274,7 +274,7 @@ exports[`modules/platform/azure/index initRepo should initialise the config for } `; -exports[`modules/platform/azure/index updatePr(prNo, title, body) should close the PR 1`] = ` +exports[`modules/platform/azure/index updatePr(prNo, title, body, platformOptions) should close the PR 1`] = ` [ [ { @@ -288,7 +288,9 @@ exports[`modules/platform/azure/index updatePr(prNo, title, body) should close t ] `; -exports[`modules/platform/azure/index updatePr(prNo, title, body) should reopen the PR 1`] = ` +exports[`modules/platform/azure/index updatePr(prNo, title, body, platformOptions) should re-approve the PR 1`] = `undefined`; + +exports[`modules/platform/azure/index updatePr(prNo, title, body, platformOptions) should reopen the PR 1`] = ` [ [ { @@ -308,7 +310,7 @@ exports[`modules/platform/azure/index updatePr(prNo, title, body) should reopen ] `; -exports[`modules/platform/azure/index updatePr(prNo, title, body) should update the PR 1`] = ` +exports[`modules/platform/azure/index updatePr(prNo, title, body, platformOptions) should update the PR 1`] = ` [ [ { @@ -321,7 +323,7 @@ exports[`modules/platform/azure/index updatePr(prNo, title, body) should update ] `; -exports[`modules/platform/azure/index updatePr(prNo, title, body) should update the PR without description 1`] = ` +exports[`modules/platform/azure/index updatePr(prNo, title, body, platformOptions) should update the PR without description 1`] = ` [ [ { diff --git a/lib/modules/platform/azure/index.spec.ts b/lib/modules/platform/azure/index.spec.ts index da8a998a7b702e..29493cd2421217 100644 --- a/lib/modules/platform/azure/index.spec.ts +++ b/lib/modules/platform/azure/index.spec.ts @@ -812,7 +812,7 @@ describe('modules/platform/azure/index', () => { }); }); - describe('updatePr(prNo, title, body)', () => { + describe('updatePr(prNo, title, body, platformOptions)', () => { it('should update the PR', async () => { await initRepo({ repository: 'some/repo' }); const updatePullRequest = jest.fn(); @@ -881,6 +881,43 @@ describe('modules/platform/azure/index', () => { }); expect(updatePullRequest.mock.calls).toMatchSnapshot(); }); + + it('should re-approve the PR', async () => { + await initRepo({ repository: 'some/repo' }); + const prResult = { + pullRequestId: 456, + createdBy: { + id: 123, + url: 'user-url', + }, + }; + const prUpdateResult = { + reviewerUrl: prResult.createdBy.url, + vote: AzurePrVote.Approved, + isFlagged: false, + isRequired: false, + }; + const updateFn = jest.fn(() => prUpdateResult); + azureApi.gitApi.mockImplementationOnce( + () => + ({ + updatePullRequest: jest.fn(() => prResult), + createPullRequestReviewer: updateFn, + getPullRequestById: jest.fn(() => ({ + pullRequestId: prResult.pullRequestId, + createdBy: prResult.createdBy, + })), + } as any) + ); + const pr = await azure.updatePr({ + number: prResult.pullRequestId, + prTitle: 'The Title', + prBody: 'Hello world', + platformOptions: { autoApprove: true }, + }); + expect(updateFn).toHaveBeenCalled(); + expect(pr).toMatchSnapshot(); + }); }); describe('ensureComment', () => { diff --git a/lib/modules/platform/azure/index.ts b/lib/modules/platform/azure/index.ts index b1eb1430369d14..def6948ea10b12 100644 --- a/lib/modules/platform/azure/index.ts +++ b/lib/modules/platform/azure/index.ts @@ -307,7 +307,9 @@ export async function findPr({ ); if (prTitle) { - prsFiltered = prsFiltered.filter((item) => item.title === prTitle); + prsFiltered = prsFiltered.filter( + (item) => item.title.toUpperCase() === prTitle.toUpperCase() + ); } switch (state) { @@ -506,6 +508,7 @@ export async function updatePr({ prTitle: title, prBody: body, state, + platformOptions, }: UpdatePrConfig): Promise { logger.debug(`updatePr(${prNo}, ${title}, body)`); @@ -527,6 +530,21 @@ export async function updatePr({ } else if (state === 'closed') { objToUpdate.status = PullRequestStatus.Abandoned; } + if (platformOptions?.autoApprove) { + const pr = await azureApiGit.getPullRequestById(prNo, config.project); + await azureApiGit.createPullRequestReviewer( + { + reviewerUrl: pr.createdBy!.url, + vote: AzurePrVote.Approved, + isFlagged: false, + isRequired: false, + }, + config.repoId, + // TODO #7154 + pr.pullRequestId!, + pr.createdBy!.id! + ); + } await azureApiGit.updatePullRequest(objToUpdate, config.repoId, prNo); } @@ -759,6 +777,10 @@ export function massageMarkdown(input: string): string { 'you tick the rebase/retry checkbox', 'rename PR to start with "rebase!"' ) + .replace( + 'checking the rebase/retry box above', + 'renaming the PR to start with "rebase!"' + ) .replace(regEx(`\n---\n\n.*?.*?\n`), '') .replace(regEx(//g), ''); } diff --git a/lib/modules/platform/bitbucket-server/index.ts b/lib/modules/platform/bitbucket-server/index.ts index b8258e58043656..9da5a7fff7a2eb 100644 --- a/lib/modules/platform/bitbucket-server/index.ts +++ b/lib/modules/platform/bitbucket-server/index.ts @@ -289,7 +289,7 @@ const isRelevantPr = (branchName: string, prTitle: string | null | undefined, state: string) => (p: Pr): boolean => p.sourceBranch === branchName && - (!prTitle || p.title === prTitle) && + (!prTitle || p.title.toUpperCase() === prTitle.toUpperCase()) && matchesState(p.state, state); // TODO: coverage (#9624) @@ -974,6 +974,10 @@ export function massageMarkdown(input: string): string { 'you tick the rebase/retry checkbox', 'rename PR to start with "rebase!"' ) + .replace( + 'checking the rebase/retry box above', + 'renaming the PR to start with "rebase!"' + ) .replace(regEx(/<\/?summary>/g), '**') .replace(regEx(/<\/?details>/g), '') .replace(regEx(`\n---\n\n.*?.*?(\n|$)`), '') diff --git a/lib/modules/platform/bitbucket/index.ts b/lib/modules/platform/bitbucket/index.ts index b76e66576851c6..b12eb09a114dae 100644 --- a/lib/modules/platform/bitbucket/index.ts +++ b/lib/modules/platform/bitbucket/index.ts @@ -293,7 +293,7 @@ export async function findPr({ const pr = prList.find( (p) => p.sourceBranch === branchName && - (!prTitle || p.title === prTitle) && + (!prTitle || p.title.toUpperCase() === prTitle.toUpperCase()) && matchesState(p.state, state) ); if (pr) { @@ -519,6 +519,10 @@ export function massageMarkdown(input: string): string { 'you tick the rebase/retry checkbox', 'by renaming this PR to start with "rebase!"' ) + .replace( + 'checking the rebase/retry box above', + 'renaming the PR to start with "rebase!"' + ) .replace(regEx(/<\/?summary>/g), '**') .replace(regEx(/<\/?(details|blockquote)>/g), '') .replace(regEx(`\n---\n\n.*?.*?\n`), '') @@ -798,7 +802,10 @@ export async function createPr({ if (platformOptions?.bbUseDefaultReviewers) { const reviewersResponse = ( await bitbucketHttp.getJson>( - `/2.0/repositories/${config.repository}/effective-default-reviewers` + `/2.0/repositories/${config.repository}/effective-default-reviewers`, + { + paginate: true, + } ) ).body; reviewers = reviewersResponse.values.map((reviewer: EffectiveReviewer) => ({ diff --git a/lib/modules/platform/codecommit/index.ts b/lib/modules/platform/codecommit/index.ts index e689415cb39457..6ce173083c4901 100644 --- a/lib/modules/platform/codecommit/index.ts +++ b/lib/modules/platform/codecommit/index.ts @@ -208,7 +208,9 @@ export async function findPr({ ); if (prTitle) { - prsFiltered = prsFiltered.filter((item) => item.title === prTitle); + prsFiltered = prsFiltered.filter( + (item) => item.title.toUpperCase() === prTitle.toUpperCase() + ); } switch (state) { @@ -307,6 +309,10 @@ export function massageMarkdown(input: string): string { 'you tick the rebase/retry checkbox', 'rename PR to start with "rebase!"' ) + .replace( + 'checking the rebase/retry box above', + 'renaming the PR to start with "rebase!"' + ) .replace(regEx(/<\/?summary>/g), '**') .replace(regEx(/<\/?details>/g), '') .replace(regEx(`\n---\n\n.*?.*?\n`), '') diff --git a/lib/modules/platform/default-scm.spec.ts b/lib/modules/platform/default-scm.spec.ts index 023948fa3dacae..7c9f7128d8de3a 100644 --- a/lib/modules/platform/default-scm.spec.ts +++ b/lib/modules/platform/default-scm.spec.ts @@ -48,4 +48,16 @@ describe('modules/platform/default-scm', () => { await defaultGitScm.isBranchModified('branchName'); expect(git.isBranchModified).toHaveBeenCalledTimes(1); }); + + it('delegate getFileList to util/git', async () => { + git.getFileList.mockResolvedValueOnce([]); + await defaultGitScm.getFileList(); + expect(git.getFileList).toHaveBeenCalledTimes(1); + }); + + it('delegate checkoutBranch to util/git', async () => { + git.checkoutBranch.mockResolvedValueOnce(''); + await defaultGitScm.checkoutBranch('branchName'); + expect(git.checkoutBranch).toHaveBeenCalledTimes(1); + }); }); diff --git a/lib/modules/platform/default-scm.ts b/lib/modules/platform/default-scm.ts index d21f3cff0b832e..f210500a9a0d42 100644 --- a/lib/modules/platform/default-scm.ts +++ b/lib/modules/platform/default-scm.ts @@ -30,4 +30,12 @@ export class DefaultGitScm implements PlatformScm { isBranchModified(branchName: string): Promise { return git.isBranchModified(branchName); } + + getFileList(): Promise { + return git.getFileList(); + } + + checkoutBranch(branchName: string): Promise { + return git.checkoutBranch(branchName); + } } diff --git a/lib/modules/platform/github/index.spec.ts b/lib/modules/platform/github/index.spec.ts index 51b25a6d951557..0789f597bcb205 100644 --- a/lib/modules/platform/github/index.spec.ts +++ b/lib/modules/platform/github/index.spec.ts @@ -174,9 +174,15 @@ describe('modules/platform/github/index', () => { .reply(200, [ { full_name: 'a/b', + archived: false, }, { full_name: 'c/d', + archived: false, + }, + { + full_name: 'e/f', + archived: true, }, null, ]); @@ -227,9 +233,15 @@ describe('modules/platform/github/index', () => { repositories: [ { full_name: 'a/b', + archived: false, }, { full_name: 'c/d', + archived: false, + }, + { + full_name: 'e/f', + archived: true, }, null, ], diff --git a/lib/modules/platform/github/index.ts b/lib/modules/platform/github/index.ts index 8f5bdab301d5ab..a82a532eb04a29 100644 --- a/lib/modules/platform/github/index.ts +++ b/lib/modules/platform/github/index.ts @@ -191,20 +191,24 @@ export async function getRepos(): Promise { try { if (platformConfig.isGHApp) { const res = await githubApi.getJson<{ - repositories: { full_name: string }[]; + repositories: GhRestRepo[]; }>(`installation/repositories?per_page=100`, { paginationField: 'repositories', paginate: 'all', }); return res.body.repositories .filter(is.nonEmptyObject) + .filter((repo) => !repo.archived) .map((repo) => repo.full_name); } else { - const res = await githubApi.getJson<{ full_name: string }[]>( + const res = await githubApi.getJson( `user/repos?per_page=100`, { paginate: 'all' } ); - return res.body.filter(is.nonEmptyObject).map((repo) => repo.full_name); + return res.body + .filter(is.nonEmptyObject) + .filter((repo) => !repo.archived) + .map((repo) => repo.full_name); } } catch (err) /* istanbul ignore next */ { logger.error({ err }, `GitHub getRepos error`); @@ -712,7 +716,7 @@ export async function findPr({ return false; } - if (prTitle && prTitle !== p.title) { + if (prTitle && prTitle.toUpperCase() !== p.title.toUpperCase()) { return false; } diff --git a/lib/modules/platform/github/types.ts b/lib/modules/platform/github/types.ts index fd06dcf31b2d82..ef01406661370b 100644 --- a/lib/modules/platform/github/types.ts +++ b/lib/modules/platform/github/types.ts @@ -26,6 +26,7 @@ export interface GhRestRepo { owner: { login: string; }; + archived: boolean; } export interface GhRestPr { diff --git a/lib/modules/platform/gitlab/index.md b/lib/modules/platform/gitlab/index.md index a4b6c25f42710f..31ab87bd630469 100644 --- a/lib/modules/platform/gitlab/index.md +++ b/lib/modules/platform/gitlab/index.md @@ -29,7 +29,7 @@ Remember to set `platform=gitlab` somewhere in your Renovate config file. If you're using a private [GitLab container registry](https://docs.gitlab.com/ee/user/packages/container_registry/), you must: -- Set the `RENOVATE_HOST_RULES` CI variable to `[{"matchHost": "${CI_REGISTRY}","username": "${GITLAB_USER_NAME}","password": "${RENOVATE_TOKEN}"}]`. +- Set the `RENOVATE_HOST_RULES` CI variable to `[{"matchHost": "${CI_REGISTRY}","username": "${GITLAB_USER_NAME}","password": "${RENOVATE_TOKEN}", "hostType": "docker"}]`. - Make sure the user that owns the `RENOVATE_TOKEN` PAT is a member of the corresponding GitLab projects/groups with the right permissions. - Make sure the `RENOVATE_TOKEN` PAT has the `read_registry` scope. diff --git a/lib/modules/platform/gitlab/index.ts b/lib/modules/platform/gitlab/index.ts index 24b69b9865ed25..eb3af69a208e34 100644 --- a/lib/modules/platform/gitlab/index.ts +++ b/lib/modules/platform/gitlab/index.ts @@ -752,7 +752,7 @@ export async function findPr({ prList.find( (p: { sourceBranch: string; title: string; state: string }) => p.sourceBranch === branchName && - (!prTitle || p.title === prTitle) && + (!prTitle || p.title.toUpperCase() === prTitle.toUpperCase()) && matchesState(p.state, state) ) ?? null ); diff --git a/lib/modules/platform/local/index.md b/lib/modules/platform/local/index.md new file mode 100644 index 00000000000000..15ede0932e21cb --- /dev/null +++ b/lib/modules/platform/local/index.md @@ -0,0 +1,22 @@ +# Local + +With the "local" platform you can perform dry runs of Renovate against the local file system. +This can be handy when testing a new Renovate configuration for example. + +## Usage + +Run the `renovate --platform=local` command in the directory you want Renovate to run in. +In this mode, Renovate defaults to `dryRun=lookup`. + +Avoid giving "repositories" arguments, as this command can only run in a _single_ directory, and it can only run in the _current working_ directory. + +You may run the command above on "plain" directories, or "Git directories". +You don't need to provide any config, as the command will run with or without "repo config". + +The command doesn't do any "compare" - or before and after analysis - if you want to test a new config then you must manually compare. + +## Limitations + +- `local>` presets can't be resolved. Normally these would point to the local platform such as GitHub, but in the case of running locally, it does not exist +- `baseBranches` are ignored +- Branch creation is not supported diff --git a/lib/modules/platform/local/index.spec.ts b/lib/modules/platform/local/index.spec.ts new file mode 100644 index 00000000000000..c226d334d79cbe --- /dev/null +++ b/lib/modules/platform/local/index.spec.ts @@ -0,0 +1,128 @@ +import * as platform from './index'; + +describe('modules/platform/local/index', () => { + describe('initPlatform', () => { + it('returns input', async () => { + expect(await platform.initPlatform({})).toMatchInlineSnapshot(` + { + "dryRun": "lookup", + "endpoint": "local", + "persistRepoData": true, + "requireConfig": "optional", + } + `); + }); + }); + + describe('getRepos', () => { + it('returns empty array', async () => { + expect(await platform.getRepos()).toEqual([]); + }); + }); + + describe('initRepo', () => { + it('returns object', async () => { + expect(await platform.initRepo()).toMatchInlineSnapshot(` + { + "defaultBranch": "", + "isFork": false, + "repoFingerprint": "", + } + `); + }); + }); + + describe('dummy functions', () => { + it('getRepoForceRebase', async () => { + expect(await platform.getRepoForceRebase()).toBe(false); + }); + + it('findIssue', async () => { + expect(await platform.findIssue()).toBeNull(); + }); + + it('getIssueList', async () => { + expect(await platform.getIssueList()).toEqual([]); + }); + + it('getRawFile', async () => { + expect(await platform.getRawFile()).toBeNull(); + }); + + it('getJsonFile', async () => { + expect(await platform.getJsonFile()).toBeNull(); + }); + + it('getPrList', async () => { + expect(await platform.getPrList()).toEqual([]); + }); + + it('ensureIssueClosing', async () => { + expect(await platform.ensureIssueClosing()).toBeUndefined(); + }); + + it('ensureIssue', async () => { + expect(await platform.ensureIssue()).toBeNull(); + }); + + it('massageMarkdown', () => { + expect(platform.massageMarkdown('foo')).toBe('foo'); + }); + + it('updatePr', async () => { + expect(await platform.updatePr()).toBeUndefined(); + }); + + it('mergePr', async () => { + expect(await platform.mergePr()).toBe(false); + }); + + it('addReviewers', async () => { + expect(await platform.addReviewers()).toBeUndefined(); + }); + + it('addAssignees', async () => { + expect(await platform.addAssignees()).toBeUndefined(); + }); + + it('createPr', async () => { + expect(await platform.createPr()).toBeNull(); + }); + + it('deleteLabel', async () => { + expect(await platform.deleteLabel()).toBeUndefined(); + }); + + it('setBranchStatus', async () => { + expect(await platform.setBranchStatus()).toBeUndefined(); + }); + + it('getBranchStatus', async () => { + expect(await platform.getBranchStatus()).toBe('red'); + }); + + it('getBranchStatusCheck', async () => { + expect(await platform.getBranchStatusCheck()).toBeNull(); + }); + + it('ensureCommentRemoval', async () => { + expect(await platform.ensureCommentRemoval()).toBeUndefined(); + }); + + it('ensureComment', async () => { + expect(await platform.ensureComment()).toBeFalse(); + }); + + it('getPr', async () => { + expect(await platform.getPr()).toBeNull(); + }); + + it('findPr', async () => { + expect(await platform.findPr()).toBeNull(); + }); + + it('getBranchPr', async () => { + expect(await platform.getBranchPr()).toBeNull(); + }); + }); +}); diff --git a/lib/modules/platform/local/index.ts b/lib/modules/platform/local/index.ts new file mode 100644 index 00000000000000..1c88d787371daf --- /dev/null +++ b/lib/modules/platform/local/index.ts @@ -0,0 +1,123 @@ +import type { BranchStatus } from '../../../types'; +import type { + Issue, + PlatformParams, + PlatformResult, + Pr, + RepoResult, +} from '../types'; + +export const id = 'local'; + +export function initPlatform(params: PlatformParams): Promise { + return Promise.resolve({ + dryRun: 'lookup', + endpoint: 'local', + persistRepoData: true, + requireConfig: 'optional', + }); +} + +export function getRepos(): Promise { + return Promise.resolve([]); +} + +export function initRepo(): Promise { + return Promise.resolve({ + defaultBranch: '', + isFork: false, + repoFingerprint: '', + }); +} + +export function getRepoForceRebase(): Promise { + return Promise.resolve(false); +} + +export function findIssue(): Promise { + return Promise.resolve(null); +} + +export function getIssueList(): Promise { + return Promise.resolve([]); +} + +export function getRawFile(): Promise { + return Promise.resolve(null); +} + +export function getJsonFile(): Promise | null> { + return Promise.resolve(null); +} + +export function getPrList(): Promise { + return Promise.resolve([]); +} + +export function ensureIssueClosing(): Promise { + return Promise.resolve(); +} + +export function ensureIssue(): Promise { + return Promise.resolve(null); +} + +export function massageMarkdown(input: string): string { + return input; +} + +export function updatePr(): Promise { + return Promise.resolve(); +} + +export function mergePr(): Promise { + return Promise.resolve(false); +} + +export function addReviewers(): Promise { + return Promise.resolve(); +} + +export function addAssignees(): Promise { + return Promise.resolve(); +} + +export function createPr(): Promise { + return Promise.resolve(null); +} + +export function deleteLabel(): Promise { + return Promise.resolve(); +} + +export function setBranchStatus(): Promise { + return Promise.resolve(); +} + +export function getBranchStatus(): Promise { + return Promise.resolve('red'); +} + +export function getBranchStatusCheck(): Promise { + return Promise.resolve(null); +} + +export function ensureCommentRemoval(): Promise { + return Promise.resolve(); +} + +export function ensureComment(): Promise { + return Promise.resolve(false); +} + +export function getPr(): Promise { + return Promise.resolve(null); +} + +export function findPr(): Promise { + return Promise.resolve(null); +} + +export function getBranchPr(): Promise { + return Promise.resolve(null); +} diff --git a/lib/modules/platform/local/scm.spec.ts b/lib/modules/platform/local/scm.spec.ts new file mode 100644 index 00000000000000..6549475ad55d93 --- /dev/null +++ b/lib/modules/platform/local/scm.spec.ts @@ -0,0 +1,68 @@ +import { execSync as _execSync } from 'node:child_process'; +import { mockedFunction } from '../../../../test/util'; +import { LocalFs } from './scm'; + +jest.mock('node:child_process'); +const execSync = mockedFunction(_execSync); + +describe('modules/platform/local/scm', () => { + let localFs: LocalFs; + + beforeEach(() => { + localFs = new LocalFs(); + }); + + describe('dummy functions', () => { + it('behindBaseBranch', async () => { + expect(await localFs.isBranchBehindBase('', '')).toBe(false); + }); + + it('isBranchModified', async () => { + expect(await localFs.isBranchModified('')).toBe(false); + }); + + it('isBranchConflicted', async () => { + expect(await localFs.isBranchConflicted('', '')).toBe(false); + }); + + it('branchExists', async () => { + expect(await localFs.branchExists('')).toBe(true); + }); + + it('getBranchCommit', async () => { + expect(await localFs.getBranchCommit('')).toBeNull(); + }); + + it('deleteBranch', async () => { + expect(await localFs.deleteBranch('')).toBeUndefined(); + }); + + it('commitAndPush', async () => { + expect(await localFs.commitAndPush({} as any)).toBeNull(); + }); + + it('checkoutBranch', async () => { + expect(await localFs.checkoutBranch('')).toBe(''); + }); + }); + + describe('getFileList', () => { + it('should return file list using git', async () => { + execSync.mockReturnValueOnce('file1\nfile2'); + expect(await localFs.getFileList()).toHaveLength(2); + }); + + it('should return file list using glob', async () => { + execSync.mockImplementationOnce(() => { + throw new Error(); + }); + jest.mock('glob', () => ({ + glob: jest + .fn() + .mockImplementation(() => Promise.resolve(['file1', 'file2'])), + })); + + expect(await localFs.getFileList()).toHaveLength(2); + }); + }); +}); diff --git a/lib/modules/platform/local/scm.ts b/lib/modules/platform/local/scm.ts new file mode 100644 index 00000000000000..5b01391572c110 --- /dev/null +++ b/lib/modules/platform/local/scm.ts @@ -0,0 +1,51 @@ +import { execSync } from 'node:child_process'; +import { glob } from 'glob'; +import { logger } from '../../../logger'; +import type { CommitFilesConfig, CommitSha } from '../../../util/git/types'; +import type { PlatformScm } from '../types'; + +let fileList: string[] | undefined; +export class LocalFs implements PlatformScm { + isBranchBehindBase(branchName: string, baseBranch: string): Promise { + return Promise.resolve(false); + } + isBranchModified(branchName: string): Promise { + return Promise.resolve(false); + } + isBranchConflicted(baseBranch: string, branch: string): Promise { + return Promise.resolve(false); + } + branchExists(branchName: string): Promise { + return Promise.resolve(true); + } + getBranchCommit(branchName: string): Promise { + return Promise.resolve(null); + } + deleteBranch(branchName: string): Promise { + return Promise.resolve(); + } + commitAndPush(commitConfig: CommitFilesConfig): Promise { + return Promise.resolve(null); + } + + async getFileList(): Promise { + try { + // fetch file list using git + const stdout = execSync('git ls-files', { encoding: 'utf-8' }); + logger.debug('Got file list using git'); + fileList = stdout.split('\n'); + } catch (err) { + logger.debug('Could not get file list using git, using glob instead'); + fileList ??= await glob('**', { + dot: true, + nodir: true, + }); + } + + return fileList; + } + + checkoutBranch(branchName: string): Promise { + return Promise.resolve(''); + } +} diff --git a/lib/modules/platform/scm.ts b/lib/modules/platform/scm.ts index 9a26a35594401d..8f80217ec36da5 100644 --- a/lib/modules/platform/scm.ts +++ b/lib/modules/platform/scm.ts @@ -3,6 +3,7 @@ import type { PlatformId } from '../../constants'; import { PLATFORM_NOT_FOUND } from '../../constants/error-messages'; import { DefaultGitScm } from './default-scm'; import { GithubScm } from './github/scm'; +import { LocalFs } from './local/scm'; import type { PlatformScm } from './types'; export const platformScmImpls = new Map>(); @@ -13,6 +14,7 @@ platformScmImpls.set('bitbucket-server', DefaultGitScm); platformScmImpls.set('gitea', DefaultGitScm); platformScmImpls.set('github', GithubScm); platformScmImpls.set('gitlab', DefaultGitScm); +platformScmImpls.set('local', LocalFs); let _scm: PlatformScm | undefined; diff --git a/lib/modules/platform/types.ts b/lib/modules/platform/types.ts index 57917c8ca690e3..12205f74099bb2 100644 --- a/lib/modules/platform/types.ts +++ b/lib/modules/platform/types.ts @@ -225,4 +225,6 @@ export interface PlatformScm { getBranchCommit(branchName: string): Promise; deleteBranch(branchName: string): Promise; commitAndPush(commitConfig: CommitFilesConfig): Promise; + getFileList(): Promise; + checkoutBranch(branchName: string): Promise; } diff --git a/lib/util/array.spec.ts b/lib/util/array.spec.ts new file mode 100644 index 00000000000000..57356be7de65df --- /dev/null +++ b/lib/util/array.spec.ts @@ -0,0 +1,12 @@ +import { isNotNullOrUndefined } from './array'; + +describe('util/array', () => { + it.each` + a | exp + ${null} | ${false} + ${undefined} | ${false} + ${{ name: 'foo' }} | ${true} + `('.isNotNullOrUndefined', ({ a, exp }) => { + expect(isNotNullOrUndefined(a)).toEqual(exp); + }); +}); diff --git a/lib/util/array.ts b/lib/util/array.ts index 6862260bc20bc5..0175fabf26f8e4 100644 --- a/lib/util/array.ts +++ b/lib/util/array.ts @@ -10,3 +10,12 @@ export function coerceArray(input: T[] | null | undefined): T[] { export function sortNumeric(a: number, b: number): number { return a - b; } + +// Useful for filtering an array so that it includes values that are not null or +// undefined. This predicate acts as a type guard so that the resulting type for +// `values.filter(isNotNullOrUndefined)` is `T[]`. +export function isNotNullOrUndefined( + value: T | undefined | null +): value is T { + return !is.nullOrUndefined(value); +} diff --git a/lib/util/fs/index.spec.ts b/lib/util/fs/index.spec.ts index f200818843e2d7..361f4eff62dda9 100644 --- a/lib/util/fs/index.spec.ts +++ b/lib/util/fs/index.spec.ts @@ -134,6 +134,11 @@ describe('util/fs/index', () => { }); describe('deleteLocalFile', () => { + it('throws if platform is local', async () => { + GlobalConfig.set({ platform: 'local' }); + await expect(deleteLocalFile('foo/bar/file.txt')).rejects.toThrow(); + }); + it('deletes file', async () => { const filePath = `${localDir}/foo/bar/file.txt`; await fs.outputFile(filePath, 'foobar'); diff --git a/lib/util/fs/index.ts b/lib/util/fs/index.ts index 5493a6397f8493..2a9b2793975755 100644 --- a/lib/util/fs/index.ts +++ b/lib/util/fs/index.ts @@ -65,6 +65,10 @@ export async function writeLocalFile( } export async function deleteLocalFile(fileName: string): Promise { + // This a failsafe and hopefully will never be triggered + if (GlobalConfig.get('platform') === 'local') { + throw new Error('Cannot delete file when platform=local'); + } const localDir = GlobalConfig.get('localDir'); if (localDir) { const localFileName = ensureLocalPath(fileName); diff --git a/lib/util/git/index.ts b/lib/util/git/index.ts index 806bda394b9889..5d086bef8612ca 100644 --- a/lib/util/git/index.ts +++ b/lib/util/git/index.ts @@ -377,8 +377,16 @@ export function isCloned(): boolean { export async function syncGit(): Promise { if (gitInitialized) { + // istanbul ignore if + if (process.env.RENOVATE_X_CLEAR_HOOKS) { + await git.raw(['config', 'core.hooksPath', '/dev/null']); + } return; } + // istanbul ignore if: failsafe + if (GlobalConfig.get('platform') === 'local') { + throw new Error('Cannot sync git when platform=local'); + } gitInitialized = true; const localDir = GlobalConfig.get('localDir')!; logger.debug(`Initializing git repository into ${localDir}`); diff --git a/lib/util/git/private-key.spec.ts b/lib/util/git/private-key.spec.ts index d3921992b2df42..9ecbbe74f0b261 100644 --- a/lib/util/git/private-key.spec.ts +++ b/lib/util/git/private-key.spec.ts @@ -1,9 +1,10 @@ +import { Fixtures } from '../../../test/fixtures'; import { mocked } from '../../../test/util'; import * as exec_ from '../exec'; import { configSigningKey, writePrivateKey } from './private-key'; import { setPrivateKey } from '.'; -jest.mock('fs-extra'); +jest.mock('fs-extra', () => Fixtures.fsExtra()); jest.mock('../exec'); const exec = mocked(exec_); diff --git a/lib/util/http/bitbucket.spec.ts b/lib/util/http/bitbucket.spec.ts index ac7bb06e010322..7f74909d1c8175 100644 --- a/lib/util/http/bitbucket.spec.ts +++ b/lib/util/http/bitbucket.spec.ts @@ -55,4 +55,34 @@ describe('util/http/bitbucket', () => { statusCode: 200, }); }); + + it('paginates', async () => { + httpMock + .scope(baseUrl) + .get('/some-url') + .reply(200, { + values: ['a'], + page: '1', + next: `${baseUrl}/some-url?page=2`, + }) + .get('/some-url?page=2') + .reply(200, { + values: ['b', 'c'], + page: '2', + next: `${baseUrl}/some-url?page=3`, + }) + .get('/some-url?page=3') + .reply(200, { + values: ['d'], + page: '3', + }); + const res = await api.getJson('some-url', { paginate: true }); + expect(res.body).toEqual({ + page: '1', + pagelen: 4, + size: 4, + values: ['a', 'b', 'c', 'd'], + next: undefined, + }); + }); }); diff --git a/lib/util/http/bitbucket.ts b/lib/util/http/bitbucket.ts index 628ef154cba1fe..15bea99b04860a 100644 --- a/lib/util/http/bitbucket.ts +++ b/lib/util/http/bitbucket.ts @@ -1,4 +1,7 @@ -import type { HttpOptions, HttpResponse, InternalHttpOptions } from './types'; +import is from '@sindresorhus/is'; +import type { PagedResult } from '../../modules/platform/bitbucket/types'; +import { parseUrl, resolveBaseUrl } from '../url'; +import type { HttpOptions, HttpResponse } from './types'; import { Http } from '.'; let baseUrl = 'https://api.bitbucket.org/'; @@ -7,16 +10,79 @@ export const setBaseUrl = (url: string): void => { baseUrl = url; }; -export class BitbucketHttp extends Http { - constructor(type = 'bitbucket', options?: HttpOptions) { +export interface BitbucketHttpOptions extends HttpOptions { + paginate?: boolean; +} + +export class BitbucketHttp extends Http { + constructor(type = 'bitbucket', options?: BitbucketHttpOptions) { super(type, options); } - protected override request( - url: string | URL, - options?: InternalHttpOptions + protected override async request( + path: string, + options?: BitbucketHttpOptions ): Promise> { const opts = { baseUrl, ...options }; - return super.request(url, opts); + + const result = await super.request(path, opts); + + if (opts.paginate && isPagedResult(result.body)) { + const resultBody = result.body as PagedResult; + + let nextPage = getPageFromURL(resultBody.next); + + while (is.nonEmptyString(nextPage)) { + const nextPath = getNextPagePath(path, nextPage); + + // istanbul ignore if + if (is.nullOrUndefined(nextPath)) { + break; + } + + const nextResult = await super.request>( + nextPath, + options + ); + + resultBody.values.push(...nextResult.body.values); + + nextPage = getPageFromURL(nextResult.body?.next); + } + + // Override other page-related attributes + resultBody.pagelen = resultBody.values.length; + resultBody.size = resultBody.values.length; + resultBody.next = undefined; + } + + return result; } } + +function getPageFromURL(url: string | undefined): string | null { + const resolvedURL = parseUrl(url); + + if (is.nullOrUndefined(resolvedURL)) { + return null; + } + + return resolvedURL.searchParams.get('page'); +} + +function getNextPagePath(path: string, nextPage: string): string | null { + const resolvedURL = parseUrl(resolveBaseUrl(baseUrl, path)); + + // istanbul ignore if + if (is.nullOrUndefined(resolvedURL)) { + return null; + } + + resolvedURL.searchParams.set('page', nextPage); + + return resolvedURL.toString(); +} + +function isPagedResult(obj: any): obj is PagedResult { + return is.nonEmptyObject(obj) && Array.isArray(obj.values); +} diff --git a/lib/util/template/index.ts b/lib/util/template/index.ts index f3e4a8809c7e6f..da16eaa90f8cfd 100644 --- a/lib/util/template/index.ts +++ b/lib/util/template/index.ts @@ -100,6 +100,7 @@ export const allowedFields = { isRange: 'true if the new value is a range', isSingleVersion: 'true if the upgrade is to a single version rather than a range', + isVulnerabilityAlert: 'true if the upgrade is a vulnerability alert', logJSON: 'ChangeLogResult object for the upgrade', manager: 'The (package) manager which detected the dependency', newDigest: 'The new digest value', @@ -143,6 +144,8 @@ export const allowedFields = { version: 'The version number of the changelog', versioning: 'The versioning scheme in use', versions: 'An array of ChangeLogRelease objects in the upgrade', + vulnerabilitySeverity: + 'The severity for a vulnerability alert upgrade (eg: LOW, MODERATE, HIGH, CRITICAL)', }; const prBodyFields = [ diff --git a/lib/util/vulnerability/utils.spec.ts b/lib/util/vulnerability/utils.spec.ts new file mode 100644 index 00000000000000..0d9aa2ee34bae1 --- /dev/null +++ b/lib/util/vulnerability/utils.spec.ts @@ -0,0 +1,129 @@ +import { getHighestVulnerabilitySeverity } from './utils'; + +describe('util/vulnerability/utils', () => { + it('parent CRITICAL vulnerability severity rating is maintained', () => { + const parentConfig = { + vulnerabilitySeverity: 'CRITICAL', + }; + + const childConfig = { + vulnerabilitySeverity: 'MODERATE', + }; + + const severity = getHighestVulnerabilitySeverity(parentConfig, childConfig); + + expect(severity).toBe('CRITICAL'); + }); + + it('child CRITICAL vulnerability severity rating is maintained', () => { + const parentConfig = { + vulnerabilitySeverity: 'MODERATE', + }; + + const childConfig = { + vulnerabilitySeverity: 'CRITICAL', + }; + + const severity = getHighestVulnerabilitySeverity(parentConfig, childConfig); + + expect(severity).toBe('CRITICAL'); + }); + + it('parent HIGH vulnerability severity rating is maintained', () => { + const parentConfig = { + vulnerabilitySeverity: 'HIGH', + }; + + const childConfig = { + vulnerabilitySeverity: 'MODERATE', + }; + + const severity = getHighestVulnerabilitySeverity(parentConfig, childConfig); + + expect(severity).toBe('HIGH'); + }); + + it('child HIGH vulnerability severity rating is maintained', () => { + const parentConfig = { + vulnerabilitySeverity: 'MODERATE', + }; + + const childConfig = { + vulnerabilitySeverity: 'HIGH', + }; + + const severity = getHighestVulnerabilitySeverity(parentConfig, childConfig); + + expect(severity).toBe('HIGH'); + }); + + it('parent MODERATE vulnerability severity rating is maintained', () => { + const parentConfig = { + vulnerabilitySeverity: 'MODERATE', + }; + + const childConfig = { + vulnerabilitySeverity: 'LOW', + }; + + const severity = getHighestVulnerabilitySeverity(parentConfig, childConfig); + + expect(severity).toBe('MODERATE'); + }); + + it('child MODERATE vulnerability severity rating is maintained', () => { + const parentConfig = { + vulnerabilitySeverity: 'LOW', + }; + + const childConfig = { + vulnerabilitySeverity: 'MODERATE', + }; + + const severity = getHighestVulnerabilitySeverity(parentConfig, childConfig); + + expect(severity).toBe('MODERATE'); + }); + + it('parent LOW vulnerability severity rating is maintained', () => { + const parentConfig = { + vulnerabilitySeverity: 'LOW', + }; + + const childConfig = { + vulnerabilitySeverity: undefined, + }; + + const severity = getHighestVulnerabilitySeverity(parentConfig, childConfig); + + expect(severity).toBe('LOW'); + }); + + it('child LOW vulnerability severity rating is maintained', () => { + const parentConfig = { + vulnerabilitySeverity: undefined, + }; + + const childConfig = { + vulnerabilitySeverity: 'LOW', + }; + + const severity = getHighestVulnerabilitySeverity(parentConfig, childConfig); + + expect(severity).toBe('LOW'); + }); + + it('handled undefined parent and child vulnerability severity', () => { + const parentConfig = { + vulnerabilitySeverity: undefined, + }; + + const childConfig = { + vulnerabilitySeverity: undefined, + }; + + const severity = getHighestVulnerabilitySeverity(parentConfig, childConfig); + + expect(severity).toBeUndefined(); + }); +}); diff --git a/lib/util/vulnerability/utils.ts b/lib/util/vulnerability/utils.ts new file mode 100644 index 00000000000000..5607a6de9237b8 --- /dev/null +++ b/lib/util/vulnerability/utils.ts @@ -0,0 +1,26 @@ +const severityOrder: Record = { + LOW: 1, + MODERATE: 2, + HIGH: 3, + CRITICAL: 4, +}; + +export function getHighestVulnerabilitySeverity< + T extends Record, + TChild extends Record | undefined +>(parent: T, child: TChild): string { + const parentVulSeverity = parent.vulnerabilitySeverity?.toUpperCase(); + const childVulSeverity = child?.vulnerabilitySeverity?.toUpperCase(); + + if (childVulSeverity === undefined) { + return parentVulSeverity; + } + + if (parentVulSeverity === undefined) { + return childVulSeverity; + } + + return severityOrder[parentVulSeverity] >= severityOrder[childVulSeverity] + ? parentVulSeverity + : childVulSeverity; +} diff --git a/lib/workers/global/autodiscover.spec.ts b/lib/workers/global/autodiscover.spec.ts index e401268d6fd148..ae75305ad695a9 100644 --- a/lib/workers/global/autodiscover.spec.ts +++ b/lib/workers/global/autodiscover.spec.ts @@ -25,6 +25,19 @@ describe('workers/global/autodiscover', () => { }); }); + it('throws if local and repositories defined', async () => { + config.platform = 'local'; + config.repositories = ['a']; + await expect(autodiscoverRepositories(config)).rejects.toThrow(); + }); + + it('returns local', async () => { + config.platform = 'local'; + expect((await autodiscoverRepositories(config)).repositories).toEqual([ + 'local', + ]); + }); + it('returns if not autodiscovering', async () => { expect(await autodiscoverRepositories(config)).toEqual(config); }); diff --git a/lib/workers/global/autodiscover.ts b/lib/workers/global/autodiscover.ts index 332961b82e1506..943f739d67f247 100644 --- a/lib/workers/global/autodiscover.ts +++ b/lib/workers/global/autodiscover.ts @@ -13,6 +13,19 @@ function repoName(value: string | { repository: string }): string { export async function autodiscoverRepositories( config: AllConfig ): Promise { + if (config.platform === 'local') { + if (config.repositories?.length) { + logger.debug( + { repositories: config.repositories }, + 'Found repositories when in local mode' + ); + throw new Error( + 'Invalid configuration: repositories list not supported when platform=local' + ); + } + config.repositories = ['local']; + return config; + } if (!config.autodiscover) { if (!config.repositories?.length) { logger.warn( diff --git a/lib/workers/global/index.spec.ts b/lib/workers/global/index.spec.ts index daae4d825c3036..0bc6aec1add7c0 100644 --- a/lib/workers/global/index.spec.ts +++ b/lib/workers/global/index.spec.ts @@ -1,11 +1,11 @@ import { expect } from '@jest/globals'; import { ERROR, WARN } from 'bunyan'; -import * as _fs from 'fs-extra'; +import fs from 'fs-extra'; import { logger, mocked } from '../../../test/util'; import * as _presets from '../../config/presets'; import { CONFIG_PRESETS_INVALID } from '../../constants/error-messages'; import { DockerDatasource } from '../../modules/datasource/docker'; -import * as _platform from '../../modules/platform'; +import * as platform from '../../modules/platform'; import * as secrets from '../../util/sanitize'; import * as repositoryWorker from '../repository'; import * as configParser from './config/parse'; @@ -16,20 +16,33 @@ jest.mock('../repository'); jest.mock('../../util/fs'); jest.mock('../../config/presets'); -jest.mock('fs-extra'); -const fs = mocked(_fs); -const platform = mocked(_platform); +jest.mock('fs-extra', () => { + const realFs = jest.requireActual('fs-extra'); + return { + ensureDir: jest.fn(), + remove: jest.fn(), + readFile: jest.fn((file: string, options: any) => { + if (file.endsWith('.wasm.gz')) { + return realFs.readFile(file, options); + } + return undefined; + }), + writeFile: jest.fn(), + outputFile: jest.fn(), + }; +}); // imports are readonly const presets = mocked(_presets); const addSecretForSanitizing = jest.spyOn(secrets, 'addSecretForSanitizing'); const parseConfigs = jest.spyOn(configParser, 'parseConfigs'); +const initPlatform = jest.spyOn(platform, 'initPlatform'); describe('workers/global/index', () => { beforeEach(() => { logger.getProblems.mockImplementationOnce(() => []); - platform.initPlatform.mockImplementation((input) => Promise.resolve(input)); + initPlatform.mockImplementation((input) => Promise.resolve(input)); delete process.env.AWS_SECRET_ACCESS_KEY; delete process.env.AWS_SESSION_TOKEN; }); @@ -82,6 +95,15 @@ describe('workers/global/index', () => { expect(repositoryWorker.renovateRepository).not.toHaveBeenCalled(); }); + it('handles local', async () => { + parseConfigs.mockResolvedValueOnce({ + platform: 'local', + }); + await expect(globalWorker.start()).resolves.toBe(0); + expect(parseConfigs).toHaveBeenCalledTimes(1); + expect(repositoryWorker.renovateRepository).toHaveBeenCalledTimes(1); + }); + it('processes repositories', async () => { parseConfigs.mockResolvedValueOnce({ gitAuthor: 'a@b.com', diff --git a/lib/workers/global/index.ts b/lib/workers/global/index.ts index 52e7beb5ea9a67..80869a7426c87e 100644 --- a/lib/workers/global/index.ts +++ b/lib/workers/global/index.ts @@ -37,10 +37,13 @@ export async function getRepositoryConfig( ); // TODO: types (#7154) const platform = GlobalConfig.get('platform')!; - repoConfig.localDir = upath.join( - repoConfig.baseDir, - `./repos/${platform}/${repoConfig.repository}` - ); + repoConfig.localDir = + platform === 'local' + ? process.cwd() + : upath.join( + repoConfig.baseDir, + `./repos/${platform}/${repoConfig.repository}` + ); await fs.ensureDir(repoConfig.localDir); delete repoConfig.baseDir; return configParser.filterConfig(repoConfig, 'repository'); diff --git a/lib/workers/repository/config-migration/branch/create.spec.ts b/lib/workers/repository/config-migration/branch/create.spec.ts index d1e450be9d8598..6f854ccc3c0075 100644 --- a/lib/workers/repository/config-migration/branch/create.spec.ts +++ b/lib/workers/repository/config-migration/branch/create.spec.ts @@ -2,7 +2,6 @@ import type { Indent } from 'detect-indent'; import { Fixtures } from '../../../../../test/fixtures'; import { RenovateConfig, getConfig, partial } from '../../../../../test/util'; import { scm } from '../../../../modules/platform/scm'; -import { checkoutBranch } from '../../../../util/git'; import { createConfigMigrationBranch } from './create'; import { MigratedDataFactory } from './migrated-data'; import type { MigratedData } from './migrated-data'; @@ -37,7 +36,7 @@ describe('workers/repository/config-migration/branch/create', () => { describe('createConfigMigrationBranch', () => { it('applies the default commit message', async () => { await createConfigMigrationBranch(config, migratedConfigData); - expect(checkoutBranch).toHaveBeenCalledWith(config.defaultBranch); + expect(scm.checkoutBranch).toHaveBeenCalledWith(config.defaultBranch); expect(scm.commitAndPush).toHaveBeenCalledWith({ branchName: 'renovate/migrate-config', baseBranch: 'dev', @@ -60,7 +59,7 @@ describe('workers/repository/config-migration/branch/create', () => { await createConfigMigrationBranch(config, migratedConfigData); - expect(checkoutBranch).toHaveBeenCalledWith(config.defaultBranch); + expect(scm.checkoutBranch).toHaveBeenCalledWith(config.defaultBranch); expect(scm.commitAndPush).toHaveBeenCalledWith({ branchName: 'renovate/migrate-config', baseBranch: 'dev', @@ -84,7 +83,7 @@ describe('workers/repository/config-migration/branch/create', () => { const message = `PREFIX: migrate config renovate.json`; await createConfigMigrationBranch(config, migratedConfigData); - expect(checkoutBranch).toHaveBeenCalledWith(config.defaultBranch); + expect(scm.checkoutBranch).toHaveBeenCalledWith(config.defaultBranch); expect(scm.commitAndPush).toHaveBeenCalledWith({ branchName: 'renovate/migrate-config', baseBranch: 'dev', @@ -109,7 +108,7 @@ describe('workers/repository/config-migration/branch/create', () => { const message = `Migrate config renovate.json ${suffix}`; await createConfigMigrationBranch(config, migratedConfigData); - expect(checkoutBranch).toHaveBeenCalledWith(config.defaultBranch); + expect(scm.checkoutBranch).toHaveBeenCalledWith(config.defaultBranch); expect(scm.commitAndPush).toHaveBeenCalledWith({ branchName: 'renovate/migrate-config', baseBranch: 'dev', @@ -135,7 +134,7 @@ describe('workers/repository/config-migration/branch/create', () => { await createConfigMigrationBranch(config, migratedConfigData); - expect(checkoutBranch).toHaveBeenCalledWith(config.defaultBranch); + expect(scm.checkoutBranch).toHaveBeenCalledWith(config.defaultBranch); expect(scm.commitAndPush).toHaveBeenCalledWith({ branchName: 'renovate/migrate-config', baseBranch: 'dev', @@ -160,7 +159,7 @@ describe('workers/repository/config-migration/branch/create', () => { await createConfigMigrationBranch(config, migratedConfigData); - expect(checkoutBranch).toHaveBeenCalledWith(config.defaultBranch); + expect(scm.checkoutBranch).toHaveBeenCalledWith(config.defaultBranch); expect(scm.commitAndPush).toHaveBeenCalledWith({ branchName: 'renovate/migrate-config', baseBranch: 'dev', diff --git a/lib/workers/repository/config-migration/branch/create.ts b/lib/workers/repository/config-migration/branch/create.ts index e29fa072495720..e188e212f5a82e 100644 --- a/lib/workers/repository/config-migration/branch/create.ts +++ b/lib/workers/repository/config-migration/branch/create.ts @@ -2,7 +2,6 @@ import { GlobalConfig } from '../../../../config/global'; import type { RenovateConfig } from '../../../../config/types'; import { logger } from '../../../../logger'; import { scm } from '../../../../modules/platform/scm'; -import { checkoutBranch } from '../../../../util/git'; import { getMigrationBranchName } from '../common'; import { ConfigMigrationCommitMessageFactory } from './commit-message'; import { MigratedDataFactory } from './migrated-data'; @@ -29,7 +28,7 @@ export async function createConfigMigrationBranch( return Promise.resolve(null); } - await checkoutBranch(config.defaultBranch!); + await scm.checkoutBranch(config.defaultBranch!); const contents = await MigratedDataFactory.applyPrettierFormatting( migratedConfigData ); diff --git a/lib/workers/repository/config-migration/branch/index.spec.ts b/lib/workers/repository/config-migration/branch/index.spec.ts index a9f89c60599ebb..34edd71ef7c3b7 100644 --- a/lib/workers/repository/config-migration/branch/index.spec.ts +++ b/lib/workers/repository/config-migration/branch/index.spec.ts @@ -56,7 +56,7 @@ describe('workers/repository/config-migration/branch/index', () => { const res = await checkConfigMigrationBranch(config, migratedData); // TODO: types (#7154) expect(res).toBe(`${config.branchPrefix!}migrate-config`); - expect(git.checkoutBranch).toHaveBeenCalledTimes(1); + expect(scm.checkoutBranch).toHaveBeenCalledTimes(1); expect(git.commitFiles).toHaveBeenCalledTimes(0); expect(logger.debug).toHaveBeenCalledWith( 'Config Migration PR already exists' @@ -72,7 +72,7 @@ describe('workers/repository/config-migration/branch/index', () => { const res = await checkConfigMigrationBranch(config, migratedData); // TODO: types (#7154) expect(res).toBe(`${config.branchPrefix!}migrate-config`); - expect(git.checkoutBranch).toHaveBeenCalledTimes(0); + expect(scm.checkoutBranch).toHaveBeenCalledTimes(0); expect(git.commitFiles).toHaveBeenCalledTimes(0); }); @@ -83,7 +83,7 @@ describe('workers/repository/config-migration/branch/index', () => { const res = await checkConfigMigrationBranch(config, migratedData); // TODO: types (#7154) expect(res).toBe(`${config.branchPrefix!}migrate-config`); - expect(git.checkoutBranch).toHaveBeenCalledTimes(1); + expect(scm.checkoutBranch).toHaveBeenCalledTimes(1); expect(git.commitFiles).toHaveBeenCalledTimes(0); expect(logger.debug).toHaveBeenCalledWith('Need to create migration PR'); }); @@ -98,7 +98,7 @@ describe('workers/repository/config-migration/branch/index', () => { const res = await checkConfigMigrationBranch(config, migratedData); // TODO: types (#7154) expect(res).toBe(`${config.branchPrefix!}migrate-config`); - expect(git.checkoutBranch).toHaveBeenCalledTimes(0); + expect(scm.checkoutBranch).toHaveBeenCalledTimes(0); expect(git.commitFiles).toHaveBeenCalledTimes(0); }); @@ -112,7 +112,7 @@ describe('workers/repository/config-migration/branch/index', () => { scm.branchExists.mockResolvedValueOnce(true); const res = await checkConfigMigrationBranch(config, migratedData); expect(res).toBeNull(); - expect(git.checkoutBranch).toHaveBeenCalledTimes(0); + expect(scm.checkoutBranch).toHaveBeenCalledTimes(0); expect(scm.commitAndPush).toHaveBeenCalledTimes(0); expect(scm.deleteBranch).toHaveBeenCalledTimes(1); expect(logger.debug).toHaveBeenCalledWith( diff --git a/lib/workers/repository/config-migration/branch/index.ts b/lib/workers/repository/config-migration/branch/index.ts index 25ee7ea7bc960b..3142269212c71e 100644 --- a/lib/workers/repository/config-migration/branch/index.ts +++ b/lib/workers/repository/config-migration/branch/index.ts @@ -4,7 +4,6 @@ import { logger } from '../../../../logger'; import { FindPRConfig, Pr, platform } from '../../../../modules/platform'; import { ensureComment } from '../../../../modules/platform/comment'; import { scm } from '../../../../modules/platform/scm'; -import { checkoutBranch } from '../../../../util/git'; import { getMigrationBranchName } from '../common'; import { ConfigMigrationCommitMessageFactory } from './commit-message'; import { createConfigMigrationBranch } from './create'; @@ -67,7 +66,7 @@ export async function checkConfigMigrationBranch( await createConfigMigrationBranch(config, migratedConfigData); } if (!GlobalConfig.get('dryRun')) { - await checkoutBranch(configMigrationBranch); + await scm.checkoutBranch(configMigrationBranch); } return configMigrationBranch; } diff --git a/lib/workers/repository/config-migration/branch/migrated-data.spec.ts b/lib/workers/repository/config-migration/branch/migrated-data.spec.ts index 09f7bcecc13af8..3f1b32301a0d61 100644 --- a/lib/workers/repository/config-migration/branch/migrated-data.spec.ts +++ b/lib/workers/repository/config-migration/branch/migrated-data.spec.ts @@ -1,11 +1,10 @@ import detectIndent from 'detect-indent'; import { Fixtures } from '../../../../../test/fixtures'; -import { mockedFunction } from '../../../../../test/util'; +import { mockedFunction, scm } from '../../../../../test/util'; import { migrateConfig } from '../../../../config/migration'; import { logger } from '../../../../logger'; import { readLocalFile } from '../../../../util/fs'; -import { getFileList } from '../../../../util/git'; import { detectRepoFileConfig } from '../../init/merge'; import { MigratedDataFactory } from './migrated-data'; @@ -141,7 +140,7 @@ describe('workers/repository/config-migration/branch/migrated-data', () => { }); beforeEach(() => { - mockedFunction(getFileList).mockResolvedValue([]); + mockedFunction(scm.getFileList).mockResolvedValue([]); }); it('does not format when no prettier config is present', async () => { @@ -173,7 +172,7 @@ describe('workers/repository/config-migration/branch/migrated-data', () => { it('formats when prettier config file is found', async () => { const formatted = formattedMigratedData.content; - mockedFunction(getFileList).mockResolvedValue(['.prettierrc']); + mockedFunction(scm.getFileList).mockResolvedValue(['.prettierrc']); await MigratedDataFactory.getAsync(); await expect( MigratedDataFactory.applyPrettierFormatting(migratedData) diff --git a/lib/workers/repository/config-migration/branch/migrated-data.ts b/lib/workers/repository/config-migration/branch/migrated-data.ts index e0f1ec79832ed5..1961a817ad96d5 100644 --- a/lib/workers/repository/config-migration/branch/migrated-data.ts +++ b/lib/workers/repository/config-migration/branch/migrated-data.ts @@ -5,8 +5,8 @@ import upath from 'upath'; import { migrateConfig } from '../../../../config/migration'; import { prettier } from '../../../../expose.cjs'; import { logger } from '../../../../logger'; +import { scm } from '../../../../modules/platform/scm'; import { readLocalFile } from '../../../../util/fs'; -import { getFileList } from '../../../../util/git'; import { detectRepoFileConfig } from '../../init/merge'; export interface MigratedData { @@ -43,7 +43,7 @@ export async function applyPrettierFormatting( ): Promise { try { logger.trace('applyPrettierFormatting - START'); - const fileList = await getFileList(); + const fileList = await scm.getFileList(); let prettierExists = fileList.some((file) => prettierConfigFilenames.has(file) ); diff --git a/lib/workers/repository/config-migration/branch/rebase.spec.ts b/lib/workers/repository/config-migration/branch/rebase.spec.ts index 1ac78e49ad3322..4f7ebdb6884e06 100644 --- a/lib/workers/repository/config-migration/branch/rebase.spec.ts +++ b/lib/workers/repository/config-migration/branch/rebase.spec.ts @@ -9,7 +9,6 @@ import { scm, } from '../../../../../test/util'; import { GlobalConfig } from '../../../../config/global'; -import { checkoutBranch } from '../../../../util/git'; import { MigratedDataFactory } from './migrated-data'; import type { MigratedData } from './migrated-data'; import { jsonStripWhitespaces, rebaseMigrationBranch } from './rebase'; @@ -60,7 +59,7 @@ describe('workers/repository/config-migration/branch/rebase', () => { await rebaseMigrationBranch(config, migratedConfigData); - expect(checkoutBranch).toHaveBeenCalledTimes(0); + expect(scm.checkoutBranch).toHaveBeenCalledTimes(0); expect(scm.commitAndPush).toHaveBeenCalledTimes(0); }); @@ -76,7 +75,7 @@ describe('workers/repository/config-migration/branch/rebase', () => { await rebaseMigrationBranch(config, migratedConfigData); - expect(checkoutBranch).toHaveBeenCalledTimes(0); + expect(scm.checkoutBranch).toHaveBeenCalledTimes(0); expect(scm.commitAndPush).toHaveBeenCalledTimes(0); expect(git.getFile).toHaveBeenCalledTimes(1); } @@ -93,7 +92,7 @@ describe('workers/repository/config-migration/branch/rebase', () => { await rebaseMigrationBranch(config, migratedConfigData); - expect(checkoutBranch).toHaveBeenCalledWith(config.defaultBranch); + expect(scm.checkoutBranch).toHaveBeenCalledWith(config.defaultBranch); expect(scm.commitAndPush).toHaveBeenCalledTimes(1); }); @@ -111,7 +110,7 @@ describe('workers/repository/config-migration/branch/rebase', () => { await rebaseMigrationBranch(config, migratedConfigData); - expect(checkoutBranch).toHaveBeenCalledWith(config.defaultBranch); + expect(scm.checkoutBranch).toHaveBeenCalledWith(config.defaultBranch); expect(scm.commitAndPush).toHaveBeenCalledTimes(1); expect(scm.commitAndPush).toHaveBeenCalledWith({ branchName: 'renovate/migrate-config', @@ -144,7 +143,7 @@ describe('workers/repository/config-migration/branch/rebase', () => { await rebaseMigrationBranch(config, migratedConfigData); - expect(checkoutBranch).toHaveBeenCalledTimes(0); + expect(scm.checkoutBranch).toHaveBeenCalledTimes(0); expect(scm.commitAndPush).toHaveBeenCalledTimes(0); } ); diff --git a/lib/workers/repository/config-migration/branch/rebase.ts b/lib/workers/repository/config-migration/branch/rebase.ts index cb932c11484244..a638401972ea06 100644 --- a/lib/workers/repository/config-migration/branch/rebase.ts +++ b/lib/workers/repository/config-migration/branch/rebase.ts @@ -3,7 +3,7 @@ import { GlobalConfig } from '../../../../config/global'; import type { RenovateConfig } from '../../../../config/types'; import { logger } from '../../../../logger'; import { scm } from '../../../../modules/platform/scm'; -import { checkoutBranch, getFile } from '../../../../util/git'; +import { getFile } from '../../../../util/git'; import { quickStringify } from '../../../../util/stringify'; import { getMigrationBranchName } from '../common'; import { ConfigMigrationCommitMessageFactory } from './commit-message'; @@ -42,7 +42,7 @@ export async function rebaseMigrationBranch( ); const commitMessage = commitMessageFactory.getCommitMessage(); - await checkoutBranch(config.defaultBranch!); + await scm.checkoutBranch(config.defaultBranch!); contents = await MigratedDataFactory.applyPrettierFormatting( migratedConfigData ); diff --git a/lib/workers/repository/extract/index.spec.ts b/lib/workers/repository/extract/index.spec.ts index 4b39cf0be52636..ca553db2971951 100644 --- a/lib/workers/repository/extract/index.spec.ts +++ b/lib/workers/repository/extract/index.spec.ts @@ -1,4 +1,4 @@ -import { getConfig, git, mocked } from '../../../../test/util'; +import { getConfig, mocked, scm } from '../../../../test/util'; import type { RenovateConfig } from '../../../config/types'; import { logger } from '../../../logger'; import * as _managerFiles from './manager-files'; @@ -16,7 +16,7 @@ describe('workers/repository/extract/index', () => { beforeEach(() => { jest.resetAllMocks(); - git.getFileList.mockResolvedValue(fileList); + scm.getFileList.mockResolvedValue(fileList); config = getConfig(); }); diff --git a/lib/workers/repository/extract/index.ts b/lib/workers/repository/extract/index.ts index a2337de4a45d26..9f4f45cc9bcb39 100644 --- a/lib/workers/repository/extract/index.ts +++ b/lib/workers/repository/extract/index.ts @@ -3,7 +3,7 @@ import { getManagerConfig, mergeChildConfig } from '../../../config'; import type { ManagerConfig, RenovateConfig } from '../../../config/types'; import { logger } from '../../../logger'; import { getManagerList, hashMap } from '../../../modules/manager'; -import { getFileList } from '../../../util/git'; +import { scm } from '../../../modules/platform/scm'; import type { ExtractResult, WorkerExtractConfig } from '../../types'; import { getMatchingFiles } from './file-match'; import { getManagerPackageFiles } from './manager-files'; @@ -20,7 +20,7 @@ export async function extractAllDependencies( ); } const extractList: WorkerExtractConfig[] = []; - const fileList = await getFileList(); + const fileList = await scm.getFileList(); const tryConfig = (managerConfig: ManagerConfig): void => { const matchingFileList = getMatchingFiles(managerConfig, fileList); diff --git a/lib/workers/repository/init/merge.spec.ts b/lib/workers/repository/init/merge.spec.ts index 5a03b80b845ced..595275293d4223 100644 --- a/lib/workers/repository/init/merge.spec.ts +++ b/lib/workers/repository/init/merge.spec.ts @@ -2,11 +2,11 @@ import { RenovateConfig, fs, getConfig, - git, logger, mocked, partial, platform, + scm, } from '../../../../test/util'; import * as _migrateAndValidate from '../../../config/migrate-validate'; import * as _migrate from '../../../config/migration'; @@ -44,7 +44,7 @@ describe('workers/repository/init/merge', () => { }); it('returns config if not found', async () => { - git.getFileList.mockResolvedValue(['package.json']); + scm.getFileList.mockResolvedValue(['package.json']); fs.readLocalFile.mockResolvedValue('{}'); expect(await detectRepoFileConfig()).toEqual({}); }); @@ -56,7 +56,7 @@ describe('workers/repository/init/merge', () => { partial({ configFileName: 'renovate.json' }) ); platform.getRawFile.mockRejectedValueOnce(new Error()); - git.getFileList.mockResolvedValue(['package.json']); + scm.getFileList.mockResolvedValue(['package.json']); fs.readLocalFile.mockResolvedValue('{}'); expect(await detectRepoFileConfig()).toEqual({}); expect(logger.logger.debug).toHaveBeenCalledWith( @@ -65,7 +65,7 @@ describe('workers/repository/init/merge', () => { }); it('uses package.json config if found', async () => { - git.getFileList.mockResolvedValue(['package.json']); + scm.getFileList.mockResolvedValue(['package.json']); const pJson = JSON.stringify({ name: 'something', renovate: { @@ -86,7 +86,7 @@ describe('workers/repository/init/merge', () => { }); it('massages package.json renovate string', async () => { - git.getFileList.mockResolvedValue(['package.json']); + scm.getFileList.mockResolvedValue(['package.json']); const pJson = JSON.stringify({ name: 'something', renovate: 'github>renovatebot/renovate', @@ -100,7 +100,7 @@ describe('workers/repository/init/merge', () => { }); it('returns error if cannot parse', async () => { - git.getFileList.mockResolvedValue(['package.json', 'renovate.json']); + scm.getFileList.mockResolvedValue(['package.json', 'renovate.json']); fs.readLocalFile.mockResolvedValue('cannot parse'); expect(await detectRepoFileConfig()).toEqual({ configFileName: 'renovate.json', @@ -112,7 +112,7 @@ describe('workers/repository/init/merge', () => { }); it('throws error if duplicate keys', async () => { - git.getFileList.mockResolvedValue(['package.json', '.renovaterc']); + scm.getFileList.mockResolvedValue(['package.json', '.renovaterc']); fs.readLocalFile.mockResolvedValue( '{ "enabled": true, "enabled": false }' ); @@ -130,7 +130,7 @@ describe('workers/repository/init/merge', () => { const configFileRaw = `{ // this is json5 format }`; - git.getFileList.mockResolvedValue(['package.json', 'renovate.json5']); + scm.getFileList.mockResolvedValue(['package.json', 'renovate.json5']); fs.readLocalFile.mockResolvedValue(configFileRaw); expect(await detectRepoFileConfig()).toEqual({ configFileName: 'renovate.json5', @@ -140,7 +140,7 @@ describe('workers/repository/init/merge', () => { }); it('finds .github/renovate.json', async () => { - git.getFileList.mockResolvedValue([ + scm.getFileList.mockResolvedValue([ 'package.json', '.github/renovate.json', ]); @@ -153,7 +153,7 @@ describe('workers/repository/init/merge', () => { }); it('finds .gitlab/renovate.json', async () => { - git.getFileList.mockResolvedValue([ + scm.getFileList.mockResolvedValue([ 'package.json', '.gitlab/renovate.json', ]); @@ -166,7 +166,7 @@ describe('workers/repository/init/merge', () => { }); it('finds .renovaterc.json', async () => { - git.getFileList.mockResolvedValue(['package.json', '.renovaterc.json']); + scm.getFileList.mockResolvedValue(['package.json', '.renovaterc.json']); fs.readLocalFile.mockResolvedValue('{}'); platform.getRawFile.mockResolvedValueOnce('{"something":"new"}'); expect(await detectRepoFileConfig()).toEqual({ @@ -184,7 +184,7 @@ describe('workers/repository/init/merge', () => { }); it('finds .renovaterc.json5', async () => { - git.getFileList.mockResolvedValue(['package.json', '.renovaterc.json5']); + scm.getFileList.mockResolvedValue(['package.json', '.renovaterc.json5']); fs.readLocalFile.mockResolvedValue('{}'); platform.getRawFile.mockResolvedValueOnce('{"something":"new"}'); expect(await detectRepoFileConfig()).toEqual({ @@ -225,7 +225,7 @@ describe('workers/repository/init/merge', () => { }); it('throws error if misconfigured', async () => { - git.getFileList.mockResolvedValue(['package.json', '.renovaterc.json']); + scm.getFileList.mockResolvedValue(['package.json', '.renovaterc.json']); fs.readLocalFile.mockResolvedValue('{}'); migrateAndValidate.migrateAndValidate.mockResolvedValueOnce({ errors: [{ topic: 'dep', message: 'test error' }], @@ -241,7 +241,7 @@ describe('workers/repository/init/merge', () => { }); it('migrates nested config', async () => { - git.getFileList.mockResolvedValue(['renovate.json']); + scm.getFileList.mockResolvedValue(['renovate.json']); fs.readLocalFile.mockResolvedValue('{}'); migrateAndValidate.migrateAndValidate.mockImplementation((_, c) => { // We shouldn't see packageRules here (avoids #14827). @@ -271,7 +271,7 @@ describe('workers/repository/init/merge', () => { }); it('ignores presets', async () => { - git.getFileList.mockResolvedValue(['renovate.json']); + scm.getFileList.mockResolvedValue(['renovate.json']); fs.readLocalFile.mockResolvedValue('{}'); migrateAndValidate.migrateAndValidate.mockResolvedValue({ extends: ['config:base'], @@ -290,7 +290,7 @@ describe('workers/repository/init/merge', () => { }); it('continues if no errors', async () => { - git.getFileList.mockResolvedValue(['package.json', '.renovaterc.json']); + scm.getFileList.mockResolvedValue(['package.json', '.renovaterc.json']); fs.readLocalFile.mockResolvedValue('{}'); migrateAndValidate.migrateAndValidate.mockResolvedValue({ warnings: [], diff --git a/lib/workers/repository/init/merge.ts b/lib/workers/repository/init/merge.ts index 04c3cba0e61cba..38d6a2ba0d6188 100644 --- a/lib/workers/repository/init/merge.ts +++ b/lib/workers/repository/init/merge.ts @@ -17,17 +17,17 @@ import { import { logger } from '../../../logger'; import * as npmApi from '../../../modules/datasource/npm'; import { platform } from '../../../modules/platform'; +import { scm } from '../../../modules/platform/scm'; import { ExternalHostError } from '../../../types/errors/external-host-error'; import { getCache } from '../../../util/cache/repository'; import { readLocalFile } from '../../../util/fs'; -import { getFileList } from '../../../util/git'; import * as hostRules from '../../../util/host-rules'; import * as queue from '../../../util/http/queue'; import * as throttle from '../../../util/http/throttle'; import type { RepoFileConfig } from './types'; async function detectConfigFile(): Promise { - const fileList = await getFileList(); + const fileList = await scm.getFileList(); for (const fileName of configFileNames) { if (fileName === 'package.json') { try { diff --git a/lib/workers/repository/onboarding/branch/check.spec.ts b/lib/workers/repository/onboarding/branch/check.spec.ts index add73cf44a0076..74684f8f809352 100644 --- a/lib/workers/repository/onboarding/branch/check.spec.ts +++ b/lib/workers/repository/onboarding/branch/check.spec.ts @@ -4,6 +4,7 @@ import { mocked, partial, platform, + scm, } from '../../../../../test/util'; import { REPOSITORY_CLOSED_ONBOARDING } from '../../../../constants/error-messages'; import { logger } from '../../../../logger'; @@ -20,6 +21,7 @@ describe('workers/repository/onboarding/branch/check', () => { const config = partial({ requireConfig: 'required', suppressNotifications: [], + onboarding: true, }); it('skips normal onboarding check if onboardingCache is valid', async () => { @@ -48,7 +50,7 @@ describe('workers/repository/onboarding/branch/check', () => { onboardingBranchSha: 'onboarding-sha', }, }); - git.getFileList.mockResolvedValue([]); + scm.getFileList.mockResolvedValue([]); await isOnboarded(config); expect(logger.debug).not.toHaveBeenCalledWith( 'Onboarding cache is valid. Repo is not onboarded' @@ -58,7 +60,7 @@ describe('workers/repository/onboarding/branch/check', () => { it('continues with normal logic if closedPr exists', async () => { cache.getCache.mockReturnValue({}); platform.findPr.mockResolvedValue(partial()); - git.getFileList.mockResolvedValue([]); + scm.getFileList.mockResolvedValue([]); await expect(isOnboarded(config)).rejects.toThrow( REPOSITORY_CLOSED_ONBOARDING ); diff --git a/lib/workers/repository/onboarding/branch/check.ts b/lib/workers/repository/onboarding/branch/check.ts index 25f7d21a229c54..bf3abf09f1fe03 100644 --- a/lib/workers/repository/onboarding/branch/check.ts +++ b/lib/workers/repository/onboarding/branch/check.ts @@ -7,13 +7,14 @@ import { import { logger } from '../../../../logger'; import { Pr, platform } from '../../../../modules/platform'; import { ensureComment } from '../../../../modules/platform/comment'; +import { scm } from '../../../../modules/platform/scm'; import { getCache } from '../../../../util/cache/repository'; import { readLocalFile } from '../../../../util/fs'; -import { getBranchCommit, getFileList } from '../../../../util/git'; +import { getBranchCommit } from '../../../../util/git'; async function findFile(fileName: string): Promise { logger.debug(`findFile(${fileName})`); - const fileList = await getFileList(); + const fileList = await scm.getFileList(); return fileList.includes(fileName); } @@ -68,6 +69,7 @@ export async function isOnboarded(config: RenovateConfig): Promise { // if onboarding cache is present and base branch has not been updated branch is not onboarded // if closed pr exists then presence of onboarding cache doesn't matter as we need to skip onboarding if ( + config.onboarding && !pr && onboardingBranchCache && onboardingBranchCache.defaultBranchSha === diff --git a/lib/workers/repository/onboarding/branch/index.spec.ts b/lib/workers/repository/onboarding/branch/index.spec.ts index 04fc5ed1856d67..d9821294b5880b 100644 --- a/lib/workers/repository/onboarding/branch/index.spec.ts +++ b/lib/workers/repository/onboarding/branch/index.spec.ts @@ -3,7 +3,6 @@ import { RenovateConfig, fs, getConfig, - git, mocked, platform, scm, @@ -55,7 +54,7 @@ describe('workers/repository/onboarding/branch/index', () => { config = getConfig(); config.repository = 'some/repo'; OnboardingState.prUpdateRequested = false; - git.getFileList.mockResolvedValue([]); + scm.getFileList.mockResolvedValue([]); cache.getCache.mockReturnValue({}); }); @@ -96,7 +95,7 @@ describe('workers/repository/onboarding/branch/index', () => { ' "$schema": "https://docs.renovatebot.com/renovate-schema.json"\n' + '}\n' ); - git.getFileList.mockResolvedValue(['package.json']); + scm.getFileList.mockResolvedValue(['package.json']); fs.readLocalFile.mockResolvedValue('{}'); await checkOnboardingBranch(config); const file = scm.commitAndPush.mock.calls[0][0] @@ -121,7 +120,7 @@ describe('workers/repository/onboarding/branch/index', () => { ' "extends": ["some/renovate-config"]\n' + '}\n' ); - git.getFileList.mockResolvedValue(['package.json']); + scm.getFileList.mockResolvedValue(['package.json']); fs.readLocalFile.mockResolvedValue('{}'); await checkOnboardingBranch(config); const expectConfig = { @@ -156,7 +155,7 @@ describe('workers/repository/onboarding/branch/index', () => { it('handles skipped onboarding, requireConfig=required, and a config file', async () => { config.requireConfig = 'required'; config.onboarding = false; - git.getFileList.mockResolvedValueOnce(['renovate.json']); + scm.getFileList.mockResolvedValueOnce(['renovate.json']); const res = await checkOnboardingBranch(config); expect(res.repoIsOnboarded).toBeTrue(); }); @@ -171,7 +170,7 @@ describe('workers/repository/onboarding/branch/index', () => { it('handles skipped onboarding, requireConfig=required, and no config file', async () => { config.requireConfig = 'required'; config.onboarding = false; - git.getFileList.mockResolvedValueOnce(['package.json']); + scm.getFileList.mockResolvedValueOnce(['package.json']); fs.readLocalFile.mockResolvedValueOnce('{}'); const onboardingResult = checkOnboardingBranch(config); await expect(onboardingResult).rejects.toThrow('disabled'); @@ -179,7 +178,7 @@ describe('workers/repository/onboarding/branch/index', () => { it('detects repo is onboarded via file', async () => { cache.getCache.mockReturnValue(dummyCache); - git.getFileList.mockResolvedValueOnce(['renovate.json']); + scm.getFileList.mockResolvedValueOnce(['renovate.json']); const res = await checkOnboardingBranch(config); expect(res.repoIsOnboarded).toBeTrue(); expect(onboardingCache.deleteOnboardingCache).toHaveBeenCalledTimes(1); // removes onboarding cache when repo is onboarded @@ -187,7 +186,7 @@ describe('workers/repository/onboarding/branch/index', () => { it('handles removed cached file name', async () => { cache.getCache.mockReturnValue({ configFileName: '.renovaterc' }); - git.getFileList.mockResolvedValueOnce(['renovate.json']); + scm.getFileList.mockResolvedValueOnce(['renovate.json']); const res = await checkOnboardingBranch(config); expect(res.repoIsOnboarded).toBeTrue(); }); @@ -236,7 +235,7 @@ describe('workers/repository/onboarding/branch/index', () => { }); it('detects repo is onboarded via package.json config', async () => { - git.getFileList.mockResolvedValueOnce(['package.json']); + scm.getFileList.mockResolvedValueOnce(['package.json']); fs.readLocalFile.mockResolvedValueOnce('{"renovate":{}}'); const res = await checkOnboardingBranch(config); expect(res.repoIsOnboarded).toBeTrue(); @@ -264,14 +263,14 @@ describe('workers/repository/onboarding/branch/index', () => { it('updates onboarding branch', async () => { cache.getCache.mockReturnValue(dummyCache); - git.getFileList.mockResolvedValue(['package.json']); + scm.getFileList.mockResolvedValue(['package.json']); platform.findPr.mockResolvedValue(null); platform.getBranchPr.mockResolvedValueOnce(mock()); rebase.rebaseOnboardingBranch.mockResolvedValueOnce('123test'); const res = await checkOnboardingBranch(config); expect(res.repoIsOnboarded).toBeFalse(); expect(res.branchList).toEqual(['renovate/configure']); - expect(git.checkoutBranch).toHaveBeenCalledTimes(1); + expect(scm.checkoutBranch).toHaveBeenCalledTimes(1); expect(onboardingCache.setOnboardingCache).toHaveBeenCalledTimes(1); // update onboarding cache expect(scm.commitAndPush).toHaveBeenCalledTimes(0); }); @@ -281,7 +280,7 @@ describe('workers/repository/onboarding/branch/index', () => { GlobalConfig.set({ platform: 'github' }); config.onboardingRebaseCheckbox = true; OnboardingState.prUpdateRequested = false; - git.getFileList.mockResolvedValueOnce(['package.json']); + scm.getFileList.mockResolvedValueOnce(['package.json']); platform.findPr.mockResolvedValueOnce(null); rebase.rebaseOnboardingBranch.mockResolvedValueOnce(null); }); @@ -297,7 +296,7 @@ describe('workers/repository/onboarding/branch/index', () => { `Platform '${pl}' does not support extended markdown` ); expect(OnboardingState.prUpdateRequested).toBeTrue(); - expect(git.checkoutBranch).toHaveBeenCalledTimes(1); + expect(scm.checkoutBranch).toHaveBeenCalledTimes(1); expect(scm.commitAndPush).toHaveBeenCalledTimes(0); }); @@ -311,7 +310,7 @@ describe('workers/repository/onboarding/branch/index', () => { `No rebase checkbox was found in the onboarding PR` ); expect(OnboardingState.prUpdateRequested).toBeTrue(); - expect(git.checkoutBranch).toHaveBeenCalledTimes(1); + expect(scm.checkoutBranch).toHaveBeenCalledTimes(1); expect(scm.commitAndPush).toHaveBeenCalledTimes(0); }); @@ -326,7 +325,7 @@ describe('workers/repository/onboarding/branch/index', () => { ); expect(OnboardingState.prUpdateRequested).toBeTrue(); ``; - expect(git.checkoutBranch).toHaveBeenCalledTimes(1); + expect(scm.checkoutBranch).toHaveBeenCalledTimes(1); expect(scm.commitAndPush).toHaveBeenCalledTimes(0); }); @@ -337,7 +336,7 @@ describe('workers/repository/onboarding/branch/index', () => { await checkOnboardingBranch(config); expect(OnboardingState.prUpdateRequested).toBeFalse(); - expect(git.checkoutBranch).toHaveBeenCalledTimes(1); + expect(scm.checkoutBranch).toHaveBeenCalledTimes(1); expect(scm.commitAndPush).toHaveBeenCalledTimes(0); }); }); diff --git a/lib/workers/repository/onboarding/branch/index.ts b/lib/workers/repository/onboarding/branch/index.ts index c8f50ecefa8575..e131f8069a4a66 100644 --- a/lib/workers/repository/onboarding/branch/index.ts +++ b/lib/workers/repository/onboarding/branch/index.ts @@ -8,11 +8,8 @@ import { } from '../../../../constants/error-messages'; import { logger } from '../../../../logger'; import { Pr, platform } from '../../../../modules/platform'; -import { - checkoutBranch, - getBranchCommit, - setGitAuthor, -} from '../../../../util/git'; +import { scm } from '../../../../modules/platform/scm'; +import { getBranchCommit, setGitAuthor } from '../../../../util/git'; import { extractAllDependencies } from '../../extract'; import { mergeRenovateConfig } from '../../init/merge'; import { OnboardingState } from '../common'; @@ -108,7 +105,7 @@ export async function checkOnboardingBranch( if (!GlobalConfig.get('dryRun')) { logger.debug('Checkout onboarding branch.'); // TODO #7154 - await checkoutBranch(onboardingBranch!); + await scm.checkoutBranch(onboardingBranch!); } // TODO #7154 const branchList = [onboardingBranch!]; diff --git a/lib/workers/repository/process/extract-update.spec.ts b/lib/workers/repository/process/extract-update.spec.ts index 47b8e67e3a232f..2dc09dccfd6def 100644 --- a/lib/workers/repository/process/extract-update.spec.ts +++ b/lib/workers/repository/process/extract-update.spec.ts @@ -1,4 +1,4 @@ -import { git, logger, mocked, scm } from '../../../../test/util'; +import { logger, mocked, scm } from '../../../../test/util'; import type { PackageFile } from '../../../modules/manager/types'; import * as _repositoryCache from '../../../util/cache/repository'; import type { BaseBranchCache } from '../../../util/cache/repository/types'; @@ -50,7 +50,7 @@ describe('workers/repository/process/extract-update', () => { suppressNotifications: ['deprecationWarningIssues'], }; repositoryCache.getCache.mockReturnValueOnce({ scan: {} }); - git.checkoutBranch.mockResolvedValueOnce('123test'); + scm.checkoutBranch.mockResolvedValueOnce('123test'); const packageFiles = await extract(config); const res = await lookup(config, packageFiles); expect(res).toEqual({ @@ -81,7 +81,7 @@ describe('workers/repository/process/extract-update', () => { addLabels: 'npm', }, }; - git.checkoutBranch.mockResolvedValueOnce('123test'); + scm.checkoutBranch.mockResolvedValueOnce('123test'); repositoryCache.getCache.mockReturnValueOnce({ scan: {} }); const packageFiles = await extract(config); expect(packageFiles).toBeUndefined(); @@ -105,7 +105,7 @@ describe('workers/repository/process/extract-update', () => { }, }); scm.getBranchCommit.mockResolvedValueOnce('123test'); - git.checkoutBranch.mockResolvedValueOnce('123test'); + scm.checkoutBranch.mockResolvedValueOnce('123test'); const res = await extract(config); expect(res).toEqual(packageFiles); }); @@ -121,7 +121,7 @@ describe('workers/repository/process/extract-update', () => { appendVulnerabilityPackageRules: appendVulnerabilityPackageRulesMock, }); repositoryCache.getCache.mockReturnValueOnce({ scan: {} }); - git.checkoutBranch.mockResolvedValueOnce('123test'); + scm.checkoutBranch.mockResolvedValueOnce('123test'); const packageFiles = await extract(config); await lookup(config, packageFiles); @@ -138,7 +138,7 @@ describe('workers/repository/process/extract-update', () => { }; createVulnerabilitiesMock.mockRejectedValueOnce(new Error()); repositoryCache.getCache.mockReturnValueOnce({ scan: {} }); - git.checkoutBranch.mockResolvedValueOnce('123test'); + scm.checkoutBranch.mockResolvedValueOnce('123test'); const packageFiles = await extract(config); await lookup(config, packageFiles); diff --git a/lib/workers/repository/process/extract-update.ts b/lib/workers/repository/process/extract-update.ts index d10df7303bbaee..66e0c14bdf36db 100644 --- a/lib/workers/repository/process/extract-update.ts +++ b/lib/workers/repository/process/extract-update.ts @@ -8,7 +8,6 @@ import { getCache } from '../../../util/cache/repository'; import type { BaseBranchCache } from '../../../util/cache/repository/types'; import { checkGithubToken as ensureGithubToken } from '../../../util/check-token'; import { fingerprint } from '../../../util/fingerprint'; -import { checkoutBranch } from '../../../util/git'; import type { BranchConfig } from '../../types'; import { extractAllDependencies } from '../extract'; import { generateFingerprintConfig } from '../extract/extract-fingerprint-config'; @@ -137,7 +136,7 @@ export async function extract( logger.info({ err }, 'Error deleting cached dep updates'); } } else { - await checkoutBranch(baseBranch!); + await scm.checkoutBranch(baseBranch!); const extractResult = (await extractAllDependencies(config)) || {}; packageFiles = extractResult.packageFiles; const { extractionFingerprints } = extractResult; diff --git a/lib/workers/repository/process/index.ts b/lib/workers/repository/process/index.ts index 0593f539bc42bb..cced4e1702d921 100644 --- a/lib/workers/repository/process/index.ts +++ b/lib/workers/repository/process/index.ts @@ -108,7 +108,7 @@ export async function extractDependencies( branchList: [], packageFiles: null!, }; - if (config.baseBranches?.length) { + if (GlobalConfig.get('platform') !== 'local' && config.baseBranches?.length) { config.baseBranches = unfoldBaseBranches(config.baseBranches); logger.debug({ baseBranches: config.baseBranches }, 'baseBranches'); const extracted: Record> = {}; diff --git a/lib/workers/repository/process/types.ts b/lib/workers/repository/process/types.ts index fd739b99a7d1c3..e59c569da1d8ab 100644 --- a/lib/workers/repository/process/types.ts +++ b/lib/workers/repository/process/types.ts @@ -17,3 +17,9 @@ export interface DependencyVulnerabilities { versioningApi: VersioningApi; vulnerabilities: Vulnerability[]; } + +export interface SeverityDetails { + cvssVector: string; + score: string; + severityLevel?: string; +} diff --git a/lib/workers/repository/process/vulnerabilities.ts b/lib/workers/repository/process/vulnerabilities.ts index b3745a7692d463..93483f716a0009 100644 --- a/lib/workers/repository/process/vulnerabilities.ts +++ b/lib/workers/repository/process/vulnerabilities.ts @@ -18,7 +18,11 @@ import { import { sanitizeMarkdown } from '../../../util/markdown'; import * as p from '../../../util/promises'; import { regEx } from '../../../util/regex'; -import type { DependencyVulnerabilities, Vulnerability } from './types'; +import type { + DependencyVulnerabilities, + SeverityDetails, + Vulnerability, +} from './types'; export class Vulnerabilities { private osvOffline: OsvOffline | undefined; @@ -454,12 +458,19 @@ export class Vulnerabilities { logger.debug( `Setting allowed version ${fixedVersion} to fix vulnerability ${vulnerability.id} in ${packageName} ${depVersion}` ); + + const severityDetails = this.extractSeverityDetails( + vulnerability, + affected + ); + return { matchDatasources: [datasource], matchPackageNames: [packageName], matchCurrentVersion: depVersion, allowedVersions: fixedVersion, isVulnerabilityAlert: true, + vulnerabilitySeverity: severityDetails.severityLevel?.toUpperCase(), prBodyNotes: this.generatePrBodyNotes(vulnerability, affected), force: { ...packageFileConfig.vulnerabilityAlerts, @@ -513,24 +524,16 @@ export class Vulnerabilities { content += `#### Details\n${details ?? 'No details.'}\n`; content += '#### Severity\n'; - const cvssVector = - vulnerability.severity?.find((e) => e.type === 'CVSS_V3')?.score ?? - vulnerability.severity?.[0]?.score ?? - (affected.database_specific?.cvss as string); // RUSTSEC - if (cvssVector) { - const [baseScore, severity] = this.evaluateCvssVector(cvssVector); - const score = baseScore ? `${baseScore} / 10 (${severity})` : 'Unknown'; - content += `- CVSS Score: ${score}\n`; - content += `- Vector String: \`${cvssVector}\`\n`; - } else if ( - vulnerability.id.startsWith('GHSA-') && - vulnerability.database_specific?.severity - ) { - const severity = vulnerability.database_specific.severity as string; - content += - severity.charAt(0).toUpperCase() + - severity.slice(1).toLowerCase() + - '\n'; + const severityDetails = this.extractSeverityDetails( + vulnerability, + affected + ); + + if (severityDetails.cvssVector) { + content += `- CVSS Score: ${severityDetails.score}\n`; + content += `- Vector String: \`${severityDetails.cvssVector}\`\n`; + } else if (severityDetails.severityLevel) { + content += `${severityDetails.severityLevel}\n`; } else { content += 'Unknown severity.\n'; } @@ -558,4 +561,36 @@ export class Vulnerabilities { return [sanitizeMarkdown(content)]; } + + private extractSeverityDetails( + vulnerability: Osv.Vulnerability, + affected: Osv.Affected + ): SeverityDetails { + let severityLevel: string | undefined; + let score = 'Unknown'; + + const cvssVector = + vulnerability.severity?.find((e) => e.type === 'CVSS_V3')?.score ?? + vulnerability.severity?.[0]?.score ?? + (affected.database_specific?.cvss as string); // RUSTSEC + + if (cvssVector) { + const [baseScore, severity] = this.evaluateCvssVector(cvssVector); + severityLevel = severity; + score = baseScore ? `${baseScore} / 10 (${severityLevel})` : 'Unknown'; + } else if ( + vulnerability.id.startsWith('GHSA-') && + vulnerability.database_specific?.severity + ) { + const severity = vulnerability.database_specific.severity as string; + severityLevel = + severity.charAt(0).toUpperCase() + severity.slice(1).toLowerCase(); + } + + return { + cvssVector, + score, + severityLevel, + }; + } } diff --git a/lib/workers/repository/update/branch/__snapshots__/commit.spec.ts.snap b/lib/workers/repository/update/branch/__snapshots__/commit.spec.ts.snap deleted file mode 100644 index 9de011dc077203..00000000000000 --- a/lib/workers/repository/update/branch/__snapshots__/commit.spec.ts.snap +++ /dev/null @@ -1,22 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`workers/repository/update/branch/commit commitFilesToBranch commits files 1`] = ` -[ - [ - { - "baseBranch": undefined, - "branchName": "renovate/some-branch", - "files": [ - { - "contents": "some contents", - "path": "package.json", - "type": "addition", - }, - ], - "force": false, - "message": "some commit message", - "platformCommit": false, - }, - ], -] -`; diff --git a/lib/workers/repository/update/branch/artifacts.spec.ts b/lib/workers/repository/update/branch/artifacts.spec.ts index ca63fa5ec3db7b..1744205fbcfbf5 100644 --- a/lib/workers/repository/update/branch/artifacts.spec.ts +++ b/lib/workers/repository/update/branch/artifacts.spec.ts @@ -1,4 +1,4 @@ -import { getConfig, platform } from '../../../../../test/util'; +import { platform } from '../../../../../test/util'; import { GlobalConfig } from '../../../../config/global'; import type { BranchConfig } from '../../../types'; import { setArtifactErrorStatus } from './artifacts'; @@ -9,14 +9,13 @@ describe('workers/repository/update/branch/artifacts', () => { beforeEach(() => { GlobalConfig.set({}); jest.resetAllMocks(); - // TODO #7154 incompatible types config = { - ...getConfig(), + baseBranch: 'base-branch', manager: 'some-manager', branchName: 'renovate/pin', upgrades: [], artifactErrors: [{ lockFile: 'some' }], - } as BranchConfig; + } satisfies BranchConfig; }); describe('setArtifactsErrorStatus', () => { diff --git a/lib/workers/repository/update/branch/automerge.spec.ts b/lib/workers/repository/update/branch/automerge.spec.ts index 81bafafe388f26..87bb468913a999 100644 --- a/lib/workers/repository/update/branch/automerge.spec.ts +++ b/lib/workers/repository/update/branch/automerge.spec.ts @@ -1,4 +1,4 @@ -import { getConfig, git, platform } from '../../../../../test/util'; +import { getConfig, git, platform, scm } from '../../../../../test/util'; import { GlobalConfig } from '../../../../config/global'; import type { RenovateConfig } from '../../../../config/types'; import * as schedule from '../branch/schedule'; @@ -70,7 +70,7 @@ describe('workers/repository/update/branch/automerge', () => { const res = await tryBranchAutomerge(config); expect(res).toBe('failed'); - expect(git.checkoutBranch).toHaveBeenCalled(); + expect(scm.checkoutBranch).toHaveBeenCalled(); }); it('returns true if automerge succeeds', async () => { @@ -82,7 +82,7 @@ describe('workers/repository/update/branch/automerge', () => { const res = await tryBranchAutomerge(config); expect(res).toBe('automerged'); - expect(git.checkoutBranch).toHaveBeenCalledWith('test-branch'); + expect(scm.checkoutBranch).toHaveBeenCalledWith('test-branch'); }); it('returns true if automerge succeeds (dry-run)', async () => { diff --git a/lib/workers/repository/update/branch/automerge.ts b/lib/workers/repository/update/branch/automerge.ts index 8db027b4cc24f1..b6595e2ec118e9 100644 --- a/lib/workers/repository/update/branch/automerge.ts +++ b/lib/workers/repository/update/branch/automerge.ts @@ -2,7 +2,8 @@ import { GlobalConfig } from '../../../../config/global'; import type { RenovateConfig } from '../../../../config/types'; import { logger } from '../../../../logger'; import { platform } from '../../../../modules/platform'; -import { checkoutBranch, mergeBranch } from '../../../../util/git'; +import { scm } from '../../../../modules/platform/scm'; +import { mergeBranch } from '../../../../util/git'; import { isScheduledNow } from './schedule'; import { resolveBranchStatus } from './status-checks'; @@ -42,7 +43,7 @@ export async function tryBranchAutomerge( // TODO: types (#7154) logger.info(`DRY-RUN: Would automerge branch ${config.branchName!}`); } else { - await checkoutBranch(config.baseBranch!); + await scm.checkoutBranch(config.baseBranch!); await mergeBranch(config.branchName!); } logger.info({ branch: config.branchName }, 'Branch automerged'); diff --git a/lib/workers/repository/update/branch/check-existing.spec.ts b/lib/workers/repository/update/branch/check-existing.spec.ts index c8cee3ed04366e..2e1af5eef1a7cd 100644 --- a/lib/workers/repository/update/branch/check-existing.spec.ts +++ b/lib/workers/repository/update/branch/check-existing.spec.ts @@ -1,4 +1,4 @@ -import { getConfig, partial, platform } from '../../../../../test/util'; +import { partial, platform } from '../../../../../test/util'; import { logger } from '../../../../logger'; import type { Pr } from '../../../../modules/platform'; import type { BranchConfig } from '../../../types'; @@ -9,12 +9,13 @@ describe('workers/repository/update/branch/check-existing', () => { let config: BranchConfig; beforeEach(() => { - // TODO: incompatible types (#7154) config = { - ...getConfig(), + baseBranch: 'base-branch', + manager: 'some-manager', + upgrades: [], branchName: 'some-branch', prTitle: 'some-title', - } as BranchConfig; + } satisfies BranchConfig; jest.resetAllMocks(); }); diff --git a/lib/workers/repository/update/branch/commit.spec.ts b/lib/workers/repository/update/branch/commit.spec.ts index 9147cf48e428f8..f98f3b57f7d842 100644 --- a/lib/workers/repository/update/branch/commit.spec.ts +++ b/lib/workers/repository/update/branch/commit.spec.ts @@ -1,4 +1,4 @@ -import { getConfig, scm } from '../../../../../test/util'; +import { scm } from '../../../../../test/util'; import { GlobalConfig } from '../../../../config/global'; import type { BranchConfig } from '../../../types'; import { commitFilesToBranch } from './commit'; @@ -8,9 +8,9 @@ describe('workers/repository/update/branch/commit', () => { let config: BranchConfig; beforeEach(() => { - // TODO: incompatible types (#7154) config = { - ...getConfig(), + baseBranch: 'base-branch', + manager: 'some-manager', branchName: 'renovate/some-branch', commitMessage: 'some commit message', semanticCommits: 'disabled', @@ -19,7 +19,7 @@ describe('workers/repository/update/branch/commit', () => { updatedPackageFiles: [], updatedArtifacts: [], upgrades: [], - } as BranchConfig; + } satisfies BranchConfig; jest.resetAllMocks(); scm.commitAndPush.mockResolvedValueOnce('123test'); GlobalConfig.reset(); @@ -38,7 +38,24 @@ describe('workers/repository/update/branch/commit', () => { }); await commitFilesToBranch(config); expect(scm.commitAndPush).toHaveBeenCalledTimes(1); - expect(scm.commitAndPush.mock.calls).toMatchSnapshot(); + expect(scm.commitAndPush.mock.calls).toEqual([ + [ + { + baseBranch: 'base-branch', + branchName: 'renovate/some-branch', + files: [ + { + contents: 'some contents', + path: 'package.json', + type: 'addition', + }, + ], + force: false, + message: 'some commit message', + platformCommit: false, + }, + ], + ]); }); it('dry runs', async () => { diff --git a/lib/workers/repository/update/branch/get-updated.spec.ts b/lib/workers/repository/update/branch/get-updated.spec.ts index 9dc3d2702d20c1..f6ec285d2f4a0e 100644 --- a/lib/workers/repository/update/branch/get-updated.spec.ts +++ b/lib/workers/repository/update/branch/get-updated.spec.ts @@ -1,4 +1,4 @@ -import { getConfig, git, mocked } from '../../../../../test/util'; +import { git, mocked } from '../../../../../test/util'; import { GitRefsDatasource } from '../../../../modules/datasource/git-refs'; import * as _batectWrapper from '../../../../modules/manager/batect-wrapper'; import * as _bundler from '../../../../modules/manager/bundler'; @@ -32,11 +32,12 @@ describe('workers/repository/update/branch/get-updated', () => { let config: BranchConfig; beforeEach(() => { - // TODO: incompatible types (#7154) config = { - ...getConfig(), + baseBranch: 'base-branch', + manager: 'some-manager', + branchName: 'renovate/pin', upgrades: [], - } as BranchConfig; + } satisfies BranchConfig; npm.updateDependency = jest.fn(); git.getFile.mockResolvedValueOnce('existing content'); }); diff --git a/lib/workers/repository/update/branch/index.spec.ts b/lib/workers/repository/update/branch/index.spec.ts index 636803ec391f9d..aaec7534c8499c 100644 --- a/lib/workers/repository/update/branch/index.spec.ts +++ b/lib/workers/repository/update/branch/index.spec.ts @@ -2253,13 +2253,13 @@ describe('workers/repository/update/branch/index', () => { }); config.baseBranch = 'main'; await branchWorker.processBranch(config); - expect(git.checkoutBranch).toHaveBeenLastCalledWith('main'); + expect(scm.checkoutBranch).toHaveBeenLastCalledWith('main'); // Check that the last checkoutBranch call is after the only commitFilesToBranch call - const checkoutBranchCalledTimes = git.checkoutBranch.mock.calls.length; + const checkoutBranchCalledTimes = scm.checkoutBranch.mock.calls.length; expect( commit.commitFilesToBranch.mock.invocationCallOrder[0] ).toBeLessThan( - git.checkoutBranch.mock.invocationCallOrder[ + scm.checkoutBranch.mock.invocationCallOrder[ checkoutBranchCalledTimes - 1 ] ); diff --git a/lib/workers/repository/update/branch/index.ts b/lib/workers/repository/update/branch/index.ts index ba93eee78e8414..7b61df3f669645 100644 --- a/lib/workers/repository/update/branch/index.ts +++ b/lib/workers/repository/update/branch/index.ts @@ -25,7 +25,6 @@ import { scm } from '../../../../modules/platform/scm'; import { ExternalHostError } from '../../../../types/errors/external-host-error'; import { getElapsedMs } from '../../../../util/date'; import { emojify } from '../../../../util/emoji'; -import { checkoutBranch } from '../../../../util/git'; import { getMergeConfidenceLevel, isActiveConfidenceLevel, @@ -406,7 +405,7 @@ export async function processBranch( // TODO: types (#7154) logger.debug(`Using reuseExistingBranch: ${config.reuseExistingBranch!}`); if (!(config.reuseExistingBranch && config.skipBranchUpdate)) { - await checkoutBranch(config.baseBranch); + await scm.checkoutBranch(config.baseBranch); const res = await getUpdatedPackageFiles(config); // istanbul ignore if if (res.artifactErrors && config.artifactErrors) { @@ -524,7 +523,7 @@ export async function processBranch( commitSha = await commitFilesToBranch(config); // Checkout to base branch to ensure that the next branch processing always starts with git being on the baseBranch // baseBranch is not checked out at the start of processBranch() due to pull/16246 - await checkoutBranch(config.baseBranch); + await scm.checkoutBranch(config.baseBranch); updatesVerified = true; } // istanbul ignore if diff --git a/lib/workers/repository/update/pr/automerge.spec.ts b/lib/workers/repository/update/pr/automerge.spec.ts index dfe4913afb4770..b602de45160ef3 100644 --- a/lib/workers/repository/update/pr/automerge.spec.ts +++ b/lib/workers/repository/update/pr/automerge.spec.ts @@ -1,4 +1,4 @@ -import { getConfig, partial, platform, scm } from '../../../../../test/util'; +import { partial, platform, scm } from '../../../../../test/util'; import { GlobalConfig } from '../../../../config/global'; import type { Pr } from '../../../../modules/platform'; import type { BranchConfig } from '../../../types'; @@ -12,10 +12,12 @@ describe('workers/repository/update/pr/automerge', () => { let pr: Pr; beforeEach(() => { - // TODO #7154 incompatible types config = { - ...getConfig(), - } as BranchConfig; + baseBranch: 'base-branch', + manager: 'some-manager', + branchName: 'renovate/pin', + upgrades: [], + } satisfies BranchConfig; pr = partial(); }); diff --git a/lib/workers/repository/update/pr/changelog/release-notes.spec.ts b/lib/workers/repository/update/pr/changelog/release-notes.spec.ts index 978bef659f4a44..500c1ed58936bc 100644 --- a/lib/workers/repository/update/pr/changelog/release-notes.spec.ts +++ b/lib/workers/repository/update/pr/changelog/release-notes.spec.ts @@ -87,9 +87,11 @@ describe('workers/repository/update/pr/changelog/release-notes', () => { describe('addReleaseNotes()', () => { it('returns null if input is null/undefined', async () => { - expect(await addReleaseNotes(null, {} as BranchUpgradeConfig)).toBeNull(); expect( - await addReleaseNotes(undefined, {} as BranchUpgradeConfig) + await addReleaseNotes(null, partial()) + ).toBeNull(); + expect( + await addReleaseNotes(undefined, partial()) ).toBeNull(); }); @@ -108,7 +110,6 @@ describe('workers/repository/update/pr/changelog/release-notes', () => { it('returns ChangeLogResult', async () => { const input = { - a: 1, project: { type: 'github', repository: 'https://github.com/nodeca/js-yaml', @@ -118,7 +119,6 @@ describe('workers/repository/update/pr/changelog/release-notes', () => { expect( await addReleaseNotes(input as never, partial()) ).toEqual({ - a: 1, hasReleaseNotes: false, project: { repository: 'https://github.com/nodeca/js-yaml', @@ -138,7 +138,6 @@ describe('workers/repository/update/pr/changelog/release-notes', () => { it('returns ChangeLogResult without release notes', async () => { const input = { - a: 1, project: partial({ type: 'gitlab', repository: 'https://gitlab.com/gitlab-org/gitter/webapp/', @@ -149,11 +148,10 @@ describe('workers/repository/update/pr/changelog/release-notes', () => { compare: { url: '' }, }), ], - } as ChangeLogResult; + } satisfies ChangeLogResult; expect( await addReleaseNotes(input, partial()) ).toEqual({ - a: 1, hasReleaseNotes: false, project: { repository: 'https://gitlab.com/gitlab-org/gitter/webapp/', diff --git a/lib/workers/repository/updates/generate.ts b/lib/workers/repository/updates/generate.ts index 3dd6b922615837..e02cf5012b7c20 100644 --- a/lib/workers/repository/updates/generate.ts +++ b/lib/workers/repository/updates/generate.ts @@ -226,7 +226,7 @@ export function generateBranchConfig( regEx(/to vv(\d)/), 'to v$1' ); - if (upgrade.toLowerCase) { + if (upgrade.toLowerCase && upgrade.commitMessageLowerCase !== 'never') { // We only need to lowercase the first line const splitMessage = upgrade.commitMessage.split(newlineRegex); splitMessage[0] = splitMessage[0].toLowerCase(); @@ -249,7 +249,7 @@ export function generateBranchConfig( ); throw new Error(CONFIG_SECRETS_EXPOSED); } - if (upgrade.toLowerCase) { + if (upgrade.toLowerCase && upgrade.commitMessageLowerCase !== 'never') { upgrade.prTitle = upgrade.prTitle.toLowerCase(); } } else { diff --git a/package.json b/package.json index 7e79c608a9eb93..96a630697c56d0 100644 --- a/package.json +++ b/package.json @@ -142,6 +142,7 @@ "@aws-sdk/client-rds": "3.314.0", "@aws-sdk/client-s3": "3.312.0", "@breejs/later": "4.1.0", + "@cdktf/hcl2json": "0.16.1", "@cheap-glitch/mi-cron": "1.0.1", "@iarna/toml": "3.0.0", "@opentelemetry/api": "1.4.1", @@ -155,13 +156,13 @@ "@opentelemetry/sdk-trace-node": "1.12.0", "@opentelemetry/semantic-conventions": "1.12.0", "@qnighy/marshal": "0.1.3", - "@renovatebot/osv-offline": "1.2.4", - "@renovatebot/pep440": "2.1.14", - "@renovatebot/ruby-semver": "2.1.12", + "@renovatebot/osv-offline": "1.2.5", + "@renovatebot/pep440": "2.1.15", + "@renovatebot/ruby-semver": "3.0.1", "@sindresorhus/is": "4.6.0", "@types/ms": "0.7.31", "@types/tmp": "0.2.3", - "@yarnpkg/core": "3.4.0", + "@yarnpkg/core": "3.5.1", "@yarnpkg/parsers": "2.5.1", "agentkeepalive": "4.3.0", "aggregate-error": "3.1.0", @@ -169,7 +170,7 @@ "aws4": "1.12.0", "azure-devops-node-api": "12.0.0", "bunyan": "1.8.15", - "cacache": "17.0.5", + "cacache": "17.1.0", "cacheable-lookup": "5.0.4", "chalk": "4.1.2", "changelog-filename-regex": "2.0.1", @@ -195,10 +196,10 @@ "global-agent": "3.0.0", "good-enough-parser": "1.1.22", "got": "11.8.6", + "glob": "10.2.2", "graph-data-structure": "3.3.0", "handlebars": "4.7.7", "hasha": "5.2.2", - "hcl2-parser": "1.0.3", "ignore": "5.2.4", "ini": "4.1.0", "js-yaml": "4.1.0", @@ -221,7 +222,7 @@ "parse-link-header": "2.0.0", "prettier": "2.8.8", "quick-lru": "5.1.1", - "redis": "4.6.5", + "redis": "4.6.6", "remark": "13.0.0", "remark-github": "10.1.0", "safe-stable-stringify": "2.4.3", @@ -278,18 +279,18 @@ "@types/marshal": "0.5.1", "@types/moo": "0.5.5", "@types/nock": "10.0.3", - "@types/node": "18.16.3", + "@types/node": "18.16.8", "@types/parse-link-header": "2.0.1", "@types/semver": "7.3.13", "@types/semver-stable": "3.0.0", "@types/semver-utils": "1.1.1", - "@types/tar": "6.1.4", + "@types/tar": "6.1.5", "@types/traverse": "0.6.32", "@types/url-join": "4.0.1", "@types/validate-npm-package-name": "4.0.0", "@types/xmldoc": "1.1.6", - "@typescript-eslint/eslint-plugin": "5.59.1", - "@typescript-eslint/parser": "5.59.1", + "@typescript-eslint/eslint-plugin": "5.59.5", + "@typescript-eslint/parser": "5.59.5", "aws-sdk-client-mock": "2.1.1", "callsite": "1.0.0", "common-tags": "1.8.2", @@ -297,7 +298,7 @@ "cross-env": "7.0.3", "diff": "5.1.0", "emojibase-data": "7.0.1", - "eslint": "8.39.0", + "eslint": "8.40.0", "eslint-config-prettier": "8.8.0", "eslint-formatter-gha": "1.4.2", "eslint-import-resolver-typescript": "3.5.5", @@ -307,7 +308,6 @@ "eslint-plugin-promise": "6.1.1", "eslint-plugin-typescript-enum": "2.1.0", "expect-more-jest": "5.5.0", - "glob": "10.2.2", "graphql": "16.6.0", "husky": "8.0.3", "jest": "29.5.0", @@ -319,16 +319,16 @@ "memfs": "3.5.1", "mock-fs": "5.2.0", "mockdate": "3.0.5", - "nock": "13.3.0", + "nock": "13.3.1", "npm-run-all": "4.1.5", "rimraf": "5.0.0", - "semantic-release": "21.0.1", + "semantic-release": "21.0.2", "strip-ansi": "6.0.1", - "tar": "6.1.13", + "tar": "6.1.14", "tmp-promise": "3.0.3", "ts-jest": "29.1.0", "ts-node": "10.9.1", - "type-fest": "3.9.0", + "type-fest": "3.10.0", "typescript": "5.0.4", "unified": "9.2.2" }, diff --git a/test/fixtures.ts b/test/fixtures.ts index 3b3198122074ca..35afd650a566ee 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -1,8 +1,10 @@ -import type fs from 'node:fs'; +import fs from 'node:fs'; import type { PathLike, Stats } from 'node:fs'; import { jest } from '@jest/globals'; import callsite from 'callsite'; import { DirectoryJSON, fs as memfs, vol } from 'memfs'; +import type { TDataOut } from 'memfs/lib/encoding'; +import type { IOptions } from 'memfs/lib/volume'; import upath from 'upath'; // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion @@ -93,7 +95,7 @@ export class Fixtures { ...memfs, pathExists: jest.fn(pathExists), remove: jest.fn(memfs.promises.rm), - readFile: jest.fn(memfs.promises.readFile), + readFile: jest.fn(readFile), writeFile: jest.fn(memfs.promises.writeFile), outputFile: jest.fn(outputFile), stat: jest.fn(stat), @@ -107,6 +109,17 @@ export class Fixtures { } } +export function readFile( + fileName: string, + options: IOptions +): Promise { + if (fileName.endsWith('.wasm') || fileName.endsWith('.wasm.gz')) { + return fs.promises.readFile(fileName, options as any); + } + + return memfs.promises.readFile(fileName, options); +} + export async function outputFile( file: string, data: string | Buffer | Uint8Array diff --git a/tools/docs/manager.ts b/tools/docs/manager.ts index a3c0641526be6f..03f089130f7263 100644 --- a/tools/docs/manager.ts +++ b/tools/docs/manager.ts @@ -75,9 +75,10 @@ sidebar_label: ${displayName} .join(', '); md += `This manager supports extracting the following datasources: ${escapedDatasources}.\n\n`; - md += '## References'; - md += formatUrls(urls).replace('**References**:', ''); - + if (urls?.length) { + md += '## References'; + md += formatUrls(urls).replace('**References**:', ''); + } md += '## Default config\n\n'; md += '```json\n'; md += JSON.stringify(definition.defaultConfig, null, 2) + '\n'; diff --git a/yarn.lock b/yarn.lock index 3c83b670fd84c3..928f04e5d55747 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1525,6 +1525,13 @@ resolved "https://registry.yarnpkg.com/@breejs/later/-/later-4.1.0.tgz#9246907f46cc9e9c9af37d791ab468d98921bcc1" integrity sha512-QgGnZ9b7o4k0Ai1ZbTJWwZpZcFK9d+Gb+DyNt4UT9x6IEIs5HVu0iIlmgzGqN+t9MoJSpSPo9S/Mm51UtHr3JA== +"@cdktf/hcl2json@0.16.1": + version "0.16.1" + resolved "https://registry.yarnpkg.com/@cdktf/hcl2json/-/hcl2json-0.16.1.tgz#c5d4c965e150d29579d6b53a9fe6c946fca7e614" + integrity sha512-c0AcAzpUYREmkonxai3Jjd7P0c0/TMB+ga3Ir/02MDSdyV8VQ+1CLK2nVMJJqJTsGZkxrZGVj/jlwA6coDYaqg== + dependencies: + fs-extra "^11.1.1" + "@cheap-glitch/mi-cron@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@cheap-glitch/mi-cron/-/mi-cron-1.0.1.tgz#111f4ce746c269aedf74533ac806881763a68f99" @@ -1564,14 +1571,14 @@ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.1.tgz#cdd35dce4fa1a89a4fd42b1599eb35b3af408884" integrity sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ== -"@eslint/eslintrc@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.2.tgz#01575e38707add677cf73ca1589abba8da899a02" - integrity sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ== +"@eslint/eslintrc@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.3.tgz#4910db5505f4d503f27774bf356e3704818a0331" + integrity sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.5.1" + espree "^9.5.2" globals "^13.19.0" ignore "^5.2.0" import-fresh "^3.2.1" @@ -1579,10 +1586,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.39.0": - version "8.39.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.39.0.tgz#58b536bcc843f4cd1e02a7e6171da5c040f4d44b" - integrity sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng== +"@eslint/js@8.40.0": + version "8.40.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.40.0.tgz#3ba73359e11f5a7bd3e407f70b3528abfae69cec" + integrity sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA== "@gar/promisify@^1.1.3": version "1.1.3" @@ -2448,10 +2455,10 @@ resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.2.0.tgz#d3fd6d3c0af3ef92f26767b56414a370c7b63b71" integrity sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg== -"@redis/client@1.5.6": - version "1.5.6" - resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.5.6.tgz#869cc65718d7d5493ef655a71dc40f3bc64a1b28" - integrity sha512-dFD1S6je+A47Lj22jN/upVU2fj4huR7S9APd7/ziUXsIXDL+11GPYti4Suv5y8FuXaN+0ZG4JF+y1houEJ7ToA== +"@redis/client@1.5.7": + version "1.5.7" + resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.5.7.tgz#92cc5c98c76f189e37d24f0e1e17e104c6af17d4" + integrity sha512-gaOBOuJPjK5fGtxSseaKgSvjiZXQCdLlGg9WYQst+/GRUjmXaiB5kVkeQMRtPc7Q2t93XZcJfBMSwzs/XS9UZw== dependencies: cluster-key-slot "1.1.2" generic-pool "3.9.0" @@ -2480,47 +2487,45 @@ "@renovate/eslint-plugin@file:./tools/eslint": version "0.0.0" -"@renovatebot/osv-offline-db@1.3.2": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@renovatebot/osv-offline-db/-/osv-offline-db-1.3.2.tgz#960f687fce4555f3d802ff855318eeb9d34a9215" - integrity sha512-02sMhxKjmRvHm6HIdTEyVIAIdEdYWLOU/ecQuiP2hvgXg28P6GWj/BkAbQ8GU0uLfBMZiqB4DK9MKoI4wx7BJw== +"@renovatebot/osv-offline-db@1.3.3": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@renovatebot/osv-offline-db/-/osv-offline-db-1.3.3.tgz#371d0a22ece6b2f1012c525dfd4005ee4e6580f1" + integrity sha512-Q7PNdRbMio2GYFqanuHByln20viVpyNhE4CmcY6ly2TVx42YbuRBrZ57JY5RVid0icArVagkW4mjrDh9CawOgA== dependencies: - "@seald-io/nedb" "^4.0.1" + "@seald-io/nedb" "^4.0.2" -"@renovatebot/osv-offline@1.2.4": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@renovatebot/osv-offline/-/osv-offline-1.2.4.tgz#cf2d79e8aabf3b15570c8a3178372a3df04ef7b9" - integrity sha512-9r97Fn8dmdbAtqNAM4z3ECyxo6cpcQ1acMuAQoFcC0xemuFlzkrP+BY0KI/U/qQG2IojEfW+9TU+C3o0ZDS94w== +"@renovatebot/osv-offline@1.2.5": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@renovatebot/osv-offline/-/osv-offline-1.2.5.tgz#40bdb5c22eb1cd66ec3a79d3a25de40fbdce6bb4" + integrity sha512-PRzSyAhel/coD68Z8WK5nFw9UJ8vLCvQQb+aRzwwmMEuSzAbe1FnVbvEZ/wae3sDy57nvMuVhk358InMYHYyVQ== dependencies: "@octokit/rest" "^19.0.7" - "@renovatebot/osv-offline-db" "1.3.2" + "@renovatebot/osv-offline-db" "1.3.3" adm-zip "~0.5.10" fs-extra "^11.1.1" got "^11.8.6" luxon "^3.3.0" node-fetch "^2.6.9" -"@renovatebot/pep440@2.1.14": - version "2.1.14" - resolved "https://registry.yarnpkg.com/@renovatebot/pep440/-/pep440-2.1.14.tgz#fc0e354cf6e892a8e17d61a4ea7b82b9e309476b" - integrity sha512-Wl35WmAaflt/evQx3EOI6vhlbLJG9kw/+RxRabNOTB4aK2hyGDyha5h8yzjLDD14Gfp74z1RXivIetNsPjRgKA== +"@renovatebot/pep440@2.1.15": + version "2.1.15" + resolved "https://registry.yarnpkg.com/@renovatebot/pep440/-/pep440-2.1.15.tgz#b481ec1a9f2782debd8f3c787999529a46157019" + integrity sha512-zJQ6ZLzgD3HsyyBLnDiviimxfripKHzPYmRqgIHb9Ettre3ivrkaz6F1zbeowf0kRBxB8rh8qit0Ftn4oBsg+w== -"@renovatebot/ruby-semver@2.1.12": - version "2.1.12" - resolved "https://registry.yarnpkg.com/@renovatebot/ruby-semver/-/ruby-semver-2.1.12.tgz#001ae01d3576336e5865fdae5970d9cb89241167" - integrity sha512-9j42JmVRqDmdeEauCcMCRyvy3NA7z7ftuVUkUhB98NU1HUwmxiz1OExgPvxeNRIW/MryNXvUvPNT9dfDT75anw== - dependencies: - tslib "^2.5.0" +"@renovatebot/ruby-semver@3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@renovatebot/ruby-semver/-/ruby-semver-3.0.1.tgz#968c7dcf8b46a97df15aeca393fe2b2cae564d26" + integrity sha512-sbNb8/J/bI8ZYFrgwpEnYAZl8WDrB9OvEPbnEw4hakAMNLJ4H9ByrzywmJa+xqmPXcF4rOqFqqURMiLZkBXwtQ== "@seald-io/binary-search-tree@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@seald-io/binary-search-tree/-/binary-search-tree-1.0.3.tgz#165a9a456eaa30d15885b25db83861bcce2c6a74" integrity sha512-qv3jnwoakeax2razYaMsGI/luWdliBLHTdC6jU55hQt1hcFqzauH/HsBollQ7IR4ySTtYhT+xyHoijpA16C+tA== -"@seald-io/nedb@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@seald-io/nedb/-/nedb-4.0.1.tgz#bfd62b5468e6b03e4f3a16dabd12c9d55aab8856" - integrity sha512-E2l7EUEMkSO3WLydRxRWjVWzijzWehzKmy/q9ekbgaOOQjYD7zHdS8z3KsM8+aQ910agfREZCR+3YCc5Xuc9dg== +"@seald-io/nedb@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@seald-io/nedb/-/nedb-4.0.2.tgz#44bc5f9b86e44f7434c5af8064cc7f8e079fc3a8" + integrity sha512-gJ91fT1sgh2cLXYVcTSh7khZ8LdemI8+SojCdpZ5wy+DUQ4fSrEwGqOwbdV49NDs2BBO6GeBpSb8CnhG2IW1rw== dependencies: "@seald-io/binary-search-tree" "^1.0.3" localforage "^1.9.0" @@ -2597,10 +2602,10 @@ semver "^7.1.2" tempy "^3.0.0" -"@semantic-release/release-notes-generator@^10.0.0": - version "10.0.3" - resolved "https://registry.yarnpkg.com/@semantic-release/release-notes-generator/-/release-notes-generator-10.0.3.tgz#85f7ca78bfa6b01fb5fda0ac48112855d69171dc" - integrity sha512-k4x4VhIKneOWoBGHkx0qZogNjCldLPRiAjnIpMnlUh6PtaWXp/T+C9U7/TaNDDtgDa5HMbHl4WlREdxHio6/3w== +"@semantic-release/release-notes-generator@^11.0.0": + version "11.0.1" + resolved "https://registry.yarnpkg.com/@semantic-release/release-notes-generator/-/release-notes-generator-11.0.1.tgz#38a7d66e9a762915bea36a006dbb2d41fbcb4a66" + integrity sha512-4deWsiY4Rg80oc9Ms11N20BIDgYkPMys4scNYQpi2Njdrtw5Z55nXKNsUN3kn6Sy/nI9dqqbp5L63TL4luI5Bw== dependencies: conventional-changelog-angular "^5.0.0" conventional-changelog-writer "^5.0.0" @@ -2609,9 +2614,9 @@ debug "^4.0.0" get-stream "^6.0.0" import-from "^4.0.0" - into-stream "^6.0.0" - lodash "^4.17.4" - read-pkg-up "^7.0.0" + into-stream "^7.0.0" + lodash-es "^4.17.21" + read-pkg-up "^9.0.0" "@sigstore/protobuf-specs@^0.1.0": version "0.1.0" @@ -3056,11 +3061,16 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@18.16.3": +"@types/node@*": version "18.16.3" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.3.tgz#6bda7819aae6ea0b386ebc5b24bdf602f1b42b01" integrity sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q== +"@types/node@18.16.8": + version "18.16.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.8.tgz#fcd9bd0a793aba2701caff4aeae7c988d4da6ce5" + integrity sha512-p0iAXcfWCOTCBbsExHIDFCfwsqFwBTgETJveKMT+Ci3LY9YqQCI91F5S+TB20+aRCXpcWfvx5Qr5EccnwCm2NA== + "@types/node@^13.7.0": version "13.13.52" resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.52.tgz#03c13be70b9031baaed79481c0c0cfb0045e53f7" @@ -3125,10 +3135,10 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== -"@types/tar@6.1.4": - version "6.1.4" - resolved "https://registry.yarnpkg.com/@types/tar/-/tar-6.1.4.tgz#cf8497e1ebdc09212fd51625cd2eb5ca18365ad1" - integrity sha512-Cp4oxpfIzWt7mr2pbhHT2OTXGMAL0szYCzuf8lRWyIMCgsx6/Hfc3ubztuhvzXHXgraTQxyOCmmg7TDGIMIJJQ== +"@types/tar@6.1.5": + version "6.1.5" + resolved "https://registry.yarnpkg.com/@types/tar/-/tar-6.1.5.tgz#90ccb3b6a35430e7427410d50eed564e85feaaff" + integrity sha512-qm2I/RlZij5RofuY7vohTpYNaYcrSQlN2MyjucQc7ZweDwaEWkdN/EeNh6e9zjK6uEm6PwjdMXkcj05BxZdX1Q== dependencies: "@types/node" "*" minipass "^4.0.0" @@ -3187,15 +3197,15 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@5.59.1": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz#9b09ee1541bff1d2cebdcb87e7ce4a4003acde08" - integrity sha512-AVi0uazY5quFB9hlp2Xv+ogpfpk77xzsgsIEWyVS7uK/c7MZ5tw7ZPbapa0SbfkqE0fsAMkz5UwtgMLVk2BQAg== +"@typescript-eslint/eslint-plugin@5.59.5": + version "5.59.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.5.tgz#f156827610a3f8cefc56baeaa93cd4a5f32966b4" + integrity sha512-feA9xbVRWJZor+AnLNAr7A8JRWeZqHUf4T9tlP+TN04b05pFVhO5eN7/O93Y/1OUlLMHKbnJisgDURs/qvtqdg== dependencies: "@eslint-community/regexpp" "^4.4.0" - "@typescript-eslint/scope-manager" "5.59.1" - "@typescript-eslint/type-utils" "5.59.1" - "@typescript-eslint/utils" "5.59.1" + "@typescript-eslint/scope-manager" "5.59.5" + "@typescript-eslint/type-utils" "5.59.5" + "@typescript-eslint/utils" "5.59.5" debug "^4.3.4" grapheme-splitter "^1.0.4" ignore "^5.2.0" @@ -3210,14 +3220,14 @@ dependencies: "@typescript-eslint/utils" "5.59.1" -"@typescript-eslint/parser@5.59.1": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.1.tgz#73c2c12127c5c1182d2e5b71a8fa2a85d215cbb4" - integrity sha512-nzjFAN8WEu6yPRDizIFyzAfgK7nybPodMNFGNH0M9tei2gYnYszRDqVA0xlnRjkl7Hkx2vYrEdb6fP2a21cG1g== +"@typescript-eslint/parser@5.59.5": + version "5.59.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.5.tgz#63064f5eafbdbfb5f9dfbf5c4503cdf949852981" + integrity sha512-NJXQC4MRnF9N9yWqQE2/KLRSOLvrrlZb48NGVfBa+RuPMN6B7ZcK5jZOvhuygv4D64fRKnZI4L4p8+M+rfeQuw== dependencies: - "@typescript-eslint/scope-manager" "5.59.1" - "@typescript-eslint/types" "5.59.1" - "@typescript-eslint/typescript-estree" "5.59.1" + "@typescript-eslint/scope-manager" "5.59.5" + "@typescript-eslint/types" "5.59.5" + "@typescript-eslint/typescript-estree" "5.59.5" debug "^4.3.4" "@typescript-eslint/scope-manager@5.59.1": @@ -3228,13 +3238,21 @@ "@typescript-eslint/types" "5.59.1" "@typescript-eslint/visitor-keys" "5.59.1" -"@typescript-eslint/type-utils@5.59.1": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.1.tgz#63981d61684fd24eda2f9f08c0a47ecb000a2111" - integrity sha512-ZMWQ+Oh82jWqWzvM3xU+9y5U7MEMVv6GLioM3R5NJk6uvP47kZ7YvlgSHJ7ERD6bOY7Q4uxWm25c76HKEwIjZw== +"@typescript-eslint/scope-manager@5.59.5": + version "5.59.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.5.tgz#33ffc7e8663f42cfaac873de65ebf65d2bce674d" + integrity sha512-jVecWwnkX6ZgutF+DovbBJirZcAxgxC0EOHYt/niMROf8p4PwxxG32Qdhj/iIQQIuOflLjNkxoXyArkcIP7C3A== dependencies: - "@typescript-eslint/typescript-estree" "5.59.1" - "@typescript-eslint/utils" "5.59.1" + "@typescript-eslint/types" "5.59.5" + "@typescript-eslint/visitor-keys" "5.59.5" + +"@typescript-eslint/type-utils@5.59.5": + version "5.59.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.5.tgz#485b0e2c5b923460bc2ea6b338c595343f06fc9b" + integrity sha512-4eyhS7oGym67/pSxA2mmNq7X164oqDYNnZCUayBwJZIRVvKpBCMBzFnFxjeoDeShjtO6RQBHBuwybuX3POnDqg== + dependencies: + "@typescript-eslint/typescript-estree" "5.59.5" + "@typescript-eslint/utils" "5.59.5" debug "^4.3.4" tsutils "^3.21.0" @@ -3243,6 +3261,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.1.tgz#03f3fedd1c044cb336ebc34cc7855f121991f41d" integrity sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg== +"@typescript-eslint/types@5.59.5": + version "5.59.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.5.tgz#e63c5952532306d97c6ea432cee0981f6d2258c7" + integrity sha512-xkfRPHbqSH4Ggx4eHRIO/eGL8XL4Ysb4woL8c87YuAo8Md7AUjyWKa9YMwTL519SyDPrfEgKdewjkxNCVeJW7w== + "@typescript-eslint/typescript-estree@5.59.1": version "5.59.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz#4aa546d27fd0d477c618f0ca00b483f0ec84c43c" @@ -3256,6 +3279,19 @@ semver "^7.3.7" tsutils "^3.21.0" +"@typescript-eslint/typescript-estree@5.59.5": + version "5.59.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.5.tgz#9b252ce55dd765e972a7a2f99233c439c5101e42" + integrity sha512-+XXdLN2CZLZcD/mO7mQtJMvCkzRfmODbeSKuMY/yXbGkzvA9rJyDY5qDYNoiz2kP/dmyAxXquL2BvLQLJFPQIg== + dependencies: + "@typescript-eslint/types" "5.59.5" + "@typescript-eslint/visitor-keys" "5.59.5" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + "@typescript-eslint/utils@5.59.1", "@typescript-eslint/utils@^5.10.0": version "5.59.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.1.tgz#d89fc758ad23d2157cfae53f0b429bdf15db9473" @@ -3270,6 +3306,20 @@ eslint-scope "^5.1.1" semver "^7.3.7" +"@typescript-eslint/utils@5.59.5": + version "5.59.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.5.tgz#15b3eb619bb223302e60413adb0accd29c32bcae" + integrity sha512-sCEHOiw+RbyTii9c3/qN74hYDPNORb8yWCoPLmB7BIflhplJ65u2PBpdRla12e3SSTJ2erRkPjz7ngLHhUegxA== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@types/json-schema" "^7.0.9" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.59.5" + "@typescript-eslint/types" "5.59.5" + "@typescript-eslint/typescript-estree" "5.59.5" + eslint-scope "^5.1.1" + semver "^7.3.7" + "@typescript-eslint/visitor-keys@5.59.1": version "5.59.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz#0d96c36efb6560d7fb8eb85de10442c10d8f6058" @@ -3278,19 +3328,27 @@ "@typescript-eslint/types" "5.59.1" eslint-visitor-keys "^3.3.0" -"@yarnpkg/core@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@yarnpkg/core/-/core-3.4.0.tgz#1684dfcbd4e68ff28a11a25154d415f7de2c8224" - integrity sha512-EwkRU7TjZrpAPA2cqVBtMD95unti9uSXefGJbhCVANOfuY6tjBoqSt8uSf6BSXUUoJkxBWwewjTn0v0tgoLBkQ== +"@typescript-eslint/visitor-keys@5.59.5": + version "5.59.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.5.tgz#ba5b8d6791a13cf9fea6716af1e7626434b29b9b" + integrity sha512-qL+Oz+dbeBRTeyJTIy0eniD3uvqU7x+y1QceBismZ41hd4aBSRh8UAw4pZP0+XzLuPZmx4raNMq/I+59W2lXKA== + dependencies: + "@typescript-eslint/types" "5.59.5" + eslint-visitor-keys "^3.3.0" + +"@yarnpkg/core@3.5.1": + version "3.5.1" + resolved "https://registry.yarnpkg.com/@yarnpkg/core/-/core-3.5.1.tgz#bb1f13ffed9828a8b4d22980615bb7f1569daefe" + integrity sha512-BkbqhE7Ti/Kk2zl0MF2TPosUSh7yS9iaZWnmcXfzKPcPatY0NTg1wX+OuW/eXMFAHudXXmMMte9Ve2f6Kj7itQ== dependencies: "@arcanis/slice-ansi" "^1.1.1" "@types/semver" "^7.1.0" "@types/treeify" "^1.0.0" - "@yarnpkg/fslib" "^2.10.1" + "@yarnpkg/fslib" "^2.10.3" "@yarnpkg/json-proxy" "^2.1.1" - "@yarnpkg/libzip" "^2.2.4" + "@yarnpkg/libzip" "^2.3.0" "@yarnpkg/parsers" "^2.5.1" - "@yarnpkg/pnp" "^3.3.0" + "@yarnpkg/pnp" "^3.3.2" "@yarnpkg/shell" "^3.2.5" camelcase "^5.3.1" chalk "^3.0.0" @@ -3316,7 +3374,15 @@ tslib "^1.13.0" tunnel "^0.0.6" -"@yarnpkg/fslib@^2.10.1", "@yarnpkg/fslib@^2.10.2", "@yarnpkg/fslib@^2.5.0", "@yarnpkg/fslib@^2.9.0": +"@yarnpkg/fslib@^2.10.3": + version "2.10.3" + resolved "https://registry.yarnpkg.com/@yarnpkg/fslib/-/fslib-2.10.3.tgz#a8c9893df5d183cf6362680b9f1c6d7504dd5717" + integrity sha512-41H+Ga78xT9sHvWLlFOZLIhtU6mTGZ20pZ29EiZa97vnxdohJD2AF42rCoAoWfqUz486xY6fhjMH+DYEM9r14A== + dependencies: + "@yarnpkg/libzip" "^2.3.0" + tslib "^1.13.0" + +"@yarnpkg/fslib@^2.5.0", "@yarnpkg/fslib@^2.9.0": version "2.10.2" resolved "https://registry.yarnpkg.com/@yarnpkg/fslib/-/fslib-2.10.2.tgz#16e419a1412084f152e8eec16183a7f80bc73684" integrity sha512-6WfQrPEV8QVpDPw5kd5s5jsb3QLqwVFSGZy3rEjl3p2FZ3OtIfYcLbFirOxXj2jXiKQDe7XbYsw1WjSf8K94gw== @@ -3332,7 +3398,7 @@ "@yarnpkg/fslib" "^2.5.0" tslib "^1.13.0" -"@yarnpkg/libzip@^2.2.4", "@yarnpkg/libzip@^2.3.0": +"@yarnpkg/libzip@^2.3.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@yarnpkg/libzip/-/libzip-2.3.0.tgz#fe1e762e47669f6e2c960fc118436608d834e3be" integrity sha512-6xm38yGVIa6mKm/DUCF2zFFJhERh/QWp1ufm4cNUvxsONBmfPg8uZ9pZBdOmF6qFGr/HlT6ABBkCSx/dlEtvWg== @@ -3348,13 +3414,13 @@ js-yaml "^3.10.0" tslib "^1.13.0" -"@yarnpkg/pnp@^3.3.0": - version "3.3.1" - resolved "https://registry.yarnpkg.com/@yarnpkg/pnp/-/pnp-3.3.1.tgz#8ce0777f2950cbadc9978ba3bd3407b4b9c2a640" - integrity sha512-iA2IKk2hOQqEBBHBZ3j004mCfzsPGx5PNjgGn8kb6/VMXokX4mhZzPNI/q7ipI0+UaIMeDXvIzwdGL2GSmgsTA== +"@yarnpkg/pnp@^3.3.2": + version "3.3.2" + resolved "https://registry.yarnpkg.com/@yarnpkg/pnp/-/pnp-3.3.2.tgz#348e1c4ac3ad4ff60a2ba5f274ec7d6f97fe2727" + integrity sha512-3rJbY9/wB7fLrjZtgcYyqtWHK3ihmCbFFqnjghH3MnBYABeh5IgB83BjSfbIT5s/BtVIg583yBHok02OO4ighg== dependencies: "@types/node" "^13.7.0" - "@yarnpkg/fslib" "^2.10.2" + "@yarnpkg/fslib" "^2.10.3" "@yarnpkg/shell@^3.2.5": version "3.2.5" @@ -3884,21 +3950,20 @@ bunyan@1.8.15: mv "~2" safe-json-stringify "~1" -cacache@17.0.5: - version "17.0.5" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-17.0.5.tgz#6dbec26c11f1f6a2b558bc11ed3316577c339ebc" - integrity sha512-Y/PRQevNSsjAPWykl9aeGz8Pr+OI6BYM9fYDNMvOkuUiG9IhG4LEmaYrZZZvioMUEQ+cBCxT0v8wrnCURccyKA== +cacache@17.1.0: + version "17.1.0" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-17.1.0.tgz#b7286ef941dafe55b461cdcdceda71cacc1eb98d" + integrity sha512-hXpFU+Z3AfVmNuiLve1qxWHMq0RSIt5gjCKAHi/M6DktwFwDdAXAtunl1i4WSKaaVcU9IsRvXFg42jTHigcC6Q== dependencies: "@npmcli/fs" "^3.1.0" fs-minipass "^3.0.0" - glob "^9.3.1" + glob "^10.2.2" lru-cache "^7.7.1" - minipass "^4.0.0" + minipass "^5.0.0" minipass-collect "^1.0.2" minipass-flush "^1.0.5" minipass-pipeline "^1.2.4" p-map "^4.0.0" - promise-inflight "^1.0.1" ssri "^10.0.0" tar "^6.1.11" unique-filename "^3.0.0" @@ -5001,20 +5066,25 @@ eslint-scope@^7.2.0: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.0: +eslint-visitor-keys@^3.3.0: version "3.4.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz#c7f0f956124ce677047ddbc192a68f999454dedc" integrity sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ== -eslint@8.39.0: - version "8.39.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.39.0.tgz#7fd20a295ef92d43809e914b70c39fd5a23cf3f1" - integrity sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og== +eslint-visitor-keys@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994" + integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== + +eslint@8.40.0: + version "8.40.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.40.0.tgz#a564cd0099f38542c4e9a2f630fa45bf33bc42a4" + integrity sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.4.0" - "@eslint/eslintrc" "^2.0.2" - "@eslint/js" "8.39.0" + "@eslint/eslintrc" "^2.0.3" + "@eslint/js" "8.40.0" "@humanwhocodes/config-array" "^0.11.8" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" @@ -5025,8 +5095,8 @@ eslint@8.39.0: doctrine "^3.0.0" escape-string-regexp "^4.0.0" eslint-scope "^7.2.0" - eslint-visitor-keys "^3.4.0" - espree "^9.5.1" + eslint-visitor-keys "^3.4.1" + espree "^9.5.2" esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" @@ -5052,14 +5122,14 @@ eslint@8.39.0: strip-json-comments "^3.1.0" text-table "^0.2.0" -espree@^9.5.1: - version "9.5.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.1.tgz#4f26a4d5f18905bf4f2e0bd99002aab807e96dd4" - integrity sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg== +espree@^9.5.2: + version "9.5.2" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.2.tgz#e994e7dc33a082a7a82dceaf12883a829353215b" + integrity sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw== dependencies: acorn "^8.8.0" acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.4.0" + eslint-visitor-keys "^3.4.1" esprima@^4.0.0, esprima@~4.0.0: version "4.0.1" @@ -5601,7 +5671,7 @@ glob@^8.0.1: minimatch "^5.0.1" once "^1.3.0" -glob@^9.3.1, glob@^9.3.2: +glob@^9.3.2: version "9.3.5" resolved "https://registry.yarnpkg.com/glob/-/glob-9.3.5.tgz#ca2ed8ca452781a3009685607fdf025a899dfe21" integrity sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q== @@ -5810,11 +5880,6 @@ hasha@5.2.2: is-stream "^2.0.0" type-fest "^0.8.0" -hcl2-parser@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/hcl2-parser/-/hcl2-parser-1.0.3.tgz#096d0ff5a3c46707ace54fcb7571317f5828ff0e" - integrity sha512-NQUm/BFF+2nrBfeqDhhsy4DxxiLHgkeE3FywtjFiXnjSUaio3w4Tz1MQ3vGJBUhyArzOXJ24pO7JwE5LAn7Ncg== - he@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -6021,10 +6086,10 @@ internal-slot@^1.0.5: has "^1.0.3" side-channel "^1.0.4" -into-stream@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-6.0.0.tgz#4bfc1244c0128224e18b8870e85b2de8e66c6702" - integrity sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA== +into-stream@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-7.0.0.tgz#d1a211e146be8acfdb84dabcbf00fe8205e72936" + integrity sha512-2dYz766i9HprMBasCMvHMuazJ7u4WzhJwo5kb3iPSiW/iRYV6uPari3zHoqZlnuaR7V1bEiNMxikhp37rdBXbw== dependencies: from2 "^2.3.0" p-is-promise "^3.0.0" @@ -7846,10 +7911,10 @@ nise@^5.1.2: just-extend "^4.0.2" path-to-regexp "^1.7.0" -nock@13.3.0: - version "13.3.0" - resolved "https://registry.yarnpkg.com/nock/-/nock-13.3.0.tgz#b13069c1a03f1ad63120f994b04bfd2556925768" - integrity sha512-HHqYQ6mBeiMc+N038w8LkMpDCRquCHWeNmN3v6645P3NhN2+qXOBqvPqo7Rt1VyCMzKhJ733wZqw5B7cQVFNPg== +nock@13.3.1: + version "13.3.1" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.3.1.tgz#f22d4d661f7a05ebd9368edae1b5dc0a62d758fc" + integrity sha512-vHnopocZuI93p2ccivFyGuUfzjq2fxNyNurp7816mlT5V5HF4SzXu8lvLrVzBbNqzs+ODooZ6OksuSUNM7Njkw== dependencies: debug "^4.1.0" json-stringify-safe "^5.0.1" @@ -8851,7 +8916,7 @@ read-package-json@^6.0.0, read-package-json@^6.0.1: normalize-package-data "^5.0.0" npm-normalize-package-bin "^3.0.0" -read-pkg-up@^7.0.0, read-pkg-up@^7.0.1: +read-pkg-up@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== @@ -8860,7 +8925,7 @@ read-pkg-up@^7.0.0, read-pkg-up@^7.0.1: read-pkg "^5.2.0" type-fest "^0.8.1" -read-pkg-up@^9.1.0: +read-pkg-up@^9.0.0, read-pkg-up@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-9.1.0.tgz#38ca48e0bc6c6b260464b14aad9bcd4e5b1fbdc3" integrity sha512-vaMRR1AC1nrd5CQM0PhlRsO5oc2AAigqr7cCrZ/MW/Rsaflz4RlgzkpL4qoU/z1F6wrbd85iFv1OQj/y5RdGvg== @@ -8970,13 +9035,13 @@ redeyed@~2.1.0: dependencies: esprima "~4.0.0" -redis@4.6.5: - version "4.6.5" - resolved "https://registry.yarnpkg.com/redis/-/redis-4.6.5.tgz#f32fbde44429e96f562bb0c9b1db0143ab8cfa4f" - integrity sha512-O0OWA36gDQbswOdUuAhRL6mTZpHFN525HlgZgDaVNgCJIAZR3ya06NTESb0R+TUZ+BFaDpz6NnnVvoMx9meUFg== +redis@4.6.6: + version "4.6.6" + resolved "https://registry.yarnpkg.com/redis/-/redis-4.6.6.tgz#46d4f2d149d1634d6ef53db5747412a0ef7974ec" + integrity sha512-aLs2fuBFV/VJ28oLBqYykfnhGGkFxvx0HdCEBYdJ99FFbSEMZ7c1nVKwR6ZRv+7bb7JnC0mmCzaqu8frgOYhpA== dependencies: "@redis/bloom" "1.2.0" - "@redis/client" "1.5.6" + "@redis/client" "1.5.7" "@redis/graph" "1.1.0" "@redis/json" "1.0.4" "@redis/search" "1.1.2" @@ -9216,16 +9281,16 @@ sax@^1.2.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -semantic-release@21.0.1: - version "21.0.1" - resolved "https://registry.yarnpkg.com/semantic-release/-/semantic-release-21.0.1.tgz#f2cb0eb1d0b1883efdac2883ec40342ac5c6ff30" - integrity sha512-UhGxTUXHJQCBFgEQRZszLOHDpMduDSHGq3Q+30Bu+g0GbXh/EW508+kuFHezP5m0mN8xINW8hooiR3dzSV5ZLA== +semantic-release@21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/semantic-release/-/semantic-release-21.0.2.tgz#c8610c25864c761f3a3acc2eb39c166f4e0200e4" + integrity sha512-Hl6lyJdZ0pAYD07Z1FIUmg06UzSC3fEjHS7U31YppNQ8jOwjjt7pVzW9OfpoO0vbmqD3Tc+b/iZh5fqvKt01OA== dependencies: "@semantic-release/commit-analyzer" "^9.0.2" "@semantic-release/error" "^3.0.0" "@semantic-release/github" "^8.0.0" "@semantic-release/npm" "^10.0.2" - "@semantic-release/release-notes-generator" "^10.0.0" + "@semantic-release/release-notes-generator" "^11.0.0" aggregate-error "^4.0.1" cosmiconfig "^8.0.0" debug "^4.0.0" @@ -9807,7 +9872,19 @@ tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== -tar@6.1.13, tar@^6.0.5, tar@^6.1.11, tar@^6.1.13, tar@^6.1.2: +tar@6.1.14: + version "6.1.14" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.14.tgz#e87926bec1cfe7c9e783a77a79f3e81c1cfa3b66" + integrity sha512-piERznXu0U7/pW7cdSn7hjqySIVTYT6F76icmFk7ptU7dDYlXTm5r9A6K04R2vU3olYgoKeo1Cg3eeu5nhftAw== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +tar@^6.0.5, tar@^6.1.11, tar@^6.1.13, tar@^6.1.2: version "6.1.13" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.13.tgz#46e22529000f612180601a6fe0680e7da508847b" integrity sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw== @@ -10056,10 +10133,10 @@ type-detect@4.0.8, type-detect@^4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== -type-fest@3.9.0, type-fest@^3.8.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.9.0.tgz#36a9e46e6583649f9e6098b267bc577275e9e4f4" - integrity sha512-hR8JP2e8UiH7SME5JZjsobBlEiatFoxpzCP+R3ZeCo7kAaG1jXQE5X/buLzogM6GJu8le9Y4OcfNuIQX0rZskA== +type-fest@3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.10.0.tgz#d75f17a22be8816aea6315ab2739fe1c0c211863" + integrity sha512-hmAPf1datm+gt3c2mvu0sJyhFy6lTkIGf0GzyaZWxRLnabQfPUqg6tF95RPg6sLxKI7nFLGdFxBcf2/7+GXI+A== type-fest@^0.13.1: version "0.13.1" @@ -10101,6 +10178,11 @@ type-fest@^2.0.0, type-fest@^2.12.2, type-fest@^2.5.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== +type-fest@^3.8.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.9.0.tgz#36a9e46e6583649f9e6098b267bc577275e9e4f4" + integrity sha512-hR8JP2e8UiH7SME5JZjsobBlEiatFoxpzCP+R3ZeCo7kAaG1jXQE5X/buLzogM6GJu8le9Y4OcfNuIQX0rZskA== + typed-array-length@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb"