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.js ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด octokit
์(๋ฅผ) ์ค์นํ๊ณ ๊ฐ์ ธ์์ผ ํฉ๋๋ค. ์ด ๊ฐ์ด๋์์๋ ES6์ ๋ฐ๋ผ import ๋ฌธ์ ์ฌ์ฉํฉ๋๋ค. ๋ค์ํ ์ค์น ๋ฐ ๊ฐ์ ธ์ค๊ธฐ ๋ฉ์๋์ ๋ํ ์์ธํ ์ ๋ณด๋ Octokit.js ์ถ๊ฐ ์ ๋ณด ์ฌ์ฉ ์น์
์ ์ฐธ์กฐํ์ธ์.
์ธ์คํด์คํ ๋ฐ ์ธ์ฆ
๊ฒฝ๊ณ
์ธ์ฆ ์๊ฒฉ ์ฆ๋ช ์ ์ํธ์ฒ๋ผ ์ฒ๋ฆฌํฉ๋๋ค.
์๊ฒฉ ์ฆ๋ช ์ ์์ ํ๊ฒ ์ ์งํ๊ธฐ ์ํด ๋น๋ฐ๋ก ์ ์ฅํ๊ณ GitHub Actions๋ฅผ ํตํด ์คํฌ๋ฆฝํธ๋ฅผ ์คํํ ์ ์์ต๋๋ค. ์์ธํ ๋ด์ฉ์ GitHub Actions์์ ๋น๋ฐ ์ฌ์ฉ์(๋ฅผ) ์ฐธ์กฐํ์ธ์.
๋ํ ์๊ฒฉ ์ฆ๋ช ์ Codespaces ๋น๋ฐ๋ก ์ ์ฅํ๊ณ Codespaces์์ ์คํฌ๋ฆฝํธ๋ฅผ ์คํํ ์๋ ์์ต๋๋ค. ์์ธํ ๋ด์ฉ์ GitHub Codespaces์ ๋ํ ๊ณ์ ๋ณ ๋น๋ฐ ๊ด๋ฆฌ์(๋ฅผ) ์ฐธ์กฐํ์ธ์.
์ด(๊ฐ) ์ต์ ์ ์ฌ์ฉํ ์ ์๋ ๊ฒฝ์ฐ, ๋ค๋ฅธ CLI ์๋น์ค๋ฅผ ์ฌ์ฉํ์ฌ ์๊ฒฉ ์ฆ๋ช ์ ์์ ํ๊ฒ ์ ์ฅํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
personal access token์(๋ฅผ) ์ฌ์ฉํ์ฌ ์ธ์ฆ
๊ฐ์ธ์ฉ์ผ๋ก GitHub REST API๋ฅผ ์ฌ์ฉํ๋ ค๋ ๊ฒฝ์ฐ personal access token๋ฅผ ๋ง๋ค ์ ์์ต๋๋ค. personal access token์ ๋ง๋๋ ๋ฐฉ๋ฒ์ ๋ํ ์์ธํ ๋ด์ฉ์ ๊ฐ์ธ์ฉ ์ก์ธ์ค ํ ํฐ ๊ด๋ฆฌ์(๋ฅผ) ์ฐธ์กฐํ์ธ์.
๋จผ์ octokit
์์ Octokit
๋ฅผ ๊ฐ์ ธ์ต๋๋ค. ๊ทธ๋ฐ ๋ค์ Octokit
์ธ์คํด์ค๋ฅผ ๋ง๋ค ๋ personal access token์(๋ฅผ) ํจ์คํฉ๋๋ค. ๋ค์ ์์ ์์๋ YOUR-TOKEN
์ personal access 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 ์ฑ ๋ฑ๋ก ๋ฐ GitHub ์ฑ์ ์ฌ์ฉํ ์ธ์ฆ ์ ๋ณด์(๋ฅผ) ์ฐธ์กฐํ์ธ์.
octokit
์์ Octokit
๋ฅผ ๊ฐ์ ธ์ค๋ ๋์ App
์ ๊ฐ์ ธ์ต๋๋ค. ๋ค์ ์์ ์์๋ 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 ์๋ํฌ์ธํธ์(๋ฅผ) ์ฐธ์กฐํ์ธ์.
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_TOKEN
์ ๋น๋ฐ์ ์ด๋ฆ์ผ๋ก ๋ฐ๊ฟ๋๋ค. ๋น๋ฐ์ ๋ํ ์์ธํ ๋ด์ฉ์ GitHub Actions์์ ๋น๋ฐ ์ฌ์ฉ์(๋ฅผ) ์ฐธ์กฐํ์ธ์.
๋ํ run
ํค์๋๋ฅผ ์ฌ์ฉํ์ฌGitHub Actions ์ํฌํ๋ก์์ JavaScript ์คํฌ๋ฆฝํธ๋ฅผ ์คํํ ์ ์์ผ๋ฉฐ GITHUB_TOKEN
์ ๊ฐ์ ํ๊ฒฝ ๋ณ์๋ก ์ ์ฅํ ์ ์์ต๋๋ค. ์คํฌ๋ฆฝํธ๋ ํ๊ฒฝ ๋ณ์์ process.env.VARIABLE_NAME
๋ก ์ก์ธ์คํ ์ ์์ต๋๋ค.
์๋ฅผ ๋ค์ด ์ด ์ํฌํ๋ก ๋จ๊ณ์์ TOKEN
์ด๋ผ๋ ํ๊ฒฝ ๋ณ์์ GITHUB_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
๋ฉ์๋๋ ์๋ต์์ ์ํ๋ ๋ฐ์ดํฐ๋ง ์์งํ๋ ๋ฐ ์ฌ์ฉํ ์ ์๋ ์ ํ์ ๋งต ํจ์๋ฅผ ์๋ฝํฉ๋๋ค. ์ด๋ ๊ฒ ํ๋ฉด ์คํฌ๋ฆฝํธ์ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ด ์ค์ด๋ญ๋๋ค. 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}];
}
}
}
rest
์๋ํฌ์ธํธ ๋ฉ์๋์์๋ paginate
๋ฉ์๋๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค. 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๋ ์๋ต(response.status
) ๋ฐ ์๋ต ํค๋(response.headers
)์ HTTP ์ํ ์ฝ๋๋ฅผ ํฌํจํ๋ ์ค๋ฅ๋ฅผ throwํฉ๋๋ค. ์ฝ๋์์ ์ด๋ฌํ ์ค๋ฅ๋ฅผ ์ฒ๋ฆฌํด์ผ ํฉ๋๋ค. ์๋ฅผ ๋ค์ด 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"
์ด ๋ฉ๋๋ค. ์๋ต ํค๋์๋ ํ์ฌ ํธ๋ํฝ๋ฅ ์ ํ ์ฐฝ์ด ์ฌ์ค์ ๋๋ ์๊ฐ์ UTC Epoch ์ด ๋จ์๋ก ์๋ ค์ฃผ๋ x-ratelimit-reset
ํค๋๊ฐ ํฌํจ๋ฉ๋๋ค. 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
๋ฉ์๋๋ ํ๋ผ๋ฏธ์ค๋ฅผ ๋ฐํํฉ๋๋ค. ์์ฒญ์ด ์ฑ๊ณตํ๋ฉด ํ๋ผ๋ฏธ์ค๋ ์๋ํฌ์ธํธ์์ ๋ฐํ๋ ๋ฐ์ดํฐ ๋ฐฐ์ด๋ก ํ์ธ๋ฉ๋๋ค. 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
๋ฅผ ๋ง๋ญ๋๋ค. personal access token ๋์ GitHub App์(๋ฅผ) ์ฌ์ฉํ์ฌ ์ธ์ฆํ๋ ค๋ ๊ฒฝ์ฐ Octokit
๋์ App
์ ๊ฐ์ ธ์ค๊ณ ์ธ์คํด์คํํฉ๋๋ค. ์์ธํ ์ ๋ณด๋ 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์์ GitHub Docs ๋ฆฌํฌ์งํ ๋ฆฌ๋ฅผ ๊ฒ์ํ์ฌ Octokit.js๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ์ดํด๋ด ๋๋ค.