关于 Octokit.js
如果您想使用 JavaScript 编写一个与 GitHub REST API 交互的脚本,GitHub 建议您使用 Octokit.js SDK。Octokit.js 由 GitHub 维护。该 SDK 实施了最佳实践,使您更容易通过 JavaScript 与 REST API 交互。Octokit.js 可与所有现代浏览器、Node.js 和 Deno 协同工作。有关 Octokit.js 的更多信息,请参阅 Octokit.js 自述文件。
先决条件
本指南假设您熟悉 JavaScript 和 GitHub REST API。有关 REST API 的更多信息,请参阅“REST API 入门”。
您必须安装并导入 octokit
才能使用 Octokit.js 库。本指南使用与 ES6 兼容的导入语句。有关不同安装和导入方法的更多信息,请参阅 Octokit.js 自述文件的用法部分。
实例化和身份验证
警告:请像对待密码一样对待您的身份验证凭据。
为了保护您的凭据安全,您可以将凭据存储为秘密,并通过 GitHub Actions 运行您的脚本。有关更多信息,请参阅“在 GitHub Actions 中使用秘密”。
您也可以将凭据存储为 Codespaces 秘密,并在 Codespaces 中运行您的脚本。有关更多信息,请参阅“管理您的 GitHub Codespaces 的帐户特定秘密”。
如果这些选项不可行,请考虑使用其他 CLI 服务来安全地存储您的凭据。
使用个人访问令牌进行身份验证
如果您想将 GitHub REST API 用于个人用途,您可以创建一个个人访问令牌。有关创建个人访问令牌的更多信息,请参阅“管理您的个人访问令牌”。
首先,从 octokit
中导入 Octokit
。然后,在创建 Octokit
实例时传递您的个人访问令牌。在以下示例中,将 YOUR-TOKEN
替换为对您的个人访问令牌的引用。
import { Octokit } from "octokit"; const octokit = new Octokit({ auth: 'YOUR-TOKEN', });
import { Octokit } from "octokit";
const octokit = new Octokit({
auth: 'YOUR-TOKEN',
});
使用 GitHub App 进行身份验证
如果您想代表组织或其他用户使用 API,GitHub 建议您使用 GitHub App。如果某个端点对 GitHub Apps 可用,该端点的 REST 参考文档将指示需要哪种类型的 GitHub App 令牌。有关更多信息,请参阅“注册 GitHub App”和“关于使用 GitHub App 进行身份验证”。
不要从 octokit
中导入 Octokit
,而是导入 App
。在以下示例中,将 APP_ID
替换为您的应用 ID 的引用。将 PRIVATE_KEY
替换为您的应用私钥的引用。将 INSTALLATION_ID
替换为要代表其进行身份验证的应用安装的 ID。您可以在应用设置页面上找到应用 ID 并生成私钥。有关更多信息,请参阅 "管理 GitHub 应用的私钥"。您可以使用 GET /users/{username}/installation
、GET /repos/{owner}/{repo}/installation
或 GET /orgs/{org}/installation
端点获取安装 ID。有关更多信息,请参阅 "GitHub 应用的 REST API 端点"。
import { App } from "octokit"; const app = new App({ appId: APP_ID, privateKey: PRIVATE_KEY, }); const octokit = await app.getInstallationOctokit(INSTALLATION_ID);
import { App } from "octokit";
const app = new App({
appId: APP_ID,
privateKey: PRIVATE_KEY,
});
const octokit = await app.getInstallationOctokit(INSTALLATION_ID);
在 GitHub Actions 中进行身份验证
如果您想在 GitHub Actions 工作流程中使用 API,GitHub 建议您使用内置的 GITHUB_TOKEN
进行身份验证,而不是创建令牌。您可以使用 permissions
键授予 GITHUB_TOKEN
权限。有关 GITHUB_TOKEN
的更多信息,请参阅 "自动令牌身份验证"。
如果您的工作流程需要访问工作流程存储库之外的资源,那么您将无法使用 GITHUB_TOKEN
。在这种情况下,将您的凭据存储为一个秘密,并将以下示例中的 GITHUB_TOKEN
替换为您的秘密的名称。有关秘密的更多信息,请参阅 "在 GitHub Actions 中使用秘密"。
如果您使用 run
关键字在 GitHub Actions 工作流程中执行 JavaScript 脚本,则可以将 GITHUB_TOKEN
的值存储为环境变量。您的脚本可以访问环境变量,方法是使用 process.env.VARIABLE_NAME
。
例如,此工作流程步骤将 GITHUB_TOKEN
存储在名为 TOKEN
的环境变量中
- name: Run script
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
node .github/actions-scripts/use-the-api.mjs
工作流程运行的脚本使用 process.env.TOKEN
进行身份验证
import { Octokit } from "octokit"; const octokit = new Octokit({ auth: process.env.TOKEN, });
import { Octokit } from "octokit";
const octokit = new Octokit({
auth: process.env.TOKEN,
});
在没有身份验证的情况下实例化
您可以使用 REST API,而无需进行身份验证,尽管您的速率限制会更低,并且您将无法使用某些端点。要创建 Octokit
的实例,而无需进行身份验证,请不要传递 auth
参数。
import { Octokit } from "octokit"; const octokit = new Octokit({ });
import { Octokit } from "octokit";
const octokit = new Octokit({ });
发出请求
Octokit 支持多种请求方式。如果您知道端点的 HTTP 动词和路径,可以使用 `request` 方法进行请求。如果您想利用 IDE 和类型推断的自动补全功能,可以使用 `rest` 方法。对于分页端点,可以使用 `paginate` 方法请求多页数据。
使用 `request` 方法进行请求
要使用 `request` 方法进行请求,请将 HTTP 方法和路径作为第一个参数传递。将任何正文、查询或路径参数作为对象传递给第二个参数。例如,要向 `/repos/{owner}/{repo}/issues` 发出 `GET` 请求并传递 `owner`、`repo` 和 `per_page` 参数
await octokit.request("GET /repos/{owner}/{repo}/issues", { owner: "github", repo: "docs", per_page: 2 });
await octokit.request("GET /repos/{owner}/{repo}/issues", {
owner: "github",
repo: "docs",
per_page: 2
});
`request` 方法会自动传递 `Accept: application/vnd.github+json` 标头。要传递其他标头或不同的 `Accept` 标头,请在作为第二个参数传递的对象中添加 `headers` 属性。`headers` 属性的值是一个对象,其中标头名称作为键,标头值作为值。例如,要发送 `content-type` 标头,其值为 `text/plain`,以及 `x-github-api-version` 标头,其值为 `2022-11-28`
await octokit.request("POST /markdown/raw", { text: "Hello **world**", headers: { "content-type": "text/plain", "x-github-api-version": "2022-11-28", }, });
await octokit.request("POST /markdown/raw", {
text: "Hello **world**",
headers: {
"content-type": "text/plain",
"x-github-api-version": "2022-11-28",
},
});
使用 `rest` 端点方法进行请求
每个 REST API 端点在 Octokit 中都有一个关联的 `rest` 端点方法。这些方法通常会在您的 IDE 中自动补全,以方便使用。您可以将任何参数作为对象传递给该方法。
await octokit.rest.issues.listForRepo({ owner: "github", repo: "docs", per_page: 2 });
await octokit.rest.issues.listForRepo({
owner: "github",
repo: "docs",
per_page: 2
});
此外,如果您使用的是 TypeScript 等类型化语言,您可以导入类型以与这些方法一起使用。有关更多信息,请参阅 plugin-rest-endpoint-methods.js README 中的 TypeScript 部分。
进行分页请求
如果端点是分页的,并且您想获取不止一页的结果,可以使用 `paginate` 方法。`paginate` 将获取下一页的结果,直到到达最后一页,然后将所有结果作为单个数组返回。一些端点将分页结果作为对象中的数组返回,而不是将分页结果作为数组返回。`paginate` 始终返回一个项目数组,即使原始结果是一个对象。
例如,以下示例获取了 github/docs
存储库中的所有问题。虽然它一次请求 100 个问题,但该函数在到达最后一页数据之前不会返回。
const issueData = await octokit.paginate("GET /repos/{owner}/{repo}/issues", { owner: "github", repo: "docs", per_page: 100, headers: { "x-github-api-version": "2022-11-28", }, });
const issueData = await octokit.paginate("GET /repos/{owner}/{repo}/issues", {
owner: "github",
repo: "docs",
per_page: 100,
headers: {
"x-github-api-version": "2022-11-28",
},
});
paginate
方法接受一个可选的映射函数,您可以使用它来收集您想要从响应中获取的数据。这减少了脚本的内存使用量。映射函数可以接受第二个参数 done
,您可以调用它在到达最后一页之前结束分页。这使您可以获取部分页面。例如,以下示例将继续获取结果,直到返回标题中包含“test”的问题。对于返回的数据页面,仅存储问题标题和作者。
const issueData = await octokit.paginate("GET /repos/{owner}/{repo}/issues", { owner: "github", repo: "docs", per_page: 100, headers: { "x-github-api-version": "2022-11-28", }, }, (response, done) => response.data.map((issue) => { if (issue.title.includes("test")) { done() } return ({title: issue.title, author: issue.user.login}) }) );
const issueData = await octokit.paginate("GET /repos/{owner}/{repo}/issues", {
owner: "github",
repo: "docs",
per_page: 100,
headers: {
"x-github-api-version": "2022-11-28",
},
},
(response, done) => response.data.map((issue) => {
if (issue.title.includes("test")) {
done()
}
return ({title: issue.title, author: issue.user.login})
})
);
您可以使用 octokit.paginate.iterator()
逐页迭代,而不是一次获取所有结果。例如,以下示例一次获取一页结果,并在获取下一页之前处理页面中的每个对象。一旦遇到标题中包含“test”的问题,脚本就会停止迭代并返回已处理的每个对象的标题和作者。迭代器是获取分页数据的最高效方法。
const iterator = octokit.paginate.iterator("GET /repos/{owner}/{repo}/issues", { owner: "github", repo: "docs", per_page: 100, headers: { "x-github-api-version": "2022-11-28", }, }); let issueData = [] let breakLoop = false for await (const {data} of iterator) { if (breakLoop) break for (const issue of data) { if (issue.title.includes("test")) { breakLoop = true break } else { issueData = [...issueData, {title: issue.title, author: issue.user.login}]; } } }
const iterator = octokit.paginate.iterator("GET /repos/{owner}/{repo}/issues", {
owner: "github",
repo: "docs",
per_page: 100,
headers: {
"x-github-api-version": "2022-11-28",
},
});
let issueData = []
let breakLoop = false
for await (const {data} of iterator) {
if (breakLoop) break
for (const issue of data) {
if (issue.title.includes("test")) {
breakLoop = true
break
} else {
issueData = [...issueData, {title: issue.title, author: issue.user.login}];
}
}
}
您也可以将 paginate
方法与 rest
端点方法一起使用。将 rest
端点方法作为第一个参数传递。将任何参数作为第二个参数传递。
const iterator = octokit.paginate.iterator(octokit.rest.issues.listForRepo, { owner: "github", repo: "docs", per_page: 100, headers: { "x-github-api-version": "2022-11-28", }, });
const iterator = octokit.paginate.iterator(octokit.rest.issues.listForRepo, {
owner: "github",
repo: "docs",
per_page: 100,
headers: {
"x-github-api-version": "2022-11-28",
},
});
有关分页的更多信息,请参阅“在 REST API 中使用分页”。
捕获错误
捕获所有错误
有时,GitHub REST API 会返回错误。例如,如果您的访问令牌已过期或您省略了必需的参数,您将收到错误。Octokit.js 在遇到除 400 Bad Request
、401 Unauthorized
、403 Forbidden
、404 Not Found
和 422 Unprocessable Entity
之外的错误时会自动重试请求。如果即使在重试后也发生 API 错误,Octokit.js 会抛出一个错误,其中包含响应的 HTTP 状态代码 (response.status
) 和响应头 (response.headers
)。您应该在代码中处理这些错误。例如,您可以使用 try/catch 块来捕获错误。
let filesChanged = [] try { const iterator = octokit.paginate.iterator("GET /repos/{owner}/{repo}/pulls/{pull_number}/files", { owner: "github", repo: "docs", pull_number: 22809, per_page: 100, headers: { "x-github-api-version": "2022-11-28", }, }); for await (const {data} of iterator) { filesChanged = [...filesChanged, ...data.map(fileData => fileData.filename)]; } } catch (error) { if (error.response) { console.error(`Error! Status: ${error.response.status}. Message: ${error.response.data.message}`) } console.error(error) }
let filesChanged = []
try {
const iterator = octokit.paginate.iterator("GET /repos/{owner}/{repo}/pulls/{pull_number}/files", {
owner: "github",
repo: "docs",
pull_number: 22809,
per_page: 100,
headers: {
"x-github-api-version": "2022-11-28",
},
});
for await (const {data} of iterator) {
filesChanged = [...filesChanged, ...data.map(fileData => fileData.filename)];
}
} catch (error) {
if (error.response) {
console.error(`Error! Status: ${error.response.status}. Message: ${error.response.data.message}`)
}
console.error(error)
}
处理预期错误代码
有时,GitHub 使用 4xx 状态代码来指示非错误响应。如果您使用的端点执行此操作,您可以为特定错误添加额外的处理。例如,GET /user/starred/{owner}/{repo}
端点如果存储库未加星标,将返回 404
。以下示例使用 404
响应来指示存储库未加星标;所有其他错误代码都被视为错误。
try { await octokit.request("GET /user/starred/{owner}/{repo}", { owner: "github", repo: "docs", headers: { "x-github-api-version": "2022-11-28", }, }); console.log(`The repository is starred by me`); } catch (error) { if (error.status === 404) { console.log(`The repository is not starred by me`); } else { console.error(`An error occurred while checking if the repository is starred: ${error?.response?.data?.message}`); } }
try {
await octokit.request("GET /user/starred/{owner}/{repo}", {
owner: "github",
repo: "docs",
headers: {
"x-github-api-version": "2022-11-28",
},
});
console.log(`The repository is starred by me`);
} catch (error) {
if (error.status === 404) {
console.log(`The repository is not starred by me`);
} else {
console.error(`An error occurred while checking if the repository is starred: ${error?.response?.data?.message}`);
}
}
处理速率限制错误
如果您收到速率限制错误,您可能需要等待一段时间后重试您的请求。当您受到速率限制时,GitHub 会返回一个 403 Forbidden
错误,并且 x-ratelimit-remaining
响应头值将为 "0"
。响应头将包含一个 x-ratelimit-reset
头,它告诉您当前速率限制窗口重置的时间,以 UTC 纪元秒为单位。您可以在 x-ratelimit-reset
指定的时间后重试您的请求。
async function requestRetry(route, parameters) { try { const response = await octokit.request(route, parameters); return response } catch (error) { if (error.response && error.status === 403 && error.response.headers['x-ratelimit-remaining'] === '0') { const resetTimeEpochSeconds = error.response.headers['x-ratelimit-reset']; const currentTimeEpochSeconds = Math.floor(Date.now() / 1000); const secondsToWait = resetTimeEpochSeconds - currentTimeEpochSeconds; console.log(`You have exceeded your rate limit. Retrying in ${secondsToWait} seconds.`); setTimeout(requestRetry, secondsToWait * 1000, route, parameters); } else { console.error(error); } } } const response = await requestRetry("GET /repos/{owner}/{repo}/issues", { owner: "github", repo: "docs", per_page: 2 })
async function requestRetry(route, parameters) {
try {
const response = await octokit.request(route, parameters);
return response
} catch (error) {
if (error.response && error.status === 403 && error.response.headers['x-ratelimit-remaining'] === '0') {
const resetTimeEpochSeconds = error.response.headers['x-ratelimit-reset'];
const currentTimeEpochSeconds = Math.floor(Date.now() / 1000);
const secondsToWait = resetTimeEpochSeconds - currentTimeEpochSeconds;
console.log(`You have exceeded your rate limit. Retrying in ${secondsToWait} seconds.`);
setTimeout(requestRetry, secondsToWait * 1000, route, parameters);
} else {
console.error(error);
}
}
}
const response = await requestRetry("GET /repos/{owner}/{repo}/issues", {
owner: "github",
repo: "docs",
per_page: 2
})
使用响应
request
方法返回一个 promise,如果请求成功,该 promise 将解析为一个对象。该对象的属性包括 data
(端点返回的响应主体)、status
(HTTP 响应代码)、url
(请求的 URL)和 headers
(包含响应头的对象)。除非另有说明,否则响应主体为 JSON 格式。某些端点不返回响应主体;在这种情况下,data
属性将被省略。
const response = await octokit.request("GET /repos/{owner}/{repo}/issues/{issue_number}", { owner: "github", repo: "docs", issue_number: 11901, headers: { "x-github-api-version": "2022-11-28", }, }); console.log(`The status of the response is: ${response.status}`) console.log(`The request URL was: ${response.url}`) console.log(`The x-ratelimit-remaining response header is: ${response.headers["x-ratelimit-remaining"]}`) console.log(`The issue title is: ${response.data.title}`)
const response = await octokit.request("GET /repos/{owner}/{repo}/issues/{issue_number}", {
owner: "github",
repo: "docs",
issue_number: 11901,
headers: {
"x-github-api-version": "2022-11-28",
},
});
console.log(`The status of the response is: ${response.status}`)
console.log(`The request URL was: ${response.url}`)
console.log(`The x-ratelimit-remaining response header is: ${response.headers["x-ratelimit-remaining"]}`)
console.log(`The issue title is: ${response.data.title}`)
类似地,paginate
方法返回一个 promise。如果请求成功,该 promise 将解析为端点返回的数据数组。与 request
方法不同,paginate
方法不返回状态代码、URL 或头。
const data = await octokit.paginate("GET /repos/{owner}/{repo}/issues", { owner: "github", repo: "docs", per_page: 100, headers: { "x-github-api-version": "2022-11-28", }, }); console.log(`${data.length} issues were returned`) console.log(`The title of the first issue is: ${data[0].title}`)
const data = await octokit.paginate("GET /repos/{owner}/{repo}/issues", {
owner: "github",
repo: "docs",
per_page: 100,
headers: {
"x-github-api-version": "2022-11-28",
},
});
console.log(`${data.length} issues were returned`)
console.log(`The title of the first issue is: ${data[0].title}`)
示例脚本
这是一个使用 Octokit.js 的完整示例脚本。该脚本导入 Octokit
并创建一个新的 Octokit
实例。如果您想使用 GitHub App 而不是个人访问令牌进行身份验证,则应导入并实例化 App
而不是 Octokit
。有关更多信息,请参阅 "使用 GitHub App 进行身份验证"。
getChangedFiles
函数获取拉取请求中更改的所有文件。commentIfDataFilesChanged
函数调用 getChangedFiles
函数。如果拉取请求更改的任何文件在文件路径中包含 /data/
,则该函数将在拉取请求中添加评论。
import { Octokit } from "octokit"; const octokit = new Octokit({ auth: 'YOUR-TOKEN', }); async function getChangedFiles({owner, repo, pullNumber}) { let filesChanged = [] try { const iterator = octokit.paginate.iterator("GET /repos/{owner}/{repo}/pulls/{pull_number}/files", { owner: owner, repo: repo, pull_number: pullNumber, per_page: 100, headers: { "x-github-api-version": "2022-11-28", }, }); for await (const {data} of iterator) { filesChanged = [...filesChanged, ...data.map(fileData => fileData.filename)]; } } catch (error) { if (error.response) { console.error(`Error! Status: ${error.response.status}. Message: ${error.response.data.message}`) } console.error(error) } return filesChanged } async function commentIfDataFilesChanged({owner, repo, pullNumber}) { const changedFiles = await getChangedFiles({owner, repo, pullNumber}); const filePathRegex = new RegExp(/\/data\//, "i"); if (!changedFiles.some(fileName => filePathRegex.test(fileName))) { return; } try { const {data: comment} = await octokit.request("POST /repos/{owner}/{repo}/issues/{issue_number}/comments", { owner: owner, repo: repo, issue_number: pullNumber, body: `It looks like you changed a data file. These files are auto-generated. \n\nYou must revert any changes to data files before your pull request will be reviewed.`, headers: { "x-github-api-version": "2022-11-28", }, }); return comment.html_url; } catch (error) { if (error.response) { console.error(`Error! Status: ${error.response.status}. Message: ${error.response.data.message}`) } console.error(error) } } await commentIfDataFilesChanged({owner: "github", repo: "docs", pullNumber: 191});
import { Octokit } from "octokit";
const octokit = new Octokit({
auth: 'YOUR-TOKEN',
});
async function getChangedFiles({owner, repo, pullNumber}) {
let filesChanged = []
try {
const iterator = octokit.paginate.iterator("GET /repos/{owner}/{repo}/pulls/{pull_number}/files", {
owner: owner,
repo: repo,
pull_number: pullNumber,
per_page: 100,
headers: {
"x-github-api-version": "2022-11-28",
},
});
for await (const {data} of iterator) {
filesChanged = [...filesChanged, ...data.map(fileData => fileData.filename)];
}
} catch (error) {
if (error.response) {
console.error(`Error! Status: ${error.response.status}. Message: ${error.response.data.message}`)
}
console.error(error)
}
return filesChanged
}
async function commentIfDataFilesChanged({owner, repo, pullNumber}) {
const changedFiles = await getChangedFiles({owner, repo, pullNumber});
const filePathRegex = new RegExp(/\/data\//, "i");
if (!changedFiles.some(fileName => filePathRegex.test(fileName))) {
return;
}
try {
const {data: comment} = await octokit.request("POST /repos/{owner}/{repo}/issues/{issue_number}/comments", {
owner: owner,
repo: repo,
issue_number: pullNumber,
body: `It looks like you changed a data file. These files are auto-generated. \n\nYou must revert any changes to data files before your pull request will be reviewed.`,
headers: {
"x-github-api-version": "2022-11-28",
},
});
return comment.html_url;
} catch (error) {
if (error.response) {
console.error(`Error! Status: ${error.response.status}. Message: ${error.response.data.message}`)
}
console.error(error)
}
}
await commentIfDataFilesChanged({owner: "github", repo: "docs", pullNumber: 191});
下一步
- 要了解更多关于 Octokit.js 的信息,请参阅 Octokit.js 文档。
- 要查看一些真实的例子,请查看 GitHub Docs 如何使用 Octokit.js,方法是 搜索 GitHub Docs 仓库。