Skip to content

Commit e5b48dc

Browse files
committed
[ci] Add ghstack /land bot
Summary: Test Plan: Reviewers: Subscribers: Tasks: Tags:
1 parent 3366146 commit e5b48dc

File tree

4 files changed

+466
-0
lines changed

4 files changed

+466
-0
lines changed
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
#!/usr/bin/env node
2+
// JS rewrite of https://github.com/Chillee/ghstack_land_example/blob/main/.github/workflows/scripts/ghstack-perm-check.py
3+
'use strict';
4+
5+
const {spawnSync} = require('child_process');
6+
const process = require('process');
7+
const {Octokit} = require('@octokit/rest');
8+
9+
const OWNER = 'facebook';
10+
const REPO = 'react';
11+
12+
async function must(cond, msg, octokit, issue_number) {
13+
if (!cond) {
14+
console.error(msg);
15+
try {
16+
await octokit.issues.createComment({
17+
owner: OWNER,
18+
repo: REPO,
19+
issue_number,
20+
body: `ghstack bot failed: ${msg}`,
21+
});
22+
} catch (error) {
23+
console.error('Failed to post comment:', error);
24+
}
25+
process.exit(1);
26+
}
27+
}
28+
29+
async function main() {
30+
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
31+
if (!GITHUB_TOKEN) {
32+
console.error('GITHUB_TOKEN environment variable is not set.');
33+
process.exit(1);
34+
}
35+
36+
const octokit = new Octokit({auth: GITHUB_TOKEN});
37+
const prNumber = parseInt(process.argv[2]);
38+
const headRef = process.argv[3];
39+
40+
console.log(headRef);
41+
await must(
42+
headRef && /^gh\/[A-Za-z0-9-]+\/[0-9]+\/head$/.test(headRef),
43+
'Not a ghstack PR',
44+
octokit,
45+
OWNER,
46+
REPO,
47+
prNumber
48+
);
49+
50+
const origRef = headRef.replace('/head', '/orig');
51+
52+
console.log(':: Fetching newest main...');
53+
let result = spawnSync('git', ['fetch', 'origin', 'main'], {
54+
stdio: 'inherit',
55+
});
56+
await must(
57+
result.status === 0,
58+
"Can't fetch main",
59+
octokit,
60+
OWNER,
61+
REPO,
62+
prNumber
63+
);
64+
65+
console.log(':: Fetching orig branch...');
66+
result = spawnSync('git', ['fetch', 'origin', origRef], {stdio: 'inherit'});
67+
await must(
68+
result.status === 0,
69+
"Can't fetch orig branch",
70+
octokit,
71+
OWNER,
72+
REPO,
73+
prNumber
74+
);
75+
76+
result = spawnSync(
77+
'git',
78+
['log', 'FETCH_HEAD...$(git merge-base FETCH_HEAD origin/main)'],
79+
{shell: true}
80+
);
81+
const out = result.stdout.toString();
82+
await must(
83+
result.status === 0,
84+
'`git log` command failed!',
85+
octokit,
86+
OWNER,
87+
REPO,
88+
prNumber
89+
);
90+
91+
const regex =
92+
/Pull Request resolved: https:\/\/github\.com\/.*?\/pull\/([0-9]+)/g;
93+
const prNumbers = [];
94+
let match;
95+
while ((match = regex.exec(out)) !== null) {
96+
prNumbers.push(parseInt(match[1], 10));
97+
}
98+
console.log(prNumbers);
99+
await must(
100+
prNumbers.length && prNumbers[0] === prNumber,
101+
'Extracted PR numbers not seems right!',
102+
octokit,
103+
OWNER,
104+
REPO,
105+
prNumber
106+
);
107+
108+
for (const n of prNumbers) {
109+
process.stdout.write(`:: Checking PR status #${n}... `);
110+
111+
let prObj;
112+
try {
113+
const {data} = await octokit.pulls.get({
114+
owner: OWNER,
115+
repo: REPO,
116+
pull_number: n,
117+
});
118+
prObj = data;
119+
} catch (error) {
120+
await must(
121+
false,
122+
'Error Getting PR Object!',
123+
octokit,
124+
OWNER,
125+
REPO,
126+
prNumber
127+
);
128+
}
129+
130+
let reviews;
131+
try {
132+
const {data} = await octokit.request(
133+
'GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews',
134+
{
135+
owner: OWNER,
136+
repo: REPO,
137+
pull_number: prNumber,
138+
headers: {
139+
'X-GitHub-Api-Version': '2022-11-28',
140+
},
141+
}
142+
);
143+
reviews = data;
144+
} catch (error) {
145+
await must(
146+
false,
147+
'Error Getting PR Reviews!',
148+
octokit,
149+
OWNER,
150+
REPO,
151+
prNumber
152+
);
153+
}
154+
155+
let approved = false;
156+
for (const review of reviews) {
157+
if (review.state === 'COMMENTED') continue;
158+
159+
await must(
160+
['APPROVED', 'DISMISSED'].includes(review.state),
161+
`@${review.user.login} has stamped PR #${n} \`${review.state}\`, please resolve it first!`,
162+
octokit,
163+
OWNER,
164+
REPO,
165+
prNumber
166+
);
167+
if (review.state === 'APPROVED') {
168+
approved = true;
169+
}
170+
}
171+
await must(
172+
approved,
173+
`PR #${n} is not approved yet!`,
174+
octokit,
175+
OWNER,
176+
REPO,
177+
prNumber
178+
);
179+
180+
let checkruns;
181+
try {
182+
const {data} = await octokit.checks.listForRef({
183+
owner: OWNER,
184+
repo: REPO,
185+
ref: prObj.head.sha,
186+
});
187+
checkruns = data;
188+
} catch (error) {
189+
await must(
190+
false,
191+
'Error getting check runs status!',
192+
octokit,
193+
OWNER,
194+
REPO,
195+
prNumber
196+
);
197+
}
198+
199+
for (const cr of checkruns.check_runs) {
200+
const status = cr.conclusion ? cr.conclusion : cr.status;
201+
const name = cr.name;
202+
if (name === 'Copilot for PRs') continue;
203+
await must(
204+
['success', 'neutral'].includes(status),
205+
`PR #${n} check-run \`${name}\`'s status \`${status}\` is not success!`,
206+
octokit,
207+
OWNER,
208+
REPO,
209+
prNumber
210+
);
211+
}
212+
console.log('SUCCESS!');
213+
}
214+
215+
console.log(':: All PRs are ready to be landed!');
216+
}
217+
218+
main().catch(err => {
219+
console.error('Unexpected error:', err);
220+
process.exit(1);
221+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "ghstack-perm-check",
3+
"version": "0.0.0",
4+
"private": true,
5+
"scripts": {
6+
"check-permissions": "node ./check_permissions.js"
7+
},
8+
"license": "MIT",
9+
"dependencies": {
10+
"@octokit/rest": "^21.1.1"
11+
}
12+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2+
# yarn lockfile v1
3+
4+
5+
"@octokit/auth-token@^5.0.0":
6+
version "5.1.2"
7+
resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-5.1.2.tgz#68a486714d7a7fd1df56cb9bc89a860a0de866de"
8+
integrity sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==
9+
10+
"@octokit/core@^6.1.4":
11+
version "6.1.4"
12+
resolved "https://registry.yarnpkg.com/@octokit/core/-/core-6.1.4.tgz#f5ccf911cc95b1ce9daf6de425d1664392f867db"
13+
integrity sha512-lAS9k7d6I0MPN+gb9bKDt7X8SdxknYqAMh44S5L+lNqIN2NuV8nvv3g8rPp7MuRxcOpxpUIATWprO0C34a8Qmg==
14+
dependencies:
15+
"@octokit/auth-token" "^5.0.0"
16+
"@octokit/graphql" "^8.1.2"
17+
"@octokit/request" "^9.2.1"
18+
"@octokit/request-error" "^6.1.7"
19+
"@octokit/types" "^13.6.2"
20+
before-after-hook "^3.0.2"
21+
universal-user-agent "^7.0.0"
22+
23+
"@octokit/endpoint@^10.1.3":
24+
version "10.1.3"
25+
resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-10.1.3.tgz#bfe8ff2ec213eb4216065e77654bfbba0fc6d4de"
26+
integrity sha512-nBRBMpKPhQUxCsQQeW+rCJ/OPSMcj3g0nfHn01zGYZXuNDvvXudF/TYY6APj5THlurerpFN4a/dQAIAaM6BYhA==
27+
dependencies:
28+
"@octokit/types" "^13.6.2"
29+
universal-user-agent "^7.0.2"
30+
31+
"@octokit/graphql@^8.1.2":
32+
version "8.2.1"
33+
resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-8.2.1.tgz#0cb83600e6b4009805acc1c56ae8e07e6c991b78"
34+
integrity sha512-n57hXtOoHrhwTWdvhVkdJHdhTv0JstjDbDRhJfwIRNfFqmSo1DaK/mD2syoNUoLCyqSjBpGAKOG0BuwF392slw==
35+
dependencies:
36+
"@octokit/request" "^9.2.2"
37+
"@octokit/types" "^13.8.0"
38+
universal-user-agent "^7.0.0"
39+
40+
"@octokit/openapi-types@^24.2.0":
41+
version "24.2.0"
42+
resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-24.2.0.tgz#3d55c32eac0d38da1a7083a9c3b0cca77924f7d3"
43+
integrity sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==
44+
45+
"@octokit/plugin-paginate-rest@^11.4.2":
46+
version "11.6.0"
47+
resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.6.0.tgz#e5e9ff3530e867c3837fdbff94ce15a2468a1f37"
48+
integrity sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==
49+
dependencies:
50+
"@octokit/types" "^13.10.0"
51+
52+
"@octokit/plugin-request-log@^5.3.1":
53+
version "5.3.1"
54+
resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz#ccb75d9705de769b2aa82bcd105cc96eb0c00f69"
55+
integrity sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==
56+
57+
"@octokit/plugin-rest-endpoint-methods@^13.3.0":
58+
version "13.5.0"
59+
resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.5.0.tgz#d8c8ca2123b305596c959a9134dfa8b0495b0ba6"
60+
integrity sha512-9Pas60Iv9ejO3WlAX3maE1+38c5nqbJXV5GrncEfkndIpZrJ/WPMRd2xYDcPPEt5yzpxcjw9fWNoPhsSGzqKqw==
61+
dependencies:
62+
"@octokit/types" "^13.10.0"
63+
64+
"@octokit/request-error@^6.1.7":
65+
version "6.1.7"
66+
resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-6.1.7.tgz#44fc598f5cdf4593e0e58b5155fe2e77230ff6da"
67+
integrity sha512-69NIppAwaauwZv6aOzb+VVLwt+0havz9GT5YplkeJv7fG7a40qpLt/yZKyiDxAhgz0EtgNdNcb96Z0u+Zyuy2g==
68+
dependencies:
69+
"@octokit/types" "^13.6.2"
70+
71+
"@octokit/request@^9.2.1", "@octokit/request@^9.2.2":
72+
version "9.2.2"
73+
resolved "https://registry.yarnpkg.com/@octokit/request/-/request-9.2.2.tgz#754452ec4692d7fdc32438a14e028eba0e6b2c09"
74+
integrity sha512-dZl0ZHx6gOQGcffgm1/Sf6JfEpmh34v3Af2Uci02vzUYz6qEN6zepoRtmybWXIGXFIK8K9ylE3b+duCWqhArtg==
75+
dependencies:
76+
"@octokit/endpoint" "^10.1.3"
77+
"@octokit/request-error" "^6.1.7"
78+
"@octokit/types" "^13.6.2"
79+
fast-content-type-parse "^2.0.0"
80+
universal-user-agent "^7.0.2"
81+
82+
"@octokit/rest@^21.1.1":
83+
version "21.1.1"
84+
resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-21.1.1.tgz#7a70455ca451b1d253e5b706f35178ceefb74de2"
85+
integrity sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg==
86+
dependencies:
87+
"@octokit/core" "^6.1.4"
88+
"@octokit/plugin-paginate-rest" "^11.4.2"
89+
"@octokit/plugin-request-log" "^5.3.1"
90+
"@octokit/plugin-rest-endpoint-methods" "^13.3.0"
91+
92+
"@octokit/types@^13.10.0", "@octokit/types@^13.6.2", "@octokit/types@^13.8.0":
93+
version "13.10.0"
94+
resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.10.0.tgz#3e7c6b19c0236c270656e4ea666148c2b51fd1a3"
95+
integrity sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==
96+
dependencies:
97+
"@octokit/openapi-types" "^24.2.0"
98+
99+
before-after-hook@^3.0.2:
100+
version "3.0.2"
101+
resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-3.0.2.tgz#d5665a5fa8b62294a5aa0a499f933f4a1016195d"
102+
integrity sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==
103+
104+
fast-content-type-parse@^2.0.0:
105+
version "2.0.1"
106+
resolved "https://registry.yarnpkg.com/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz#c236124534ee2cb427c8d8e5ba35a4856947847b"
107+
integrity sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==
108+
109+
universal-user-agent@^7.0.0, universal-user-agent@^7.0.2:
110+
version "7.0.2"
111+
resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-7.0.2.tgz#52e7d0e9b3dc4df06cc33cb2b9fd79041a54827e"
112+
integrity sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==

0 commit comments

Comments
 (0)