1. 环境与工具准备
目标 是在一个 GitHub 组织 下,快速定位所有仓库的 开放 PR。为此,你需要一个合适的 Node.js 环境 和 Octokit 客户端。
在本节中,你将了解如何配置本地开发环境、安装 @octokit/rest 包,以及获取一个可用于认证的 GitHub 访问令牌。
1.1 选择合适的 Octokit 版本
Octokit 提供多种语言绑定,本文以 JavaScript/Node.js 为主,利用 @octokit/rest 实现对 GitHub API 的高效调用。
# 安装必要包
npm install @octokit/rest @octokit/auth-token
使用 npm 安装后,你就可以在代码中创建一个 Octokit 实例,并通过 访问令牌 进行认证。
1.2 获取 GitHub 访问令牌
为了访问组织下的仓库和 PR 数据,你需要一个 个人访问令牌,权限应包含 repo 及 read:org 权限。
# 手动获取令牌的步骤
1) 登录 GitHub
2) 进入 Settings > Developer settings > Personal access tokens
3) 生成 token,勾选 repo、read:org 等权限
4) 将 token 存储在环境变量中,例如 GITHUB_TOKEN
在代码中使用时,请确保通过 环境变量 或 密钥管理 的方式传递令牌,以避免明文泄露。
2. 架构设计与查询目标
明确目标有助于实现 高效查询:需要从一个 组织 的全部仓库中筛选出 开放 PR,并尽可能少的调用次数完成数据收集。
设计要点包括 分批分页获取仓库、对每个仓库执行 并发请求、以及统一聚合结果以便后续分析。
2.1 明确需要的数据字段
在起步阶段,关注以下字段可以快速得到需要的信息:仓库名、仓库所属组织、PR 编号、PR 状态、PR 标题、PR 创建时间。
// 需要的数据字段示例
{repo: { name, full_name },pullRequest: { number, title, state, created_at, html_url }
}
随后可以将这些字段映射到一个统一的 结果模型,以便后续分析和报表生成。
3. 使用 Octokit 获取组织下的仓库列表
3.1 认证并创建 Octokit 实例
通过 令牌认证,可以创建一个全局的 Octokit 实例,随后进行 API 调用。
const { Octokit } = require("@octokit/rest");
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
令牌认证是实现稳定访问的关键,确保环境变量 GITHUB_TOKEN 不被外泄。
3.2 获取组织的仓库列表
使用 listForOrg 接口来获取组织下的所有仓库,注意要处理分页和仓库的可访问性。
async function listAllRepos(org) {const perPage = 100;const repos = [];for (let page = 1; ; page++) {const response = await octokit.rest.repos.listForOrg({ org, type: "all", per_page: perPage, page });repos.push(...response.data);if (response.data.length < perPage) break;}return repos;
}
分页处理可以确保你覆盖整个组织的仓库集合,同时减少单次请求的压力。
4. 遍历仓库并筛选开放 PR
4.1 遍历每个仓库的开放 PR
对每个仓库执行 PR 列表查询,并通过 state: open 过滤,必要时结合 pull request 状态过滤。
async function listOpenPRs(owner, repo) {const perPage = 100;const prs = [];for (let page = 1; ; page++) {const response = await octokit.rest.pulls.list({ owner, repo, state: "open", per_page: perPage, page });prs.push(...response.data);if (response.data.length < perPage) break;}return prs;
}
并发执行可显著提升总体吞吐量,但要
4.2 聚合结果与去重策略
聚合时应使用 唯一标识(如 仓库 full_name + PR number)进行去重,确保最终结果没有重复条目。

async function collectOpenPRsForOrg(org) {const repos = await listAllRepos(org);const results = [];// 简单串行或简单并发控制for (const r of repos) {const prs = await listOpenPRs(r.owner.login, r.name);for (const pr of prs) {results.push({ repo: r.full_name, number: pr.number, title: pr.title, url: pr.html_url, created_at: pr.created_at });}}// 结果去重可基于 full_name + numberconst unique = Array.from(new Map(results.map(x => [`${x.repo}#${x.number}`, x] )).values());return unique;
}5. 实操示例与完整代码
5.1 最小可执行脚本
以下是一份最小化但可直接运行的 Node.js 脚本,结合前文的函数,能从指定 GitHub 组织获取所有 开放 PR,并输出聚合结果。
// require dotenv 以载入环境变量
require("dotenv").config();
const { Octokit } = require("@octokit/rest");
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });async function listAllRepos(org) {const perPage = 100;const repos = [];for (let page = 1; ; page++) {const resp = await octokit.rest.repos.listForOrg({ org, type: "all", per_page: perPage, page });repos.push(...resp.data);if (resp.data.length < perPage) break;}return repos;
}async function listOpenPRs(owner, repo) {const perPage = 100;const prs = [];for (let page = 1; ; page++) {const resp = await octokit.rest.pulls.list({ owner, repo, state: "open", per_page: perPage, page });prs.push(...resp.data);if (resp.data.length < perPage) break;}return prs;
}async function main() {const org = process.env.GITHUB_ORG;const repos = await listAllRepos(org);const results = [];for (const r of repos) {const prs = await listOpenPRs(r.owner.login, r.name);for (const pr of prs) {results.push({ repo: r.full_name, number: pr.number, title: pr.title, url: pr.html_url, created_at: pr.created_at });}}// 去重const unique = Array.from(new Map(results.map(x => [`${x.repo}#${x.number}`, x] )).values());console.log(JSON.stringify(unique, null, 2));
}
main().catch(console.error);
6. 性能与稳定性优化要点
6.1 并发限制与速率限制
GitHub API 有速率限制,默认对未授权的请求和授权请求有不同限制。通过并发控制和 使用缓存,可以降低重复请求带来的开销。
// 使用 Promise.all 限制并发
const pQueue = [];
const MAX_CONCURRENCY = 5;
for (const r of repos) {while (pQueue.length >= MAX_CONCURRENCY) {await Promise.race(pQueue);}const p = listOpenPRs(r.owner.login, r.name).then(prs => {// 处理 prs});pQueue.push(p);p.finally(() => pQueue.splice(pQueue.indexOf(p), 1));
}
6.2 错误处理与重试
网络波动和速率限制可能导致请求失败,建议实现 重试机制,并在重试时实现 指数退避。
async function retry(fn, retries = 3) {let lastError;for (let i = 0; i < retries; i++) {try {return await fn();} catch (e) {lastError = e;const backoff = Math.pow(2, i) * 100;await new Promise(res => setTimeout(res, backoff));}}throw lastError;
} 

