Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gitlab): allow override mergeable check attemps and use exponential backoff #26008

Merged
merged 1 commit into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 15 additions & 8 deletions docs/usage/self-hosted-experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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`

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.
Expand Down Expand Up @@ -129,14 +144,6 @@ If set, Renovate will rewrite GitHub Enterprise Server's pagination responses to
!!! note
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`

If set, Renovate will export OpenTelemetry data to the supplied endpoint.
Expand Down
36 changes: 18 additions & 18 deletions lib/modules/platform/gitlab/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ describe('modules/platform/gitlab/index', () => {
});
delete process.env.GITLAB_IGNORE_REPO_URL;
delete process.env.RENOVATE_X_GITLAB_BRANCH_STATUS_DELAY;
delete process.env.RENOVATE_X_GITLAB_AUTO_MERGEABLE_CHECK_ATTEMPS;
});

async function initFakePlatform(version: string) {
Expand Down Expand Up @@ -1791,17 +1792,12 @@ describe('modules/platform/gitlab/index', () => {
.get('/api/v4/projects/undefined/merge_requests/12345')
.reply(200)
.get('/api/v4/projects/undefined/merge_requests/12345')
.reply(200, {
merge_status: 'can_be_merged',
pipeline: {
id: 29626725,
sha: '2be7ddb704c7b6b83732fdd5b9f09d5a397b5f8f',
ref: 'patch-28',
status: 'success',
},
})
.reply(200)
.get('/api/v4/projects/undefined/merge_requests/12345')
.reply(200)
.put('/api/v4/projects/undefined/merge_requests/12345/merge')
.reply(200);
process.env.RENOVATE_X_GITLAB_AUTO_MERGEABLE_CHECK_ATTEMPS = '3';
expect(
await gitlab.createPr({
sourceBranch: 'some-branch',
Expand All @@ -1813,15 +1809,19 @@ describe('modules/platform/gitlab/index', () => {
usePlatformAutomerge: true,
},
}),
).toMatchInlineSnapshot(`
{
"id": 1,
"iid": 12345,
"number": 12345,
"sourceBranch": "some-branch",
"title": "some title",
}
`);
).toEqual({
id: 1,
iid: 12345,
number: 12345,
sourceBranch: 'some-branch',
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 () => {
Expand Down
13 changes: 8 additions & 5 deletions lib/modules/platform/gitlab/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import * as git from '../../../util/git';
import * as hostRules from '../../../util/host-rules';
import { setBaseUrl } from '../../../util/http/gitlab';
import type { HttpResponse } from '../../../util/http/types';
import { parseInteger } from '../../../util/number';
import * as p from '../../../util/promises';
import { regEx } from '../../../util/regex';
import { sanitize } from '../../../util/sanitize';
Expand Down Expand Up @@ -644,7 +645,11 @@ async function tryPrAutomerge(
}

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`.
for (let attempt = 1; attempt <= retryTimes; attempt += 1) {
Expand All @@ -658,7 +663,7 @@ async function tryPrAutomerge(
if (body.merge_status === desiredStatus && body.pipeline !== null) {
break;
}
await setTimeout(500 * attempt);
await setTimeout(250 * attempt ** 2); // exponential backoff
}

await gitlabApi.putJson(
Expand Down Expand Up @@ -938,9 +943,7 @@ export async function setBranchStatus({
try {
// give gitlab some time to create pipelines for the sha
await setTimeout(
process.env.RENOVATE_X_GITLAB_BRANCH_STATUS_DELAY
? parseInt(process.env.RENOVATE_X_GITLAB_BRANCH_STATUS_DELAY, 10)
: 1000,
parseInteger(process.env.RENOVATE_X_GITLAB_BRANCH_STATUS_DELAY, 1000),
);

await gitlabApi.postJson(url, { body: options });
Expand Down
16 changes: 15 additions & 1 deletion lib/util/number.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { coerceNumber } from './number';
import { coerceNumber, parseInteger } from './number';

describe('util/number', () => {
it.each`
Expand All @@ -9,4 +9,18 @@ describe('util/number', () => {
`('coerceNumber($val, $def) = $expected', ({ val, def, 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);
});
});
19 changes: 19 additions & 0 deletions lib/util/number.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import is from '@sindresorhus/is';

/**
* Coerces a value to a number with optional default value.
* @param val the value to coerce
Expand All @@ -10,3 +12,20 @@ export function coerceNumber(
): number {
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;
}