关于 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 Apps 的私钥”。您可以使用GET /users/{username}/installation
、GET /repos/{owner}/{repo}/installation
或GET /orgs/{org}/installation
端点获取安装 ID。有关详细信息,请参阅“GitHub Apps 的 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
属性的值是一个对象,其中标头名称作为键,标头值作为值。例如,要发送值为text/plain
的content-type
标头和值为2022-11-28
的x-github-api-version
标头。
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 自述文件中的 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,如果请求成功,则解析为一个对象。对象的属性为 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 存储库。