关于 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 中使用机密”。
如果无法做到这一点,请考虑使用其他 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', });
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 App 令牌。 有关详细信息,请参阅“注册 GitHub 应用”和“关于使用 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。 有关详细信息,请参阅“GitHub Apps 的 REST API 终结点”。 将 HOSTNAME
替换为 你的 GitHub Enterprise Server 实例 的名称。
import { App } from "octokit"; const app = new App({ appId: APP_ID, privateKey: PRIVATE_KEY, Octokit: Octokit.defaults({ baseUrl: "http(s)://HOSTNAME/api/v3", }), }); const octokit = await app.getInstallationOctokit(INSTALLATION_ID);
import { App } from "octokit";
const app = new App({
appId: APP_ID,
privateKey: PRIVATE_KEY,
Octokit: Octokit.defaults({
baseUrl: "http(s)://HOSTNAME/api/v3",
}),
});
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({ baseUrl: "http(s)://HOSTNAME/api/v3", auth: 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", });
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 });
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
方法接受可选的 map 函数,可用于仅从响应中收集所需的数据。 这将减少脚本的内存使用量。 map 函数可以采用第二个参数 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
方法返回一个承诺,如果请求成功,该承诺将解析为对象。 对象属性包括 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
方法返回一个承诺。 如果请求成功,承诺将解析为终结点返回的数据数组。 与 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 而不是 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, 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({
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,
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 文档如何通过搜索 GitHub 文档存储库使用 Octokit.js。