跳至主要内容

使用 REST API 和 JavaScript 编写脚本

使用 Octokit.js SDK 编写一个与 REST API 交互的脚本。

关于 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 替换为对您的个人访问令牌的引用。

JavaScript
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}/installationGET /repos/{owner}/{repo}/installationGET /orgs/{org}/installation 端点获取安装 ID。有关更多信息,请参阅 "GitHub 应用的 REST API 端点"。

JavaScript
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 进行身份验证

JavaScript
import { Octokit } from "octokit";

const octokit = new Octokit({ 
  auth: process.env.TOKEN,
});

在没有身份验证的情况下实例化

您可以使用 REST API,而无需进行身份验证,尽管您的速率限制会更低,并且您将无法使用某些端点。要创建 Octokit 的实例,而无需进行身份验证,请不要传递 auth 参数。

JavaScript
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` 参数

JavaScript
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`

JavaScript
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 中自动补全,以方便使用。您可以将任何参数作为对象传递给该方法。

JavaScript
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 个问题,但该函数在到达最后一页数据之前不会返回。

JavaScript
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”的问题。对于返回的数据页面,仅存储问题标题和作者。

JavaScript
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”的问题,脚本就会停止迭代并返回已处理的每个对象的标题和作者。迭代器是获取分页数据的最高效方法。

JavaScript
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 端点方法作为第一个参数传递。将任何参数作为第二个参数传递。

JavaScript
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 Request401 Unauthorized403 Forbidden404 Not Found422 Unprocessable Entity 之外的错误时会自动重试请求。如果即使在重试后也发生 API 错误,Octokit.js 会抛出一个错误,其中包含响应的 HTTP 状态代码 (response.status) 和响应头 (response.headers)。您应该在代码中处理这些错误。例如,您可以使用 try/catch 块来捕获错误。

JavaScript
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 响应来指示存储库未加星标;所有其他错误代码都被视为错误。

JavaScript
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 指定的时间后重试您的请求。

JavaScript
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 属性将被省略。

JavaScript
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 或头。

JavaScript
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/,则该函数将在拉取请求中添加评论。

JavaScript
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});

下一步