mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-11 06:26:26 +00:00
feat(gitlab): allow override mergeable check attemps and use exponential backoff (#26008)
This commit is contained in:
parent
859b22eb3e
commit
8b4bfbd77d
5 changed files with 75 additions and 32 deletions
|
@ -40,6 +40,21 @@ If set to any string, Renovate will use this as the `user-agent` it sends with H
|
||||||
If set to an integer, Renovate will use this as max page number for docker tags lookup on docker registries, instead of the default 20 pages.
|
If set to an integer, Renovate will use this as max page number for docker tags lookup on docker registries, instead of the default 20 pages.
|
||||||
This is useful for registries which ignores the `n` parameter in the query string and only return 50 tags per page.
|
This is useful for registries which ignores the `n` parameter in the query string and only return 50 tags per page.
|
||||||
|
|
||||||
|
## `RENOVATE_X_GITLAB_AUTO_MERGEABLE_CHECK_ATTEMPS`
|
||||||
|
|
||||||
|
If set to an positive integer, Renovate will use this as the number of attempts to check if a merge request on GitLab is mergable before trying to automerge.
|
||||||
|
The formula for the delay between attempts is `250 * attempt * attempt` milliseconds.
|
||||||
|
|
||||||
|
Default value: `5` (attempts results in max. 13.75 seconds timeout).
|
||||||
|
|
||||||
|
## `RENOVATE_X_GITLAB_BRANCH_STATUS_DELAY`
|
||||||
|
|
||||||
|
Adjust default time (in milliseconds) given to GitLab to create pipelines for a commit pushed by Renovate.
|
||||||
|
|
||||||
|
Can be useful for slow-running, self-hosted GitLab instances that don't react fast enough for the default delay to help.
|
||||||
|
|
||||||
|
Default value: `1000` (milliseconds).
|
||||||
|
|
||||||
## `RENOVATE_X_HARD_EXIT`
|
## `RENOVATE_X_HARD_EXIT`
|
||||||
|
|
||||||
If set to any value, Renovate will use a "hard" `process.exit()` once all work is done, even if a sub-process is otherwise delaying Node.js from exiting.
|
If set to any value, Renovate will use a "hard" `process.exit()` once all work is done, even if a sub-process is otherwise delaying Node.js from exiting.
|
||||||
|
@ -129,14 +144,6 @@ If set, Renovate will rewrite GitHub Enterprise Server's pagination responses to
|
||||||
!!! note
|
!!! note
|
||||||
For the GitHub Enterprise Server platform only.
|
For the GitHub Enterprise Server platform only.
|
||||||
|
|
||||||
## `RENOVATE_X_GITLAB_BRANCH_STATUS_DELAY`
|
|
||||||
|
|
||||||
Adjust default time (in milliseconds) given to GitLab to create pipelines for a commit pushed by Renovate.
|
|
||||||
|
|
||||||
Can be useful for slow-running, self-hosted GitLab instances that don't react fast enough for the default delay to help.
|
|
||||||
|
|
||||||
Default value: `1000` (milliseconds).
|
|
||||||
|
|
||||||
## `OTEL_EXPORTER_OTLP_ENDPOINT`
|
## `OTEL_EXPORTER_OTLP_ENDPOINT`
|
||||||
|
|
||||||
If set, Renovate will export OpenTelemetry data to the supplied endpoint.
|
If set, Renovate will export OpenTelemetry data to the supplied endpoint.
|
||||||
|
|
|
@ -51,6 +51,7 @@ describe('modules/platform/gitlab/index', () => {
|
||||||
});
|
});
|
||||||
delete process.env.GITLAB_IGNORE_REPO_URL;
|
delete process.env.GITLAB_IGNORE_REPO_URL;
|
||||||
delete process.env.RENOVATE_X_GITLAB_BRANCH_STATUS_DELAY;
|
delete process.env.RENOVATE_X_GITLAB_BRANCH_STATUS_DELAY;
|
||||||
|
delete process.env.RENOVATE_X_GITLAB_AUTO_MERGEABLE_CHECK_ATTEMPS;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function initFakePlatform(version: string) {
|
async function initFakePlatform(version: string) {
|
||||||
|
@ -1791,17 +1792,12 @@ describe('modules/platform/gitlab/index', () => {
|
||||||
.get('/api/v4/projects/undefined/merge_requests/12345')
|
.get('/api/v4/projects/undefined/merge_requests/12345')
|
||||||
.reply(200)
|
.reply(200)
|
||||||
.get('/api/v4/projects/undefined/merge_requests/12345')
|
.get('/api/v4/projects/undefined/merge_requests/12345')
|
||||||
.reply(200, {
|
.reply(200)
|
||||||
merge_status: 'can_be_merged',
|
.get('/api/v4/projects/undefined/merge_requests/12345')
|
||||||
pipeline: {
|
.reply(200)
|
||||||
id: 29626725,
|
|
||||||
sha: '2be7ddb704c7b6b83732fdd5b9f09d5a397b5f8f',
|
|
||||||
ref: 'patch-28',
|
|
||||||
status: 'success',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.put('/api/v4/projects/undefined/merge_requests/12345/merge')
|
.put('/api/v4/projects/undefined/merge_requests/12345/merge')
|
||||||
.reply(200);
|
.reply(200);
|
||||||
|
process.env.RENOVATE_X_GITLAB_AUTO_MERGEABLE_CHECK_ATTEMPS = '3';
|
||||||
expect(
|
expect(
|
||||||
await gitlab.createPr({
|
await gitlab.createPr({
|
||||||
sourceBranch: 'some-branch',
|
sourceBranch: 'some-branch',
|
||||||
|
@ -1813,15 +1809,19 @@ describe('modules/platform/gitlab/index', () => {
|
||||||
usePlatformAutomerge: true,
|
usePlatformAutomerge: true,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
).toMatchInlineSnapshot(`
|
).toEqual({
|
||||||
{
|
id: 1,
|
||||||
"id": 1,
|
iid: 12345,
|
||||||
"iid": 12345,
|
number: 12345,
|
||||||
"number": 12345,
|
sourceBranch: 'some-branch',
|
||||||
"sourceBranch": "some-branch",
|
title: 'some title',
|
||||||
"title": "some title",
|
});
|
||||||
}
|
|
||||||
`);
|
expect(timers.setTimeout.mock.calls).toMatchObject([
|
||||||
|
[250],
|
||||||
|
[1000],
|
||||||
|
[2250],
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('raises with squash enabled when repository squash option is default_on', async () => {
|
it('raises with squash enabled when repository squash option is default_on', async () => {
|
||||||
|
|
|
@ -23,6 +23,7 @@ import * as git from '../../../util/git';
|
||||||
import * as hostRules from '../../../util/host-rules';
|
import * as hostRules from '../../../util/host-rules';
|
||||||
import { setBaseUrl } from '../../../util/http/gitlab';
|
import { setBaseUrl } from '../../../util/http/gitlab';
|
||||||
import type { HttpResponse } from '../../../util/http/types';
|
import type { HttpResponse } from '../../../util/http/types';
|
||||||
|
import { parseInteger } from '../../../util/number';
|
||||||
import * as p from '../../../util/promises';
|
import * as p from '../../../util/promises';
|
||||||
import { regEx } from '../../../util/regex';
|
import { regEx } from '../../../util/regex';
|
||||||
import { sanitize } from '../../../util/sanitize';
|
import { sanitize } from '../../../util/sanitize';
|
||||||
|
@ -644,7 +645,11 @@ async function tryPrAutomerge(
|
||||||
}
|
}
|
||||||
|
|
||||||
const desiredStatus = 'can_be_merged';
|
const desiredStatus = 'can_be_merged';
|
||||||
const retryTimes = 8; // results in max. 5 min. timeout if no pipeline created
|
// The default value of 5 attempts results in max. 13.75 seconds timeout if no pipeline created.
|
||||||
|
const retryTimes = parseInteger(
|
||||||
|
process.env.RENOVATE_X_GITLAB_AUTO_MERGEABLE_CHECK_ATTEMPS,
|
||||||
|
5,
|
||||||
|
);
|
||||||
|
|
||||||
// Check for correct merge request status before setting `merge_when_pipeline_succeeds` to `true`.
|
// Check for correct merge request status before setting `merge_when_pipeline_succeeds` to `true`.
|
||||||
for (let attempt = 1; attempt <= retryTimes; attempt += 1) {
|
for (let attempt = 1; attempt <= retryTimes; attempt += 1) {
|
||||||
|
@ -658,7 +663,7 @@ async function tryPrAutomerge(
|
||||||
if (body.merge_status === desiredStatus && body.pipeline !== null) {
|
if (body.merge_status === desiredStatus && body.pipeline !== null) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
await setTimeout(500 * attempt);
|
await setTimeout(250 * attempt ** 2); // exponential backoff
|
||||||
}
|
}
|
||||||
|
|
||||||
await gitlabApi.putJson(
|
await gitlabApi.putJson(
|
||||||
|
@ -938,9 +943,7 @@ export async function setBranchStatus({
|
||||||
try {
|
try {
|
||||||
// give gitlab some time to create pipelines for the sha
|
// give gitlab some time to create pipelines for the sha
|
||||||
await setTimeout(
|
await setTimeout(
|
||||||
process.env.RENOVATE_X_GITLAB_BRANCH_STATUS_DELAY
|
parseInteger(process.env.RENOVATE_X_GITLAB_BRANCH_STATUS_DELAY, 1000),
|
||||||
? parseInt(process.env.RENOVATE_X_GITLAB_BRANCH_STATUS_DELAY, 10)
|
|
||||||
: 1000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await gitlabApi.postJson(url, { body: options });
|
await gitlabApi.postJson(url, { body: options });
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { coerceNumber } from './number';
|
import { coerceNumber, parseInteger } from './number';
|
||||||
|
|
||||||
describe('util/number', () => {
|
describe('util/number', () => {
|
||||||
it.each`
|
it.each`
|
||||||
|
@ -9,4 +9,18 @@ describe('util/number', () => {
|
||||||
`('coerceNumber($val, $def) = $expected', ({ val, def, expected }) => {
|
`('coerceNumber($val, $def) = $expected', ({ val, def, expected }) => {
|
||||||
expect(coerceNumber(val, def)).toBe(expected);
|
expect(coerceNumber(val, def)).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
val | def | expected
|
||||||
|
${1} | ${2} | ${2}
|
||||||
|
${undefined} | ${2} | ${2}
|
||||||
|
${undefined} | ${undefined} | ${0}
|
||||||
|
${''} | ${undefined} | ${0}
|
||||||
|
${'-1'} | ${undefined} | ${0}
|
||||||
|
${'1.1'} | ${undefined} | ${0}
|
||||||
|
${'a'} | ${undefined} | ${0}
|
||||||
|
${'5'} | ${undefined} | ${5}
|
||||||
|
`('parseInteger($val, $def) = $expected', ({ val, def, expected }) => {
|
||||||
|
expect(parseInteger(val, def)).toBe(expected);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import is from '@sindresorhus/is';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Coerces a value to a number with optional default value.
|
* Coerces a value to a number with optional default value.
|
||||||
* @param val the value to coerce
|
* @param val the value to coerce
|
||||||
|
@ -10,3 +12,20 @@ export function coerceNumber(
|
||||||
): number {
|
): number {
|
||||||
return val ?? def ?? 0;
|
return val ?? def ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a value as a finite positive integer with optional default value.
|
||||||
|
* If no default value is provided, the default value is 0.
|
||||||
|
* @param val Value to parse as finite integer.
|
||||||
|
* @param def Optional default value.
|
||||||
|
* @returns The parsed value or the default value if the parsed value is not finite.
|
||||||
|
*/
|
||||||
|
export function parseInteger(
|
||||||
|
val: string | undefined | null,
|
||||||
|
def?: number,
|
||||||
|
): number {
|
||||||
|
// Number.parseInt returns NaN if the value is not a finite integer.
|
||||||
|
const parsed =
|
||||||
|
is.string(val) && /^\d+$/.test(val) ? Number.parseInt(val, 10) : Number.NaN;
|
||||||
|
return Number.isFinite(parsed) ? parsed : def ?? 0;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue