Skip to content

Commit 5cf8553

Browse files
authored
ci: update merge conflict bot to trigger on main (#1247)
Signed-off-by: cheese-cakee <[email protected]>
1 parent 89ac599 commit 5cf8553

File tree

3 files changed

+163
-52
lines changed

3 files changed

+163
-52
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// .github/scripts/bot-merge-conflict.js
2+
3+
const BOT_SIGNATURE = '[MergeConflictBotSignature-v1]';
4+
5+
module.exports = async ({ github, context, core }) => {
6+
const { owner, repo } = context.repo;
7+
8+
// Validate event context
9+
if (context.eventName !== 'push' && context.eventName !== 'pull_request_target') {
10+
console.log(`Unsupported event type: ${context.eventName}. Skipping.`);
11+
return;
12+
}
13+
14+
// Check for dry-run mode
15+
const dryRun = process.env.DRY_RUN === 'true';
16+
17+
// Fetch PR with retry logic for unknown state
18+
async function getPrWithRetry(prNumber) {
19+
for (let i = 0; i < 10; i++) {
20+
const { data: pr } = await github.rest.pulls.get({
21+
owner, repo, pull_number: prNumber
22+
});
23+
24+
if (pr.mergeable_state !== 'unknown') return pr;
25+
26+
console.log(`PR #${prNumber} state is 'unknown'. Retrying (${i+1}/10)...`);
27+
await new Promise(r => setTimeout(r, 2000));
28+
}
29+
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
30+
if (pr.mergeable_state === 'unknown') {
31+
console.warn(`PR #${prNumber} state still 'unknown' after 10 retries.`);
32+
}
33+
return pr;
34+
}
35+
36+
// Post comment
37+
async function notifyUser(prNumber) {
38+
const { data: comments } = await github.rest.issues.listComments({
39+
owner, repo, issue_number: prNumber,
40+
});
41+
42+
if (comments.some(c => c.body.includes(BOT_SIGNATURE))) {
43+
console.log(`Already commented on PR #${prNumber}. Skipping.`);
44+
return;
45+
}
46+
47+
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](https://github.com/${owner}/${repo}/blob/main/docs/sdk_developers/merge_conflicts.md)\n- [Rebasing Guide](https://github.com/${owner}/${repo}/blob/main/docs/sdk_developers/rebasing.md)\n\nThank you for contributing!\n<!-- \nFrom the Hiero Python SDK Team\n${BOT_SIGNATURE} -->`;
48+
49+
if (dryRun) {
50+
console.log(`[DRY RUN] Would post comment to PR #${prNumber}: ${body}`);
51+
return;
52+
}
53+
54+
await github.rest.issues.createComment({
55+
owner, repo, issue_number: prNumber, body: body
56+
});
57+
}
58+
59+
// Set commit status
60+
async function setCommitStatus(sha, state, description) {
61+
if (dryRun) {
62+
console.log(`[DRY RUN] Would set status for ${sha}: ${state} - ${description}`);
63+
return;
64+
}
65+
66+
await github.rest.repos.createCommitStatus({
67+
owner, repo, sha: sha, state: state,
68+
context: 'Merge Conflict Detector',
69+
description: description,
70+
target_url: `${process.env.GITHUB_SERVER_URL}/${owner}/${repo}/actions/runs/${context.runId}`
71+
});
72+
}
73+
74+
// Main
75+
let prsToCheck = [];
76+
77+
// Push to main
78+
if (context.eventName === 'push') {
79+
console.log("Triggered by Push to Main. Fetching all open PRs...");
80+
const openPrs = await github.paginate(github.rest.pulls.list, {
81+
owner, repo, state: 'open', base: 'main', per_page: 100
82+
});
83+
prsToCheck = openPrs.map(pr => pr.number);
84+
}
85+
// PR update
86+
else {
87+
console.log("Triggered by PR update.");
88+
if (!context.payload.pull_request?.number) {
89+
core.setFailed('Missing pull_request data in event payload');
90+
return;
91+
}
92+
prsToCheck.push(context.payload.pull_request.number);
93+
}
94+
95+
for (const prNumber of prsToCheck) {
96+
try {
97+
console.log(`Checking PR #${prNumber}...`);
98+
const pr = await getPrWithRetry(prNumber);
99+
100+
if (pr.mergeable_state === 'unknown') {
101+
console.log(`PR #${prNumber} state is still 'unknown'. Skipping conflict check.`);
102+
continue;
103+
}
104+
105+
if (pr.mergeable_state === 'dirty') {
106+
console.log(`Conflict detected in PR #${prNumber}`);
107+
await notifyUser(prNumber);
108+
109+
// Push events: set commit status on PR head SHA
110+
// PR events: fail the workflow run (creates a check on the PR)
111+
if (context.eventName === 'push') {
112+
await setCommitStatus(pr.head.sha, 'failure', 'Conflicts detected with main');
113+
} else {
114+
core.setFailed(`Merge conflicts detected in PR #${prNumber}.`);
115+
}
116+
} else {
117+
console.log(`PR #${prNumber} is clean.`);
118+
// For push events, set success status; PR events rely on workflow run success
119+
if (context.eventName === 'push') {
120+
await setCommitStatus(pr.head.sha, 'success', 'No conflicts detected');
121+
}
122+
}
123+
} catch (error) {
124+
console.error(`Error checking PR #${prNumber}: ${error.message}`);
125+
if (context.eventName !== 'push') {
126+
throw error; // Re-throw for PR events to fail the workflow
127+
}
128+
// For push events, log and continue to check remaining PRs
129+
}
130+
}
131+
};
Lines changed: 30 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,50 @@
1-
name: PythonBot - Check Merge Conflicts
1+
name: Merge Conflict Bot
22

33
on:
44
pull_request_target:
55
types: [opened, synchronize, reopened]
6+
push:
7+
branches:
8+
- main
9+
workflow_dispatch:
10+
inputs:
11+
dry_run:
12+
description: 'Run in dry-run mode (no comments or status updates)'
13+
type: boolean
14+
default: true
615

716
permissions:
817
contents: read
918
pull-requests: write
19+
issues: write
20+
statuses: write
1021

1122
concurrency:
12-
group: "check-conflicts-${{ github.event.pull_request.number }}"
23+
group: "check-conflicts-${{ github.event.pull_request.number || github.sha }}"
1324
cancel-in-progress: true
1425

1526
jobs:
1627
check-conflicts:
1728
runs-on: ubuntu-latest
18-
1929
steps:
20-
- name: Harden the runner (Audit all outbound calls)
21-
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
30+
- name: Checkout code
31+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
32+
with:
33+
ref: ${{ github.event.repository.default_branch }}
34+
35+
- name: Harden the runner
36+
uses: step-security/harden-runner@20cf3052978e1b6646b35198a5d69ed51a6c9d71 # v2.14.0
2237
with:
2338
egress-policy: audit
24-
39+
2540
- name: Check for merge conflicts
41+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
2642
env:
27-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
28-
run: |
29-
PR_NUMBER=${{ github.event.pull_request.number }}
30-
REPO="${{ github.repository }}"
31-
32-
echo "Checking merge status for PR #$PR_NUMBER in repository $REPO..."
33-
34-
for i in {1..10}; do
35-
PR_JSON=$(gh api repos/$REPO/pulls/$PR_NUMBER)
36-
MERGEABLE_STATE=$(echo "$PR_JSON" | jq -r '.mergeable_state')
37-
38-
echo "Attempt $i: Current mergeable state: $MERGEABLE_STATE"
39-
40-
if [ "$MERGEABLE_STATE" != "unknown" ]; then
41-
break
42-
fi
43-
44-
echo "State is 'unknown', waiting 2 seconds..."
45-
sleep 2
46-
done
47-
48-
if [ "$MERGEABLE_STATE" = "dirty" ]; then
49-
COMMENT=$(cat <<EOF
50-
Hi, this is MergeConflictBot.
51-
Your pull request cannot be merged because it contains **merge conflicts**.
52-
53-
Please resolve these conflicts locally and push the changes.
54-
55-
To assist you, please read:
56-
- [Resolving Merge Conflicts](docs/sdk_developers/merge_conflicts.md)
57-
- [Rebasing Guide](docs/sdk_developers/rebasing.md)
58-
59-
Thank you for contributing!
60-
61-
From the Hiero Python SDK Team
62-
EOF
63-
)
64-
65-
gh pr view $PR_NUMBER --repo $REPO --json comments --jq '.comments[].body' | grep -F "MergeConflictBot" >/dev/null || \
66-
(gh pr comment $PR_NUMBER --repo $REPO --body "$COMMENT" && echo "Comment added to PR #$PR_NUMBER")
67-
68-
exit 1
69-
else
70-
echo "No merge conflicts detected (State: $MERGEABLE_STATE)."
71-
fi
43+
DRY_RUN: ${{ inputs.dry_run || 'false' }}
44+
with:
45+
script: |
46+
const path = require('path')
47+
const scriptPath = path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/bot-merge-conflict.js')
48+
console.log(`Loading script from: ${scriptPath}`)
49+
const script = require(scriptPath)
50+
await script({github, context, core})

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Changelog
1+
# Changelog
22

33
All notable changes to this project will be documented in this file.
44
This project adheres to [Semantic Versioning](https://semver.org).
@@ -71,6 +71,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
7171
- Added support for include duplicates in get transaction receipt query (#1166)
7272
- Added `.github/workflows/cron-check-broken-links.yml` workflow to perform scheduled monthly Markdown link validation across the entire repository with automatic issue creation for broken links ([#1210](https://github.com/hiero-ledger/hiero-sdk-python/issues/1210))
7373
- Coderabbit prompt for .github
74+
- Added merge conflict bot workflow (`.github/workflows/bot-merge-conflict.yml`) and helper script (`.github/scripts/bot-merge-conflict.js`) to detect and notify about PR merge conflicts, with retry logic for unknown mergeable states, idempotent commenting, and push-to-main recheck logic (#1247)
7475

7576
### Changed
7677
- Bumped requests from 2.32.3 to 2.32.4 to 2.32.5

0 commit comments

Comments
 (0)