Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions _documentation/src/docs/tech-docs/10_setup.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ Specify the api token value used when ucascade is approving a merge-request thro
* No default value.
* If not set, `ucascade` will merge the merge-requests *without* approving them first. If set, `ucascade` will approve the merge-request using that value.

==== Max retry attempts

Specify the maximum number of retry attempts for operations that require waiting.

* key: `ucascade.max-retry-attempts`
* default value: `60`
* Each retry waits 1 second before the next attempt, so a value of 60 means waiting up to 60 seconds.

=== GitLab: Webhook Setup

In the corresponding repository or group configure a https://docs.gitlab.com/ee/user/project/integrations/webhooks.html[Webhook] pointing to the location where ucascade is available:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ For a given merge request `merge` event, 3 different independent actions can be

* `previous_auto_mr_merged`: if the merged-request event corresponds to the merge of an auto merge-request, and some other auto-merge requests are open between the same two branches, then the oldest auto merge request is merged.
* `created_auto_mr`: if the merged-request event corresponds to the merge of any merge request that requires an auto merge-request to be created (to cascade the change to the next branch), it is created and potentially directly merged. With the "blocking" and "replay" endpoints, the returned object contains a `created_auto_mr.ucascade_state` field indicating if the action was successful or why it was not. It can take the values of:
** MERGED: MR was created and merged succesfully
** MERGED: MR was created and merged successfully
** NOT_MERGED_CONFLICTS: MR was created but not merged due to existing conflicts between source and target branches that must be manually resolved
** NOT_MERGED_CONCURRENT_MRS: MR was created but not merged due to another existing open merge request between the same source and target branches
** NOT_MERGED_MISSING_APPROVALS: MR was created but not merged because it did not receive the required approvals within the configured timeout period
** NOT_MERGED_UNKNOWN_REASON: MR was created but not merged due to an unknown/unexpected reason
* `existing_branch_deleted`: if the merged-request event corresponds to the **merge** of an auto merge-request and the source branch is still present, it gets deleted.

Expand Down Expand Up @@ -62,6 +63,8 @@ This explains how ucascade determines if a new auto-merge request is created or
include::{diagramsdir}/technical-workflow.puml[]
----

Before attempting to merge, `ucascade` waits for the merge request to receive the required approvals. This is useful when approvals may arrive shortly after the MR is created (e.g., via automated approval workflows). The waiting period is controlled by the `ucascade.max-retry-attempts` configuration property (see xref:10_setup.adoc[Setup]).

During the final stage of the success case -- merging the MR -- `ucascade` verifies if that MR has any pipeline configured. If it has, the flag `merge_when_pipeline_succeeds` is set to `true`, meaning that the merge is only to happen after all its pipelines have finished successfully. Otherwise, if it doesn't have any pipeline, this flag is set to `false`, preventing GitLab to return a 405 error (see https://gitlab.com/gitlab-org/gitlab/-/issues/355455[issue #355455] for more details).

==== existing_branch_deleted
Expand Down
21 changes: 14 additions & 7 deletions src/main/java/service/GitLabService.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ public class GitLabService {
@ConfigProperty(name = "gitlab.api.token.approver")
Optional<String> apiTokenApprover;

@ConfigProperty(name = "ucascade.max-retry-attempts", defaultValue = "60")
int maxRetryAttempts;

@Inject
GitInfo gitInfo;

Expand All @@ -70,7 +73,6 @@ public class GitLabService {
private static final String UCASCADE_TAG = "[ucascade]";
private static final String UCASCADE_BRANCH_PATTERN_PREFIX = "^mr(\\d+)_";
private static final String UCASCADE_BRANCH_PATTERN = UCASCADE_BRANCH_PATTERN_PREFIX + ".+";
private static final int MAX_RETRY_ATTEMPTS = 60;

private GitLabApi gitlab;
private GitLabApi gitlabApprover;
Expand Down Expand Up @@ -404,7 +406,12 @@ private MergeRequestResult acceptAutoMergeRequest(String gitlabEventUUID, MergeR
final Long mrNumber = mr.getIid();
final Long project = mr.getProjectId();

if (!isMergeRequestApproved(gitlabEventUUID, mr)) {
int countDownWaitingApprovals = maxRetryAttempts;
while (!isMergeRequestApproved(gitlabEventUUID, mr) && countDownWaitingApprovals-- > 0) {
Log.infof("GitlabEvent: '%s' | Waiting for approvals in MR '!%d' in project '%d'. Retrying... %d", gitlabEventUUID, mrNumber, project, Math.abs(countDownWaitingApprovals - maxRetryAttempts));
sleepSeconds(1);
}
if (countDownWaitingApprovals < 0) {
mr = assignMrToCascadeResponsible(gitlabEventUUID, mr);
Log.warnf("GitlabEvent: '%s' | Merge request '!%d' in project '%d' cannot be accepted because it does not have the required amount of approvals", gitlabEventUUID, mrNumber, project);
MergeRequestResult result = new MergeRequestResult(mr);
Expand All @@ -427,21 +434,21 @@ private MergeRequestResult acceptAutoMergeRequest(String gitlabEventUUID, MergeR
.withMergeCommitMessage(String.format("%s Automatic merge: '%s' -> '%s'", UCASCADE_TAG, sourceBranchPretty, targetBranch))
.withShouldRemoveSourceBranch(mr.getForceRemoveSourceBranch());

int countDown = MAX_RETRY_ATTEMPTS;
while (state != MergeRequestUcascadeState.MERGED && countDown-- > 0) {
int countDownWaitingMerged = maxRetryAttempts;
while (state != MergeRequestUcascadeState.MERGED && countDownWaitingMerged-- > 0) {
boolean hasPipeline = hasPipeline(gitlabEventUUID, mr);
acceptMrParams.withMergeWhenPipelineSucceeds(hasPipeline);
Log.infof("GitlabEvent: '%s' | Merging MR '!%d': '%s' -> '%s' when pipeline succeeds: '%b'", gitlabEventUUID, mrNumber, sourceBranch, targetBranch, hasPipeline);
try {
mr = mrApi.acceptMergeRequest(project, mrNumber, acceptMrParams);
state = MergeRequestUcascadeState.MERGED;
} catch (GitLabApiException e) {
if (countDown == 0) {
if (countDownWaitingMerged == 0) {
Log.warnf(e, "GitlabEvent: '%s' | Cannot accept merge request '!%d' from '%s' into '%s' in project '%d'", gitlabEventUUID, mrNumber, sourceBranch, targetBranch, project);
state = MergeRequestUcascadeState.NOT_MERGED_UNKNOWN_REASON;
mr = assignMrToCascadeResponsible(gitlabEventUUID, mr);
} else {
Log.infof("GitlabEvent: '%s' | Merge failed for MR '!%d', status '%s'. Retrying... %d", gitlabEventUUID, mrNumber, mergeStatus, Math.abs(countDown - MAX_RETRY_ATTEMPTS));
Log.infof("GitlabEvent: '%s' | Merge failed for MR '!%d', status '%s'. Retrying... %d", gitlabEventUUID, mrNumber, mergeStatus, Math.abs(countDownWaitingMerged - maxRetryAttempts));
sleepSeconds(1);
}
}
Expand Down Expand Up @@ -499,7 +506,7 @@ private String addMrPrefixPattern(String branchName) {

private MergeRequest waitForMrReady(String gitlabEventUUID, MergeRequest mr) {
boolean approverExists = gitlabApprover != null;
int countDown = MAX_RETRY_ATTEMPTS;
int countDown = maxRetryAttempts;
String mrStatus = mr.getDetailedMergeStatus();
while (!isMrReady(mrStatus, approverExists, mr.getHasConflicts()) && countDown-- > 0) {
sleepSeconds(1);
Expand Down
2 changes: 1 addition & 1 deletion src/test/java/com/unblu/ucascade/UcascadeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,7 @@ void testMergeWithoutApprovals() throws Exception {
.body(endsWith("\n}"))
.body("created_auto_mr.ucascade_state", equalTo(MergeRequestUcascadeState.NOT_MERGED_MISSING_APPROVALS.name()));

verifyRequests(12);
verifyRequests(13);
}

@Test
Expand Down
3 changes: 2 additions & 1 deletion src/test/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
gitlab.api.token=token_used_in_unit_tests_1234
gitlab.api.token.approver=approver_token_used_in_unit_tests_1234
gitlab.api.token.approver=approver_token_used_in_unit_tests_1234
ucascade.max-retry-attempts=1
Loading