DaleStudy ์กฐ์ง์ GitHub App์ด ๋ฐ๋ ์นํ ๊ณผ ์๋ํ ์์ฒญ์ ์ฒ๋ฆฌํ๋ Cloudflare Worker. ์ฃผ ๋์์ leetcode-study ์ ์ฅ์๋ค.
- ์ด์: https://github.dalestudy.com (workers.dev: https://github.dalestudy.workers.dev)
- ์คํ: Cloudflare Workers, JavaScript(ES Modules), Vitest, Bun
bun install
bun run test # ์ ์ฒด ํ
์คํธ (CI ์ ๋ฐฐํฌ ์ ์ ๊ฐ์ ๋ช
๋ น์ ๋๋ค)
bun run test -- handlers/webhooks.test.js
wrangler dev # http://localhost:8787
wrangler tail # ์ด์ ๋ก๊ทธindex.js ์๋ํฌ์ธํธ ๋ผ์ฐํ
๋ง ํ๋ค (POST ๋ง ๋ฐ๋๋ค)
handlers/ ๊ธฐ๋ฅ๋ณ ํธ๋ค๋ฌ์ ๋จ์ ํ
์คํธ(*.test.js)
utils/ ๊ณตํต ์ ํธ๋ฆฌํฐ์ ๋จ์ ํ
์คํธ (github.js: App ์ธ์ฆ, prActions.js: ์น์ธยท๋ณํฉ ๊ณตํต ๋ก์ง)
tests/ Worker runtime smoke test, subrequest ์์ฐ, ๊ต์ฐจ ๋ชจ๋ ํ
์คํธ
wrangler.jsonc Worker ์ค์ ๊ณผ ๊ณต๊ฐ ๊ฐ(vars)
์ ๊ธฐ๋ฅ์ handlers/ ์ ํธ๋ค๋ฌ๋ฅผ ๋ง๋ค๊ณ index.js ์ ๊ฒฝ๋ก๋ฅผ ์ถ๊ฐํ๋ค. ํ์ํ GitHub App ๊ถํ์ด ์๋์ง ํ์ธํ๊ณ , ํธ๋ค๋ฌ ์์ ํ
์คํธ๋ฅผ ๋๋ค.
repo_owner ๋ฅผ ๋ฐ๋ ์๋ํฌ์ธํธ๋ DaleStudy ๊ฐ ์๋๋ฉด 403 ์ด๋ค (์๋ตํ๋ฉด DaleStudy). /check-weeks ๋ ์ ์ฅ์๋ leetcode-study ๋ง ๋ฐ๋๋ค.
| ๊ฒฝ๋ก | ํ๋ ์ผ |
|---|---|
POST /webhooks |
GitHub App ์นํ ์์ (์๋ "์นํ ") |
POST /check-weeks |
์ด๋ฆฐ PR ์ ์ฒด์ Week ์ค์ ์ ๊ฒ์ฌํด ๊ฒฝ๊ณ ๋๊ธ์ ๋ฌ๊ฑฐ๋ ์ง์ด๋ค. { "repo_name" } |
POST /approve-prs |
ํด๋น ์ฃผ์ฐจ ํ์ด PR ์ ์ผ๊ด ์น์ธํ๋ค. { "repo_name", "week": "Week 8", "excludes"? } |
POST /merge-prs |
ํด๋น ์ฃผ์ฐจ์ ์น์ธ๋ PR ์ ์ผ๊ด ๋ณํฉํ๋ค. { "repo_name", "week", "excludes"?, "merge_method"? } (๊ธฐ๋ณธ squash) |
POST /resolve-iteration |
๋ ์ง๊ฐ ๋ช ๊ธฐ ๋ช ์ฃผ์ฐจ์ธ์ง ํ๋ก์ ํธ ๋ณด๋์์ ์ฝ๋๋ค. { "date": "2026-08-08" } |
POST /internal/* |
์นํ
ํธ๋ค๋ฌ๊ฐ AI ์์
์ ๋ณ๋ invocation ์ผ๋ก ๋๊ธธ ๋ ์ด๋ค (X-Internal-Secret ์ธ์ฆ) |
์น์ธยท๋ณํฉ์ ๊ณตํต ๊ท์น์ utils/prActions.js ์ ์๋ค. ์์ฝํ๋ฉด Week ๊ฐ ๋ค๋ฅด๊ฑฐ๋ Status ๊ฐ Solving ์ธ PR, Draft PR ์ ๊ฑด๋๋ฐ๊ณ , ์น์ธ์ CI ๊ฐ ํต๊ณผํ PR ์๋ง, ๋ณํฉ์ ์น์ธ ๋ฆฌ๋ทฐ๊ฐ ์๊ณ mergeable_state ๊ฐ clean ์ธ PR ์๋ง ํ๋ค.
/resolve-iteration ์ด ์๋ ์ด์ : Actions ์ ๊ธฐ๋ณธ GITHUB_TOKEN ์ org ํ๋ก์ ํธ ๋ณด๋๋ฅผ ์ฝ์ง ๋ชปํด์(read:project ์์) App ํ ํฐ์ ๊ฐ์ง ์ด Worker ๊ฐ ๋์ ์ฝ๋๋ค. ์ฃผ์ฐจ๋ ๋ณด๋์ Week(Iteration) ํ๋์์ ์ค๋ฏ๋ก ํ๋์ฝ๋ฉํ์ง ์๋๋ค.
handlers/webhooks.js ๊ฐ leetcode-study ์ ์ฅ์์ ์ด๋ฒคํธ๋ง ์ฒ๋ฆฌํ๋ค.
| ์ด๋ฒคํธ | ํ๋ ์ผ |
|---|---|
projects_v2_item |
Week ์ค์ ์ด ๋ฐ๋๋ฉด ๊ฒฝ๊ณ ๋๊ธ์ ๋ฌ๊ฑฐ๋ ์ง์ด๋ค |
pull_request (opened, reopened, synchronize) |
Week ๊ฒ์ฌ, AI ๋ฆฌ๋ทฐ check run(aiReview) |
issue_comment, pull_request_review_comment (created) |
@dalestudy ๋ฉ์
์ ์๋ตํ๋ค. approveยท์น์ธ ์ด๋ฉด PR ์ ์น์ธํ๊ณ , ๊ทธ ๋ฐ์๋ AI ๋ฆฌ๋ทฐ๋ฅผ ๋จ๋ค |
Week ๊ฒ์ฌ๋ ์ ๋ชฉ์ Week ์ซ์ ๋๋ Onboarding์ด ํฌํจ๋ ํ์ด PR์ ๋์์ผ๋ก ํ๊ณ , ๊ฒฝ๊ณ ๋๊ธ์ ๋ด์ด ๋จ ๊ฒ๋ง ์ง์ด๋ค. AI ๋ฆฌ๋ทฐ๋ ์ ๋ชฉ๊ณผ ์๊ด์์ด ๋๋ฆฌ๊ณ ([user] Onboarding ๊ฐ์ PR ๋ ํฌํจ), ํ์ด ํ์ผ์ด ์์ผ๋ฉด AI ๋ฅผ ๋ถ๋ฅด์ง ์๊ณ ๊ฑด๋๋ด๋ค.
AI ๋ฆฌ๋ทฐ๋ ํ์ด ํ์ผ๋ณ ์๊ณ ๋ฆฌ์ฆ ํจํด, ์๋ํ ์ ๊ทผ๋ฒ ์ผ์น ์ฌ๋ถ(problem-categories.json ๊ธฐ์ค), ์๊ฐ/๊ณต๊ฐ ๋ณต์ก๋์ ์์ฑ์์ ์ด๋ฒ ๊ธฐ์ ํ์ต ํํฉ์ PR head ์ปค๋ฐ์ check run ํ๋(AI ๋ฆฌ๋ทฐ)์ ๋ด๋๋ค. ๋ํ ํญ์๋ ๊ทธ check run ์ผ๋ก ๊ฐ๋ ๋งํฌ ๋๊ธ ํ๋๋ง ์ ์งํ๋ค.
Worker ํ ๋ฒ์ ์คํ(invocation)์ subrequest ๋ฅผ 50 ๊ฐ๊น์ง ์ธ ์ ์๋ค. ํ์ผ์ด ๋ง์ PR ์์ ๋์น์ง ์๋๋ก, ์นํ ํธ๋ค๋ฌ๋ AI ๋ฆฌ๋ทฐ๋ฅผ ์๊ธฐ ์์ ์๊ฒ ๋ค์ ์์ฒญํด ๋ฐ๋ก ์คํํ๋ค.
GitHub webhook
โผ
[#1] webhook ํธ๋ค๋ฌ โ 50 ์์ฐ
โโ ctx.waitUntil(fetch("/internal/ai-review")) โ [#2] aiReview โ ๋
๋ฆฝ 50 ์์ฐ
INTERNAL_SECRET ๊ณผ WORKER_URL ์ด ๋ ๋ค ์์ด์ผ ์ผ์ง๋ค. ํ๋๋ผ๋ ์์ผ๋ฉด ๊ฐ์ invocation ์์ ์คํ๋๋ค.
tests/subrequest-budget.test.js ๊ฐ fetch ํ์(aiReview ํ์ผ 15 ๊ฐ์ 40, AI ํธ์ถ ํฌํจ)๋ฅผ ํ๊ท ํ
์คํธ๋ก ๊ณ ์ ํ๋ค.
main ์ ๋จธ์ง๋๋ฉด GitHub Actions(.github/workflows/deploy.yml)๊ฐ ํ
์คํธ ํ GitHub secret ์ Worker secret ์ผ๋ก ๋ฃ์ผ๋ฉฐ ๋ฐฐํฌํ๋ค. PR ์์๋ integration.yaml ์ด ํ
์คํธ๋ง ๋๋ค. ์นํ
์ ๋ฐ๋ ์๋ฒ๋ผ PR ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ ๋์ง ์๋๋ค.
| Worker ์์ ์ฝ๋ ์ด๋ฆ | ์ด๋์ ์ค๋ |
|---|---|
APP_ID, WORKER_URL |
wrangler.jsonc ์ vars (๊ณต๊ฐ ๊ฐ) |
PRIVATE_KEY |
GitHub org secret APP_PRIVATE_KEY |
AI |
wrangler.jsonc ์ AI ๋ฐ์ธ๋ฉ. ๊ฐ์ ๊ณ์ ์ AI Gateway dalestudy ๋ฅผ ํ ํฐ ์์ด ๋ถ๋ฅธ๋ค |
INTERNAL_SECRET |
GitHub ์ ์ฅ์ secret INTERNAL_SECRET |
WEBHOOK_SECRET |
GitHub ์ ์ฅ์ secret WEBHOOK_SECRET. GitHub App ์นํ
์ค์ ์ secret ๊ณผ ๊ฐ์ ๊ฐ์ด๋ค |
GitHub secret ์ ๋ฐฐํฌํ ๋๋ง๋ค Worker secret ์ผ๋ก ๋ค์ด๊ฐ๋ฏ๋ก wrangler secret put ์ผ๋ก ๋ฐ๋ก ๋ฐ๊พธ์ง ์๋๋ค. ๋ค์ ๋ฐฐํฌ ๋ GitHub ๊ฐ์ผ๋ก ๋ฎ์ธ๋ค.
๋ก์ปฌ์์๋ .dev.vars ์ ๊ฐ์ ์ด๋ฆ์ผ๋ก ๋ฃ๋๋ค. .dev.vars, *.pem ์ ์ปค๋ฐํ์ง ์๋๋ค.
OpenAI ํค๋ Worker ์ ๋์ง ์๋๋ค. AI Gateway dalestudy ์ Provider Keys(BYOK)์ ์ ์ฅ๋ผ ์๊ณ , ๊ฒ์ดํธ์จ์ด๋ BYOK only ๋ผ ํค๊ฐ ์์ผ๋ฉด Unified Billing ์ผ๋ก ๋์ด๊ฐ์ง ์๊ณ ์คํจํ๋ค. AI ๋ฐ์ธ๋ฉ์ wrangler dev ์์๋ ์๊ฒฉ ๊ฒ์ดํธ์จ์ด๋ฅผ ๋ถ๋ฅด๋ฏ๋ก wrangler login ์ด dalestudy ๊ณ์ ์ ๋์ด ์์ด์ผ ํ๋ค.
https://github.com/organizations/DaleStudy/settings/apps/dalestudy
- Webhook URL:
https://github.dalestudy.com/webhooks, Webhook secret ์ ์ ์ฅ์ secretWEBHOOK_SECRET๊ณผ ๊ฐ์ ๊ฐ./webhooks๋ ์๋ช (X-Hub-Signature-256)์ด ๋ง์ง ์๊ฑฐ๋WEBHOOK_SECRET์ด ์์ผ๋ฉด ๋ฐ์ง ์๋๋ค - Repository permissions: Checks(Read & write), Contents(Read), Issues(Read & write), Metadata(Read), Pull requests(Read & write), Projects(Read & write)
- ์ด๋ฒคํธ: Issue comments, Pull request review comments, Projects v2 item, Pull requests
- DaleStudy ์กฐ์ง์ ์ค์น๋ผ ์๊ณ leetcode-study ์ ์ ๊ทผํ ์ ์์ด์ผ ํ๋ค
ํ
์คํธ๋ ๋์ ํ์ผ ์์ *.test.js ๋ก ๋๋ค. vitest ์ describeยทitยทexpectยทvi ๋ฅผ ์ฐ๊ณ , ์ธ๋ถ ๋ชจ๋์ vi.mock(), fetch ๋ globalThis.fetch = vi.fn() ์ผ๋ก ๋ฐ๊พผ๋ค. Worker runtime ๊ณผ ๋ฐ์ธ๋ฉ์ด ํ์ํ ํ
์คํธ๋ cloudflare:test, cloudflare:workers ๋ฅผ ์ด๋ค (@cloudflare/vitest-pool-workers ๊ฐ wrangler.jsonc ๋ฅผ ์ฝ๋๋ค).
vi.mock("../utils/github.js", () => ({
generateGitHubAppToken: vi.fn().mockResolvedValue("fake-token"),
}));- Workers ๋ฐํ์์ด๋ค. Node.js ๋ชจ๋๊ณผ Octokit ๊ฐ์ npm ํจํค์ง๋ ๋๋ถ๋ถ ๋์ํ์ง ์๋๋ค.
fetch์ Web Crypto API ๋ก ์ง์ ๊ตฌํํ๋ค. Private Key ๋ PKCS8, PKCS1 ๋ชจ๋ ๋ฐ๋๋ค (utils/github.js). - GraphQL ์ฟผ๋ฆฌ์ ๊ฐ์ ๋ฌธ์์ด๋ก ์ง์ ๋ผ์ ๋ฃ๋๋ค. GraphQL ๋ณ์ ๋ฌธ๋ฒ์ ์ฐ์ง ์์ผ๋ฏ๋ก ์ ๋ ฅ๊ฐ ๊ฒ์ฆ์ ๋น ๋จ๋ฆฌ์ง ์๋๋ค.
- ์๋ต์๋ CORS ํค๋(
Access-Control-Allow-Origin: *)๊ฐ ๋ถ๊ณ , ์๋ฌ๋ 500 ์ผ๋ก ๋๋ ค์ค๋ค. ์์ธ์wrangler tail๋ก ๋ณธ๋ค. - 401 ์ด ๋๋ฉด
APP_ID์PRIVATE_KEY๊ฐ ๊ฐ์ App ์ ๊ฒ์ธ์ง ํ์ธํ๋ค.
- Node.js ๋ด์ฅ ๋ชจ๋์ด๋ Octokit ๊ฐ์ Workers ๋ฐํ์์์ ๋์ํ์ง ์๋ ํจํค์ง๋ฅผ ์ฐ๋ฉด ์ง์ ํ๋ค.
- GraphQL ์ฟผ๋ฆฌ์ ๋ฌธ์์ด๋ก ๋ผ์ ๋ฃ๋ ๊ฐ์ ๋ฐ๋์ ๊ฒ์ฆ๋์ด์ผ ํ๋ค. ์ฌ์ฉ์ ์ ๋ ฅ(PR ์ ๋ชฉ, ๋๊ธ ๋ฑ)์ด ๊ฒ์ฆ ์์ด ๋ค์ด๊ฐ๋ฉด ์ฌ๊ฐํ ๋ฌธ์ ๋ก ์ง์ ํ๋ค.
- ์ ์๋ํฌ์ธํธ๋ ๊ธฐ์กด์ฒ๋ผ
repo_owner๊ฐDaleStudy๊ฐ ์๋๋ฉด 403์ ๋๋ ค์ผ ํ๊ณ ,/internal/*๋X-Internal-Secret์ ๊ฒ์ฌํด์ผ ํ๋ค. aiReview์ fetch๊ฐ ๋์ด๋๋ฉดtests/subrequest-budget.test.js์ ๊ณ ์ ๊ฐ๋ ํจ๊ป ๊ฐฑ์ ๋์ด์ผ ํ๋ค.- ์ ์ํฌ๋ฆฟ์
wrangler secret put์ ์ ๋ก ์ถ๊ฐํ๋ฉด ์ง์ ํ๋ค. ์ํฌ๋ฆฟ์ GitHub secret์ผ๋ก ๋๊ณ ๋ฐฐํฌ ์ํฌํ๋ก๊ฐ ๋ฃ๋๋ค.