Forums

Articles
Create
cancel
Showing results for 
Search instead for 
Did you mean: 

unable to Auto-assign reviewers on PR creation through custom check

Khushboo Kumari
I'm New Here
I'm New Here
Those new to the Atlassian Community have posted less than three times. Give them a warm welcome!
August 11, 2025

Hi,
Currently, I’ve successfully implemented the custom check at the workspace level and enforced restrictions to prevent the removal of auto-assigned reviewers — ensuring that these reviewers cannot be edited.

However, I’m encountering an issue during the merge process. Even when an auto-assigned reviewer approves the pull request, I'm still unable to merge the code into the main branch

forge logs:

<redacted>



code for the reference:

index.js:


import api, { route } from '@forge/api';

// ==== CONFIGURATION ====
// From Forge logs: REQUIRED_REVIEWERS
const REQUIRED_REVIEWERS = [
'<redacted>',
'<redacted>'
];

// ==== HANDLERS ====

// Trigger: Assign reviewers when PR is created
export async function prtrigger(event) {
console.log("🔍 Incoming PR event payload:", JSON.stringify(event, null, 2));
try {
const workspaceUuid = event.workspace.uuid;
const repoUuid = event.repository.uuid;
const prId = event.pullrequest.id;

const prAuthor = await getPRAuthor(workspaceUuid, repoUuid, prId);
const reviewersToAssign = REQUIRED_REVIEWERS.filter(id => id !== prAuthor);

await assignPR(workspaceUuid, repoUuid, prId, reviewersToAssign);
await commentOnPR(workspaceUuid, repoUuid, prId, reviewersToAssign);
} catch (error) {
console.error('Error in prtrigger:', error.stack || error.message);
}
}

// Trigger: Re-add required reviewers if removed
export async function ensureReviewers(event) {
try {
const workspaceUuid = event.workspace.uuid;
const repoUuid = event.repository.uuid;
const prId = event.pullrequest.id;

const res = await api.asApp().requestBitbucket(
route`/2.0/repositories/${workspaceUuid}/${repoUuid}/pullrequests/${prId}`
);
const data = await res.json();

const currentReviewers = data.reviewers.map(r => r.account_id);
const missing = REQUIRED_REVIEWERS.filter(id => !currentReviewers.includes(id));

if (missing.length > 0) {
const updatedReviewers = [...new Set([...currentReviewers, ...missing])];
await assignPR(workspaceUuid, repoUuid, prId, updatedReviewers);
console.log(`➕ Re-added missing reviewers: ${missing}`);
}
} catch (error) {
console.error('❌ Error in ensureReviewers:', error.stack || error.message);
}
}

// Merge Check: Pass if at least one required reviewer approved
export async function mergeCheckHandler(event) {
try {
console.log("🔍 Incoming merge check event:", JSON.stringify(event, null, 2));

const workspaceUuid = event.workspace.uuid;
const repoUuid = event.repository.uuid;
const prId = event.pullrequest.id;

console.log(`📌 Workspace: ${workspaceUuid}, Repo: ${repoUuid}, PR: ${prId}`);
console.log("📌 REQUIRED_REVIEWERS:", REQUIRED_REVIEWERS);

// Step 1: Fetch PR details
const prRes = await api.asApp().requestBitbucket(
route`/2.0/repositories/${workspaceUuid}/${repoUuid}/pullrequests/${prId}`
);
console.log(`📡 PR details status: ${prRes.status}`);
const prData = await prRes.json();
console.log("📜 PR details:", JSON.stringify(prData, null, 2));

const currentReviewers = prData.reviewers.map(r => r.account_id);
console.log("👥 Assigned reviewers:", currentReviewers);

// Step 2: Ensure all required reviewers are assigned
const missingReviewers = REQUIRED_REVIEWERS.filter(id => !currentReviewers.includes(id));
if (missingReviewers.length > 0) {
console.log("⚠️ Missing reviewers:", missingReviewers);
return {
status: 'FAILED',
message: `❌ Missing required reviewers: ${missingReviewers.join(', ')}`
};
}

// Step 3: Fetch PR activity for approvals
const activityRes = await api.asApp().requestBitbucket(
route`/2.0/repositories/${workspaceUuid}/${repoUuid}/pullrequests/${prId}/activity`
);
console.log(`📡 PR activity status: ${activityRes.status}`);
const activityData = await activityRes.json();
console.log("📜 Activity raw:", JSON.stringify(activityData, null, 2));

const approvals = (activityData.values || [])
.filter(item => item.approval?.user?.account_id)
.map(item => item.approval.user.account_id);

console.log("✅ Approved reviewer IDs:", approvals);

// Step 4: Pass if at least one required reviewer approved
const hasApprovalFromRequired = REQUIRED_REVIEWERS.some(id => approvals.includes(id));
console.log(`🔍 Approval from at least one required reviewer: ${hasApprovalFromRequired}`);

if (!hasApprovalFromRequired) {
return {
status: 'FAILED',
message: `❌ Need approval from at least one required reviewer. Approved IDs: ${approvals.join(', ')}`
};
}

// Step 5: Passed
console.log("🎉 Merge check passed");
return { status: 'PASSED' };

} catch (error) {
console.error('❌ Error in mergeCheckHandler:', error.stack || error.message);
return {
status: 'FAILED',
message: '⚠️ Error validating reviewers. Check Forge logs.'
};
}
}

// ==== HELPERS ====

const getPRAuthor = async (workspaceUuid, repoUuid, prId) => {
const res = await api.asApp().requestBitbucket(
route`/2.0/repositories/${workspaceUuid}/${repoUuid}/pullrequests/${prId}`
);
const data = await res.json();
return data.author.account_id;
};

const assignPR = async (workspaceUuid, repoUuid, prId, reviewers) => {
const body = { reviewers: reviewers.map(id => ({ account_id: id })) };
const res = await api.asApp().requestBitbucket(
route`/2.0/repositories/${workspaceUuid}/${repoUuid}/pullrequests/${prId}`,
{
method: 'PUT',
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
body: JSON.stringify(body)
}
);
return await res.json();
};

const commentOnPR = async (workspaceUuid, repoUuid, prId, reviewers) => {
const body = {
content: { raw: `The PR has been automatically assigned to: ${reviewers.map(id => `@{${id}}`).join(', ')}` }
};
await api.asApp().requestBitbucket(
route`/2.0/repositories/${workspaceUuid}/${repoUuid}/pullrequests/${prId}/comments`,
{
method: 'POST',
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
body: JSON.stringify(body)
}
);
};


manifest.yml

modules:
trigger:
- key: pullrequest-created-event
function: main
events:
- avi:bitbucket:created:pullrequest
- key: pullrequest-updated-event
function: ensureReviewers
events:
- avi:bitbucket:updated:pullrequest

bitbucket:mergeCheck:
- key: enforce-reviewers-check
function: mergeCheckHandler
name: Enforce Required Reviewers
description: Ensures required reviewers are present and at least one approves before merging
triggers:
- on-merge

function:
- key: main
handler: index.prtrigger
- key: ensureReviewers
handler: index.ensureReviewers
- key: mergeCheckHandler
handler: index.mergeCheckHandler

permissions:
scopes:
- read:pullrequest:bitbucket
- write:pullrequest:bitbucket
- read:repository:bitbucket
- read:workspace:bitbucket



app:
<redacted>
runtime:
name: nodejs22.x





issue: Even after a reviewer approves, the merge is still being blocked




1 answer

0 votes
Theodora Boudale
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
August 12, 2025

Hi and welcome to the community!

I haven't been involved with Forge app development, so I'm afraid I can't offer much help. I just wanted to recommend reaching out to the 'Forge for Bitbucket Cloud' group (this group is specifically for Forge with Bitbucket Cloud):

After joining the group, you can create a discussion by using the Let's discuss option:

Screenshot 2025-08-12 at 12.17.03.png


Just a head up, I removed the forge logs and some IDs from the code, as they included private info (like UUIDs, account IDs, workspace IDs, etc). When posting logs publicly, because ensure that private details are replaced with dummy values.

Kind regards,
Theodora

Suggest an answer

Log in or Sign up to answer
DEPLOYMENT TYPE
CLOUD
PRODUCT PLAN
PREMIUM
TAGS
AUG Leaders

Atlassian Community Events