Skip to content

Commit c8d9163

Browse files
committed
feat: suggest force pushing to update pull requests
Add a callout tip to the bot's comment to guide new contributors away from closing pull requests to make corrections. Suggest modifying existing commits and updating the branch using git push --force (or git push --force-with-lease) instead. Signed-off-by: Josef Schlehofer <pepe.schlehofer@gmail.com>
1 parent 31112ca commit c8d9163

5 files changed

Lines changed: 129 additions & 0 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ Some configuration keys offer advanced options:
105105
* `check_patch_headers`: Can be `true` (default, hard error), `"warning"` (non-blocking), or `false` to disable.
106106
* `check_trailing_newline`: Can be `true` (default, hard error), `"warning"` (non-blocking), or `false` to disable.
107107
* `check_pkg_release`: Can be `"warning"`, `"error"`, or `false` to disable.
108+
* `show_force_push_tip`: Set to `true` (default) to append a helpful tip regarding how to correct validation errors using force-pushing. Set to `false` to disable.
108109
* `enable_stale_bot`: Set to `true` to enable the stale PR bot cleanup for this repository. Defaults to `false` (opt-in).
109110

110111
Here is a comprehensive example containing all available toggle options:
@@ -118,6 +119,7 @@ Here is a comprehensive example containing all available toggle options:
118119
"check_signature": true,
119120
"allow_autosquash": true,
120121
"enable_comments": true,
122+
"show_force_push_tip": true,
121123
"max_subject_len_soft": 60,
122124
"max_subject_len_hard": 80,
123125
"max_body_line_len": 100,

cloudflare-worker/src/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const DEFAULT_CONFIG = {
77
check_signature: true,
88
allow_autosquash: true,
99
enable_comments: true,
10+
show_force_push_tip: true,
1011
max_subject_len_soft: 60,
1112
max_subject_len_hard: 80,
1213
max_body_line_len: 100,

cloudflare-worker/src/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,11 @@ async function handleWebhook(request, env) {
613613
});
614614
}
615615

616+
if (!allPassed && CONFIG.show_force_push_tip) {
617+
commentBody += "> [!TIP]\n";
618+
commentBody += "> **Do not close this pull request** to make corrections. Instead, modify your existing commits (e.g. `git commit --amend`) and update the branch using `git push --force-with-lease --force-if-includes`. The checks will re-run automatically.\n\n";
619+
}
620+
616621
// Add feedback link & version footer
617622
let footerMd = `\n\n---\n<sub>Something broken? Consider [reporting an issue](https://github.com/openwrt/openwrt-bot-worker/issues/new).</sub>`;
618623

cloudflare-worker/test/index.test.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1786,4 +1786,124 @@ describe('Backport Cherry-pick and Bypass Validation', () => {
17861786
fetchMock = null;
17871787
}
17881788
});
1789+
1790+
test('includes or excludes force push TIP based on CONFIG.show_force_push_tip', async () => {
1791+
const originalImportKey = crypto.subtle.importKey;
1792+
crypto.subtle.importKey = async (format, keyData, algorithm, extractable, keyUsages) => {
1793+
if (algorithm.name === "RSASSA-PKCS1-v1_5") {
1794+
return { type: 'private', extractable: false, algorithm, usages: keyUsages };
1795+
}
1796+
return originalImportKey.call(crypto.subtle, format, keyData, algorithm, extractable, keyUsages);
1797+
};
1798+
const originalSign = crypto.subtle.sign;
1799+
crypto.subtle.sign = async (algorithm, key, data) => {
1800+
if (algorithm === "RSASSA-PKCS1-v1_5") {
1801+
return new ArrayBuffer(256);
1802+
}
1803+
return originalSign.call(crypto.subtle, algorithm, key, data);
1804+
};
1805+
1806+
const payload = JSON.stringify({
1807+
action: 'opened',
1808+
pull_request: {
1809+
user: { login: 'someuser', type: 'User' },
1810+
number: 123,
1811+
title: 'test pr',
1812+
base: { ref: 'main' },
1813+
head: { ref: 'feature', sha: 'headsha123' },
1814+
commits_url: 'https://api.github.com/repos/test/repo/pulls/123/commits',
1815+
labels: []
1816+
},
1817+
installation: { id: 456 },
1818+
repository: { full_name: 'test/repo' }
1819+
});
1820+
const secret = 'mysecret';
1821+
const signature = await calculateHmac(secret, payload);
1822+
1823+
const runTestWithConfig = async (config) => {
1824+
let postedComments = [];
1825+
fetchMock = async (url, options) => {
1826+
if (url.includes('/access_tokens')) {
1827+
return new Response(JSON.stringify({ token: 'mocktoken' }), { status: 200 });
1828+
}
1829+
if (url.includes('/formalities.json')) {
1830+
return new Response(JSON.stringify({
1831+
check_branch: false,
1832+
enable_comments: true,
1833+
require_linked_github_account: false,
1834+
require_body: false,
1835+
...config
1836+
}), { status: 200 });
1837+
}
1838+
if (url.includes('/labels')) {
1839+
return new Response(JSON.stringify([]), { status: 200 });
1840+
}
1841+
if (url.includes('/pulls/123/commits')) {
1842+
return new Response(JSON.stringify([
1843+
{
1844+
sha: 'sha123',
1845+
html_url: 'https://github.com/test/repo/commit/sha123',
1846+
commit: {
1847+
message: 'INVALID COMMIT MESSAGE',
1848+
author: { name: 'John Doe', email: 'john@doe.com' },
1849+
committer: { name: 'John Doe', email: 'john@doe.com' }
1850+
}
1851+
}
1852+
]), { status: 200 });
1853+
}
1854+
if (url.includes('/commits/sha123')) {
1855+
return new Response(JSON.stringify({
1856+
parents: [{ sha: 'parent-sha' }],
1857+
commit: {
1858+
message: 'INVALID COMMIT MESSAGE',
1859+
author: { name: 'John Doe', email: 'john@doe.com' },
1860+
committer: { name: 'John Doe', email: 'john@doe.com' }
1861+
}
1862+
}), { status: 200 });
1863+
}
1864+
if (url.endsWith('/pulls/123') && options && options.headers && options.headers.Accept === 'application/vnd.github.patch') {
1865+
return new Response('Mock patch', { status: 200 });
1866+
}
1867+
if (url.includes('/issues/123/comments')) {
1868+
if (options && (options.method === 'POST' || options.method === 'PATCH')) {
1869+
postedComments.push(JSON.parse(options.body).body);
1870+
}
1871+
return new Response(JSON.stringify([]), { status: 200 });
1872+
}
1873+
return new Response(JSON.stringify({}), { status: 200 });
1874+
};
1875+
1876+
const request = new Request('http://localhost/webhook', {
1877+
method: 'POST',
1878+
body: payload,
1879+
headers: {
1880+
'x-hub-signature-256': signature,
1881+
'x-github-event': 'pull_request'
1882+
}
1883+
});
1884+
await worker.fetch(request, {
1885+
WEBHOOK_SECRET: secret,
1886+
APP_ID: '12345',
1887+
PRIVATE_KEY: 'YW55Y29udGVudA=='
1888+
}, {});
1889+
1890+
return postedComments;
1891+
};
1892+
1893+
try {
1894+
// 1. With show_force_push_tip enabled (default behavior)
1895+
const commentsWithTip = await runTestWithConfig({ show_force_push_tip: true });
1896+
assert.ok(commentsWithTip.length > 0);
1897+
assert.ok(commentsWithTip[0].includes('git push --force-with-lease --force-if-includes'));
1898+
1899+
// 2. With show_force_push_tip disabled
1900+
const commentsWithoutTip = await runTestWithConfig({ show_force_push_tip: false });
1901+
assert.ok(commentsWithoutTip.length > 0);
1902+
assert.ok(!commentsWithoutTip[0].includes('git push --force-with-lease'));
1903+
} finally {
1904+
crypto.subtle.importKey = originalImportKey;
1905+
crypto.subtle.sign = originalSign;
1906+
fetchMock = null;
1907+
}
1908+
});
17891909
});

cloudflare-worker/test/validators.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const CONFIG = {
1111
check_signature: true,
1212
allow_autosquash: true,
1313
enable_comments: true,
14+
show_force_push_tip: true,
1415
max_subject_len_soft: 60,
1516
max_subject_len_hard: 80,
1617
max_body_line_len: 100,

0 commit comments

Comments
 (0)