Compare commits

...

21 commits

Author SHA1 Message Date
HonkingGoose
a769c88f33
Merge ae54b7069f into f97189c600 2025-01-08 13:59:52 +01:00
Mark Reuter
f97189c600
feat(bazel-module): Support git_repository (#33415)
Some checks are pending
Build / setup (push) Waiting to run
Build / setup-build (push) Waiting to run
Build / prefetch (push) Blocked by required conditions
Build / lint-eslint (push) Blocked by required conditions
Build / lint-prettier (push) Blocked by required conditions
Build / lint-docs (push) Blocked by required conditions
Build / lint-other (push) Blocked by required conditions
Build / (push) Blocked by required conditions
Build / codecov (push) Blocked by required conditions
Build / coverage-threshold (push) Blocked by required conditions
Build / test-success (push) Blocked by required conditions
Build / build (push) Blocked by required conditions
Build / build-docs (push) Blocked by required conditions
Build / test-e2e (push) Blocked by required conditions
Build / release (push) Blocked by required conditions
Code scanning / CodeQL-Build (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
whitesource-scan / WS_SCAN (push) Waiting to run
2025-01-08 12:34:19 +00:00
Trim21
59455c0512
feat(pre-commit): support python additional_dependencies (#33417) 2025-01-08 12:30:31 +00:00
Maxime Brunet
147b620187
feat(poetry): support GCloud credentials for Google Artifact Registry when locking (#32586)
Co-authored-by: Rhys Arkins <rhys@arkins.net>
2025-01-08 12:28:11 +00:00
RahulGautamSingh
39fb207a83
refactor(workers/reconfigure): update code structure (#33340) 2025-01-08 12:18:30 +00:00
RahulGautamSingh
db31a1634c
fix(gitlab): truncate comment (#33348) 2025-01-08 12:07:30 +00:00
RahulGautamSingh
5282f7c080
fix(github): remove deleted issue from issues cache (#33349) 2025-01-08 11:42:12 +00:00
renovate[bot]
974a8a8b51
feat(deps): update ghcr.io/renovatebot/base-image docker tag to v9.29.0 (#33468)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-08 11:09:53 +00:00
renovate[bot]
2bfc754634
fix(deps): update ghcr.io/renovatebot/base-image docker tag to v9.28.2 (#33467)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-08 10:09:59 +00:00
lbarros-asml
0fae10626b
fix(bundler): authentication using only env vars (#33339)
Co-authored-by: Leandro <grandinetti@gmail.com>
Co-authored-by: Rhys Arkins <rhys@arkins.net>
2025-01-08 10:03:03 +00:00
Justin Clareburt
50e53440f4
docs: documentation for Mend-hosted Renovate plans (#33451)
Co-authored-by: Rhys Arkins <rhys@arkins.net>
2025-01-08 08:56:14 +00:00
Sergei Zharinov
c043653c4b
refactor(nuget): Support skip-version during extract (#33437) 2025-01-08 05:58:47 +00:00
renovate[bot]
351d9ef3e8
chore(deps): update jaegertracing/jaeger docker tag to v2.2.0 (#33458)
Some checks are pending
Build / setup (push) Waiting to run
Build / setup-build (push) Waiting to run
Build / prefetch (push) Blocked by required conditions
Build / lint-eslint (push) Blocked by required conditions
Build / lint-prettier (push) Blocked by required conditions
Build / lint-docs (push) Blocked by required conditions
Build / lint-other (push) Blocked by required conditions
Build / (push) Blocked by required conditions
Build / codecov (push) Blocked by required conditions
Build / coverage-threshold (push) Blocked by required conditions
Build / test-success (push) Blocked by required conditions
Build / build (push) Blocked by required conditions
Build / build-docs (push) Blocked by required conditions
Build / test-e2e (push) Blocked by required conditions
Build / release (push) Blocked by required conditions
Code scanning / CodeQL-Build (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
whitesource-scan / WS_SCAN (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-08 04:10:01 +00:00
renovate[bot]
c04c64f5e7
chore(deps): update dependency @types/node to v20.17.11 (#33457)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-08 02:10:47 +00:00
Pascal Berger
2c75a8d4f7
feat(presets): Add redirect URL for SkiaSharp NuGet packages (#33452)
Some checks are pending
Build / setup-build (push) Waiting to run
Build / setup (push) Waiting to run
Build / prefetch (push) Blocked by required conditions
Build / lint-eslint (push) Blocked by required conditions
Build / lint-prettier (push) Blocked by required conditions
Build / lint-docs (push) Blocked by required conditions
Build / lint-other (push) Blocked by required conditions
Build / (push) Blocked by required conditions
Build / codecov (push) Blocked by required conditions
Build / coverage-threshold (push) Blocked by required conditions
Build / test-success (push) Blocked by required conditions
Build / build (push) Blocked by required conditions
Build / build-docs (push) Blocked by required conditions
Build / test-e2e (push) Blocked by required conditions
Build / release (push) Blocked by required conditions
Code scanning / CodeQL-Build (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
whitesource-scan / WS_SCAN (push) Waiting to run
2025-01-07 16:13:55 +00:00
HonkingGoose
ae54b7069f
Update switching-bot-identity.md
Co-authored-by: Sebastian Poxhofer <secustor@users.noreply.github.com>
2024-08-26 12:30:10 +02:00
HonkingGoose
8708557f44
Merge branch 'main' into docs/30385-new-page-switching-bot-id 2024-08-26 10:04:38 +02:00
HonkingGoose
0edddf4a28
Put new page in the Configuration sidebar 2024-07-29 11:20:36 +02:00
HonkingGoose
2411a6f713
Update switching-bot-identity.md 2024-07-29 11:18:29 +02:00
HonkingGoose
d2b04461aa
Update switching-bot-identity.md 2024-07-29 11:15:41 +02:00
HonkingGoose
3f65694a00
docs: new page about switching bot identity 2024-07-29 10:32:09 +02:00
44 changed files with 1198 additions and 753 deletions

View file

@ -9,6 +9,7 @@ nav:
- 'Self-hosted': 'self-hosted-configuration.md'
- 'Presets': 'config-presets.md'
- 'Validation': 'config-validation.md'
- 'Switching bot identity': 'switching-bot-identity.md'
- ... | mend-hosted
- ... | key-concepts
- ... | modules

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View file

@ -145,6 +145,19 @@ archive_override(
Renovate ignores [`multiple_version_override`](https://bazel.build/rules/lib/globals/module#multiple_version_override).
`multiple_version_override` does not affect the processing of version updates for a module.
### `git_repository`
If Renovate finds a [`git_repository`](https://bazel.build/rules/lib/repo/git#git_repository), it evaluates the `commit` value at the specified `remote`.
`remote` is limited to github repos: `https://github.com/<owner>/<repo>.git`
```python
git_repository(
name = "rules_foo",
remote = "https://github.com/fooexample/rules_foo.git",
commit = "8c94e11c2b05b6f25ced5f23cd07d0cfd36edc1a",
)
```
## Legacy `WORKSPACE` files
Renovate extracts dependencies from the following repository rules:
@ -160,7 +173,7 @@ Renovate extracts dependencies from the following repository rules:
It also recognizes when these repository rule names are prefixed with an underscore.
For example, `_http_archive` is treated the same as `http_archive`.
### `git_repository`
### `git_repository` (legacy)
Renovate updates any `git_repository` declaration that has the following:

View file

@ -14,7 +14,7 @@ name: renovate-otel-demo
services:
# Jaeger for storing traces
jaeger:
image: jaegertracing/jaeger:2.1.0
image: jaegertracing/jaeger:2.2.0
ports:
- '16686:16686' # Web UI
- '4317' # OTLP gRPC

View file

@ -1,5 +1,6 @@
title: Mend-hosted Apps
nav:
- 'Renovate Plans': 'renovate-plans.md'
- 'Configuration': 'hosted-apps-config.md'
- 'Credentials': 'credentials.md'
- 'Migrating Secrets': 'migrating-secrets.md'

View file

@ -0,0 +1,53 @@
# Renovate Plans on Mend-Hosted Apps
Mend provides cloud hosting services for running Renovate on repositories hosted on the following cloud platforms:
- GitHub
- Bitbucket Cloud
- Azure DevOps
Mend Cloud will regularly schedule Renovate jobs against all installed repositories.
It also listens to webhooks and enqueues a Renovate job when relevant changes occur in a repo, or when actions are triggered from the Renovate PRs or Dashboard issue.
There is a web UI with functionality to view and interact with installed repositories, their jobs and job logs.
## Accessing Mend Cloud via the Web UI
Users can access the cloud-hosted Renovate service via the Developer Portal at [https://developer.mend.io/](https://developer.mend.io/).
Developers can log in with OAuth credentials from their cloud-based Git repository.
![Developer Portal sign-in screen](../assets/images/portal-sign-in.png)
Features of the Developer Portal include:
- Ability to install, uninstall and view installed repositories
- Trigger Renovate jobs to run on demand
- View logs for all Renovate jobs
- Configure settings that apply at the Org-level or Repo-level
## Resources and Scheduling
The plan assigned to each Org determines the resources, scheduling and concurrency of Renoate jobs.
Mend Cloud has free and paid Renovate plans. Details of the plans are shown in the table below.
| | Community (Free) | Pioneer (Free) | OSS Select (Free) | Enterprise |
| ----------------------------- | ---------------- | -------------- | ----------------- | ------------ |
| Concurrent jobs per Org | 1 | 8 | 2 | 16 |
| Job scheduling (active repos) | Every 4 hours | Every 4 hours | Hourly | Hourly (\*1) |
| Job runner CPUs | 1 CPU | 1 CPU | 1 CPU | 2 CPU |
| Job runner Memory | 2Gb | 3.5Gb | 6Gb | 8Gb |
| Job runner Disk space | 15Gb | 15Gb | 25Gb | 40Gb |
| Job timeout | 30 minutes | 30 minutes | 60 minutes | 60 minutes |
| Merge Confidence Workflows | Not included | Not included | Included | Included |
| Mend.io Helpdesk Support | Not included | Not included | Not Included | Included |
(1) Bitbucket repositories on the Renovate Enterprise plan are scheduled to run every 4 hours, to avoid hitting rate limits on GitHub APIs.
### Plan descriptions
**Community (Free)** - This plan is available for free for all repositories.
**Pioneer (Free)** - This plan is available for a limited time for Orgs that were installed on Renovate Cloud before 2025. Users on this plan will be transitioned to other plans over time.
**OSS Select (Free)** - This is a premium plan granted for free to selected OSS Orgs. If you would like your Org to be considered for the free OSS Select plan, create a “[Suggest an Idea](https://github.com/renovatebot/renovate/discussions/categories/suggest-an-idea)” item on the Renovate discussions board on GitHub. Acceptance is at the discretion of Mend.io.
**Enterprise** - A supported, paid plan available for purchase through Mend.io. Contact Mend at [sales@mend.io](mailto:sales@mend.io) for purchase details.

View file

@ -0,0 +1,52 @@
# Switching bot identity
Renovate uses its bot identity to know:
- if a PR is authored by Renovate bot
- if you, or some other bot, pushed commits into the PR branch
- Editor note: list other things Renovate uses the bot id for here
## Reasons to switch bot identity
Common reasons to switch bot identity are:
- Migrating between Mend-hosted and self-hosted
- Renaming the bot (self-hosted)
- Editor note: Maybe there are other reasons too, please comment
### Migrating from Mend-hosted to self-hosted
Most users start with the Mend-hosted bot, and are happy with this.
After a while, some users want to switch to self-hosting, for more control.
When you migrate you must tell your new self-hosted Renovate bot to take over from the old Mend-hosted bot.
### Renaming the bot (self-hosted)
Maybe you started self-hosting Renovate, and called your bot `@old-bot-name`.
But the name no longer fits, so you want to use `@new-bot-name`.
Follow these steps:
1. Start of ordered list with steps
1. Second step
1. Third step, and so on
### Other situations
Editor note: Please comment about other cases where you need to switch bot identity.
## How to switch bot identity
Looks like the steps are:
1. Put old bot name in the `gitIgnoredAuthors` config option
1. Set `ignorePrAuthor` to `true`
1. Let the "new" bot take over from the "old bot"
## Questions from the editor
- Is the `gitAuthor` field the bot identity?
- What's `gitIgnoredAuthors` for? It looks like you can ignore commits from the old bot with it?
- We also have `ignorePrAuthor` which if set to `true` fetches the _whole_ list of PRs, instead of just fetching Renovate PRs. The docs say this is the one to use to ignore old bot names. But the description of `ignorePrAuthor` only mentions the full list fetching, nothing about the name/id of the bot.
- It's easy to confuse `gitIgnoredAuthors` and `ignorePrAuthor` they have similar names, but do different things.

View file

@ -505,7 +505,10 @@
"shiki": "https://github.com/shikijs/shiki",
"shopify-app-bridge": "https://github.com/Shopify/app-bridge",
"sitecore-jss": "https://github.com/Sitecore/jss",
"skiasharp": "https://github.com/mono/SkiaSharp",
"skiasharp": [
"https://github.com/mono/SkiaSharp",
"https://go.microsoft.com/fwlink/?linkid=868515"
],
"slack-net": "https://github.com/soxtoby/SlackNet",
"slf4j": "https://github.com/qos-ch/slf4j",
"slim-message-bus": "https://github.com/zarusz/SlimMessageBus",

View file

@ -392,5 +392,31 @@ describe('modules/manager/bazel-module/extract', () => {
},
]);
});
it('returns git_repository dependencies', async () => {
const input = codeBlock`
git_repository(
name = "rules_foo",
commit = "850cb49c8649e463b80ef7984e7c744279746170",
remote = "https://github.com/example/rules_foo.git",
)
`;
const result = await extractPackageFile(input, 'MODULE.bazel');
if (!result) {
throw new Error('Expected a result.');
}
expect(result.deps).toHaveLength(1);
expect(result.deps).toEqual(
expect.arrayContaining([
{
datasource: GithubTagsDatasource.id,
depType: 'git_repository',
depName: 'rules_foo',
currentDigest: '850cb49c8649e463b80ef7984e7c744279746170',
packageName: 'example/rules_foo',
},
]),
);
});
});
});

View file

@ -8,7 +8,10 @@ import type { RecordFragment } from './fragments';
import { parse } from './parser';
import { RuleToMavenPackageDep, fillRegistryUrls } from './parser/maven';
import { RuleToDockerPackageDep } from './parser/oci';
import { RuleToBazelModulePackageDep } from './rules';
import {
GitRepositoryToPackageDep,
RuleToBazelModulePackageDep,
} from './rules';
import * as rules from './rules';
export async function extractPackageFile(
@ -18,9 +21,14 @@ export async function extractPackageFile(
try {
const records = parse(content);
const pfc = await extractBazelPfc(records, packageFile);
const gitRepositoryDeps = extractGitRepositoryDeps(records);
const mavenDeps = extractMavenDeps(records);
const dockerDeps = LooseArray(RuleToDockerPackageDep).parse(records);
if (gitRepositoryDeps.length) {
pfc.deps.push(...gitRepositoryDeps);
}
if (mavenDeps.length) {
pfc.deps.push(...mavenDeps);
}
@ -57,6 +65,12 @@ async function extractBazelPfc(
return pfc;
}
function extractGitRepositoryDeps(
records: RecordFragment[],
): PackageDependency[] {
return LooseArray(GitRepositoryToPackageDep).parse(records);
}
function extractMavenDeps(records: RecordFragment[]): PackageDependency[] {
return LooseArray(RuleToMavenPackageDep)
.transform(fillRegistryUrls)

View file

@ -315,5 +315,37 @@ describe('modules/manager/bazel-module/parser/index', () => {
),
]);
});
it('finds the git_repository', () => {
const input = codeBlock`
git_repository(
name = "rules_foo",
remote = "https://github.com/example/rules_foo.git",
commit = "6a2c2e22849b3e6b33d5ea9aa72222d4803a986a",
patches = ["//:rules_foo.patch"],
patch_strip = 1,
)
`;
const res = parse(input);
expect(res).toEqual([
fragments.record(
{
rule: fragments.string('git_repository'),
name: fragments.string('rules_foo'),
patches: fragments.array(
[fragments.string('//:rules_foo.patch')],
true,
),
commit: fragments.string(
'6a2c2e22849b3e6b33d5ea9aa72222d4803a986a',
),
remote: fragments.string(
'https://github.com/example/rules_foo.git',
),
},
true,
),
]);
});
});
});

View file

@ -9,6 +9,7 @@ const supportedRules = [
'git_override',
'local_path_override',
'single_version_override',
'git_repository',
];
const supportedRulesRegex = regEx(`^${supportedRules.join('|')}$`);

View file

@ -10,6 +10,7 @@ import type {
OverridePackageDep,
} from './rules';
import {
GitRepositoryToPackageDep,
RuleToBazelModulePackageDep,
bazelModulePackageDepToPackageDependency,
processModulePkgDeps,
@ -72,6 +73,19 @@ const singleVersionOverrideWithoutVersionAndRegistryPkgDep: BasePackageDep = {
depName: 'rules_foo',
skipReason: 'ignored',
};
const gitRepositoryForGithubPkgDep: BasePackageDep = {
datasource: GithubTagsDatasource.id,
depType: 'git_repository',
depName: 'rules_foo',
packageName: 'example/rules_foo',
currentDigest: '850cb49c8649e463b80ef7984e7c744279746170',
};
const gitRepositoryForUnsupportedPkgDep: BasePackageDep = {
depType: 'git_repository',
depName: 'rules_foo',
currentDigest: '850cb49c8649e463b80ef7984e7c744279746170',
skipReason: 'unsupported-datasource',
};
describe('modules/manager/bazel-module/rules', () => {
describe('RuleToBazelModulePackageDep', () => {
@ -129,6 +143,30 @@ describe('modules/manager/bazel-module/rules', () => {
});
});
describe('GitRepositoryToPackageDep', () => {
const gitRepositoryWithGihubHost = fragments.record({
rule: fragments.string('git_repository'),
name: fragments.string('rules_foo'),
remote: fragments.string('https://github.com/example/rules_foo.git'),
commit: fragments.string('850cb49c8649e463b80ef7984e7c744279746170'),
});
const gitRepositoryWithUnsupportedHost = fragments.record({
rule: fragments.string('git_repository'),
name: fragments.string('rules_foo'),
remote: fragments.string('https://nobuenos.com/example/rules_foo.git'),
commit: fragments.string('850cb49c8649e463b80ef7984e7c744279746170'),
});
it.each`
msg | a | exp
${'git_repository, GitHub host'} | ${gitRepositoryWithGihubHost} | ${gitRepositoryForGithubPkgDep}
${'git_repository, unsupported host'} | ${gitRepositoryWithUnsupportedHost} | ${gitRepositoryForUnsupportedPkgDep}
`('.parse() with $msg', ({ a, exp }) => {
const pkgDep = GitRepositoryToPackageDep.parse(a);
expect(pkgDep).toEqual(exp);
});
});
describe('.toPackageDependencies()', () => {
const expectedBazelDepNoOverrides: PackageDependency[] = [bazelDepPkgDep];
const expectedBazelDepAndGitOverride: PackageDependency[] = [

View file

@ -242,3 +242,28 @@ export function toPackageDependencies(
): PackageDependency[] {
return collectByModule(packageDeps).map(processModulePkgDeps).flat();
}
export const GitRepositoryToPackageDep = RecordFragmentSchema.extend({
children: z.object({
rule: StringFragmentSchema.extend({
value: z.literal('git_repository'),
}),
name: StringFragmentSchema,
remote: StringFragmentSchema,
commit: StringFragmentSchema,
}),
}).transform(({ children: { rule, name, remote, commit } }): BasePackageDep => {
const gitRepo: BasePackageDep = {
depType: rule.value,
depName: name.value,
currentDigest: commit.value,
};
const ghPackageName = githubPackageName(remote.value);
if (is.nonEmptyString(ghPackageName)) {
gitRepo.datasource = GithubTagsDatasource.id;
gitRepo.packageName = ghPackageName;
} else {
gitRepo.skipReason = 'unsupported-datasource';
}
return gitRepo;
});

View file

@ -438,8 +438,8 @@ describe('modules/manager/bundler/artifacts', () => {
bundlerHostRules.findAllAuthenticatable.mockReturnValue([
{
hostType: 'bundler',
matchHost: 'gems.private.com',
resolvedHost: 'gems.private.com',
matchHost: 'gems-private.com',
resolvedHost: 'gems-private.com',
username: 'some-user',
password: 'some-password',
},
@ -470,7 +470,7 @@ describe('modules/manager/bundler/artifacts', () => {
'docker run --rm --name=renovate_sidecar --label=renovate_child ' +
'-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' +
'-v "/tmp/cache":"/tmp/cache" ' +
'-e BUNDLE_GEMS__PRIVATE__COM ' +
'-e BUNDLE_GEMS___PRIVATE__COM ' +
'-e GEM_HOME ' +
'-e CONTAINERBASE_CACHE_DIR ' +
'-w "/tmp/github/some/repo" ' +
@ -487,218 +487,6 @@ describe('modules/manager/bundler/artifacts', () => {
},
]);
});
it('injects bundler host configuration as command with bundler < 2', async () => {
GlobalConfig.set({ ...adminConfig, binarySource: 'docker' });
fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
fs.readLocalFile.mockResolvedValueOnce('1.2.0');
// ruby
datasource.getPkgReleases.mockResolvedValueOnce({
releases: [
{ version: '1.0.0' },
{ version: '1.2.0' },
{ version: '1.3.0' },
],
});
bundlerHostRules.findAllAuthenticatable.mockReturnValue([
{
hostType: 'bundler',
matchHost: 'gems-private.com',
resolvedHost: 'gems-private.com',
username: 'some-user',
password: 'some-password',
},
]);
bundlerHostRules.getAuthenticationHeaderValue.mockReturnValue(
'some-user:some-password',
);
const execSnapshots = mockExecAll();
git.getRepoStatus.mockResolvedValueOnce(
partial<StatusResult>({
modified: ['Gemfile.lock'],
}),
);
fs.readLocalFile.mockResolvedValueOnce('Updated Gemfile.lock');
expect(
await updateArtifacts({
packageFileName: 'Gemfile',
updatedDeps: [{ depName: 'foo' }, { depName: 'bar' }],
newPackageFileContent: 'Updated Gemfile content',
config: {
...config,
constraints: {
bundler: '1.2',
},
},
}),
).toEqual([updatedGemfileLock]);
expect(execSnapshots).toMatchObject([
{ cmd: 'docker pull ghcr.io/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 GEM_HOME ' +
'-e CONTAINERBASE_CACHE_DIR ' +
'-w "/tmp/github/some/repo" ' +
'ghcr.io/containerbase/sidecar' +
' bash -l -c "' +
'install-tool ruby 1.2.0' +
' && ' +
'install-tool bundler 1.2' +
' && ' +
'ruby --version' +
' && ' +
'bundler config --local gems-private.com some-user:some-password' +
' && ' +
'bundler lock --update foo bar' +
'"',
},
]);
});
it('injects bundler host configuration as command with bundler >= 2', async () => {
GlobalConfig.set({ ...adminConfig, binarySource: 'docker' });
fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
fs.readLocalFile.mockResolvedValueOnce('1.2.0');
// ruby
datasource.getPkgReleases.mockResolvedValueOnce({
releases: [
{ version: '1.0.0' },
{ version: '1.2.0' },
{ version: '1.3.0' },
],
});
bundlerHostRules.findAllAuthenticatable.mockReturnValue([
{
hostType: 'bundler',
matchHost: 'gems-private.com',
resolvedHost: 'gems-private.com',
username: 'some-user',
password: 'some-password',
},
]);
bundlerHostRules.getAuthenticationHeaderValue.mockReturnValue(
'some-user:some-password',
);
const execSnapshots = mockExecAll();
git.getRepoStatus.mockResolvedValueOnce(
partial<StatusResult>({
modified: ['Gemfile.lock'],
}),
);
fs.readLocalFile.mockResolvedValueOnce('Updated Gemfile.lock');
expect(
await updateArtifacts({
packageFileName: 'Gemfile',
updatedDeps: [{ depName: 'foo' }, { depName: 'bar' }],
newPackageFileContent: 'Updated Gemfile content',
config: {
...config,
constraints: {
bundler: '2.1',
},
},
}),
).toEqual([updatedGemfileLock]);
expect(execSnapshots).toMatchObject([
{ cmd: 'docker pull ghcr.io/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 GEM_HOME ' +
'-e CONTAINERBASE_CACHE_DIR ' +
'-w "/tmp/github/some/repo" ' +
'ghcr.io/containerbase/sidecar' +
' bash -l -c "' +
'install-tool ruby 1.2.0' +
' && ' +
'install-tool bundler 2.1' +
' && ' +
'ruby --version' +
' && ' +
'bundler config set --local gems-private.com some-user:some-password' +
' && ' +
'bundler lock --update foo bar' +
'"',
},
]);
});
it('injects bundler host configuration as command with bundler == latest', async () => {
GlobalConfig.set({ ...adminConfig, binarySource: 'docker' });
fs.readLocalFile.mockResolvedValueOnce('Current Gemfile.lock');
fs.readLocalFile.mockResolvedValueOnce('1.2.0');
// ruby
datasource.getPkgReleases.mockResolvedValueOnce({
releases: [
{ version: '1.0.0' },
{ version: '1.2.0' },
{ version: '1.3.0' },
],
});
// bundler
datasource.getPkgReleases.mockResolvedValueOnce({
releases: [{ version: '1.17.2' }, { version: '2.3.5' }],
});
bundlerHostRules.findAllAuthenticatable.mockReturnValue([
{
hostType: 'bundler',
matchHost: 'gems-private.com',
resolvedHost: 'gems-private.com',
username: 'some-user',
password: 'some-password',
},
]);
bundlerHostRules.getAuthenticationHeaderValue.mockReturnValue(
'some-user:some-password',
);
const execSnapshots = mockExecAll();
git.getRepoStatus.mockResolvedValueOnce(
partial<StatusResult>({
modified: ['Gemfile.lock'],
}),
);
fs.readLocalFile.mockResolvedValueOnce('Updated Gemfile.lock');
expect(
await updateArtifacts({
packageFileName: 'Gemfile',
updatedDeps: [{ depName: 'foo' }, { depName: 'bar' }],
newPackageFileContent: 'Updated Gemfile content',
config,
}),
).toEqual([updatedGemfileLock]);
expect(execSnapshots).toMatchObject([
{ cmd: 'docker pull ghcr.io/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 GEM_HOME ' +
'-e CONTAINERBASE_CACHE_DIR ' +
'-w "/tmp/github/some/repo" ' +
'ghcr.io/containerbase/sidecar' +
' bash -l -c "' +
'install-tool ruby 1.2.0' +
' && ' +
'install-tool bundler 1.3.0' +
' && ' +
'ruby --version' +
' && ' +
'bundler config set --local gems-private.com some-user:some-password' +
' && ' +
'bundler lock --update foo bar' +
'"',
},
]);
});
});
it('returns error when failing in lockFileMaintenance true', async () => {

View file

@ -1,4 +1,3 @@
import { lt } from '@renovatebot/ruby-semver';
import is from '@sindresorhus/is';
import { quote } from 'shlex';
import {
@ -17,7 +16,6 @@ import {
} from '../../../util/fs';
import { getRepoStatus } from '../../../util/git';
import { newlineRegex, regEx } from '../../../util/regex';
import { isValid } from '../../versioning/ruby';
import type { UpdateArtifact, UpdateArtifactsResult } from '../types';
import {
getBundlerConstraint,
@ -32,14 +30,17 @@ import {
const hostConfigVariablePrefix = 'BUNDLE_';
function buildBundleHostVariable(hostRule: HostRule): Record<string, string> {
if (!hostRule.resolvedHost || hostRule.resolvedHost.includes('-')) {
// istanbul ignore if: doesn't happen in practice
if (!hostRule.resolvedHost) {
return {};
}
const varName = hostConfigVariablePrefix.concat(
hostRule.resolvedHost
.toUpperCase()
.split('.')
.map((term) => term.toUpperCase())
.join('__'),
.join('__')
.split('-')
.join('___'),
);
return {
[varName]: `${getAuthenticationHeaderValue(hostRule)}`,
@ -149,47 +150,12 @@ export async function updateArtifacts(
{} as Record<string, string>,
);
// Detect hosts with a hyphen '-' in the url.
// Those cannot be added with environment variables but need to be added
// with the bundler config
const bundlerHostRulesAuthCommands: string[] = bundlerHostRules.reduce(
(authCommands: string[], hostRule) => {
if (hostRule.resolvedHost?.includes('-')) {
// TODO: fix me, hostrules can missing all auth
const creds = getAuthenticationHeaderValue(hostRule);
authCommands.push(`${quote(hostRule.resolvedHost)} ${quote(creds)}`);
}
return authCommands;
},
[],
);
const bundler = getBundlerConstraint(
updateArtifact,
existingLockFileContent,
);
const preCommands = ['ruby --version'];
// Bundler < 2 has a different config option syntax than >= 2
if (
bundlerHostRulesAuthCommands &&
bundler &&
isValid(bundler) &&
lt(bundler, '2')
) {
preCommands.push(
...bundlerHostRulesAuthCommands.map(
(authCommand) => `bundler config --local ${authCommand}`,
),
);
} else if (bundlerHostRulesAuthCommands) {
preCommands.push(
...bundlerHostRulesAuthCommands.map(
(authCommand) => `bundler config set --local ${authCommand}`,
),
);
}
const execOptions: ExecOptions = {
cwdFile: lockFileName,
userConfiguredEnv: config.env,

View file

@ -10,6 +10,7 @@
<Folder Include="wwwroot\" />
</ItemGroup>
<ItemGroup>
<PackageReference Version="1.2.3" />
<PackageReference Include="Autofac" Version="4.5.0" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="4.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="1.1.2" />

View file

@ -14,6 +14,43 @@ exports[`modules/manager/nuget/extract extractPackageFile() extracts all depende
"depName": "Microsoft.VisualStudio.Web.CodeGeneration.Tools",
"depType": "nuget",
},
{
"currentValue": "undefined",
"datasource": "nuget",
"depName": "NotUpdatable3",
"depType": "nuget",
"skipReason": "invalid-version",
},
{
"datasource": "nuget",
"depName": "NotUpdatable3",
"depType": "nuget",
"skipReason": "invalid-version",
},
{
"datasource": "nuget",
"depName": "NotUpdatable3",
"depType": "nuget",
"skipReason": "invalid-version",
},
{
"datasource": "nuget",
"depName": "NotUpdatable3",
"depType": "nuget",
"skipReason": "invalid-version",
},
{
"datasource": "nuget",
"depName": "NotUpdatable2",
"depType": "nuget",
"skipReason": "invalid-version",
},
{
"datasource": "nuget",
"depName": "NotUpdatable1",
"depType": "nuget",
"skipReason": "invalid-version",
},
{
"currentValue": "1.2.3",
"datasource": "nuget",
@ -115,6 +152,36 @@ exports[`modules/manager/nuget/extract extractPackageFile() extracts all depende
"depName": "Microsoft.VisualStudio.Web.CodeGeneration.Tools",
"depType": "nuget",
},
{
"datasource": "nuget",
"depName": "NotUpdatable3",
"depType": "nuget",
"skipReason": "invalid-version",
},
{
"datasource": "nuget",
"depName": "NotUpdatable3",
"depType": "nuget",
"skipReason": "invalid-version",
},
{
"datasource": "nuget",
"depName": "NotUpdatable3",
"depType": "nuget",
"skipReason": "invalid-version",
},
{
"datasource": "nuget",
"depName": "NotUpdatable2",
"depType": "nuget",
"skipReason": "invalid-version",
},
{
"datasource": "nuget",
"depName": "NotUpdatable1",
"depType": "nuget",
"skipReason": "invalid-version",
},
{
"currentValue": "1.2.3",
"datasource": "nuget",

View file

@ -57,7 +57,7 @@ describe('modules/manager/nuget/extract', () => {
const sample = Fixtures.get(packageFile);
const res = await extractPackageFile(sample, packageFile, config);
expect(res?.deps).toMatchSnapshot();
expect(res?.deps).toHaveLength(17);
expect(res?.deps).toHaveLength(23);
});
it('extracts msbuild sdk from the Sdk attr of Project element', async () => {
@ -157,7 +157,7 @@ describe('modules/manager/nuget/extract', () => {
const sample = Fixtures.get(packageFile);
const res = await extractPackageFile(sample, packageFile, config);
expect(res?.deps).toMatchSnapshot();
expect(res?.deps).toHaveLength(17);
expect(res?.deps).toHaveLength(22);
});
it('extracts ContainerBaseImage', async () => {

View file

@ -28,7 +28,7 @@ import { applyRegistries, findVersion, getConfiguredRegistries } from './util';
* so we don't include it in the extracting regexp
*/
const checkVersion = regEx(
`^\\s*(?:[[])?(?:(?<currentValue>[^"(,[\\]]+)\\s*(?:,\\s*[)\\]]|])?)\\s*$`,
/^\s*(?:[[])?(?:(?<currentValue>[^"(,[\]]+)\s*(?:,\s*[)\]]|])?)\s*$/,
);
const elemNames = new Set([
'PackageReference',
@ -58,23 +58,38 @@ function extractDepsFromXml(xmlNode: XmlDocument): NugetPackageDependency[] {
if (elemNames.has(name)) {
const depName = attr?.Include || attr?.Update;
const version =
if (!depName) {
continue;
}
const dep: NugetPackageDependency = {
datasource: NugetDatasource.id,
depType: 'nuget',
depName,
};
let currentValue: string | undefined =
attr?.Version ??
attr?.version ??
child.valueWithPath('Version') ??
attr?.VersionOverride ??
child.valueWithPath('VersionOverride');
const currentValue = is.nonEmptyStringAndNotWhitespace(version)
? checkVersion.exec(version)?.groups?.currentValue?.trim()
: undefined;
if (depName && currentValue) {
results.push({
datasource: NugetDatasource.id,
depType: 'nuget',
depName,
currentValue,
});
if (!is.nonEmptyStringAndNotWhitespace(currentValue)) {
dep.skipReason = 'invalid-version';
}
currentValue = checkVersion
.exec(currentValue)
?.groups?.currentValue?.trim();
if (currentValue) {
dep.currentValue = currentValue;
} else {
dep.skipReason = 'invalid-version';
}
results.push(dep);
} else if (name === 'Sdk') {
const depName = attr?.Name;
const version = attr?.Version;

View file

@ -16,6 +16,10 @@ url = "last.url"
[[tool.poetry.source]]
name = "five"
[[tool.poetry.source]]
name = "invalid-url"
url = "invalid-url"
[build-system]
requires = ["poetry_core>=1.0", "wheel"]
build-backend = "poetry.masonry.api"

View file

@ -1,4 +1,5 @@
import { codeBlock } from 'common-tags';
import { GoogleAuth as _googleAuth } from 'google-auth-library';
import { mockDeep } from 'jest-mock-extended';
import { join } from 'upath';
import { envMock, mockExecAll } from '../../../../test/exec-util';
@ -15,16 +16,26 @@ import { updateArtifacts } from '.';
const pyproject1toml = Fixtures.get('pyproject.1.toml');
const pyproject10toml = Fixtures.get('pyproject.10.toml');
const pyproject13toml = `[[tool.poetry.source]]
name = "some-gar-repo"
url = "https://someregion-python.pkg.dev/some-project/some-repo/simple/"
[build-system]
requires = ["poetry_core>=1.0", "wheel"]
build-backend = "poetry.masonry.api"
`;
jest.mock('../../../util/exec/env');
jest.mock('../../../util/fs');
jest.mock('../../datasource', () => mockDeep());
jest.mock('../../../util/host-rules', () => mockDeep());
jest.mock('google-auth-library');
process.env.CONTAINERBASE = 'true';
const datasource = mocked(_datasource);
const hostRules = mocked(_hostRules);
const googleAuth = mocked(_googleAuth);
const adminConfig: RepoGlobalConfig = {
localDir: join('/tmp/github/some/repo'),
@ -198,7 +209,99 @@ describe('modules/manager/poetry/artifacts', () => {
},
},
]);
expect(hostRules.find.mock.calls).toHaveLength(5);
expect(hostRules.find.mock.calls).toHaveLength(7);
expect(execSnapshots).toMatchObject([
{
cmd: 'poetry update --lock --no-interaction dep1',
options: {
env: {
POETRY_HTTP_BASIC_ONE_PASSWORD: 'passwordOne',
POETRY_HTTP_BASIC_ONE_USERNAME: 'usernameOne',
POETRY_HTTP_BASIC_TWO_USERNAME: 'usernameTwo',
POETRY_HTTP_BASIC_FOUR_OH_FOUR_PASSWORD: 'passwordFour',
},
},
},
]);
});
it('passes Google Artifact Registry credentials 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');
googleAuth.mockImplementationOnce(
jest.fn().mockImplementationOnce(() => ({
getAccessToken: jest.fn().mockResolvedValue('some-token'),
})),
);
const updatedDeps = [{ depName: 'dep1' }];
expect(
await updateArtifacts({
packageFileName: 'pyproject.toml',
updatedDeps,
newPackageFileContent: pyproject13toml,
config,
}),
).toEqual([
{
file: {
type: 'addition',
path: 'pyproject.lock',
contents: 'New poetry.lock',
},
},
]);
expect(hostRules.find.mock.calls).toHaveLength(3);
expect(execSnapshots).toMatchObject([
{
cmd: 'poetry update --lock --no-interaction dep1',
options: {
env: {
POETRY_HTTP_BASIC_SOME_GAR_REPO_USERNAME: 'oauth2accesstoken',
POETRY_HTTP_BASIC_SOME_GAR_REPO_PASSWORD: 'some-token',
},
},
},
]);
});
it('continues if Google auth is not configured', 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');
googleAuth.mockImplementation(
jest.fn().mockImplementation(() => ({
getAccessToken: jest.fn().mockResolvedValue(undefined),
})),
);
const updatedDeps = [{ depName: 'dep1' }];
expect(
await updateArtifacts({
packageFileName: 'pyproject.toml',
updatedDeps,
newPackageFileContent: pyproject13toml,
config,
}),
).toEqual([
{
file: {
type: 'addition',
path: 'pyproject.lock',
contents: 'New poetry.lock',
},
},
]);
expect(hostRules.find.mock.calls).toHaveLength(3);
expect(execSnapshots).toMatchObject([
{ cmd: 'poetry update --lock --no-interaction dep1' },
]);

View file

@ -17,7 +17,9 @@ import { find } from '../../../util/host-rules';
import { regEx } from '../../../util/regex';
import { Result } from '../../../util/result';
import { parse as parseToml } from '../../../util/toml';
import { parseUrl } from '../../../util/url';
import { PypiDatasource } from '../../datasource/pypi';
import { getGoogleAuthTokenRaw } from '../../datasource/util';
import type { UpdateArtifact, UpdateArtifactsResult } from '../types';
import { Lockfile, PoetrySchemaToml } from './schema';
import type { PoetryFile, PoetrySource } from './types';
@ -101,7 +103,7 @@ function getPoetrySources(content: string, fileName: string): PoetrySource[] {
return [];
}
if (!pyprojectFile.tool?.poetry) {
logger.debug(`{$fileName} contains no poetry section`);
logger.debug(`${fileName} contains no poetry section`);
return [];
}
@ -115,20 +117,42 @@ function getPoetrySources(content: string, fileName: string): PoetrySource[] {
return sourceArray;
}
function getMatchingHostRule(url: string | undefined): HostRule {
async function getMatchingHostRule(url: string | undefined): Promise<HostRule> {
const scopedMatch = find({ hostType: PypiDatasource.id, url });
return is.nonEmptyObject(scopedMatch) ? scopedMatch : find({ url });
const hostRule = is.nonEmptyObject(scopedMatch) ? scopedMatch : find({ url });
if (hostRule) {
return hostRule;
}
const parsedUrl = parseUrl(url);
if (!parsedUrl) {
logger.once.debug(`Failed to parse URL ${url}`);
return {};
}
if (parsedUrl.hostname.endsWith('.pkg.dev')) {
const accessToken = await getGoogleAuthTokenRaw();
if (accessToken) {
return {
username: 'oauth2accesstoken',
password: accessToken,
};
}
logger.once.debug(`Could not get Google access token (url=${url})`);
}
return {};
}
function getSourceCredentialVars(
async function getSourceCredentialVars(
pyprojectContent: string,
packageFileName: string,
): NodeJS.ProcessEnv {
): Promise<NodeJS.ProcessEnv> {
const poetrySources = getPoetrySources(pyprojectContent, packageFileName);
const envVars: NodeJS.ProcessEnv = {};
for (const source of poetrySources) {
const matchingHostRule = getMatchingHostRule(source.url);
const matchingHostRule = await getMatchingHostRule(source.url);
const formattedSourceName = source.name
.replace(regEx(/(\.|-)+/g), '_')
.toUpperCase();
@ -192,7 +216,10 @@ export async function updateArtifacts({
config.constraints?.poetry ??
getPoetryRequirement(newPackageFileContent, existingLockFileContent);
const extraEnv = {
...getSourceCredentialVars(newPackageFileContent, packageFileName),
...(await getSourceCredentialVars(
newPackageFileContent,
packageFileName,
)),
...getGitEnvironmentVariables(['poetry']),
PIP_CACHE_DIR: await ensureCacheDir('pip'),
};

View file

@ -13,11 +13,18 @@ repos:
rev: 19.3b0
hooks:
- id: black
language: python
additional_dependencies:
- "request==1.1.1"
- "" # broken pypi package
- repo: https://gitlab.com/psf/black
# should also detect gitlab
rev: 19.3b0
hooks:
- id: black
# missing language, not extracted
additional_dependencies:
- "urllib==24.9.0"
- repo: http://gitlab.com/psf/black
# should also detect http
rev: 19.3b0
@ -48,3 +55,7 @@ repos:
- repo: some_invalid_url
# case with invlalid url.
rev: v1.0.0
# pre-commit meta hooks
- repo: meta
hooks: []

View file

@ -10,6 +10,14 @@ exports[`modules/manager/pre-commit/extract extractPackageFile() extracts from c
"depType": "repository",
"packageName": "pre-commit/pre-commit-hooks",
},
{
"currentValue": "==1.1.1",
"currentVersion": "1.1.1",
"datasource": "pypi",
"depName": "request",
"depType": "pre-commit-python",
"packageName": "request",
},
{
"currentValue": "19.3b0",
"datasource": "github-tags",

View file

@ -2,6 +2,7 @@ import { mockDeep } from 'jest-mock-extended';
import { Fixtures } from '../../../../test/fixtures';
import { mocked } from '../../../../test/util';
import * as _hostRules from '../../../util/host-rules';
import { PypiDatasource } from '../../datasource/pypi';
import { extractPackageFile } from '.';
jest.mock('../../../util/host-rules', () => mockDeep());
@ -81,6 +82,14 @@ describe('modules/manager/pre-commit/extract', () => {
expect(result).toMatchSnapshot({
deps: [
{ depName: 'pre-commit/pre-commit-hooks', currentValue: 'v3.3.0' },
{
currentValue: '==1.1.1',
currentVersion: '1.1.1',
datasource: PypiDatasource.id,
depName: 'request',
depType: 'pre-commit-python',
packageName: 'request',
},
{ depName: 'psf/black', currentValue: '19.3b0' },
{ depName: 'psf/black', currentValue: '19.3b0' },
{ depName: 'psf/black', currentValue: '19.3b0' },

View file

@ -7,6 +7,7 @@ import { regEx } from '../../../util/regex';
import { parseSingleYaml } from '../../../util/yaml';
import { GithubTagsDatasource } from '../../datasource/github-tags';
import { GitlabTagsDatasource } from '../../datasource/gitlab-tags';
import { pep508ToPackageDependency } from '../pep621/utils';
import type { PackageDependency, PackageFileContent } from '../types';
import {
matchesPrecommitConfigHeuristic,
@ -137,6 +138,23 @@ function findDependencies(precommitFile: PreCommitConfig): PackageDependency[] {
}
const packageDependencies: PackageDependency[] = [];
precommitFile.repos.forEach((item) => {
// meta hooks is defined from pre-commit and doesn't support `additional_dependencies`
if (item.repo !== 'meta') {
item.hooks?.forEach((hook) => {
// normally language are not defined in yaml
// only support it when it's explicitly defined.
// this avoid to parse hooks from pre-commit-hooks.yaml from git repo
if (hook.language === 'python') {
hook.additional_dependencies?.map((req) => {
const dep = pep508ToPackageDependency('pre-commit-python', req);
if (dep) {
packageDependencies.push(dep);
}
});
}
});
}
if (matchesPrecommitDependencyHeuristic(item)) {
logger.trace(item, 'Matched pre-commit dependency spec');
const repository = String(item.repo);

View file

@ -26,3 +26,33 @@ To enable the `pre-commit` manager, add the following config:
```
Alternatively, add `:enablePreCommit` to your `extends` array.
### Additional Dependencies
renovate has partial support for `additional_dependencies`, currently python only.
for python hooks, you will need to **explicitly add language** to your hooks with `additional_dependencies`
to let renovatebot know what kind of dependencies they are.
For example, this work for `request`:
```yaml
- repo: https://github.com/psf/black
rev: 19.3b0
hooks:
- id: black
language: python
additional_dependencies:
- 'request==1.1.1'
```
this won't work:
```yaml
- repo: https://github.com/psf/black
rev: 19.3b0
hooks:
- id: black
additional_dependencies:
- 'request==1.1.1'
```

View file

@ -2,7 +2,13 @@ export interface PreCommitConfig {
repos: PreCommitDependency[];
}
export interface PreCommitHook {
language?: string;
additional_dependencies?: Array<string>;
}
export interface PreCommitDependency {
repo: string;
hooks?: Array<PreCommitHook>;
rev: string;
}

View file

@ -1531,6 +1531,57 @@ describe('modules/platform/github/index', () => {
});
});
describe('getIssue()', () => {
it('returns null if issues disabled', async () => {
const scope = httpMock.scope(githubApiHost);
initRepoMock(scope, 'some/repo', { hasIssuesEnabled: false });
await github.initRepo({ repository: 'some/repo' });
const res = await github.getIssue(1);
expect(res).toBeNull();
});
it('returns issue', async () => {
const scope = httpMock.scope(githubApiHost);
initRepoMock(scope, 'some/repo');
const issue = {
number: 1,
state: 'open',
title: 'title-1',
body: 'body-1',
};
scope
.get('/repos/some/repo/issues/1')
.reply(200, { ...issue, updated_at: '2022-01-01T00:00:00Z' });
await github.initRepo({ repository: 'some/repo' });
const res = await github.getIssue(1);
expect(res).toMatchObject({
...issue,
lastModified: '2022-01-01T00:00:00Z',
});
});
it('returns null if issue not found', async () => {
const scope = httpMock.scope(githubApiHost);
initRepoMock(scope, 'some/repo');
scope.get('/repos/some/repo/issues/1').reply(404);
await github.initRepo({ repository: 'some/repo' });
const res = await github.getIssue(1);
expect(res).toBeNull();
});
it('logs debug message if issue deleted', async () => {
const scope = httpMock.scope(githubApiHost);
initRepoMock(scope, 'some/repo');
scope.get('/repos/some/repo/issues/1').reply(410);
await github.initRepo({ repository: 'some/repo' });
const res = await github.getIssue(1);
expect(res).toBeNull();
expect(logger.logger.debug).toHaveBeenCalledWith(
'Issue #1 has been deleted',
);
});
});
describe('findIssue()', () => {
it('returns null if no issue', async () => {
httpMock

View file

@ -1231,7 +1231,6 @@ export async function getIssueList(): Promise<Issue[]> {
}
export async function getIssue(number: number): Promise<Issue | null> {
// istanbul ignore if
if (config.hasIssuesEnabled === false) {
return null;
}
@ -1246,8 +1245,12 @@ export async function getIssue(number: number): Promise<Issue | null> {
);
GithubIssueCache.updateIssue(issue);
return issue;
} catch (err) /* istanbul ignore next */ {
} catch (err) {
logger.debug({ err, number }, 'Error getting issue');
if (err.response?.statusCode === 410) {
logger.debug(`Issue #${number} has been deleted`);
GithubIssueCache.deleteIssue(number);
}
return null;
}
}

View file

@ -159,6 +159,32 @@ describe('modules/platform/github/issue', () => {
});
});
it('removes particular issue from the cache', () => {
cache.platform = {
github: {
issuesCache: {
'1': {
number: 1,
body: 'body-1',
state: 'open',
title: 'title-1',
lastModified: '2020-01-01T00:00:00.000Z',
},
},
},
};
GithubIssueCache.deleteIssue(1);
expect(cache).toEqual({
platform: {
github: {
issuesCache: {},
},
},
});
});
it('reconciles cache', () => {
cache.platform = {
github: {

View file

@ -85,6 +85,13 @@ export class GithubIssueCache {
}
}
static deleteIssue(number: number): void {
const cacheData = this.data;
if (cacheData) {
delete cacheData[number];
}
}
/**
* At the moment of repo initialization, repository cache is not available.
* What we can do is to store issues for later reconciliation.

View file

@ -1361,9 +1361,12 @@ export async function ensureComment({
if (topic) {
logger.debug(`Ensuring comment "${massagedTopic!}" in #${number}`);
body = `### ${topic}\n\n${sanitizedContent}`;
body = body
.replace(regEx(/Pull Request/g), 'Merge Request')
.replace(regEx(/PR/g), 'MR');
body = smartTruncate(
body
.replace(regEx(/Pull Request/g), 'Merge Request')
.replace(regEx(/PR/g), 'MR'),
maxBodyLength(),
);
comments.forEach((comment: { body: string; id: number }) => {
if (comment.body.startsWith(`### ${massagedTopic!}\n\n`)) {
commentId = comment.id;
@ -1372,7 +1375,7 @@ export async function ensureComment({
});
} else {
logger.debug(`Ensuring content-only comment in #${number}`);
body = `${sanitizedContent}`;
body = smartTruncate(`${sanitizedContent}`, maxBodyLength());
comments.forEach((comment: { body: string; id: number }) => {
if (comment.body === body) {
commentId = comment.id;

View file

@ -4,7 +4,7 @@ import { platform } from '../../../modules/platform';
import * as repositoryCache from '../../../util/cache/repository';
import { clearRenovateRefs } from '../../../util/git';
import { PackageFiles } from '../package-files';
import { validateReconfigureBranch } from '../reconfigure';
import { checkReconfigureBranch } from '../reconfigure';
import { pruneStaleBranches } from './prune';
import {
runBranchSummary,
@ -16,7 +16,7 @@ export async function finalizeRepo(
config: RenovateConfig,
branchList: string[],
): Promise<void> {
await validateReconfigureBranch(config);
await checkReconfigureBranch(config);
await repositoryCache.saveCache();
await pruneStaleBranches(config, branchList);
await ensureIssuesClosing();

View file

@ -9,7 +9,7 @@ import { scm } from '../../../modules/platform/scm';
import { getBranchList, setUserRepoConfig } from '../../../util/git';
import { escapeRegExp, regEx } from '../../../util/regex';
import { uniqueStrings } from '../../../util/string';
import { getReconfigureBranchName } from '../reconfigure';
import { getReconfigureBranchName } from '../reconfigure/utils';
async function cleanUpBranches(
config: RenovateConfig,

View file

@ -1,242 +1,42 @@
import { mock } from 'jest-mock-extended';
import type { RenovateConfig } from '../../../../test/util';
import { fs, git, mocked, partial, platform, scm } from '../../../../test/util';
import { logger, mocked, scm } from '../../../../test/util';
import { GlobalConfig } from '../../../config/global';
import { logger } from '../../../logger';
import type { Pr } from '../../../modules/platform/types';
import * as _cache from '../../../util/cache/repository';
import type { LongCommitSha } from '../../../util/git/types';
import * as _merge from '../init/merge';
import { validateReconfigureBranch } from '.';
import * as _validate from './validate';
import { checkReconfigureBranch } from '.';
jest.mock('../../../util/cache/repository');
jest.mock('../../../util/fs');
jest.mock('../../../util/git');
jest.mock('../init/merge');
jest.mock('./validate');
const cache = mocked(_cache);
const merge = mocked(_merge);
const validate = mocked(_validate);
describe('workers/repository/reconfigure/index', () => {
const config: RenovateConfig = {
branchPrefix: 'prefix/',
baseBranch: 'base',
statusCheckNames: partial<RenovateConfig['statusCheckNames']>({
configValidation: 'renovate/config-validation',
}),
};
beforeEach(() => {
config.repository = 'some/repo';
merge.detectConfigFile.mockResolvedValue('renovate.json');
scm.branchExists.mockResolvedValue(true);
cache.getCache.mockReturnValue({});
git.getBranchCommit.mockReturnValue('sha' as LongCommitSha);
fs.readLocalFile.mockResolvedValue(null);
platform.getBranchStatusCheck.mockResolvedValue(null);
GlobalConfig.reset();
scm.branchExists.mockResolvedValue(true);
validate.validateReconfigureBranch.mockResolvedValue(undefined);
});
it('no effect when running with platform=local', async () => {
GlobalConfig.set({ platform: 'local' });
await validateReconfigureBranch(config);
expect(logger.debug).toHaveBeenCalledWith(
await checkReconfigureBranch(config);
expect(logger.logger.debug).toHaveBeenCalledWith(
'Not attempting to reconfigure when running with local platform',
);
});
it('no effect on repo with no reconfigure branch', async () => {
scm.branchExists.mockResolvedValueOnce(false);
await validateReconfigureBranch(config);
expect(logger.debug).toHaveBeenCalledWith('No reconfigure branch found');
});
it('logs error if config file search fails', async () => {
const err = new Error();
merge.detectConfigFile.mockRejectedValueOnce(err as never);
await validateReconfigureBranch(config);
expect(logger.error).toHaveBeenCalledWith(
{ err },
'Error while searching for config file in reconfigure branch',
await checkReconfigureBranch(config);
expect(logger.logger.debug).toHaveBeenCalledWith(
'No reconfigure branch found',
);
});
it('throws error if config file not found in reconfigure branch', async () => {
merge.detectConfigFile.mockResolvedValue(null);
await validateReconfigureBranch(config);
expect(logger.warn).toHaveBeenCalledWith(
'No config file found in reconfigure branch',
);
});
it('logs error if config file is unreadable', async () => {
const err = new Error();
fs.readLocalFile.mockRejectedValueOnce(err as never);
await validateReconfigureBranch(config);
expect(logger.error).toHaveBeenCalledWith(
{ err },
'Error while reading config file',
);
});
it('throws error if config file is empty', async () => {
await validateReconfigureBranch(config);
expect(logger.warn).toHaveBeenCalledWith('Empty or invalid config file');
});
it('throws error if config file content is invalid', async () => {
fs.readLocalFile.mockResolvedValueOnce(`
{
"name":
}
`);
await validateReconfigureBranch(config);
expect(logger.error).toHaveBeenCalledWith(
{ err: expect.any(Object) },
'Error while parsing config file',
);
expect(platform.setBranchStatus).toHaveBeenCalledWith({
branchName: 'prefix/reconfigure',
context: 'renovate/config-validation',
description: 'Validation Failed - Unparsable config file',
state: 'red',
});
});
it('handles failed validation', async () => {
fs.readLocalFile.mockResolvedValueOnce(`
{
"enabledManagers": ["docker"]
}
`);
await validateReconfigureBranch(config);
expect(logger.debug).toHaveBeenCalledWith(
{ errors: expect.any(String) },
'Validation Errors',
);
expect(platform.setBranchStatus).toHaveBeenCalledWith({
branchName: 'prefix/reconfigure',
context: 'renovate/config-validation',
description: 'Validation Failed',
state: 'red',
});
});
it('adds comment if reconfigure PR exists', async () => {
fs.readLocalFile.mockResolvedValueOnce(`
{
"enabledManagers": ["docker"]
}
`);
platform.findPr.mockResolvedValueOnce(mock<Pr>({ number: 1 }));
await validateReconfigureBranch(config);
expect(logger.debug).toHaveBeenCalledWith(
{ errors: expect.any(String) },
'Validation Errors',
);
expect(platform.setBranchStatus).toHaveBeenCalled();
expect(platform.ensureComment).toHaveBeenCalled();
});
it('handles successful validation', async () => {
const pJson = `
{
"renovate": {
"enabledManagers": ["npm"]
}
}
`;
merge.detectConfigFile.mockResolvedValue('package.json');
fs.readLocalFile.mockResolvedValueOnce(pJson).mockResolvedValueOnce(pJson);
await validateReconfigureBranch(config);
expect(platform.setBranchStatus).toHaveBeenCalledWith({
branchName: 'prefix/reconfigure',
context: 'renovate/config-validation',
description: 'Validation Successful',
state: 'green',
});
});
it('skips adding status check if statusCheckNames.configValidation is null', async () => {
cache.getCache.mockReturnValueOnce({
reconfigureBranchCache: {
reconfigureBranchSha: 'new-sha',
isConfigValid: false,
},
});
await validateReconfigureBranch({
...config,
statusCheckNames: partial<RenovateConfig['statusCheckNames']>({
configValidation: null,
}),
});
expect(logger.debug).toHaveBeenCalledWith(
'Status check is null or an empty string, skipping status check addition.',
);
expect(platform.setBranchStatus).not.toHaveBeenCalled();
});
it('skips adding status check if statusCheckNames.configValidation is empty string', async () => {
cache.getCache.mockReturnValueOnce({
reconfigureBranchCache: {
reconfigureBranchSha: 'new-sha',
isConfigValid: false,
},
});
await validateReconfigureBranch({
...config,
statusCheckNames: partial<RenovateConfig['statusCheckNames']>({
configValidation: '',
}),
});
expect(logger.debug).toHaveBeenCalledWith(
'Status check is null or an empty string, skipping status check addition.',
);
expect(platform.setBranchStatus).not.toHaveBeenCalled();
});
it('skips validation if cache is valid', async () => {
cache.getCache.mockReturnValueOnce({
reconfigureBranchCache: {
reconfigureBranchSha: 'sha',
isConfigValid: false,
},
});
await validateReconfigureBranch(config);
expect(logger.debug).toHaveBeenCalledWith(
'Skipping validation check as branch sha is unchanged',
);
});
it('skips validation if status check present', async () => {
cache.getCache.mockReturnValueOnce({
reconfigureBranchCache: {
reconfigureBranchSha: 'new_sha',
isConfigValid: false,
},
});
platform.getBranchStatusCheck.mockResolvedValueOnce('green');
await validateReconfigureBranch(config);
expect(logger.debug).toHaveBeenCalledWith(
'Skipping validation check because status check already exists.',
);
});
it('handles non-default config file', async () => {
merge.detectConfigFile.mockResolvedValue('.renovaterc');
fs.readLocalFile.mockResolvedValueOnce(`
{
"enabledManagers": ["npm",]
}
`);
await validateReconfigureBranch(config);
expect(platform.setBranchStatus).toHaveBeenCalledWith({
branchName: 'prefix/reconfigure',
context: 'renovate/config-validation',
description: 'Validation Successful',
state: 'green',
});
it('validates reconfigure branch', async () => {
await expect(checkReconfigureBranch(config)).toResolve();
});
});

View file

@ -1,49 +1,15 @@
import is from '@sindresorhus/is';
import JSON5 from 'json5';
import { GlobalConfig } from '../../../config/global';
import type { RenovateConfig } from '../../../config/types';
import { validateConfig } from '../../../config/validation';
import { logger } from '../../../logger';
import { platform } from '../../../modules/platform';
import { ensureComment } from '../../../modules/platform/comment';
import { scm } from '../../../modules/platform/scm';
import type { BranchStatus } from '../../../types';
import { getCache } from '../../../util/cache/repository';
import { readLocalFile } from '../../../util/fs';
import { getBranchCommit } from '../../../util/git';
import { regEx } from '../../../util/regex';
import { detectConfigFile } from '../init/merge';
import {
deleteReconfigureBranchCache,
setReconfigureBranchCache,
} from './reconfigure-cache';
import { deleteReconfigureBranchCache } from './reconfigure-cache';
import { getReconfigureBranchName } from './utils';
import { validateReconfigureBranch } from './validate';
async function setBranchStatus(
branchName: string,
description: string,
state: BranchStatus,
context?: string | null,
): Promise<void> {
if (!is.nonEmptyString(context)) {
// already logged this case when validating the status check
return;
}
await platform.setBranchStatus({
branchName,
context,
description,
state,
});
}
export function getReconfigureBranchName(prefix: string): string {
return `${prefix}reconfigure`;
}
export async function validateReconfigureBranch(
export async function checkReconfigureBranch(
config: RenovateConfig,
): Promise<void> {
logger.debug('validateReconfigureBranch()');
logger.debug('checkReconfigureBranch()');
if (GlobalConfig.get('platform') === 'local') {
logger.debug(
'Not attempting to reconfigure when running with local platform',
@ -51,10 +17,8 @@ export async function validateReconfigureBranch(
return;
}
const context = config.statusCheckNames?.configValidation;
const branchName = getReconfigureBranchName(config.branchPrefix!);
const branchExists = await scm.branchExists(branchName);
const reconfigureBranch = getReconfigureBranchName(config.branchPrefix!);
const branchExists = await scm.branchExists(reconfigureBranch);
// this is something the user initiates, so skip if no branch exists
if (!branchExists) {
@ -63,141 +27,5 @@ export async function validateReconfigureBranch(
return;
}
// look for config file
// 1. check reconfigure branch cache and use the configFileName if it exists
// 2. checkout reconfigure branch and look for the config file, don't assume default configFileName
const branchSha = getBranchCommit(branchName)!;
const cache = getCache();
let configFileName: string | null = null;
const reconfigureCache = cache.reconfigureBranchCache;
// only use valid cached information
if (reconfigureCache?.reconfigureBranchSha === branchSha) {
logger.debug('Skipping validation check as branch sha is unchanged');
return;
}
if (context) {
const validationStatus = await platform.getBranchStatusCheck(
branchName,
context,
);
// if old status check is present skip validation
if (is.nonEmptyString(validationStatus)) {
logger.debug(
'Skipping validation check because status check already exists.',
);
return;
}
} else {
logger.debug(
'Status check is null or an empty string, skipping status check addition.',
);
}
try {
await scm.checkoutBranch(branchName);
configFileName = await detectConfigFile();
} catch (err) {
logger.error(
{ err },
'Error while searching for config file in reconfigure branch',
);
}
if (!is.nonEmptyString(configFileName)) {
logger.warn('No config file found in reconfigure branch');
await setBranchStatus(
branchName,
'Validation Failed - No config file found',
'red',
context,
);
setReconfigureBranchCache(branchSha, false);
await scm.checkoutBranch(config.defaultBranch!);
return;
}
let configFileRaw: string | null = null;
try {
configFileRaw = await readLocalFile(configFileName, 'utf8');
} catch (err) {
logger.error({ err }, 'Error while reading config file');
}
if (!is.nonEmptyString(configFileRaw)) {
logger.warn('Empty or invalid config file');
await setBranchStatus(
branchName,
'Validation Failed - Empty/Invalid config file',
'red',
context,
);
setReconfigureBranchCache(branchSha, false);
await scm.checkoutBranch(config.baseBranch!);
return;
}
let configFileParsed: any;
try {
configFileParsed = JSON5.parse(configFileRaw);
// no need to confirm renovate field in package.json we already do it in `detectConfigFile()`
if (configFileName === 'package.json') {
configFileParsed = configFileParsed.renovate;
}
} catch (err) {
logger.error({ err }, 'Error while parsing config file');
await setBranchStatus(
branchName,
'Validation Failed - Unparsable config file',
'red',
context,
);
setReconfigureBranchCache(branchSha, false);
await scm.checkoutBranch(config.baseBranch!);
return;
}
// perform validation and provide a passing or failing check run based on result
const validationResult = await validateConfig('repo', configFileParsed);
// failing check
if (validationResult.errors.length > 0) {
logger.debug(
{ errors: validationResult.errors.map((err) => err.message).join(', ') },
'Validation Errors',
);
// add comment to reconfigure PR if it exists
const branchPr = await platform.findPr({
branchName,
state: 'open',
includeOtherAuthors: true,
});
if (branchPr) {
let body = `There is an error with this repository's Renovate configuration that needs to be fixed.\n\n`;
body += `Location: \`${configFileName}\`\n`;
body += `Message: \`${validationResult.errors
.map((e) => e.message)
.join(', ')
.replace(regEx(/`/g), "'")}\`\n`;
await ensureComment({
number: branchPr.number,
topic: 'Action Required: Fix Renovate Configuration',
content: body,
});
}
await setBranchStatus(branchName, 'Validation Failed', 'red', context);
setReconfigureBranchCache(branchSha, false);
await scm.checkoutBranch(config.baseBranch!);
return;
}
// passing check
await setBranchStatus(branchName, 'Validation Successful', 'green', context);
setReconfigureBranchCache(branchSha, true);
await scm.checkoutBranch(config.baseBranch!);
await validateReconfigureBranch(config);
}

View file

@ -0,0 +1,3 @@
export function getReconfigureBranchName(prefix: string): string {
return `${prefix}reconfigure`;
}

View file

@ -0,0 +1,228 @@
import { mock } from 'jest-mock-extended';
import type { RenovateConfig } from '../../../../test/util';
import { fs, git, mocked, partial, platform, scm } from '../../../../test/util';
import { GlobalConfig } from '../../../config/global';
import { logger } from '../../../logger';
import type { Pr } from '../../../modules/platform/types';
import * as _cache from '../../../util/cache/repository';
import type { LongCommitSha } from '../../../util/git/types';
import * as _merge from '../init/merge';
import { validateReconfigureBranch } from './validate';
jest.mock('../../../util/cache/repository');
jest.mock('../../../util/fs');
jest.mock('../../../util/git');
jest.mock('../init/merge');
const cache = mocked(_cache);
const merge = mocked(_merge);
describe('workers/repository/reconfigure/validate', () => {
const config: RenovateConfig = {
branchPrefix: 'prefix/',
baseBranch: 'base',
statusCheckNames: partial<RenovateConfig['statusCheckNames']>({
configValidation: 'renovate/config-validation',
}),
};
beforeEach(() => {
config.repository = 'some/repo';
merge.detectConfigFile.mockResolvedValue('renovate.json');
scm.branchExists.mockResolvedValue(true);
cache.getCache.mockReturnValue({});
git.getBranchCommit.mockReturnValue('sha' as LongCommitSha);
fs.readLocalFile.mockResolvedValue(null);
platform.getBranchStatusCheck.mockResolvedValue(null);
GlobalConfig.reset();
});
it('logs error if config file search fails', async () => {
const err = new Error();
merge.detectConfigFile.mockRejectedValueOnce(err as never);
await validateReconfigureBranch(config);
expect(logger.error).toHaveBeenCalledWith(
{ err },
'Error while searching for config file in reconfigure branch',
);
});
it('throws error if config file not found in reconfigure branch', async () => {
merge.detectConfigFile.mockResolvedValue(null);
await validateReconfigureBranch(config);
expect(logger.warn).toHaveBeenCalledWith(
'No config file found in reconfigure branch',
);
});
it('logs error if config file is unreadable', async () => {
const err = new Error();
fs.readLocalFile.mockRejectedValueOnce(err as never);
await validateReconfigureBranch(config);
expect(logger.error).toHaveBeenCalledWith(
{ err },
'Error while reading config file',
);
});
it('throws error if config file is empty', async () => {
await validateReconfigureBranch(config);
expect(logger.warn).toHaveBeenCalledWith('Empty or invalid config file');
});
it('throws error if config file content is invalid', async () => {
fs.readLocalFile.mockResolvedValueOnce(`
{
"name":
}
`);
await validateReconfigureBranch(config);
expect(logger.error).toHaveBeenCalledWith(
{ err: expect.any(Object) },
'Error while parsing config file',
);
expect(platform.setBranchStatus).toHaveBeenCalledWith({
branchName: 'prefix/reconfigure',
context: 'renovate/config-validation',
description: 'Validation Failed - Unparsable config file',
state: 'red',
});
});
it('handles failed validation', async () => {
fs.readLocalFile.mockResolvedValueOnce(`
{
"enabledManagers": ["docker"]
}
`);
await validateReconfigureBranch(config);
expect(logger.debug).toHaveBeenCalledWith(
{ errors: expect.any(String) },
'Validation Errors',
);
expect(platform.setBranchStatus).toHaveBeenCalledWith({
branchName: 'prefix/reconfigure',
context: 'renovate/config-validation',
description: 'Validation Failed',
state: 'red',
});
});
it('adds comment if reconfigure PR exists', async () => {
fs.readLocalFile.mockResolvedValueOnce(`
{
"enabledManagers": ["docker"]
}
`);
platform.findPr.mockResolvedValueOnce(mock<Pr>({ number: 1 }));
await validateReconfigureBranch(config);
expect(logger.debug).toHaveBeenCalledWith(
{ errors: expect.any(String) },
'Validation Errors',
);
expect(platform.setBranchStatus).toHaveBeenCalled();
expect(platform.ensureComment).toHaveBeenCalled();
});
it('handles successful validation', async () => {
const pJson = `
{
"renovate": {
"enabledManagers": ["npm"]
}
}
`;
merge.detectConfigFile.mockResolvedValue('package.json');
fs.readLocalFile.mockResolvedValueOnce(pJson).mockResolvedValueOnce(pJson);
await validateReconfigureBranch(config);
expect(platform.setBranchStatus).toHaveBeenCalledWith({
branchName: 'prefix/reconfigure',
context: 'renovate/config-validation',
description: 'Validation Successful',
state: 'green',
});
});
it('skips adding status check if statusCheckNames.configValidation is null', async () => {
cache.getCache.mockReturnValueOnce({
reconfigureBranchCache: {
reconfigureBranchSha: 'new-sha',
isConfigValid: false,
},
});
await validateReconfigureBranch({
...config,
statusCheckNames: partial<RenovateConfig['statusCheckNames']>({
configValidation: null,
}),
});
expect(logger.debug).toHaveBeenCalledWith(
'Status check is null or an empty string, skipping status check addition.',
);
expect(platform.setBranchStatus).not.toHaveBeenCalled();
});
it('skips adding status check if statusCheckNames.configValidation is empty string', async () => {
cache.getCache.mockReturnValueOnce({
reconfigureBranchCache: {
reconfigureBranchSha: 'new-sha',
isConfigValid: false,
},
});
await validateReconfigureBranch({
...config,
statusCheckNames: partial<RenovateConfig['statusCheckNames']>({
configValidation: '',
}),
});
expect(logger.debug).toHaveBeenCalledWith(
'Status check is null or an empty string, skipping status check addition.',
);
expect(platform.setBranchStatus).not.toHaveBeenCalled();
});
it('skips validation if cache is valid', async () => {
cache.getCache.mockReturnValueOnce({
reconfigureBranchCache: {
reconfigureBranchSha: 'sha',
isConfigValid: false,
},
});
await validateReconfigureBranch(config);
expect(logger.debug).toHaveBeenCalledWith(
'Skipping validation check as branch sha is unchanged',
);
});
it('skips validation if status check present', async () => {
cache.getCache.mockReturnValueOnce({
reconfigureBranchCache: {
reconfigureBranchSha: 'new_sha',
isConfigValid: false,
},
});
platform.getBranchStatusCheck.mockResolvedValueOnce('green');
await validateReconfigureBranch(config);
expect(logger.debug).toHaveBeenCalledWith(
'Skipping validation check because status check already exists.',
);
});
it('handles non-default config file', async () => {
merge.detectConfigFile.mockResolvedValue('.renovaterc');
fs.readLocalFile.mockResolvedValueOnce(`
{
"enabledManagers": ["npm",]
}
`);
await validateReconfigureBranch(config);
expect(platform.setBranchStatus).toHaveBeenCalledWith({
branchName: 'prefix/reconfigure',
context: 'renovate/config-validation',
description: 'Validation Successful',
state: 'green',
});
});
});

View file

@ -0,0 +1,184 @@
import is from '@sindresorhus/is';
import JSON5 from 'json5';
import type { RenovateConfig } from '../../../config/types';
import { validateConfig } from '../../../config/validation';
import { logger } from '../../../logger';
import { platform } from '../../../modules/platform';
import { ensureComment } from '../../../modules/platform/comment';
import { scm } from '../../../modules/platform/scm';
import type { BranchStatus } from '../../../types';
import { getCache } from '../../../util/cache/repository';
import { readLocalFile } from '../../../util/fs';
import { getBranchCommit } from '../../../util/git';
import { regEx } from '../../../util/regex';
import { detectConfigFile } from '../init/merge';
import { setReconfigureBranchCache } from './reconfigure-cache';
import { getReconfigureBranchName } from './utils';
async function setBranchStatus(
branchName: string,
description: string,
state: BranchStatus,
context?: string | null,
): Promise<void> {
if (!is.nonEmptyString(context)) {
// already logged this case when validating the status check
return;
}
await platform.setBranchStatus({
branchName,
context,
description,
state,
});
}
export async function validateReconfigureBranch(
config: RenovateConfig,
): Promise<void> {
logger.debug('validateReconfigureBranch()');
const context = config.statusCheckNames?.configValidation;
const branchName = getReconfigureBranchName(config.branchPrefix!);
// look for config file
// 1. check reconfigure branch cache and use the configFileName if it exists
// 2. checkout reconfigure branch and look for the config file, don't assume default configFileName
const branchSha = getBranchCommit(branchName)!;
const cache = getCache();
let configFileName: string | null = null;
const reconfigureCache = cache.reconfigureBranchCache;
// only use valid cached information
if (reconfigureCache?.reconfigureBranchSha === branchSha) {
logger.debug('Skipping validation check as branch sha is unchanged');
return;
}
if (context) {
const validationStatus = await platform.getBranchStatusCheck(
branchName,
context,
);
// if old status check is present skip validation
if (is.nonEmptyString(validationStatus)) {
logger.debug(
'Skipping validation check because status check already exists.',
);
return;
}
} else {
logger.debug(
'Status check is null or an empty string, skipping status check addition.',
);
}
try {
await scm.checkoutBranch(branchName);
configFileName = await detectConfigFile();
} catch (err) {
logger.error(
{ err },
'Error while searching for config file in reconfigure branch',
);
}
if (!is.nonEmptyString(configFileName)) {
logger.warn('No config file found in reconfigure branch');
await setBranchStatus(
branchName,
'Validation Failed - No config file found',
'red',
context,
);
setReconfigureBranchCache(branchSha, false);
await scm.checkoutBranch(config.defaultBranch!);
return;
}
let configFileRaw: string | null = null;
try {
configFileRaw = await readLocalFile(configFileName, 'utf8');
} catch (err) {
logger.error({ err }, 'Error while reading config file');
}
if (!is.nonEmptyString(configFileRaw)) {
logger.warn('Empty or invalid config file');
await setBranchStatus(
branchName,
'Validation Failed - Empty/Invalid config file',
'red',
context,
);
setReconfigureBranchCache(branchSha, false);
await scm.checkoutBranch(config.baseBranch!);
return;
}
let configFileParsed: any;
try {
configFileParsed = JSON5.parse(configFileRaw);
// no need to confirm renovate field in package.json we already do it in `detectConfigFile()`
if (configFileName === 'package.json') {
configFileParsed = configFileParsed.renovate;
}
} catch (err) {
logger.error({ err }, 'Error while parsing config file');
await setBranchStatus(
branchName,
'Validation Failed - Unparsable config file',
'red',
context,
);
setReconfigureBranchCache(branchSha, false);
await scm.checkoutBranch(config.baseBranch!);
return;
}
// perform validation and provide a passing or failing check based on result
const validationResult = await validateConfig('repo', configFileParsed);
// failing check
if (validationResult.errors.length > 0) {
logger.debug(
{ errors: validationResult.errors.map((err) => err.message).join(', ') },
'Validation Errors',
);
const reconfigurePr = await platform.findPr({
branchName,
state: 'open',
includeOtherAuthors: true,
});
// add comment to reconfigure PR if it exists
if (reconfigurePr) {
let body = `There is an error with this repository's Renovate configuration that needs to be fixed.\n\n`;
body += `Location: \`${configFileName}\`\n`;
body += `Message: \`${validationResult.errors
.map((e) => e.message)
.join(', ')
.replace(regEx(/`/g), "'")}\`\n`;
await ensureComment({
number: reconfigurePr.number,
topic: 'Action Required: Fix Renovate Configuration',
content: body,
});
}
await setBranchStatus(branchName, 'Validation Failed', 'red', context);
setReconfigureBranchCache(branchSha, false);
await scm.checkoutBranch(config.baseBranch!);
return;
}
// passing check
await setBranchStatus(branchName, 'Validation Successful', 'green', context);
setReconfigureBranchCache(branchSha, true);
await scm.checkoutBranch(config.baseBranch!);
return;
}

View file

@ -299,7 +299,7 @@
"@types/mdast": "3.0.15",
"@types/moo": "0.5.9",
"@types/ms": "0.7.34",
"@types/node": "20.17.10",
"@types/node": "20.17.11",
"@types/parse-link-header": "2.0.3",
"@types/punycode": "2.1.4",
"@types/semver": "7.5.8",

View file

@ -470,8 +470,8 @@ importers:
specifier: 0.7.34
version: 0.7.34
'@types/node':
specifier: 20.17.10
version: 20.17.10
specifier: 20.17.11
version: 20.17.11
'@types/parse-link-header':
specifier: 2.0.3
version: 2.0.3
@ -540,7 +540,7 @@ importers:
version: 2.31.0(@typescript-eslint/parser@8.19.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1)
eslint-plugin-jest:
specifier: 28.10.0
version: 28.10.0(@typescript-eslint/eslint-plugin@8.19.0(@typescript-eslint/parser@8.19.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(jest@29.7.0(@types/node@20.17.10)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2)))(typescript@5.7.2)
version: 28.10.0(@typescript-eslint/eslint-plugin@8.19.0(@typescript-eslint/parser@8.19.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(jest@29.7.0(@types/node@20.17.11)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2)))(typescript@5.7.2)
eslint-plugin-jest-formatting:
specifier: 3.1.0
version: 3.1.0(eslint@8.57.1)
@ -564,16 +564,16 @@ importers:
version: 9.1.7
jest:
specifier: 29.7.0
version: 29.7.0(@types/node@20.17.10)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2))
version: 29.7.0(@types/node@20.17.11)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2))
jest-extended:
specifier: 4.0.2
version: 4.0.2(jest@29.7.0(@types/node@20.17.10)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2)))
version: 4.0.2(jest@29.7.0(@types/node@20.17.11)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2)))
jest-mock:
specifier: 29.7.0
version: 29.7.0
jest-mock-extended:
specifier: 3.0.7
version: 3.0.7(jest@29.7.0(@types/node@20.17.10)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2)))(typescript@5.7.2)
version: 3.0.7(jest@29.7.0(@types/node@20.17.11)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2)))(typescript@5.7.2)
jest-snapshot:
specifier: 29.7.0
version: 29.7.0
@ -609,10 +609,10 @@ importers:
version: 3.0.3
ts-jest:
specifier: 29.2.5
version: 29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@20.17.10)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2)))(typescript@5.7.2)
version: 29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@20.17.11)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2)))(typescript@5.7.2)
ts-node:
specifier: 10.9.2
version: 10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2)
version: 10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2)
type-fest:
specifier: 4.31.0
version: 4.31.0
@ -2121,8 +2121,8 @@ packages:
'@types/ms@0.7.34':
resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==}
'@types/node@20.17.10':
resolution: {integrity: sha512-/jrvh5h6NXhEauFFexRin69nA0uHJ5gwk4iDivp/DeoEua3uwCUto6PC86IpRITBOs4+6i2I56K5x5b6WYGXHA==}
'@types/node@20.17.11':
resolution: {integrity: sha512-Ept5glCK35R8yeyIeYlRIZtX6SLRyqMhOFTgj5SOkMpLTdw3SEHI9fHx60xaUZ+V1aJxQJODE+7/j5ocZydYTg==}
'@types/normalize-package-data@2.4.4':
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
@ -7496,27 +7496,27 @@ snapshots:
'@jest/console@29.7.0':
dependencies:
'@jest/types': 29.6.3
'@types/node': 20.17.10
'@types/node': 20.17.11
chalk: 4.1.2
jest-message-util: 29.7.0
jest-util: 29.7.0
slash: 3.0.0
'@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2))':
'@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2))':
dependencies:
'@jest/console': 29.7.0
'@jest/reporters': 29.7.0
'@jest/test-result': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
'@types/node': 20.17.10
'@types/node': 20.17.11
ansi-escapes: 4.3.2
chalk: 4.1.2
ci-info: 3.9.0
exit: 0.1.2
graceful-fs: 4.2.11
jest-changed-files: 29.7.0
jest-config: 29.7.0(@types/node@20.17.10)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2))
jest-config: 29.7.0(@types/node@20.17.11)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2))
jest-haste-map: 29.7.0
jest-message-util: 29.7.0
jest-regex-util: 29.6.3
@ -7541,7 +7541,7 @@ snapshots:
dependencies:
'@jest/fake-timers': 29.7.0
'@jest/types': 29.6.3
'@types/node': 20.17.10
'@types/node': 20.17.11
jest-mock: 29.7.0
'@jest/expect-utils@29.4.1':
@ -7563,7 +7563,7 @@ snapshots:
dependencies:
'@jest/types': 29.6.3
'@sinonjs/fake-timers': 10.3.0
'@types/node': 20.17.10
'@types/node': 20.17.11
jest-message-util: 29.7.0
jest-mock: 29.7.0
jest-util: 29.7.0
@ -7585,7 +7585,7 @@ snapshots:
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
'@jridgewell/trace-mapping': 0.3.25
'@types/node': 20.17.10
'@types/node': 20.17.11
chalk: 4.1.2
collect-v8-coverage: 1.0.2
exit: 0.1.2
@ -7655,7 +7655,7 @@ snapshots:
'@jest/schemas': 29.6.3
'@types/istanbul-lib-coverage': 2.0.6
'@types/istanbul-reports': 3.0.4
'@types/node': 20.17.10
'@types/node': 20.17.11
'@types/yargs': 17.0.33
chalk: 4.1.2
@ -8703,7 +8703,7 @@ snapshots:
'@types/aws4@1.11.6':
dependencies:
'@types/node': 20.17.10
'@types/node': 20.17.11
'@types/babel__core@7.20.5':
dependencies:
@ -8728,27 +8728,27 @@ snapshots:
'@types/better-sqlite3@7.6.12':
dependencies:
'@types/node': 20.17.10
'@types/node': 20.17.11
'@types/breejs__later@4.1.5': {}
'@types/bunyan@1.8.11':
dependencies:
'@types/node': 20.17.10
'@types/node': 20.17.11
'@types/bunyan@1.8.9':
dependencies:
'@types/node': 20.17.10
'@types/node': 20.17.11
'@types/cacache@17.0.2':
dependencies:
'@types/node': 20.17.10
'@types/node': 20.17.11
'@types/cacheable-request@6.0.3':
dependencies:
'@types/http-cache-semantics': 4.0.4
'@types/keyv': 3.1.4
'@types/node': 20.17.10
'@types/node': 20.17.11
'@types/responselike': 1.0.3
'@types/callsite@1.0.34': {}
@ -8779,7 +8779,7 @@ snapshots:
'@types/fs-extra@11.0.4':
dependencies:
'@types/jsonfile': 6.1.4
'@types/node': 20.17.10
'@types/node': 20.17.11
'@types/git-url-parse@9.0.3': {}
@ -8789,7 +8789,7 @@ snapshots:
'@types/graceful-fs@4.1.9':
dependencies:
'@types/node': 20.17.10
'@types/node': 20.17.11
'@types/http-cache-semantics@4.0.4': {}
@ -8815,13 +8815,13 @@ snapshots:
'@types/jsonfile@6.1.4':
dependencies:
'@types/node': 20.17.10
'@types/node': 20.17.11
'@types/katex@0.16.7': {}
'@types/keyv@3.1.4':
dependencies:
'@types/node': 20.17.10
'@types/node': 20.17.11
'@types/linkify-it@5.0.0': {}
@ -8840,7 +8840,7 @@ snapshots:
'@types/marshal@0.5.3':
dependencies:
'@types/node': 20.17.10
'@types/node': 20.17.11
'@types/mdast@3.0.15':
dependencies:
@ -8856,7 +8856,7 @@ snapshots:
'@types/ms@0.7.34': {}
'@types/node@20.17.10':
'@types/node@20.17.11':
dependencies:
undici-types: 6.19.8
@ -8870,7 +8870,7 @@ snapshots:
'@types/responselike@1.0.3':
dependencies:
'@types/node': 20.17.10
'@types/node': 20.17.11
'@types/semver-stable@3.0.2': {}
@ -8890,7 +8890,7 @@ snapshots:
'@types/tar@6.1.13':
dependencies:
'@types/node': 20.17.10
'@types/node': 20.17.11
minipass: 4.2.8
'@types/tmp@0.2.6': {}
@ -8915,7 +8915,7 @@ snapshots:
'@types/yauzl@2.10.3':
dependencies:
'@types/node': 20.17.10
'@types/node': 20.17.11
optional: true
'@typescript-eslint/eslint-plugin@8.19.0(@typescript-eslint/parser@8.19.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2)':
@ -9680,13 +9680,13 @@ snapshots:
optionalDependencies:
typescript: 5.7.2
create-jest@29.7.0(@types/node@20.17.10)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2)):
create-jest@29.7.0(@types/node@20.17.11)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2)):
dependencies:
'@jest/types': 29.6.3
chalk: 4.1.2
exit: 0.1.2
graceful-fs: 4.2.11
jest-config: 29.7.0(@types/node@20.17.10)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2))
jest-config: 29.7.0(@types/node@20.17.11)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2))
jest-util: 29.7.0
prompts: 2.4.2
transitivePeerDependencies:
@ -10109,13 +10109,13 @@ snapshots:
dependencies:
eslint: 8.57.1
eslint-plugin-jest@28.10.0(@typescript-eslint/eslint-plugin@8.19.0(@typescript-eslint/parser@8.19.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(jest@29.7.0(@types/node@20.17.10)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2)))(typescript@5.7.2):
eslint-plugin-jest@28.10.0(@typescript-eslint/eslint-plugin@8.19.0(@typescript-eslint/parser@8.19.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(jest@29.7.0(@types/node@20.17.11)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2)))(typescript@5.7.2):
dependencies:
'@typescript-eslint/utils': 8.19.0(eslint@8.57.1)(typescript@5.7.2)
eslint: 8.57.1
optionalDependencies:
'@typescript-eslint/eslint-plugin': 8.19.0(@typescript-eslint/parser@8.19.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2)
jest: 29.7.0(@types/node@20.17.10)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2))
jest: 29.7.0(@types/node@20.17.11)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2))
transitivePeerDependencies:
- supports-color
- typescript
@ -11146,7 +11146,7 @@ snapshots:
'@jest/expect': 29.7.0
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
'@types/node': 20.17.10
'@types/node': 20.17.11
chalk: 4.1.2
co: 4.6.0
dedent: 1.5.3
@ -11166,16 +11166,16 @@ snapshots:
- babel-plugin-macros
- supports-color
jest-cli@29.7.0(@types/node@20.17.10)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2)):
jest-cli@29.7.0(@types/node@20.17.11)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2)):
dependencies:
'@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2))
'@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2))
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
chalk: 4.1.2
create-jest: 29.7.0(@types/node@20.17.10)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2))
create-jest: 29.7.0(@types/node@20.17.11)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2))
exit: 0.1.2
import-local: 3.2.0
jest-config: 29.7.0(@types/node@20.17.10)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2))
jest-config: 29.7.0(@types/node@20.17.11)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2))
jest-util: 29.7.0
jest-validate: 29.7.0
yargs: 17.7.2
@ -11185,7 +11185,7 @@ snapshots:
- supports-color
- ts-node
jest-config@29.7.0(@types/node@20.17.10)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2)):
jest-config@29.7.0(@types/node@20.17.11)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2)):
dependencies:
'@babel/core': 7.26.0
'@jest/test-sequencer': 29.7.0
@ -11210,8 +11210,8 @@ snapshots:
slash: 3.0.0
strip-json-comments: 3.1.1
optionalDependencies:
'@types/node': 20.17.10
ts-node: 10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2)
'@types/node': 20.17.11
ts-node: 10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2)
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
@ -11240,16 +11240,16 @@ snapshots:
'@jest/environment': 29.7.0
'@jest/fake-timers': 29.7.0
'@jest/types': 29.6.3
'@types/node': 20.17.10
'@types/node': 20.17.11
jest-mock: 29.7.0
jest-util: 29.7.0
jest-extended@4.0.2(jest@29.7.0(@types/node@20.17.10)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2))):
jest-extended@4.0.2(jest@29.7.0(@types/node@20.17.11)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2))):
dependencies:
jest-diff: 29.7.0
jest-get-type: 29.6.3
optionalDependencies:
jest: 29.7.0(@types/node@20.17.10)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2))
jest: 29.7.0(@types/node@20.17.11)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2))
jest-get-type@29.6.3: {}
@ -11257,7 +11257,7 @@ snapshots:
dependencies:
'@jest/types': 29.6.3
'@types/graceful-fs': 4.1.9
'@types/node': 20.17.10
'@types/node': 20.17.11
anymatch: 3.1.3
fb-watchman: 2.0.2
graceful-fs: 4.2.11
@ -11300,16 +11300,16 @@ snapshots:
slash: 3.0.0
stack-utils: 2.0.6
jest-mock-extended@3.0.7(jest@29.7.0(@types/node@20.17.10)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2)))(typescript@5.7.2):
jest-mock-extended@3.0.7(jest@29.7.0(@types/node@20.17.11)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2)))(typescript@5.7.2):
dependencies:
jest: 29.7.0(@types/node@20.17.10)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2))
jest: 29.7.0(@types/node@20.17.11)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2))
ts-essentials: 10.0.4(typescript@5.7.2)
typescript: 5.7.2
jest-mock@29.7.0:
dependencies:
'@jest/types': 29.6.3
'@types/node': 20.17.10
'@types/node': 20.17.11
jest-util: 29.7.0
jest-pnp-resolver@1.2.3(jest-resolve@29.7.0):
@ -11344,7 +11344,7 @@ snapshots:
'@jest/test-result': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
'@types/node': 20.17.10
'@types/node': 20.17.11
chalk: 4.1.2
emittery: 0.13.1
graceful-fs: 4.2.11
@ -11372,7 +11372,7 @@ snapshots:
'@jest/test-result': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
'@types/node': 20.17.10
'@types/node': 20.17.11
chalk: 4.1.2
cjs-module-lexer: 1.4.1
collect-v8-coverage: 1.0.2
@ -11418,7 +11418,7 @@ snapshots:
jest-util@29.7.0:
dependencies:
'@jest/types': 29.6.3
'@types/node': 20.17.10
'@types/node': 20.17.11
chalk: 4.1.2
ci-info: 3.9.0
graceful-fs: 4.2.11
@ -11437,7 +11437,7 @@ snapshots:
dependencies:
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
'@types/node': 20.17.10
'@types/node': 20.17.11
ansi-escapes: 4.3.2
chalk: 4.1.2
emittery: 0.13.1
@ -11446,17 +11446,17 @@ snapshots:
jest-worker@29.7.0:
dependencies:
'@types/node': 20.17.10
'@types/node': 20.17.11
jest-util: 29.7.0
merge-stream: 2.0.0
supports-color: 8.1.1
jest@29.7.0(@types/node@20.17.10)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2)):
jest@29.7.0(@types/node@20.17.11)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2)):
dependencies:
'@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2))
'@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2))
'@jest/types': 29.6.3
import-local: 3.2.0
jest-cli: 29.7.0(@types/node@20.17.10)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2))
jest-cli: 29.7.0(@types/node@20.17.11)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2))
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
@ -12619,7 +12619,7 @@ snapshots:
'@protobufjs/path': 1.1.2
'@protobufjs/pool': 1.1.0
'@protobufjs/utf8': 1.1.0
'@types/node': 20.17.10
'@types/node': 20.17.11
long: 5.2.3
protocols@2.0.1: {}
@ -13382,12 +13382,12 @@ snapshots:
optionalDependencies:
typescript: 5.7.2
ts-jest@29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@20.17.10)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2)))(typescript@5.7.2):
ts-jest@29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@20.17.11)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2)))(typescript@5.7.2):
dependencies:
bs-logger: 0.2.6
ejs: 3.1.10
fast-json-stable-stringify: 2.1.0
jest: 29.7.0(@types/node@20.17.10)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2))
jest: 29.7.0(@types/node@20.17.11)(ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2))
jest-util: 29.7.0
json5: 2.2.3
lodash.memoize: 4.1.2
@ -13401,14 +13401,14 @@ snapshots:
'@jest/types': 29.6.3
babel-jest: 29.7.0(@babel/core@7.26.0)
ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.10)(typescript@5.7.2):
ts-node@10.9.2(@swc/core@1.10.4)(@types/node@20.17.11)(typescript@5.7.2):
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.11
'@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.4
'@types/node': 20.17.10
'@types/node': 20.17.11
acorn: 8.14.0
acorn-walk: 8.3.4
arg: 4.1.3

View file

@ -5,19 +5,19 @@ ARG BASE_IMAGE_TYPE=slim
# --------------------------------------
# slim image
# --------------------------------------
FROM ghcr.io/renovatebot/base-image:9.28.1@sha256:d012a79a5f3dc6e6067c46016405064b30fbaaac954597318a7a2122ef807444 AS slim-base
FROM ghcr.io/renovatebot/base-image:9.29.0@sha256:10e27273241a0ba63d3a298a7b1e178dbb75b84da6bc2ea7a71db7c9d1a4971c AS slim-base
# --------------------------------------
# full image
# --------------------------------------
FROM ghcr.io/renovatebot/base-image:9.28.1-full@sha256:422a843cbf6c1a3730fab9e89877bf04c49d329501a5b998488078cc6153fc03 AS full-base
FROM ghcr.io/renovatebot/base-image:9.29.0-full@sha256:7b2353855c0f59b9efdb93ce9356aff5dad7d5102f8947c4ebc906855be9177c AS full-base
ENV RENOVATE_BINARY_SOURCE=global
# --------------------------------------
# build image
# --------------------------------------
FROM --platform=$BUILDPLATFORM ghcr.io/renovatebot/base-image:9.28.1@sha256:d012a79a5f3dc6e6067c46016405064b30fbaaac954597318a7a2122ef807444 AS build
FROM --platform=$BUILDPLATFORM ghcr.io/renovatebot/base-image:9.29.0@sha256:10e27273241a0ba63d3a298a7b1e178dbb75b84da6bc2ea7a71db7c9d1a4971c AS build
# We want a specific node version here
# renovate: datasource=node-version