Skip to content

[ci] Add ghstack /land bot #32829

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
221 changes: 221 additions & 0 deletions .github/workflows/scripts/ghstack/check_permissions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
#!/usr/bin/env node
// JS rewrite of https://github.com/Chillee/ghstack_land_example/blob/main/.github/workflows/scripts/ghstack-perm-check.py
'use strict';

const {spawnSync} = require('child_process');
const process = require('process');
const {Octokit} = require('@octokit/rest');

const OWNER = 'facebook';
const REPO = 'react';

async function must(cond, msg, octokit, issue_number) {
if (!cond) {
console.error(msg);
try {
await octokit.issues.createComment({
owner: OWNER,
repo: REPO,
issue_number,
body: `ghstack bot failed: ${msg}`,
});
} catch (error) {
console.error('Failed to post comment:', error);
}
process.exit(1);
}
}

async function main() {
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
if (!GITHUB_TOKEN) {
console.error('GITHUB_TOKEN environment variable is not set.');
process.exit(1);
}

const octokit = new Octokit({auth: GITHUB_TOKEN});
const prNumber = parseInt(process.argv[2]);
const headRef = process.argv[3];

console.log(headRef);
await must(
headRef && /^gh\/[A-Za-z0-9-]+\/[0-9]+\/head$/.test(headRef),
'Not a ghstack PR',
octokit,
OWNER,
REPO,
prNumber
);

const origRef = headRef.replace('/head', '/orig');

console.log(':: Fetching newest main...');
let result = spawnSync('git', ['fetch', 'origin', 'main'], {
stdio: 'inherit',
});
await must(
result.status === 0,
"Can't fetch main",
octokit,
OWNER,
REPO,
prNumber
);

console.log(':: Fetching orig branch...');
result = spawnSync('git', ['fetch', 'origin', origRef], {stdio: 'inherit'});
await must(
result.status === 0,
"Can't fetch orig branch",
octokit,
OWNER,
REPO,
prNumber
);

result = spawnSync(
'git',
['log', 'FETCH_HEAD...$(git merge-base FETCH_HEAD origin/main)'],
{shell: true}
);
const out = result.stdout.toString();
await must(
result.status === 0,
'`git log` command failed!',
octokit,
OWNER,
REPO,
prNumber
);

const regex =
/Pull Request resolved: https:\/\/github\.com\/.*?\/pull\/([0-9]+)/g;
const prNumbers = [];
let match;
while ((match = regex.exec(out)) !== null) {
prNumbers.push(parseInt(match[1], 10));
}
console.log(prNumbers);
await must(
prNumbers.length && prNumbers[0] === prNumber,
'Extracted PR numbers not seems right!',
octokit,
OWNER,
REPO,
prNumber
);

for (const n of prNumbers) {
process.stdout.write(`:: Checking PR status #${n}... `);

let prObj;
try {
const {data} = await octokit.pulls.get({
owner: OWNER,
repo: REPO,
pull_number: n,
});
prObj = data;
} catch (error) {
await must(
false,
'Error Getting PR Object!',
octokit,
OWNER,
REPO,
prNumber
);
}

let reviews;
try {
const {data} = await octokit.request(
'GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews',
{
owner: OWNER,
repo: REPO,
pull_number: prNumber,
headers: {
'X-GitHub-Api-Version': '2022-11-28',
},
}
);
reviews = data;
} catch (error) {
await must(
false,
'Error Getting PR Reviews!',
octokit,
OWNER,
REPO,
prNumber
);
}

let approved = false;
for (const review of reviews) {
if (review.state === 'COMMENTED') continue;

await must(
['APPROVED', 'DISMISSED'].includes(review.state),
`@${review.user.login} has stamped PR #${n} \`${review.state}\`, please resolve it first!`,
octokit,
OWNER,
REPO,
prNumber
);
if (review.state === 'APPROVED') {
approved = true;
}
}
await must(
approved,
`PR #${n} is not approved yet!`,
octokit,
OWNER,
REPO,
prNumber
);

let checkruns;
try {
const {data} = await octokit.checks.listForRef({
owner: OWNER,
repo: REPO,
ref: prObj.head.sha,
});
checkruns = data;
} catch (error) {
await must(
false,
'Error getting check runs status!',
octokit,
OWNER,
REPO,
prNumber
);
}

for (const cr of checkruns.check_runs) {
const status = cr.conclusion ? cr.conclusion : cr.status;
const name = cr.name;
if (name === 'Copilot for PRs') continue;
await must(
['success', 'neutral'].includes(status),
`PR #${n} check-run \`${name}\`'s status \`${status}\` is not success!`,
octokit,
OWNER,
REPO,
prNumber
);
}
console.log('SUCCESS!');
}

console.log(':: All PRs are ready to be landed!');
}

main().catch(err => {
console.error('Unexpected error:', err);
process.exit(1);
});
12 changes: 12 additions & 0 deletions .github/workflows/scripts/ghstack/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "ghstack-perm-check",
"version": "0.0.0",
"private": true,
"scripts": {
"check-permissions": "node ./check_permissions.js"
},
"license": "MIT",
"dependencies": {
"@octokit/rest": "^21.1.1"
}
}
112 changes: 112 additions & 0 deletions .github/workflows/scripts/ghstack/yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


"@octokit/auth-token@^5.0.0":
version "5.1.2"
resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-5.1.2.tgz#68a486714d7a7fd1df56cb9bc89a860a0de866de"
integrity sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==

"@octokit/core@^6.1.4":
version "6.1.4"
resolved "https://registry.yarnpkg.com/@octokit/core/-/core-6.1.4.tgz#f5ccf911cc95b1ce9daf6de425d1664392f867db"
integrity sha512-lAS9k7d6I0MPN+gb9bKDt7X8SdxknYqAMh44S5L+lNqIN2NuV8nvv3g8rPp7MuRxcOpxpUIATWprO0C34a8Qmg==
dependencies:
"@octokit/auth-token" "^5.0.0"
"@octokit/graphql" "^8.1.2"
"@octokit/request" "^9.2.1"
"@octokit/request-error" "^6.1.7"
"@octokit/types" "^13.6.2"
before-after-hook "^3.0.2"
universal-user-agent "^7.0.0"

"@octokit/endpoint@^10.1.3":
version "10.1.3"
resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-10.1.3.tgz#bfe8ff2ec213eb4216065e77654bfbba0fc6d4de"
integrity sha512-nBRBMpKPhQUxCsQQeW+rCJ/OPSMcj3g0nfHn01zGYZXuNDvvXudF/TYY6APj5THlurerpFN4a/dQAIAaM6BYhA==
dependencies:
"@octokit/types" "^13.6.2"
universal-user-agent "^7.0.2"

"@octokit/graphql@^8.1.2":
version "8.2.1"
resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-8.2.1.tgz#0cb83600e6b4009805acc1c56ae8e07e6c991b78"
integrity sha512-n57hXtOoHrhwTWdvhVkdJHdhTv0JstjDbDRhJfwIRNfFqmSo1DaK/mD2syoNUoLCyqSjBpGAKOG0BuwF392slw==
dependencies:
"@octokit/request" "^9.2.2"
"@octokit/types" "^13.8.0"
universal-user-agent "^7.0.0"

"@octokit/openapi-types@^24.2.0":
version "24.2.0"
resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-24.2.0.tgz#3d55c32eac0d38da1a7083a9c3b0cca77924f7d3"
integrity sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==

"@octokit/plugin-paginate-rest@^11.4.2":
version "11.6.0"
resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.6.0.tgz#e5e9ff3530e867c3837fdbff94ce15a2468a1f37"
integrity sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==
dependencies:
"@octokit/types" "^13.10.0"

"@octokit/plugin-request-log@^5.3.1":
version "5.3.1"
resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz#ccb75d9705de769b2aa82bcd105cc96eb0c00f69"
integrity sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==

"@octokit/plugin-rest-endpoint-methods@^13.3.0":
version "13.5.0"
resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.5.0.tgz#d8c8ca2123b305596c959a9134dfa8b0495b0ba6"
integrity sha512-9Pas60Iv9ejO3WlAX3maE1+38c5nqbJXV5GrncEfkndIpZrJ/WPMRd2xYDcPPEt5yzpxcjw9fWNoPhsSGzqKqw==
dependencies:
"@octokit/types" "^13.10.0"

"@octokit/request-error@^6.1.7":
version "6.1.7"
resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-6.1.7.tgz#44fc598f5cdf4593e0e58b5155fe2e77230ff6da"
integrity sha512-69NIppAwaauwZv6aOzb+VVLwt+0havz9GT5YplkeJv7fG7a40qpLt/yZKyiDxAhgz0EtgNdNcb96Z0u+Zyuy2g==
dependencies:
"@octokit/types" "^13.6.2"

"@octokit/request@^9.2.1", "@octokit/request@^9.2.2":
version "9.2.2"
resolved "https://registry.yarnpkg.com/@octokit/request/-/request-9.2.2.tgz#754452ec4692d7fdc32438a14e028eba0e6b2c09"
integrity sha512-dZl0ZHx6gOQGcffgm1/Sf6JfEpmh34v3Af2Uci02vzUYz6qEN6zepoRtmybWXIGXFIK8K9ylE3b+duCWqhArtg==
dependencies:
"@octokit/endpoint" "^10.1.3"
"@octokit/request-error" "^6.1.7"
"@octokit/types" "^13.6.2"
fast-content-type-parse "^2.0.0"
universal-user-agent "^7.0.2"

"@octokit/rest@^21.1.1":
version "21.1.1"
resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-21.1.1.tgz#7a70455ca451b1d253e5b706f35178ceefb74de2"
integrity sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg==
dependencies:
"@octokit/core" "^6.1.4"
"@octokit/plugin-paginate-rest" "^11.4.2"
"@octokit/plugin-request-log" "^5.3.1"
"@octokit/plugin-rest-endpoint-methods" "^13.3.0"

"@octokit/types@^13.10.0", "@octokit/types@^13.6.2", "@octokit/types@^13.8.0":
version "13.10.0"
resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.10.0.tgz#3e7c6b19c0236c270656e4ea666148c2b51fd1a3"
integrity sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==
dependencies:
"@octokit/openapi-types" "^24.2.0"

before-after-hook@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-3.0.2.tgz#d5665a5fa8b62294a5aa0a499f933f4a1016195d"
integrity sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==

fast-content-type-parse@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz#c236124534ee2cb427c8d8e5ba35a4856947847b"
integrity sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==

universal-user-agent@^7.0.0, universal-user-agent@^7.0.2:
version "7.0.2"
resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-7.0.2.tgz#52e7d0e9b3dc4df06cc33cb2b9fd79041a54827e"
integrity sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==
Loading
Loading