关于 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 运行脚本。 有关详细信息,请参阅“加密机密”。
如果无法做到这一点,请考虑使用其他服务(如 1Password CLI)来安全地存储凭据。
使用 personal access token 进行身份验证
如果要将 GitHub REST API 用于个人用途,可以创建 personal access token。 有关创建 personal access token 的详细信息,请参阅“创建个人访问令牌”。
首先,从 octokit
导入 Octokit
。 然后,在创建 Octokit
实例时传递 personal access token。 在以下示例中,将 YOUR-TOKEN
替换为对 personal access token 的引用。 将 [hostname]
替换为 你的 GitHub Enterprise Server 实例 的名称。
import { Octokit } from "octokit";
const octokit = new Octokit({
baseUrl: "http(s)://HOSTNAME/api/v3",
auth: 'YOUR-TOKEN',
});
使用 GitHub App 进行身份验证
如果要代表组织或其他用户使用 API,GitHub 建议使用 GitHub App。 如果某个终结点可用于 GitHub Apps,则该终结点的 REST 参考文档将显示“适用于 GitHub Apps”。 有关详细信息,请参阅“创建 GitHub 应用程序”、“About authentication with a GitHub App”和“代表用户使用 GitHub 应用进行身份验证”。
导入 App
,而不是从 octokit
导入 Octokit
。 在以下示例中,将 APP_ID
替换为对应用 ID 的引用。 将 PRIVATE_KEY
替换为对应用私钥的引用。 将 INSTALLATION_ID
替换为要代表其进行身份验证的应用的安装 ID。 可以在应用的设置页面上找到应用的 ID 并生成私钥。 有关详细信息,请参阅“管理 GitHub 应用的私钥”。 可以使用 GET /users/{username}/installation
、GET /repos/{owner}/{repo}/installation
或 GET /orgs/{org}/installation
终结点获取安装 ID。 有关详细信息,请参阅 REST 参考文档中的“GitHub 应用”。
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
替换为机密的名称。 有关机密的详细信息,请参阅“加密机密”。
如果使用 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({
baseUrl: "http(s)://HOSTNAME/api/v3",
auth: process.env.TOKEN,
});
无需身份验证即可实例化
可以在不进行身份验证的情况下使用 REST API,但速率限制较低,并且无法使用某些终结点。 若要在不进行身份验证的情况下创建 Octokit
实例,请不要传递 auth
参数。 将基 URL 设置为 http(s)://HOSTNAME/api/v3
。 将 [hostname]
替换为 你的 GitHub Enterprise Server 实例 的名称。
import { Octokit } from "octokit";
const octokit = new Octokit({
baseUrl: "http(s)://HOSTNAME/api/v3",
});
发出请求
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
});
request
方法会自动传递 Accept: application/vnd.github+json
标头。 若要传递其他标头或不同的 Accept
标头,请将 headers
属性添加到作为第二个参数传递的对象。 headers
属性的值是将标头名称作为键并将标头值作为值的对象。 例如,发送值为 text/plain
的 content-type
标头:
await octokit.request("POST /markdown/raw", {
text: "Hello **world**",
headers: {
"content-type": "text/plain",
},
});
使用 rest
终结点方法发出请求
每个 REST API 终结点在 Octokit 中都有一个关联的 rest
终结点方法。 为方便起见,这些方法通常会在 IDE 中自动完成。 可以将任何参数作为对象传递给该方法。
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,
});
paginate
方法接受可选的 map 函数,可用于仅从响应中收集所需的数据。 这将减少脚本的内存使用量。 map 函数可以采用第二个参数 done
,可以调用该参数在到达最后一页之前结束分页。 这样,便可以提取页面的子集。 例如,以下示例继续提取结果,直到返回标题中包含“test”的问题。 对于返回的数据页面,仅存储问题标题和作者。
const issueData = await octokit.paginate("GET /repos/{owner}/{repo}/issues", {
owner: "github",
repo: "docs",
per_page: 100,
},
(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,
});
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,
});
有关分页的详细信息,请参阅“在 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,
});
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",
});
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
})
使用响应
request
方法返回一个承诺,如果请求成功,该承诺将解析为对象。 对象属性包括 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,
});
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
方法返回一个承诺。 如果请求成功,承诺将解析为终结点返回的数据数组。 与 request
方法不同,paginate
方法不返回状态代码、URL 或标头。
const data = await octokit.paginate("GET /repos/{owner}/{repo}/issues", {
owner: "github",
repo: "docs",
per_page: 100,
});
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 而不是 personal access token 进行身份验证,则需要导入并实例化 App
而不是 Octokit
。 有关详细信息,请参阅“使用 GitHub App 进行身份验证”。
getChangedFiles
函数获取为拉取请求更改的所有文件。 commentIfDataFilesChanged
函数调用 getChangedFiles
函数。 如果拉取请求更改的任何文件的文件路径中包含 /data/
,则该函数将对拉取请求进行注释。
import { Octokit } from "octokit";
const octokit = new Octokit({
baseUrl: "http(s)://HOSTNAME/api/v3",
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,
});
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.`,
});
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 文档如何通过搜索 GitHub 文档存储库使用 Octokit.js。