关于 Octokit.rb
如果您想使用 Ruby 编写脚本与 GitHub REST API 交互,GitHub 建议使用 Octokit.rb SDK。Octokit.rb 由 GitHub 维护。该 SDK 实现了最佳实践,并让您更容易通过 Ruby 与 REST API 交互。Octokit.rb 可在所有现代浏览器、Node.rb 和 Deno 上使用。有关 Octokit.rb 的详细信息,请参阅 Octokit.rb README。
先决条件
本指南假设您已熟悉 Ruby 和 GitHub REST API。有关 REST API 的更多信息,请参阅 REST API 入门指南。
要使用 Octokit.rb 库,必须安装并导入 octokit gem。本指南使用符合 Ruby 约定的导入语句。有关不同安装方式的更多信息,请参阅 Octokit.rb README 中的 Installation 部分。
实例化和身份验证
警告
将您的身份验证凭证视为密码。
为确保凭证安全,您可以将凭证存储为 secret,并通过 GitHub Actions 运行脚本。更多信息,请参阅 在 GitHub Actions 中使用 secret。
您也可以将凭证存储为 Codespaces secret,并在 Codespaces 中运行脚本。更多信息,请参阅 管理 GitHub Codespaces 的账户特定 secret。
如果以上方式不可行,请考虑使用其他 CLI 服务安全地存储凭证。
使用个人访问令牌进行身份验证
如果您想将 GitHub REST API 用于个人用途,可以创建个人访问令牌。有关创建个人访问令牌的详细说明,请参阅 管理个人访问令牌。
首先,`require` octokit 库。然后,通过将个人访问令牌作为 access_token 选项传入,创建一个 Octokit 实例。在下面的示例中,将 YOUR-TOKEN 替换为您的个人访问令牌。
require 'octokit' octokit = Octokit::Client.new(access_token: 'YOUR-TOKEN')
require 'octokit'
octokit = Octokit::Client.new(access_token: 'YOUR-TOKEN')
使用 GitHub 应用进行身份验证
如果您希望以组织或其他用户的身份使用 API,GitHub 建议使用 GitHub App。若该端点对 GitHub App 可用,REST 参考文档会指明需要何种类型的 GitHub App 令牌。更多信息请参阅 注册 GitHub App 和 关于使用 GitHub App 进行身份验证。
无需 `require` octokit,而是通过传入 GitHub App 信息创建 Octokit::Client 实例。在下面的示例中,将 APP_ID 替换为您的 App ID,将 PRIVATE_KEY 替换为您的私钥,将 INSTALLATION_ID 替换为您想代表其进行身份验证的 App 安装 ID。您可在 App 设置页面找到 App ID 并生成私钥。更多信息请参阅 管理 GitHub App 私钥。可以使用 GET /users/{username}/installation、GET /repos/{owner}/{repo}/installation 或 GET /orgs/{org}/installation 端点获取安装 ID。更多信息请参阅 GitHub Apps 的 REST API 端点。
require 'octokit' app = Octokit::Client.new( client_id: APP_ID, client_secret: PRIVATE_KEY, installation_id: INSTALLATION_ID ) octokit = Octokit::Client.new(bearer_token: app.create_app_installation.access_token)
require 'octokit'
app = Octokit::Client.new(
client_id: APP_ID,
client_secret: PRIVATE_KEY,
installation_id: INSTALLATION_ID
)
octokit = Octokit::Client.new(bearer_token: app.create_app_installation.access_token)
在 GitHub Actions 中进行身份验证
如果您想在 GitHub Actions 工作流中使用 API,GitHub 建议使用内置的 GITHUB_TOKEN 而不是自行创建令牌。您可以通过 permissions 键为 GITHUB_TOKEN 授予权限。有关 GITHUB_TOKEN 的更多信息,请参阅 GITHUB_TOKEN。
如果工作流需要访问工作流仓库之外的资源,则无法使用 GITHUB_TOKEN。此时,请将凭证存为 secret,并在下面的示例中将 GITHUB_TOKEN 替换为您的 secret 名称。有关 secret 的更多信息,请参阅 在 GitHub Actions 中使用 secret。
如果您在 GitHub Actions 工作流中使用 run 关键字执行 Ruby 脚本,可以将 GITHUB_TOKEN 的值存入环境变量。脚本可以通过 ENV['VARIABLE_NAME'] 读取该环境变量。
例如,下面的工作流步骤将 GITHUB_TOKEN 存入名为 TOKEN 的环境变量
- name: Run script
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ruby .github/actions-scripts/use-the-api.rb
工作流运行的脚本使用 ENV['TOKEN'] 进行身份验证
require 'octokit' octokit = Octokit::Client.new(access_token: ENV['TOKEN'])
require 'octokit'
octokit = Octokit::Client.new(access_token: ENV['TOKEN'])
无身份验证的实例化
您可以在不进行身份验证的情况下使用 REST API,但此时速率限制更低,且某些端点不可用。若要创建不带身份验证的 Octokit 实例,请勿传入 access_token 选项。
require 'octokit' octokit = Octokit::Client.new
require 'octokit'
octokit = Octokit::Client.new
发起请求
Octokit 提供多种发起请求的方式。如果您知道端点的 HTTP 动词和路径,可以使用 request 方法;如果想利用 IDE 的自动补全功能,可以使用 rest 方法。对于分页端点,可使用 paginate 方法获取多页数据。
使用 request 方法发起请求
使用 request 方法时,将 HTTP 方法和路径作为第一个参数传入。将请求体、查询或路径参数放入一个哈希作为第二个参数。例如,向 /repos/{owner}/{repo}/issues 发起 GET 请求,并传入 owner、repo、per_page 参数:
octokit.request("GET /repos/{owner}/{repo}/issues", owner: "github", repo: "docs", per_page: 2)
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 头:
octokit.request("POST /markdown/raw", text: "Hello **world**", headers: { "content-type" => "text/plain" })
octokit.request("POST /markdown/raw", text: "Hello **world**", headers: { "content-type" => "text/plain" })
使用 rest 端点方法发起请求
Octokit 为每个 REST API 端点都提供了对应的 rest 方法。这些方法在 IDE 中通常会自动补全,使用起来更方便。您可以将所有参数放入哈希传递给该方法。
octokit.rest.issues.list_for_repo(owner: "github", repo: "docs", per_page: 2)
octokit.rest.issues.list_for_repo(owner: "github", repo: "docs", per_page: 2)
发起分页请求
如果端点支持分页且您想获取多页结果,可使用 paginate 方法。paginate 会持续请求下一页,直至到达最后一页,然后以数组形式返回所有结果。少数端点会把分页结果放在对象的数组属性中返回,而不是直接返回数组。paginate 始终返回项目数组,即使原始返回是对象。
例如,下面的示例获取 github/docs 仓库的所有 Issue。虽然一次请求 100 条 Issue,但函数会一直请求,直至返回最后一页数据后才结束。
issue_data = octokit.paginate("GET /repos/{owner}/{repo}/issues", owner: "github", repo: "docs", per_page: 100)
issue_data = octokit.paginate("GET /repos/{owner}/{repo}/issues", owner: "github", repo: "docs", per_page: 100)
paginate 方法接受一个可选块,您可以在块中处理每页返回的数据。这样可以只收集响应中您需要的部分信息。例如,下面的示例会持续获取结果,直到出现标题中包含 “test” 的 Issue 为止。对于返回的每页数据,仅保存 Issue 标题和作者。
issue_data = octokit.paginate("GET /repos/{owner}/{repo}/issues", owner: "github", repo: "docs", per_page: 100) do |response, done|
response.data.map do |issue|
if issue.title.include?("test")
done.call
end
{ title: issue.title, author: issue.user.login }
end
end
issue_data = octokit.paginate("GET /repos/{owner}/{repo}/issues", owner: "github", repo: "docs", per_page: 100) do |response, done|
response.data.map do |issue|
if issue.title.include?("test")
done.call
end
{ title: issue.title, author: issue.user.login }
end
end
如果不想一次性获取所有结果,也可以使用 octokit.paginate.iterator() 逐页迭代。下面的示例每次获取一页并在获取下一页前处理该页的每个对象。遇到标题中包含 “test” 的 Issue 时,脚本停止迭代并返回已处理对象的标题和作者。迭代器是获取分页数据时最节省内存的方式。
iterator = octokit.paginate.iterator("GET /repos/{owner}/{repo}/issues", owner: "github", repo: "docs", per_page: 100)
issue_data = []
break_loop = false
iterator.each do |data|
break if break_loop
data.each do |issue|
if issue.title.include?("test")
break_loop = true
break
else
issue_data << { title: issue.title, author: issue.user.login }
end
end
end
iterator = octokit.paginate.iterator("GET /repos/{owner}/{repo}/issues", owner: "github", repo: "docs", per_page: 100)
issue_data = []
break_loop = false
iterator.each do |data|
break if break_loop
data.each do |issue|
if issue.title.include?("test")
break_loop = true
break
else
issue_data << { title: issue.title, author: issue.user.login }
end
end
end
您同样可以将 paginate 方法与 rest 端点方法结合使用。将 rest 方法作为第一个参数,参数哈希作为第二个参数传入。
iterator = octokit.paginate.iterator(octokit.rest.issues.list_for_repo, owner: "github", repo: "docs", per_page: 100)
iterator = octokit.paginate.iterator(octokit.rest.issues.list_for_repo, owner: "github", repo: "docs", per_page: 100)
有关分页的更多信息,请参阅 REST API 中的分页使用方式。
捕获错误
捕获所有错误
有时,GitHub REST API 会返回错误。例如,访问令牌过期或缺少必填参数时会报错。Octokit.rb 会在除 400 Bad Request、401 Unauthorized、403 Forbidden、404 Not Found、422 Unprocessable Entity 之外的错误时自动重试请求。如果在重试后仍然出现 API 错误,Octokit.rb 会抛出一个错误对象,其中包含响应的 HTTP 状态码(response.status)以及响应头(response.headers)。您应在代码中捕获并处理这些错误,例如使用 try/catch 块。
begin
files_changed = []
iterator = octokit.paginate.iterator("GET /repos/{owner}/{repo}/pulls/{pull_number}/files", owner: "github", repo: "docs", pull_number: 22809, per_page: 100)
iterator.each do | data |
files_changed.concat(data.map {
| file_data | file_data.filename
})
end
rescue Octokit::Error => error
if error.response
puts "Error! Status: #{error.response.status}. Message: #{error.response.data.message}"
end
puts error
end
begin
files_changed = []
iterator = octokit.paginate.iterator("GET /repos/{owner}/{repo}/pulls/{pull_number}/files", owner: "github", repo: "docs", pull_number: 22809, per_page: 100)
iterator.each do | data |
files_changed.concat(data.map {
| file_data | file_data.filename
})
end
rescue Octokit::Error => error
if error.response
puts "Error! Status: #{error.response.status}. Message: #{error.response.data.message}"
end
puts error
end
处理特定错误代码
有时,GitHub 会使用 4xx 状态码表示非错误的响应。如果您使用的端点如此,可为特定错误添加额外的处理逻辑。例如,GET /user/starred/{owner}/{repo} 若仓库未被 star,则返回 404。下面的示例将 404 当作“未 star” 的标识,其余错误码仍视为错误。
begin
octokit.request("GET /user/starred/{owner}/{repo}", owner: "github", repo: "docs")
puts "The repository is starred by me"
rescue Octokit::NotFound => error
puts "The repository is not starred by me"
rescue Octokit::Error => error
puts "An error occurred while checking if the repository is starred: #{error&.response&.data&.message}"
end
begin
octokit.request("GET /user/starred/{owner}/{repo}", owner: "github", repo: "docs")
puts "The repository is starred by me"
rescue Octokit::NotFound => error
puts "The repository is not starred by me"
rescue Octokit::Error => error
puts "An error occurred while checking if the repository is starred: #{error&.response&.data&.message}"
end
处理速率限制错误
如果收到速率限制错误,您可能需要在等待后重试请求。当受到速率限制时,GitHub 会返回 403 Forbidden,并且 x-ratelimit-remaining 响应头的值为 "0"。响应头中还会包含 x-ratelimit-reset,该值为当前速率限制窗口重置的 UTC 时间戳(秒)。您可以在该时间点之后重新发起请求。
def request_retry(route, parameters)
begin
response = octokit.request(route, parameters)
return response
rescue Octokit::RateLimitExceeded => error
reset_time_epoch_seconds = error.response.headers['x-ratelimit-reset'].to_i
current_time_epoch_seconds = Time.now.to_i
seconds_to_wait = reset_time_epoch_seconds - current_time_epoch_seconds
puts "You have exceeded your rate limit. Retrying in #{seconds_to_wait} seconds."
sleep(seconds_to_wait)
retry
rescue Octokit::Error => error
puts error
end
end
response = request_retry("GET /repos/{owner}/{repo}/issues", owner: "github", repo: "docs", per_page: 2)
def request_retry(route, parameters)
begin
response = octokit.request(route, parameters)
return response
rescue Octokit::RateLimitExceeded => error
reset_time_epoch_seconds = error.response.headers['x-ratelimit-reset'].to_i
current_time_epoch_seconds = Time.now.to_i
seconds_to_wait = reset_time_epoch_seconds - current_time_epoch_seconds
puts "You have exceeded your rate limit. Retrying in #{seconds_to_wait} seconds."
sleep(seconds_to_wait)
retry
rescue Octokit::Error => error
puts error
end
end
response = request_retry("GET /repos/{owner}/{repo}/issues", owner: "github", repo: "docs", per_page: 2)
使用响应
request 方法在请求成功时返回一个响应对象。该对象包含 data(端点返回的响应体)、status(HTTP 响应码)、url(请求的 URL)以及 headers(包含响应头的哈希)。除非另有说明,响应体均为 JSON 格式。某些端点不返回响应体,此时 data 属性会被省略。
response = octokit.request("GET /repos/{owner}/{repo}/issues/{issue_number}", owner: "github", repo: "docs", issue_number: 11901)
puts "The status of the response is: #{response.status}"
puts "The request URL was: #{response.url}"
puts "The x-ratelimit-remaining response header is: #{response.headers['x-ratelimit-remaining']}"
puts "The issue title is: #{response.data['title']}"
response = octokit.request("GET /repos/{owner}/{repo}/issues/{issue_number}", owner: "github", repo: "docs", issue_number: 11901)
puts "The status of the response is: #{response.status}"
puts "The request URL was: #{response.url}"
puts "The x-ratelimit-remaining response header is: #{response.headers['x-ratelimit-remaining']}"
puts "The issue title is: #{response.data['title']}"
同样,paginate 方法也返回一个响应对象。如果 request 成功,response 对象中同样包含 data、status、url 和 headers。
response = octokit.paginate("GET /repos/{owner}/{repo}/issues", owner: "github", repo: "docs", per_page: 100)
puts "#{response.data.length} issues were returned"
puts "The title of the first issue is: #{response.data[0]['title']}"
response = octokit.paginate("GET /repos/{owner}/{repo}/issues", owner: "github", repo: "docs", per_page: 100)
puts "#{response.data.length} issues were returned"
puts "The title of the first issue is: #{response.data[0]['title']}"
示例脚本
下面是使用 Octokit.rb 的完整示例脚本。脚本导入 Octokit 并创建一个新的 Octokit 实例。如果想使用 GitHub App 而不是个人访问令牌进行身份验证,则应导入并实例化 App 而不是 Octokit。有关详细信息,请参阅本指南中的 使用 GitHub App 进行身份验证。
get_changed_files 函数获取 Pull Request 中所有变更的文件。comment_if_data_files_changed 函数调用 get_changed_files。如果变更的文件路径中包含 /data/,则函数会在该 Pull Request 上发表评论。
require "octokit"
octokit = Octokit::Client.new(access_token: "YOUR-TOKEN")
def get_changed_files(octokit, owner, repo, pull_number)
files_changed = []
begin
iterator = octokit.paginate.iterator("GET /repos/{owner}/{repo}/pulls/{pull_number}/files", owner: owner, repo: repo, pull_number: pull_number, per_page: 100)
iterator.each do | data |
files_changed.concat(data.map {
| file_data | file_data.filename
})
end
rescue Octokit::Error => error
if error.response
puts "Error! Status: #{error.response.status}. Message: #{error.response.data.message}"
end
puts error
end
files_changed
end
def comment_if_data_files_changed(octokit, owner, repo, pull_number)
changed_files = get_changed_files(octokit, owner, repo, pull_number)
if changed_files.any ? {
| file_name | /\/data\//i.match ? (file_name)
}
begin
comment = octokit.create_pull_request_review_comment(owner, repo, pull_number, "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.")
comment.html_url
rescue Octokit::Error => error
if error.response
puts "Error! Status: #{error.response.status}. Message: #{error.response.data.message}"
end
puts error
end
end
end
# Example usage
owner = "github"
repo = "docs"
pull_number = 22809
comment_url = comment_if_data_files_changed(octokit, owner, repo, pull_number)
puts "A comment was added to the pull request: #{comment_url}"
require "octokit"
octokit = Octokit::Client.new(access_token: "YOUR-TOKEN")
def get_changed_files(octokit, owner, repo, pull_number)
files_changed = []
begin
iterator = octokit.paginate.iterator("GET /repos/{owner}/{repo}/pulls/{pull_number}/files", owner: owner, repo: repo, pull_number: pull_number, per_page: 100)
iterator.each do | data |
files_changed.concat(data.map {
| file_data | file_data.filename
})
end
rescue Octokit::Error => error
if error.response
puts "Error! Status: #{error.response.status}. Message: #{error.response.data.message}"
end
puts error
end
files_changed
end
def comment_if_data_files_changed(octokit, owner, repo, pull_number)
changed_files = get_changed_files(octokit, owner, repo, pull_number)
if changed_files.any ? {
| file_name | /\/data\//i.match ? (file_name)
}
begin
comment = octokit.create_pull_request_review_comment(owner, repo, pull_number, "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.")
comment.html_url
rescue Octokit::Error => error
if error.response
puts "Error! Status: #{error.response.status}. Message: #{error.response.data.message}"
end
puts error
end
end
end
# Example usage
owner = "github"
repo = "docs"
pull_number = 22809
comment_url = comment_if_data_files_changed(octokit, owner, repo, pull_number)
puts "A comment was added to the pull request: #{comment_url}"
注意
这只是一个基础示例。实际使用时,您可能需要加入错误处理和条件判断以应对各种场景。
后续步骤
想了解更多关于 GitHub REST API 与 Octokit.rb 的使用,请参考以下资源
- 想了解更多 Octokit.rb,请访问 Octokit.rb 文档。
- 若想查找 GitHub 所提供的 REST API 端点的详细信息,包括请求和响应结构,请参阅 GitHub REST API 文档。