广告

如何用 Octokit 高效查询 GitHub 组织下所有仓库的开放 PR:开发/运维团队的实操指南

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 数据,你需要一个 个人访问令牌,权限应包含 reporead: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;
}

并发执行可显著提升总体吞吐量,但要避免触发 API 速率限制。

4.2 聚合结果与去重策略

聚合时应使用 唯一标识(如 仓库 full_name + PR number)进行去重,确保最终结果没有重复条目。

如何用 Octokit 高效查询 GitHub 组织下所有仓库的开放 PR:开发/运维团队的实操指南

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;
}

广告