1- name : PythonBot - Check Merge Conflicts
1+ name : Merge Conflict Bot
22
33on :
44 pull_request_target :
1010permissions :
1111 contents : read
1212 pull-requests : write
13+ issues : write
14+ statuses : write
1315
1416concurrency :
15- # Use PR number if available, otherwise use the Commit SHA
1617 group : " check-conflicts-${{ github.event.pull_request.number || github.sha }}"
1718 cancel-in-progress : true
1819
@@ -25,63 +26,106 @@ jobs:
2526 uses : step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
2627 with :
2728 egress-policy : audit
28-
29+
2930 - name : Check for merge conflicts
30- env :
31- GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
32- run : |
33- REPO="${{ github.repository }}"
34- # 1. Determine which PRs to check
35- if [ "${{ github.event_name }}" == "push" ]; then
36- echo "Triggered by push to main. Fetching all open PRs..."
37- PR_NUMBERS=$(gh pr list --repo $REPO --state open --json number --jq '.[].number')
38- else
39- echo "Triggered by PR update."
40- PR_NUMBERS="${{ github.event.pull_request.number }}"
41- fi
42-
43- # 2. Loop through the list (works for 1 PR or 100 PRs)
44- for PR_NUMBER in $PR_NUMBERS; do
45- echo "---------------------------------------------------"
46- echo "Checking merge status for PR #$PR_NUMBER in repository $REPO..."
47-
48-
49- for i in {1..10}; do
50- PR_JSON=$(gh api repos/$REPO/pulls/$PR_NUMBER)
51- MERGEABLE_STATE=$(echo "$PR_JSON" | jq -r '.mergeable_state')
52-
53- echo "Attempt $i: Current mergeable state: $MERGEABLE_STATE"
54-
55- if [ "$MERGEABLE_STATE" != "unknown" ]; then
56- break
57- fi
58-
59- echo "State is 'unknown', waiting 2 seconds..."
60- sleep 2
61- done
62-
63- if [ "$MERGEABLE_STATE" = "dirty" ]; then
64- COMMENT=$(cat <<EOF
65- Hi, this is MergeConflictBot.
66- Your pull request cannot be merged because it contains **merge conflicts**.
67-
68- Please resolve these conflicts locally and push the changes.
31+ uses : actions/github-script@v7
32+ with :
33+ script : |
34+ const { owner, repo } = context.repo;
35+ const BOT_SIGNATURE = '';
6936
70- To assist you, please read:
71- - [Resolving Merge Conflicts](docs/sdk_developers/merge_conflicts.md)
72- - [Rebasing Guide](docs/sdk_developers/rebasing.md)
37+ // --- HELPERS ---
38+
39+ // 1. Fetch PR with retry logic for "unknown" state
40+ async function getPrWithRetry(prNumber) {
41+ for (let i = 0; i < 10; i++) {
42+ const { data: pr } = await github.rest.pulls.get({
43+ owner, repo, pull_number: prNumber
44+ });
45+
46+ if (pr.mergeable_state !== 'unknown') return pr;
47+
48+ console.log(`PR #${prNumber} state is 'unknown'. Retrying (${i+1}/10)...`);
49+ await new Promise(r => setTimeout(r, 2000)); // Sleep 2s
50+ }
51+ // If still unknown after 20s, return the last state
52+ const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
53+ return pr;
54+ }
7355
74- Thank you for contributing!
56+ // Post Comment
57+ async function notifyUser(prNumber) {
58+ const { data: comments } = await github.rest.issues.listComments({
59+ owner, repo, issue_number: prNumber,
60+ });
7561
76- From the Hiero Python SDK Team
77- EOF
78- )
62+ // Check if we already commented
63+ if (comments.some(c => c.body.includes(BOT_SIGNATURE))) {
64+ console.log(`Already commented on PR #${prNumber}. Skipping.`);
65+ return;
66+ }
7967
80- gh pr view $PR_NUMBER --repo $REPO --json comments --jq '.comments[].body' | grep -F "MergeConflictBot" >/dev/null || \
81- (gh pr comment $PR_NUMBER --repo $REPO --body "$COMMENT" && echo "Comment added to PR #$PR_NUMBER")
68+ 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}`;
8269
83- # REMOVED 'exit 1' here so the loop continues!
84- else
85- echo "No merge conflicts detected for PR #$PR_NUMBER (State: $MERGEABLE_STATE)."
86- fi
87- done
70+ await github.rest.issues.createComment({
71+ owner, repo, issue_number: prNumber, body: body
72+ });
73+ }
74+
75+ // Set Commit Status (Status API)
76+ async function setCommitStatus(sha, state, description) {
77+ await github.rest.repos.createCommitStatus({
78+ owner, repo, sha: sha, state: state,
79+ context: 'Merge Conflict Detector',
80+ description: description,
81+ target_url: `${process.env.GITHUB_SERVER_URL}/${owner}/${repo}/actions/runs/${context.runId}`
82+ });
83+ }
84+
85+ // --- MAIN LOGIC ---
86+
87+ let prsToCheck = [];
88+
89+ // Scenario A: Push to Main (Check all open PRs)
90+ if (context.eventName === 'push') {
91+ console.log("Triggered by Push to Main. Fetching all open PRs...");
92+ const { data: openPrs } = await github.rest.pulls.list({
93+ owner, repo, state: 'open', base: 'main'
94+ });
95+ prsToCheck = openPrs.map(pr => pr.number);
96+ }
97+ // Scenario B: PR Update (Check single PR)
98+ else {
99+ console.log("Triggered by PR update.");
100+ prsToCheck.push(context.payload.pull_request.number);
101+ }
102+
103+ let hasFailure = false;
104+
105+ for (const prNumber of prsToCheck) {
106+ console.log(`Checking PR #${prNumber}...`);
107+ const pr = await getPrWithRetry(prNumber);
108+
109+ if (pr.mergeable_state === 'dirty') {
110+ console.log(`Conflict detected in PR #${prNumber}`);
111+
112+ // Notify without spam
113+ await notifyUser(prNumber);
114+
115+ // Handle Failure based on Event
116+ if (context.eventName === 'push') {
117+ //Don't fail workflow, set Status API on the commit instead
118+ await setCommitStatus(pr.head.sha, 'failure', 'Conflicts detected with main');
119+ } else {
120+ //Fail the workflow run so PR gets a Red X immediately
121+ core.setFailed(`Merge conflicts detected in PR #${prNumber}.`);
122+ hasFailure = true;
123+ }
124+ } else {
125+ console.log(`PR #${prNumber} is clean.`);
126+ // Clean up: If we are on main, ensure we turn the status green if it was red
127+ if (context.eventName === 'push') {
128+ await setCommitStatus(pr.head.sha, 'success', 'No conflicts detected');
129+ }
130+ }
131+ }
0 commit comments