Skip to content
Draft
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
92 changes: 92 additions & 0 deletions .github/scripts/merge_conflict_helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// scripts/merge_conflict_helpers.js

const BOT_SIGNATURE = '[merge-conflict bot]';

module.exports = async ({ github, context, core }) => {
const { owner, repo } = context.repo;

// fetch PR with retry logic for unknown state
async function getPrWithRetry(prNumber) {
for (let i = 0; i < 10; i++) {
const { data: pr } = await github.rest.pulls.get({
owner, repo, pull_number: prNumber
});

if (pr.mergeable_state !== 'unknown') return pr;

console.log(`PR #${prNumber} state is 'unknown'. Retrying (${i+1}/10)...`);
await new Promise(r => setTimeout(r, 2000));
}
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
return pr;
}

// post comment
async function notifyUser(prNumber) {
const { data: comments } = await github.rest.issues.listComments({
owner, repo, issue_number: prNumber,
});

if (comments.some(c => c.body.includes(BOT_SIGNATURE))) {
console.log(`Already commented on PR #${prNumber}. Skipping.`);
return;
}

const body = `Hi, this is MergeConflictBot.\nYour pull request cannot be merged because it contains **merge conflicts**.\n\nPlease resolve these conflicts locally and push the changes.\n\nTo assist you, please read:\n- [Resolving Merge Conflicts](docs/sdk_developers/merge_conflicts.md)\n- [Rebasing Guide](docs/sdk_developers/rebasing.md)\n\nThank you for contributing!\nFrom the Hiero Python SDK Team\n\n${BOT_SIGNATURE}`;

await github.rest.issues.createComment({
owner, repo, issue_number: prNumber, body: body
});
}

//set commit status
async function setCommitStatus(sha, state, description) {
await github.rest.repos.createCommitStatus({
owner, repo, sha: sha, state: state,
context: 'Merge Conflict Detector',
description: description,
target_url: `${process.env.GITHUB_SERVER_URL}/${owner}/${repo}/actions/runs/${context.runId}`
});
}

//main
let prsToCheck = [];

//push to main
if (context.eventName === 'push') {
console.log("Triggered by Push to Main. Fetching all open PRs...");
const { data: openPrs } = await github.rest.pulls.list({
owner, repo, state: 'open', base: 'main'
});
prsToCheck = openPrs.map(pr => pr.number);
}
//PR update
else {
console.log("Triggered by PR update.");
prsToCheck.push(context.payload.pull_request.number);
}

let hasFailure = false;

for (const prNumber of prsToCheck) {
console.log(`Checking PR #${prNumber}...`);
const pr = await getPrWithRetry(prNumber);

if (pr.mergeable_state === 'dirty') {
console.log(`Conflict detected in PR #${prNumber}`);
await notifyUser(prNumber);

if (context.eventName === 'push') {
await setCommitStatus(pr.head.sha, 'failure', 'Conflicts detected with main');
} else {
core.setFailed(`Merge conflicts detected in PR #${prNumber}.`);
hasFailure = true;
}
} else {
console.log(`PR #${prNumber} is clean.`);
if (context.eventName === 'push') {
await setCommitStatus(pr.head.sha, 'success', 'No conflicts detected');
}
}
}
};
66 changes: 18 additions & 48 deletions .github/workflows/bot-merge-conflict.yml
Original file line number Diff line number Diff line change
@@ -1,71 +1,41 @@
name: PythonBot - Check Merge Conflicts
name: Merge Conflict Bot

on:
pull_request_target:
types: [opened, synchronize, reopened]
push:
branches:
- main

permissions:
contents: read
pull-requests: write
issues: write
statuses: write

concurrency:
group: "check-conflicts-${{ github.event.pull_request.number }}"
group: "check-conflicts-${{ github.event.pull_request.number || github.sha }}"
cancel-in-progress: true

jobs:
check-conflicts:
runs-on: ubuntu-latest

steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit

- name: Check for merge conflicts
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_NUMBER=${{ github.event.pull_request.number }}
REPO="${{ github.repository }}"

echo "Checking merge status for PR #$PR_NUMBER in repository $REPO..."

for i in {1..10}; do
PR_JSON=$(gh api repos/$REPO/pulls/$PR_NUMBER)
MERGEABLE_STATE=$(echo "$PR_JSON" | jq -r '.mergeable_state')

echo "Attempt $i: Current mergeable state: $MERGEABLE_STATE"

if [ "$MERGEABLE_STATE" != "unknown" ]; then
break
fi

echo "State is 'unknown', waiting 2 seconds..."
sleep 2
done
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

if [ "$MERGEABLE_STATE" = "dirty" ]; then
COMMENT=$(cat <<EOF
Hi, this is MergeConflictBot.
Your pull request cannot be merged because it contains **merge conflicts**.

Please resolve these conflicts locally and push the changes.

To assist you, please read:
- [Resolving Merge Conflicts](docs/sdk_developers/merge_conflicts.md)
- [Rebasing Guide](docs/sdk_developers/rebasing.md)

Thank you for contributing!

From the Hiero Python SDK Team
EOF
)
- name: Check for merge conflicts

gh pr view $PR_NUMBER --repo $REPO --json comments --jq '.comments[].body' | grep -F "MergeConflictBot" >/dev/null || \
(gh pr comment $PR_NUMBER --repo $REPO --body "$COMMENT" && echo "Comment added to PR #$PR_NUMBER")

exit 1
else
echo "No merge conflicts detected (State: $MERGEABLE_STATE)."
fi
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
with:
script: |
const path = require('path')
const scriptPath = path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge_conflict_helpers.js')
console.log(`Loading script from: ${scriptPath}`)
const script = require(scriptPath)
await script({github, context, core})
Loading