์ด ๋ฌธ์๋ AI ์์ด์ ํธ๊ฐ ํ๋ก์ ํธ๋ฅผ ์ดํดํ๊ณ ์์ ํ ์ ์๋๋ก ๋๋ ๊ฐ์ด๋์ ๋๋ค.
DaleStudy ์กฐ์ง์ GitHub App(https://github.com/apps/dalestudy)
Fork PR์์๋ ์๋ํ๋๋ก GitHub Projects v2์ Week ํ๋๋ฅผ ์กฐํํ๊ณ , Week ์ค์ ์ด ๋๋ฝ๋ PR์ ์๋์ผ๋ก ๊ฒฝ๊ณ ๋๊ธ์ ์์ฑํ๋ฉฐ, Week ์ค์ ์ด ์๋ฃ๋๋ฉด ๊ฒฝ๊ณ ๋๊ธ์ ์๋์ผ๋ก ์ญ์ ํ๋ค.
- ๋์ Repository: https://github.com/DaleStudy/leetcode-study
- ํธ๋ฆฌ๊ฑฐ ๋ฐฉ์:
- ์ค์๊ฐ: GitHub Organization Webhook (
projects_v2_item,pull_request์ด๋ฒคํธ) - ์๋:
POST /check-weeks์๋ํฌ์ธํธ ์ง์ ํธ์ถ
- ์ค์๊ฐ: GitHub Organization Webhook (
- Runtime: Cloudflare Workers
- Language: JavaScript (ES Modules)
- Authentication: GitHub App (JWT + Installation Token)
- APIs: GitHub REST API, GitHub GraphQL API
~/work/github/
โโโ index.js # Worker ๋ฉ์ธ ์ฝ๋ (์๋ํฌ์ธํธ ๋ผ์ฐํ
)
โโโ wrangler.jsonc # Cloudflare Workers ์ค์
โโโ .env # ๋ก์ปฌ ํ๊ฒฝ ๋ณ์ (์ปค๋ฐ ์ ์ธ)
โโโ .gitignore # Git ์ ์ธ ํ์ผ
โโโ handlers/ # ๊ธฐ๋ฅ๋ณ ํธ๋ค๋ฌ
โ โโโ check-weeks.js # PR Week ์ค์ ๊ฒ์ฌ (์๋ ํธ์ถ์ฉ)
โ โโโ webhooks.js # GitHub webhook ์ด๋ฒคํธ ์ฒ๋ฆฌ
โโโ utils/ # ๊ณตํต ์ ํธ๋ฆฌํฐ
โ โโโ cors.js # CORS ํค๋ ๋ฐ ์๋ต ์ ํธ๋ฆฌํฐ
โ โโโ github.js # GitHub ์ธ์ฆ ๋ฐ API ์ ํธ๋ฆฌํฐ
โ โโโ webhook.js # Webhook signature ๊ฒ์ฆ
โโโ README.md # ํ๋ก์ ํธ ์ค๋ช
โโโ DEPLOYMENT.md # ๋ฐฐํฌ ๊ฐ์ด๋
โโโ AGENTS.md # ์ด ํ์ผ (AI ์์ด์ ํธ ๊ฐ์ด๋)
โโโ CLAUDE.md # Claude Code ์ฐธ์กฐ ํ์ผ (AGENTS.md๋ก ๋ฆฌ๋ค์ด๋ ํธ)
โโโ *.pem # GitHub App Private Keys (์ปค๋ฐ ์ ์ธ)
- index.js (32์ค): ์๋ํฌ์ธํธ ๋ผ์ฐํ ๋ง ๋ด๋น. pathname๋ณ ํธ๋ค๋ฌ ํธ์ถ
- handlers/: ๊ธฐ๋ฅ๋ณ ํธ๋ค๋ฌ
check-weeks.js: PR Week ์ค์ ๊ฒ์ฌ, ๋๊ธ ์์ฑ/์ญ์
- utils/: ์ฌ๋ฌ ํธ๋ค๋ฌ์์ ๊ณตํต์ผ๋ก ์ฌ์ฉํ๋ ์ ํธ๋ฆฌํฐ
cors.js: CORS ํค๋ ๊ด๋ฆฌ ๋ฐ ์๋ต ์์ฑ (corsResponse,errorResponse)github.js: GitHub App ์ธ์ฆ (JWT, Installation Token), RSA ์๋ช
- ๊ธฐ๋ฅ๋ณ ํธ๋ค๋ฌ ํ์ผ ์์ฑ (์:
handlers/new-feature.js) index.js์ pathname ๋ผ์ฐํ ์ถ๊ฐ
// handlers/new-feature.js ์์ฑ
export async function newFeature(request, env) {
// ๋น์ฆ๋์ค ๋ก์ง
return corsResponse({ success: true });
}
// index.js์ ๋ผ์ฐํ
์ถ๊ฐ
import { newFeature } from "./handlers/new-feature.js";
if (url.pathname === "/new-feature") {
return newFeature(request, env);
}# ๋ก์ปฌ ๊ฐ๋ฐ ์๋ฒ ์คํ (ํฌํธ 8787)
wrangler dev
# ๋ก์ปฌ ํ
์คํธ (๋ณ๋ ํฐ๋ฏธ๋)
curl -X POST http://localhost:8787/check-weeks \
-H "Content-Type: application/json" \
-d '{"repo_owner": "DaleStudy", "repo_name": "leetcode-study"}'# Worker ๋ฐฐํฌ
wrangler deploy
# Secrets ์ค์
wrangler secret put APP_ID # GitHub App ID (์ซ์)
wrangler secret put PRIVATE_KEY # GitHub App Private Key (PEM ์ ์ฒด)
# Secrets ํ์ธ
wrangler secret list
# ์ค์๊ฐ ๋ก๊ทธ ํ์ธ
wrangler tail# ๋ฐฐํฌ๋ Worker ํ
์คํธ
curl -X POST https://github.dalestudy.com/check-weeks \
-H "Content-Type: application/json" \
-d '{"repo_owner": "DaleStudy", "repo_name": "leetcode-study"}'์ธ์ฆ ํ๋ฆ:
- RS256 ์๊ณ ๋ฆฌ์ฆ์ผ๋ก JWT ์์ฑ (Web Crypto API ์ฌ์ฉ)
- JWT๋ก Installation ID ์กฐํ
- Installation Token ๋ฐ๊ธ (10๋ถ ์ ํจ)
- ๋ชจ๋ API ์์ฒญ์ Installation Token ์ฌ์ฉ
์ธ์ฆ ๊ด๋ จ ํจ์:
generateGitHubAppToken(): GitHub App Installation Token ๋ฐ๊ธ (์ ์ฒด ํ๋ฆ ๊ด๋ฆฌ)createJWT(): RS256 JWT ์์ฑ (GitHub App ์ธ์ฆ์ฉ, 10๋ถ ์ ํจ)importPrivateKey(): PEM ํ์ Private Key๋ฅผ Web Crypto API์ฉ์ผ๋ก ๋ณํ (PKCS8/PKCS1 ๋ชจ๋ ์ง์)sign(): RS256 ์๋ช ์์ฑbase64UrlEncode(): Base64 URL-safe ์ธ์ฝ๋ฉ
ํ์ฌ ๊ตฌํ๋ ์๋ํฌ์ธํธ:
GitHub Organization webhook ์์ ์ฉ ์๋ํฌ์ธํธ
- ์ด๋ฒคํธ:
projects_v2_item,pull_request - ์ค์๊ฐ ์ฒ๋ฆฌ: Week ์ค์ ๋ณ๊ฒฝ ์ฆ์ ๊ฐ์ง ๋ฐ ๋๊ธ ์์ฑ/์ญ์
๋ชจ๋ Open PR์์ Week ์ค์ ์ ๊ฒ์ฌํ๊ณ ์๋์ผ๋ก ๋๊ธ ์์ฑ/์ญ์ (์๋ ํธ์ถ์ฉ)
Request:
repo_owner ์๋ต ์ ๊ธฐ๋ณธ๊ฐ์ผ๋ก DaleStudy๊ฐ ์ฌ์ฉ๋ฉ๋๋ค.
{
"repo_name": "leetcode-study"
}Response:
{
"success": true,
"total_prs": 3,
"checked": 3,
"commented": 1,
"deleted": 1,
"results": [
{ "pr": 1970, "week": null, "commented": true },
{ "pr": 1969, "week": "Week 8", "commented": false, "deleted": true }
]
}์ด๋ ค์๋ ๋ต์ ์ ์ถ PR์ ์ผ๊ด ์น์ธํฉ๋๋ค. excludes ๋ฐฐ์ด๋ก ํน์ PR์ ์ ์ธํฉ๋๋ค. ์ด๋ฏธ ์น์ธ๋ PR, maintenance ๋ผ๋ฒจ, Draft ์ํ์ PR์ ์๋์ผ๋ก ์คํต๋ฉ๋๋ค.
Request:
{ "repo_name": "leetcode-study", "excludes": [1972] }Response:
{
"success": true,
"action": "approve",
"repo": "DaleStudy/leetcode-study",
"total_open_prs": 5,
"processed": 2,
"approved": 2,
"skipped": 0,
"results": [
{ "pr": 1970, "title": "week8 solutions", "approved": true },
{ "pr": 1971, "title": "week8 extras", "approved": true }
]
}์ด๋ ค์๋ PR์ ์ผ๊ด ๋ณํฉํฉ๋๋ค. ๊ธฐ๋ณธ ๋ณํฉ ๋ฐฉ์์ merge์ด๋ฉฐ merge_method ๊ฐ์ผ๋ก merge | squash | rebase ์ค ์ ํํ ์ ์์ต๋๋ค. excludes๋ก ํน์ PR์ ์ ์ธํ ์ ์์ต๋๋ค. ์น์ธ ๋ฆฌ๋ทฐ๊ฐ ์๊ฑฐ๋ maintenance ๋ผ๋ฒจ์ด ๋ถ์ PR, Draft PR, GitHub mergeable_state !== "clean" PR์ ์คํต๋๋ฉฐ unknown/behind ์ํ๋ ์ต๋ 1์ด ํ ํ ๋ฒ ๋ ํ์ธํฉ๋๋ค.
Request:
{
"repo_name": "leetcode-study",
"merge_method": "squash",
"excludes": [1972]
}Response:
{
"success": true,
"action": "merge",
"repo": "DaleStudy/leetcode-study",
"merge_method": "squash",
"total_open_prs": 5,
"processed": 2,
"merged": 2,
"skipped": 0,
"results": [
{ "pr": 1970, "title": "week8 solutions", "merged": true, "sha": "abc123" },
{ "pr": 1971, "title": "week8 extras", "merged": true, "sha": "def456" }
]
}- Open PR ๋ชฉ๋ก ์กฐํ (GitHub REST API)
maintenance๋ผ๋ฒจ ์๋ PR ์คํต- ๊ฐ PR์ Week ์ค์ ํ์ธ (GitHub GraphQL API - Projects v2 ์ ๊ทผ ํ์)
- Week ์์ โ ๊ฒฝ๊ณ ๋๊ธ ์์ฑ (์ค๋ณต ๋ฐฉ์ง: Bot์ด ์์ฑํ ๊ฒฝ๊ณ ๋๊ธ์ด ์ด๋ฏธ ์์ผ๋ฉด ์คํต)
- Week ์์ โ ๊ธฐ์กด ๊ฒฝ๊ณ ๋๊ธ ์ญ์ (Bot์ด ์์ฑํ Week ๊ฒฝ๊ณ ๋๊ธ๋ง)
repo_owner !== 'DaleStudy' ์์ฒญ์ 403 Forbidden ๋ฐํ.
contents: read: PR ์ ๋ณด ์กฐํissues: write: ๋๊ธ ์์ฑ ๋ฐ ์ญ์ pull_requests: read & write: PR ๋ชฉ๋ก/์ํ ์กฐํ, ๋ฆฌ๋ทฐ ์์ฑ, ๋ณํฉ ์ํorganization_projects: read: Projects v2์ Week ํ๋ ์ ๊ทผ (GraphQL API)
์ ๋ ์ปค๋ฐ ๊ธ์ง: .env, .dev.vars, *.pem, *.key
wrangler secret put APP_ID # GitHub App ID
wrangler secret put PRIVATE_KEY # GitHub App Private Key (PEM)APP_ID=123456
PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
wrangler deploy- Production: https://github.dalestudy.com
- Worker.dev: https://github.daleseo.workers.dev
์์ธํ ๋ฐฐํฌ ๊ฐ์ด๋๋ DEPLOYMENT.md ์ฐธ๊ณ .
https://github.com/settings/apps/dalestudy
- Webhook URL:
https://github.dalestudy.com/webhooks - Webhook Secret: (์ ํ์ฌํญ) ์์ ํ ๋๋ค ๋ฌธ์์ด
- โ Active: ์ฒดํฌ
Repository permissions:
- Contents: Read
- Issues: Read & write (issue_comment ์ด๋ฒคํธ์ฉ)
- Metadata: Read
- Pull requests: Read & write
- Projects: Read & write (Projects V2)
Subscribe to events:
- โ๏ธ Issue comments (
issue_comment- AI ์ฝ๋ ๋ฆฌ๋ทฐ) - โ๏ธ Projects v2 item (
projects_v2_item- Week ์ฒดํฌ) - โ๏ธ Pull requests (
pull_request- Week ์ฒดํฌ)
# OpenAI API Key (AI ์ฝ๋ ๋ฆฌ๋ทฐ์ฉ, ํ์)
wrangler secret put OPENAI_API_KEY
# Webhook Secret (์ ํ์ฌํญ)
wrangler secret put WEBHOOK_SECRET์ ์ฅ์์ App์ด ์ค์น๋์ด ์๋์ง ํ์ธ:
https://github.com/apps/dalestudy/installations
DaleStudy organization์ ์ค์น๋์ด ์์ด์ผ ํ๋ฉฐ, leetcode-study ์ ์ฅ์ ์ ๊ทผ ๊ถํ ํ์
์ ์ฒด PR์ ํ ๋ฒ์ ๊ฒ์ฌํ๊ณ ์ถ์ ๋:
curl -X POST https://github.dalestudy.com/check-weeks \
-H "Content-Type: application/json" \
-d '{"repo_owner": "DaleStudy", "repo_name": "leetcode-study"}'- โ Node.js ๋ชจ๋ ์ฌ์ฉ ๋ถ๊ฐ (crypto, buffer ๋ฑ)
- โ Web ํ์ค API๋ง ์ฌ์ฉ (fetch, Web Crypto API)
- โ npm ํจํค์ง ๋๋ถ๋ถ ํธํ ์ ๋จ (@octokit/app ๋ฑ)
- โ ์์ JavaScript + Web APIs๋ก ๊ตฌํ
์๋ก์ด ์๋ํ ๊ธฐ๋ฅ์ ์ถ๊ฐํ ๋ ๋ค์ ๋จ๊ณ๋ฅผ ๋ฐ๋ฅด์ธ์:
- ์๋ํฌ์ธํธ ์ถ๊ฐ:
index.js์fetch()ํจ์์ ์๋ก์ด pathname ๋ผ์ฐํ ์ถ๊ฐ - ํธ๋ค๋ฌ ํจ์ ์์ฑ: ๋น์ฆ๋์ค ๋ก์ง์ ๋ณ๋ ํจ์๋ก ๋ถ๋ฆฌ (์:
handleCheckAllPrs) - GitHub App ๊ถํ ํ์ธ: ํ์ํ ๊ถํ์ด ์๋์ง ํ์ธํ๊ณ ์์ผ๋ฉด ์ถ๊ฐ
- ๋ฌธ์ ์ ๋ฐ์ดํธ: AGENTS.md, README.md์ ์ ๊ธฐ๋ฅ ๋ฌธ์ํ
- ํ
์คํธ: ๋ก์ปฌ(
wrangler dev)์์ ๋จผ์ ํ ์คํธ ํ ๋ฐฐํฌ
-
Octokit ์ฌ์ฉ ๊ธ์ง
- Cloudflare Workers์์ ์๋ํ์ง ์์
- fetch API ์ง์ ์ฌ์ฉ
-
Private Key ์ฒ๋ฆฌ
- PKCS8 ๋๋ PKCS1 ํ์ ์ง์
- Web Crypto API๋ก import
-
GraphQL ์ฟผ๋ฆฌ ์ฃผ์
- GraphQL ์ฟผ๋ฆฌ์์ ๋ณ์๋ฅผ ๋ฌธ์์ด ํ ํ๋ฆฟ์ผ๋ก ์ง์ ์ฝ์ (GraphQL ๋ณ์ ๋ฌธ๋ฒ ์ฌ์ฉ ์ ํจ)
- ์ ๋ ฅ๊ฐ ๊ฒ์ฆ์ด ์ค์ (SQL Injection ์คํ์ผ ์ทจ์ฝ์ ๋ฐฉ์ง)
-
์๋ฌ ํธ๋ค๋ง
- Worker๋ ์๋ฌ ๋ฐ์ ์ 500 ๋ฐํ
- ๋ก๊ทธ๋
wrangler tail๋ก ํ์ธ
-
CORS ํค๋
- ๋ชจ๋ ์๋ต์ CORS ํค๋ ํฌํจ (
Access-Control-Allow-Origin: *)
- ๋ชจ๋ ์๋ต์ CORS ํค๋ ํฌํจ (
-
์ฝ๋ ์ฌ์ฌ์ฉ
- GitHub ์ธ์ฆ ๋ก์ง (
generateGitHubAppToken,createJWT๋ฑ)์ ๋ชจ๋ ๊ธฐ๋ฅ์์ ๊ณตํต์ผ๋ก ์ฌ์ฉ - ์ ๊ธฐ๋ฅ ์ถ๊ฐ ์ ๊ธฐ์กด ์ ํธ๋ฆฌํฐ ํจ์ ํ์ฉ
- GitHub ์ธ์ฆ ๋ก์ง (