Bitbucket Cascade Merge issue for Forge

Hello,

I’ve had success with the Forge app on Bitbucket cloud with merge checks for JIRA issue tickets. I have since decided to enhance this function by extending it to help with Cascade merges. With Bitbucket server, we had the cascade merge functionality, with cloud, that no longer exists. I have modified my manifest.yml and index.js to try and replicate this functionality and I am running into some issues.

Manifest.yml:

permissions:
scopes:

  • ‘read:repository:bitbucket’
  • ‘read:pullrequest:bitbucket’
  • ‘write:pullrequest:bitbucket’
  • ‘write:repository:bitbucket’
    external:
    fetch:
    backend:
  • https://api.bitbucket.org/2.0’

modules:
bitbucket:mergeCheck:

  • key: jea-coding-standard-merge-check
    function: checkCommitMessages
    name: JEA Coding Standard
    description: Ensures all commits contain a JIRA issue key
    triggers:
  • on-merge

function:

  • key: checkCommitMessages
    handler: index.checkCommitMessages
  • key: cascadeMerge
    handler: index.cascadeMerge

trigger:

  • key: pullRequestMergedTrigger
    events:
  • avi:bitbucket:fulfilled:pullrequest
    function: cascadeMerge

And my index.js:

const api = require(‘@forge/api’);

exports.checkCommitMessages = async (event, context) => {
console.log(‘Received event:’, JSON.stringify(event, null, 2));
console.log(‘Received context:’, JSON.stringify(context, null, 2));

if (!event || !event.workspace || !event.repository || !event.pullrequest) {
    console.error('Invalid event data:', event);
    return { success: false, message: 'Invalid event data received' };
}

const workspaceId = event.workspace.uuid;
const repositoryId = event.repository.uuid;
const pullRequestId = event.pullrequest.id;

try {
    const commitsResponse = await api.asApp().requestBitbucket(api.route`/2.0/repositories/${workspaceId}/${repositoryId}/pullrequests/${pullRequestId}/commits`);
    if (!commitsResponse.ok) {
        throw new Error(`Failed to fetch commits: ${commitsResponse.status} ${commitsResponse.statusText}`);
    }

    const commits = await commitsResponse.json();
    const jiraTicketRegex = /^(Revert ")?([A-Z][A-Z_0-9]+-[0-9]+)[ :,\\-].*\s*"?\s*$/;

    for (const commit of commits.values) {
        if (commit.parents && commit.parents.length > 1) continue; // Skip merge commits
        if (!jiraTicketRegex.test(commit.message)) {
            console.error(`Commit message "${commit.message}" from commit ${commit.hash} failed validation.`);
            return {
                success: false,
                message: `Commit ${commit.hash} does not reference a JIRA ticket correctly: "${commit.message}"`
            };
        }
    }

    return { success: true, message: "All commit messages are correctly formatted." };
} catch (error) {
    console.error('Error occurred while checking commit messages:', error);
    return { success: false, message: `Error occurred while processing: ${error.message}` };
}

};

exports.cascadeMerge = async (event, context) => {
console.log(‘Cascade Merge Event:’, JSON.stringify(event, null, 2));
console.log(‘Pull Request Source:’, JSON.stringify(event.pullrequest.source, null, 2));
console.log(‘Pull Request Destination:’, JSON.stringify(event.pullrequest.destination, null, 2));

if (!event.pullrequest || !event.pullrequest.source || !event.pullrequest.source.branch || !event.pullrequest.destination || !event.pullrequest.destination.branch) {
    console.error('Cascade Merge Error: Invalid event structure for pullrequest source and destination');
    return;
}

const workspaceId = event.workspace.uuid;
const repositoryId = event.repository.uuid;
const sourceBranchName = event.pullrequest.source.branch.name;
const destinationBranchName = event.pullrequest.destination.branch.name;

console.log(`Received merge event from ${sourceBranchName} to ${destinationBranchName}`);
console.log(`Variables assigned - Source: ${sourceBranchName}, Destination: ${destinationBranchName}`); // Additional log

let targetBranchName = getNextBranchName(destinationBranchName);
console.log(`Starting cascade merge from source: ${sourceBranchName} to initial target: ${targetBranchName}`);

while (targetBranchName) {
    console.log(`Attempting to create PR from ${destinationBranchName} to ${targetBranchName}`);
    try {
        const prData = await createPullRequest(workspaceId, repositoryId, destinationBranchName, targetBranchName);
        console.log(`Created PR for cascade merge: ${prData.links.html.href}`);
        console.log(`Attempting to merge PR from ${destinationBranchName} to ${targetBranchName}`); // Additional log before merging

        const mergeResult = await mergePullRequest(workspaceId, repositoryId, prData.id);
        if (!mergeResult.success) {
            console.error('Merge failed due to conflict or other error:', mergeResult.error);
            break; // Stop the cascade merge process on conflict
        }

        console.log(`Merge successful: PR ${prData.id} merged from ${destinationBranchName} to ${targetBranchName}`);
        targetBranchName = getNextBranchName(targetBranchName); // Determine the next target branch, if any
        console.log(`Updated target branch to ${targetBranchName} for next iteration.`);
    } catch (error) {
        console.error('Error occurred during cascade merge:', error);
        break; // Halt the process on any error
    }
}

console.log('Cascade merge process completed.');

};

async function createPullRequest(workspaceId, repositoryId, sourceBranchName, targetBranchName) {
const createPrResponse = await api.asApp().requestBitbucket(api.route/2.0/repositories/${workspaceId}/${repositoryId}/pullrequests, {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify({
title: Cascade merge ${sourceBranchName} -> ${targetBranchName},
source: { branch: { name: sourceBranchName } },
destination: { branch: { name: targetBranchName } },
close_source_branch: false,
}),
});

if (!createPrResponse.ok) {
    const errorText = await createPrResponse.text();
    console.error(`Failed to create PR from ${sourceBranchName} to ${targetBranchName}: ${createPrResponse.status} ${createPrResponse.statusText} - ${errorText}`);
    throw new Error(`Failed to create PR: ${createPrResponse.status} ${createPrResponse.statusText} - ${errorText}`);
}

return await createPrResponse.json();

}

async function mergePullRequest(workspaceId, repositoryId, pullRequestId) {
const mergeResponse = await api.asApp().requestBitbucket(api.route/2.0/repositories/${workspaceId}/${repositoryId}/pullrequests/${pullRequestId}/merge, {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify({
type: “pullrequest”,
message: “Automated cascade merge by script”,
close_source_branch: false,
merge_strategy: “merge_commit”
}),
});

if (!mergeResponse.ok) {
    const errorText = await mergeResponse.text();
    console.error(`Merge request failed: ${mergeResponse.status} ${mergeResponse.statusText} - ${errorText}`);
    return { success: false, error: `Merge failed: ${mergeResponse.status} ${mergeResponse.statusText} - ${errorText}` };
}

return { success: true, data: await mergeResponse.json() };

}

function getNextBranchName(currentBranchName) {
const branchMapping = {
“release/23.4”: “release/24.2”,
// Add more mappings as needed
“release/24.2”: “master”,
};

const nextBranch = branchMapping[currentBranchName] || null;
console.log(`Next branch for ${currentBranchName} is ${nextBranch}`); // Added debug statement
return nextBranch;

}

The purpose of this app is to initiate a downstream merge once a PR is merged from a custom branch to “release/23.4” and then its supposed to automatically merge into the next release “release/24.2” until it hits “master” or hits a conflict. The app doesn’t automatically merge into “release/24.2” after being manually merged to “release/23.4”. Forge logs show this:

INFO 2024-03-06T20:45:52.353Z d6ed05bc-f6f3-42a2-9880-e0d00ee5b133 Cascade Merge Event: {
“timestamp”: “2024-03-06T20:45:49.267908Z”,
“actor”: {
“type”: “user”,
“accountId”: “6405ef16edd8ef2b95”,
“uuid”: “{c8a328b1-5f99-4f94-a240-baa976abe9c9}”
},
“repository”: {
“uuid”: “{c455f0f3-826f-4c47-938c-5d94397cd03d}”
},
“project”: {
“uuid”: “{277e99f2-b37a-42fe-ae90-3b3fc70f3138}”
},
“workspace”: {
“uuid”: “{a995146f-8c43-4368-9613-4664eaedbb95}”
},
“pullrequest”: {
“id”: 3658,
“state”: “MERGED”,
“source”: {
“branch”: “feature/DEVOPS-496-cascade-test”,
“commit”: {
“hash”: “31d502795036”
}
},
“destination”: {
“branch”: “release/23.4”,
“commit”: {
“hash”: “f71c93961214”
}
},
“mergeCommit”: {
“hash”: “c95c43efa71d”
}
},
“eventType”: “avi:bitbucket:fulfilled:pullrequest”,
“selfGenerated”: false,
“context”: {
“cloudId”: “a995146f-8c43-4368-9613-4664eaedbb95”,
“moduleKey”: “pullRequestMergedTrigger”
},
“contextToken”: “eyJhbGciOiJSUzI1NiIsImtpZCI6ImZvcmdlL2NvbnRleHQtdG9rZW4vZjI4NTdlYjYtZGY2My00ZjNjLTk0OTItMGMwMjJkYzFhZmY2In0.eyJjb250ZXh0Ijp7ImNsb3VkSWQiOiJhOTk1MTQ2Zi04YzQzLTQzNjgtOTYxMy00NjY0ZWFlZGJiOTUiLCJtb2R1bGVLZXkiOiJwdWxsUmVxdWVzdE1lcmdlZFRyaWdnZXIifSwiYWNjb3VudElkIjoiNzEyMDIwOmRhZmRhOWJmLTRlNjUtNDQ5Yy04ZmNjLWM2YmYyODNhOTU3ZiIsImV4dGVuc2lvbklkIj5zaW9uLzkzMzYwYzdlLTk3YWYtNDY0Yy04OWVhLTlhZGQ1ZDYwMDAwNS9mY2E5ZGRlYS04ZDZiLTRmYTctOTYwZi02N2NkZjg2ZWFjNGEvc3RhdGljL3B1bGxSZXF1ZXN0TWVyZ2VkVHJpZ2dlciIsImNvbnRleHRJZHMiOlsiYXJpOmNsb3VkOmJpdGJ1Y2tldDo6d29ya3NwYWNlL2E5OTUxNDZmLThjNDMtNDM2OC05NjEzLTQ2NjRlYWVkYmI5NSJdLCJhcHBJZCI6IjkzMzYwYzdlLTk3YWYtNDY0Yy04OWVhLTlhZGQ1ZDYwMDAwNSIsImFwcFZlcnNpb24iOiIzLjE0LjAiLCJleHRlbnNpb25UeXBlIjoiY29yZTp0cmlnZ2VyIiwidW5saWNlbnNlZCI6ZmFsc2UsImlzcyI6ImZvcmdlL2NvbnRleHQtdG9rZW4iLCJhdWQiOiJmb3JnZSIsImlhdCI6MTcwOTc1Nzk1MCwibmJmIjoxNzA5NzU3OTUwLCJleHAiOjE3MDk3NTg4NTAsImp0aSI6ImUwOWIxZjJhZTA5ZDExNzg0YzA4NDU4ODk2OWU3MWNmMWQ4MDY4NTYifQ.bOe23bLIQLyVi7g-2C2ZRxYRLejQ2Bh4bgi12muhwAbFwfRbS9c345kIbjY0IppJDXq1p5E6gPoUFCBTstVqSqcwXSrFR8MImAHObw2nlJmffl8r2mexF-QPCUQarN3JZtWAuUWMNkawBLknefZ4OASSNWU_kYYAwMJ3px_zq7ZeMwki4XmzVjY4wHuUDGtoysyLFyjmm5DWzeA7bVp_3n8n82DL-HVPO1GIkHFRD3WJ7GYYziSkEtB4YYBrcZyej3ELWcQICr-oNkW7k1M7AOy2T5LZK2-o_djN2c0Yr4lMdNEcZQSEbI1r8oY7Lsrn-YoSwX10ykwLgMiz_P-79A”
}
INFO 2024-03-06T20:45:52.353Z d6ed05bc-f6f3-42a2-9880-e0d00ee5b133 Pull Request Source: {
“branch”: “feature/DEVOPS-496-cascade-test”,
“commit”: {
“hash”: “31d502795036”
}
}
INFO 2024-03-06T20:45:52.353Z d6ed05bc-f6f3-42a2-9880-e0d00ee5b133 Pull Request Destination: {
“branch”: “release/23.4”,
“commit”: {
“hash”: “f71c93961214”
}
}
INFO 2024-03-06T20:45:52.353Z d6ed05bc-f6f3-42a2-9880-e0d00ee5b133 Received merge event from undefined to undefined
INFO 2024-03-06T20:45:52.353Z d6ed05bc-f6f3-42a2-9880-e0d00ee5b133 Variables assigned - Source: undefined, Destination: undefined
INFO 2024-03-06T20:45:52.353Z d6ed05bc-f6f3-42a2-9880-e0d00ee5b133 Next branch for undefined is null
INFO 2024-03-06T20:45:52.354Z d6ed05bc-f6f3-42a2-9880-e0d00ee5b133 Starting cascade merge from source: undefined to initial target: null
INFO 2024-03-06T20:45:52.354Z d6ed05bc-f6f3-42a2-9880-e0d00ee5b133 Cascade merge process completed.

The issue appears to be related to a misunderstanding in the variable assignment for sourceBranchName and destinationBranchName . The console.log statements indicated that the merge event was received from undefined to undefined , which suggests a problem in obtaining these branch names from the event data.

Is there any recommendation as to what to adjust in my code to get around this?

Thanks.

2 Likes