diff --git a/.all-contributorsrc b/.all-contributorsrc new file mode 100644 index 0000000..ccfc040 --- /dev/null +++ b/.all-contributorsrc @@ -0,0 +1,77 @@ +{ + "projectName": "github", + "projectOwner": "DaleStudy", + "commit": true, + "repoType": "github", + "commitConvention": "angular", + "contributors": [ + { + "login": "DaleSeo", + "name": "Dale Seo", + "avatar_url": "https://avatars.githubusercontent.com/u/5466341?v=4", + "profile": "https://daleseo.com", + "contributions": [ + "code" + ] + }, + { + "login": "soobing", + "name": "Soobin Bak", + "avatar_url": "https://avatars.githubusercontent.com/u/16860535?v=4", + "profile": "https://soobing.github.io/", + "contributions": [ + "code" + ] + }, + { + "login": "sounmind", + "name": "Evan Suhyeong Lee", + "avatar_url": "https://avatars.githubusercontent.com/u/37020415?v=4", + "profile": "https://blog.evanshlee.com", + "contributions": [ + "code" + ] + }, + { + "login": "devchanki", + "name": "Gichan Kim", + "avatar_url": "https://avatars.githubusercontent.com/u/31515116?v=4", + "profile": "https://github.com/devchanki", + "contributions": [ + "code" + ] + }, + { + "login": "lkhoony", + "name": "KwangHun Lee", + "avatar_url": "https://avatars.githubusercontent.com/u/66773320?v=4", + "profile": "https://lkhoony.notion.site/Front-End-1a3f0e5e085480668ba0ebc54986f6bc", + "contributions": [ + "code" + ] + }, + { + "login": "parkhojeong", + "name": "Hojeong Park", + "avatar_url": "https://avatars.githubusercontent.com/u/34734102?v=4", + "profile": "https://github.com/parkhojeong", + "contributions": [ + "code" + ] + }, + { + "login": "jylee2033", + "name": "Jooyoung Julia Lee", + "avatar_url": "https://avatars.githubusercontent.com/u/85793553?v=4", + "profile": "https://github.com/jylee2033", + "contributions": [ + "code" + ] + } + ], + "contributorsPerLine": 7, + "linkToUsage": false, + "files": [ + "README.md" + ] +} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..f7cff37 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,42 @@ +name: Deploy ๐Ÿš€ + +on: + push: + branches: [main] + workflow_dispatch: + +concurrency: + group: deploy-production + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v7 + + - uses: oven-sh/setup-bun@v2 + + - run: bun install --frozen-lockfile + + - run: bun run test + + # PRIVATE_KEY ๋Š” ์—ฌ๋Ÿฌ ์ค„์งœ๋ฆฌ PEM ์ด๋ผ JSON ์œผ๋กœ ์“ด๋‹ค + - name: Write secrets file + env: + PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} + AI_GATEWAY_TOKEN: ${{ secrets.AI_GATEWAY_TOKEN }} + INTERNAL_SECRET: ${{ secrets.INTERNAL_SECRET }} + WEBHOOK_SECRET: ${{ secrets.WEBHOOK_SECRET }} + run: | + jq -n --arg private_key "$PRIVATE_KEY" --arg ai "$AI_GATEWAY_TOKEN" --arg internal "$INTERNAL_SECRET" --arg webhook "$WEBHOOK_SECRET" \ + '{PRIVATE_KEY: $private_key, AI_GATEWAY_TOKEN: $ai, INTERNAL_SECRET: $internal, WEBHOOK_SECRET: $webhook}' > "$RUNNER_TEMP/secrets.json" + + - name: Deploy + uses: cloudflare/wrangler-action@v4 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + packageManager: bun + command: deploy --secrets-file ${{ runner.temp }}/secrets.json diff --git a/AGENTS.md b/AGENTS.md index 0b6ad98..3d847fc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,536 +1,114 @@ -# AI Agents Guide +# AGENTS.md -์ด ๋ฌธ์„œ๋Š” AI ์—์ด์ „ํŠธ๊ฐ€ ํ”„๋กœ์ ํŠธ๋ฅผ ์ดํ•ดํ•˜๊ณ  ์ž‘์—…ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋•๋Š” ๊ฐ€์ด๋“œ์ž…๋‹ˆ๋‹ค. +DaleStudy ์กฐ์ง์˜ [GitHub App](https://github.com/apps/dalestudy)์ด ๋ฐ›๋Š” ์›นํ›…๊ณผ ์ž๋™ํ™” ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š” Cloudflare Worker. ์ฃผ ๋Œ€์ƒ์€ [leetcode-study](https://github.com/DaleStudy/leetcode-study) ์ €์žฅ์†Œ๋‹ค. -## ํ”„๋กœ์ ํŠธ ๊ฐœ์š” +- ์šด์˜: https://github.dalestudy.com (workers.dev: https://github.dalestudy.workers.dev) +- ์Šคํƒ: Cloudflare Workers, JavaScript(ES Modules), Vitest, Bun -DaleStudy ์กฐ์ง์˜ GitHub App(https://github.com/apps/dalestudy) - -### ํ˜„์žฌ ๊ตฌํ˜„๋œ ๊ธฐ๋Šฅ - -#### 1. PR Week ์„ค์ • ์ž๋™ ๊ฒ€์‚ฌ (๋ฆฌํŠธ์ฝ”๋“œ ์Šคํ„ฐ๋””) - -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` ์—”๋“œํฌ์ธํŠธ ์ง์ ‘ ํ˜ธ์ถœ - -### ๊ธฐ์ˆ  ์Šคํƒ - -- **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 ์„ค์ •๊ณผ vars -โ”œโ”€โ”€ package.json # Vitest ํ…Œ์ŠคํŠธ ์Šคํฌ๋ฆฝํŠธ ๋ฐ devDependencies -โ”œโ”€โ”€ bun.lock # Bun ์˜์กด์„ฑ lockfile -โ”œโ”€โ”€ vitest.config.js # Cloudflare Workers Vitest integration ์„ค์ • -โ”œโ”€โ”€ .github/workflows/ # CI ์›Œํฌํ”Œ๋กœ์šฐ (bun install โ†’ bun run test) -โ”œโ”€โ”€ handlers/ # ๊ธฐ๋Šฅ๋ณ„ ํ•ธ๋“ค๋Ÿฌ์™€ ํ•ธ๋“ค๋Ÿฌ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ -โ”œโ”€โ”€ utils/ # ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ์™€ ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ -โ”œโ”€โ”€ tests/ # Worker runtime smoke test์™€ ๊ต์ฐจ ๋ชจ๋“ˆ ํ…Œ์ŠคํŠธ -โ”œโ”€โ”€ README.md # ํ”„๋กœ์ ํŠธ ์„ค๋ช… -โ”œโ”€โ”€ AGENTS.md # ์ด ํŒŒ์ผ (AI ์—์ด์ „ํŠธ ๊ฐ€์ด๋“œ) -โ”œโ”€โ”€ CLAUDE.md # Claude Code ์ฐธ์กฐ ํŒŒ์ผ (AGENTS.md๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ) -โ””โ”€โ”€ *.pem # GitHub App Private Keys (์ปค๋ฐ‹ ์ œ์™ธ) -``` - -### ์ฝ”๋“œ ๊ตฌ์กฐ ์„ค๋ช… - -- **index.js**: ์—”๋“œํฌ์ธํŠธ ๋ผ์šฐํŒ…๋งŒ ๋‹ด๋‹น. pathname๋ณ„ ํ•ธ๋“ค๋Ÿฌ ํ˜ธ์ถœ -- **handlers/**: ๊ธฐ๋Šฅ๋ณ„ ํ•ธ๋“ค๋Ÿฌ์™€ `*.test.js` ํ•ธ๋“ค๋Ÿฌ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ - - `check-weeks.js`: PR Week ์„ค์ • ๊ฒ€์‚ฌ, ๋Œ“๊ธ€ ์ž‘์„ฑ/์‚ญ์ œ - - `webhooks.js`: GitHub webhook ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ - - `internal-dispatch.js`: self-fetch ๋‚ด๋ถ€ AI ํ•ธ๋“ค๋Ÿฌ ๋””์ŠคํŒจ์น˜ - - `approve_prs.js`, `merge_prs.js`: PR ์ผ๊ด„ ์Šน์ธ/๋ณ‘ํ•ฉ -- **utils/**: ์—ฌ๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ์—์„œ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ์™€ `*.test.js` ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ - - `cors.js`: CORS ํ—ค๋” ๊ด€๋ฆฌ ๋ฐ ์‘๋‹ต ์ƒ์„ฑ (`corsResponse`, `errorResponse`) - - `github.js`: GitHub App ์ธ์ฆ (JWT, Installation Token), RSA ์„œ๋ช… - - `webhook.js`: Webhook signature ๊ฒ€์ฆ -- **tests/**: Worker runtime smoke test, subrequest budget, cross-module ํ…Œ์ŠคํŠธ -- **vitest.config.js**: `@cloudflare/vitest-pool-workers`๊ฐ€ `wrangler.jsonc`๋ฅผ ์ฝ๋„๋ก ์„ค์ • - -### ์ƒˆ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ ์‹œ - -1. ๊ธฐ๋Šฅ๋ณ„ ํ•ธ๋“ค๋Ÿฌ ํŒŒ์ผ ์ƒ์„ฑ (์˜ˆ: `handlers/new-feature.js`) -2. `index.js`์— pathname ๋ผ์šฐํŒ… ์ถ”๊ฐ€ - -```javascript -// 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); -} -``` - -## ์ฃผ์š” ๋ช…๋ น์–ด - -### ๋กœ์ปฌ ๊ฐœ๋ฐœ +## ๋ช…๋ น์–ด ```bash -# ๋กœ์ปฌ ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹คํ–‰ (ํฌํŠธ 8787) -wrangler dev - -# ๋กœ์ปฌ ํ…Œ์ŠคํŠธ (๋ณ„๋„ ํ„ฐ๋ฏธ๋„) -curl -X POST http://localhost:8787/check-weeks \ - -H "Content-Type: application/json" \ - -d '{"repo_owner": "DaleStudy", "repo_name": "leetcode-study"}' -``` - -### ๋ฐฐํฌ - -```bash -# 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 -``` - -### ํ”„๋กœ๋•์…˜ ํ…Œ์ŠคํŠธ - -```bash -# ๋ฐฐํฌ๋œ Worker ํ…Œ์ŠคํŠธ -curl -X POST https://github.dalestudy.com/check-weeks \ - -H "Content-Type: application/json" \ - -d '{"repo_owner": "DaleStudy", "repo_name": "leetcode-study"}' -``` - -## ํ•ต์‹ฌ ๊ธฐ๋Šฅ - -### 1. GitHub App ์ธ์ฆ - -์ธ์ฆ ํ๋ฆ„: - -1. RS256 ์•Œ๊ณ ๋ฆฌ์ฆ˜์œผ๋กœ JWT ์ƒ์„ฑ (Web Crypto API ์‚ฌ์šฉ) -2. JWT๋กœ Installation ID ์กฐํšŒ -3. Installation Token ๋ฐœ๊ธ‰ (10๋ถ„ ์œ ํšจ) -4. ๋ชจ๋“  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 ์ธ์ฝ”๋”ฉ - -### 2. API ์—”๋“œํฌ์ธํŠธ ๊ตฌ์กฐ - -ํ˜„์žฌ ๊ตฌํ˜„๋œ ์—”๋“œํฌ์ธํŠธ: - -#### `POST /webhooks` - -GitHub Organization webhook ์ˆ˜์‹ ์šฉ ์—”๋“œํฌ์ธํŠธ - -- **์ด๋ฒคํŠธ**: `projects_v2_item`, `pull_request` -- **์‹ค์‹œ๊ฐ„ ์ฒ˜๋ฆฌ**: Week ์„ค์ • ๋ณ€๊ฒฝ ์ฆ‰์‹œ ๊ฐ์ง€ ๋ฐ ๋Œ“๊ธ€ ์ž‘์„ฑ/์‚ญ์ œ - -#### `POST /check-weeks` - -๋ชจ๋“  Open PR์—์„œ Week ์„ค์ •์„ ๊ฒ€์‚ฌํ•˜๊ณ  ์ž๋™์œผ๋กœ ๋Œ“๊ธ€ ์ž‘์„ฑ/์‚ญ์ œ (์ˆ˜๋™ ํ˜ธ์ถœ์šฉ) - -**Request:** - -`repo_owner` ์ƒ๋žต ์‹œ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ `DaleStudy`๊ฐ€ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. - -```json -{ - "repo_name": "leetcode-study" -} -``` - -**Response:** - -```json -{ - "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 } - ] -} +bun install +bun run test # ์ „์ฒด ํ…Œ์ŠคํŠธ (CI ์™€ ๋ฐฐํฌ ์ „์— ๊ฐ™์€ ๋ช…๋ น์„ ๋ˆ๋‹ค) +bun run test -- handlers/webhooks.test.js +wrangler dev # http://localhost:8787 +wrangler tail # ์šด์˜ ๋กœ๊ทธ ``` -#### `POST /approve-prs` - -์—ด๋ ค์žˆ๋Š” ๋‹ต์•ˆ ์ œ์ถœ PR์„ ์ผ๊ด„ ์Šน์ธํ•ฉ๋‹ˆ๋‹ค. `excludes` ๋ฐฐ์—ด๋กœ ํŠน์ • PR์„ ์ œ์™ธํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฏธ ์Šน์ธ๋œ PR๊ณผ Draft ์ƒํƒœ์˜ PR์€ ์ž๋™์œผ๋กœ ์Šคํ‚ต๋ฉ๋‹ˆ๋‹ค. CI๊ฐ€ ํ†ต๊ณผํ•˜์ง€ ์•Š์€ PR(`statusCheckRollup.state !== "SUCCESS"`)๋„ ์Šคํ‚ต๋˜์–ด, ์ฒดํฌ๊ฐ€ ๊นจ์ง„ PR์—๋Š” ์Šน์ธ ๋ฆฌ๋ทฐ๋ฅผ ๋‚จ๊ธฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. +## ๊ตฌ์กฐ -**Request:** - -```json -{ "repo_name": "leetcode-study", "excludes": [1972] } ``` - -**Response:** - -```json -{ - "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 } - ] -} +index.js ์—”๋“œํฌ์ธํŠธ ๋ผ์šฐํŒ…๋งŒ ํ•œ๋‹ค (POST ๋งŒ ๋ฐ›๋Š”๋‹ค) +handlers/ ๊ธฐ๋Šฅ๋ณ„ ํ•ธ๋“ค๋Ÿฌ์™€ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ(*.test.js) +utils/ ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ์™€ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ (github.js: App ์ธ์ฆ, prActions.js: ์Šน์ธยท๋ณ‘ํ•ฉ ๊ณตํ†ต ๋กœ์ง) +tests/ Worker runtime smoke test, subrequest ์˜ˆ์‚ฐ, ๊ต์ฐจ ๋ชจ๋“ˆ ํ…Œ์ŠคํŠธ +wrangler.jsonc Worker ์„ค์ •๊ณผ ๊ณต๊ฐœ ๊ฐ’(vars) ``` -#### `POST /merge-prs` +์ƒˆ ๊ธฐ๋Šฅ์€ `handlers/` ์— ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๋งŒ๋“ค๊ณ  `index.js` ์— ๊ฒฝ๋กœ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค. ํ•„์š”ํ•œ GitHub App ๊ถŒํ•œ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๊ณ , ํ•ธ๋“ค๋Ÿฌ ์˜†์— ํ…Œ์ŠคํŠธ๋ฅผ ๋‘”๋‹ค. -์—ด๋ ค์žˆ๋Š” PR์„ ์ผ๊ด„ ๋ณ‘ํ•ฉํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ ๋ณ‘ํ•ฉ ๋ฐฉ์‹์€ `squash`์ด๋ฉฐ `merge_method` ๊ฐ’์œผ๋กœ `merge | squash | rebase` ์ค‘ ์„ ํƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. `excludes`๋กœ ํŠน์ • PR์„ ์ œ์™ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์Šน์ธ ๋ฆฌ๋ทฐ๊ฐ€ ์—†๋Š” PR, Draft PR, GitHub `mergeable_state !== "clean"` PR์€ ์Šคํ‚ต๋˜๋ฉฐ `unknown`/`behind` ์ƒํƒœ๋Š” ์ตœ๋Œ€ 1์ดˆ ํ›„ ํ•œ ๋ฒˆ ๋” ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. +## ์—”๋“œํฌ์ธํŠธ -**Request:** +`repo_owner` ๋ฅผ ๋ฐ›๋Š” ์—”๋“œํฌ์ธํŠธ๋Š” `DaleStudy` ๊ฐ€ ์•„๋‹ˆ๋ฉด 403 ์ด๋‹ค (์ƒ๋žตํ•˜๋ฉด `DaleStudy`). `/check-weeks` ๋Š” ์ €์žฅ์†Œ๋„ leetcode-study ๋งŒ ๋ฐ›๋Š”๋‹ค. -```json -{ - "repo_name": "leetcode-study", - "merge_method": "squash", - "excludes": [1972] -} -``` - -**Response:** - -```json -{ - "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" } - ] -} -``` +| ๊ฒฝ๋กœ | ํ•˜๋Š” ์ผ | +| ------------------------- | --------------------------------------------------------------------------------------------------------------- | +| `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` ์ธ์ฆ) | -#### `POST /resolve-iteration` +์Šน์ธยท๋ณ‘ํ•ฉ์˜ ๊ณตํ†ต ๊ทœ์น™์€ `utils/prActions.js` ์— ์žˆ๋‹ค. ์š”์•ฝํ•˜๋ฉด Week ๊ฐ€ ๋‹ค๋ฅด๊ฑฐ๋‚˜ Status ๊ฐ€ `Solving` ์ธ PR, Draft PR ์€ ๊ฑด๋„ˆ๋›ฐ๊ณ , ์Šน์ธ์€ CI ๊ฐ€ ํ†ต๊ณผํ•œ PR ์—๋งŒ, ๋ณ‘ํ•ฉ์€ ์Šน์ธ ๋ฆฌ๋ทฐ๊ฐ€ ์žˆ๊ณ  `mergeable_state` ๊ฐ€ `clean` ์ธ PR ์—๋งŒ ํ•œ๋‹ค. -์ฃผ์–ด์ง„ ๋‚ ์งœ๊ฐ€ ์–ด๋А ๊ธฐ์ˆ˜์˜ ๋ช‡ ์ฃผ์ฐจ์ธ์ง€ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. ๋ฆฌํŠธ์ฝ”๋“œ ์Šคํ„ฐ๋””๋Š” ๊ธฐ์ˆ˜๋งˆ๋‹ค ํ”„๋กœ์ ํŠธ ๋ณด๋“œ๋ฅผ ์ƒˆ๋กœ ๋งŒ๋“ค๊ณ  ๋ณด๋“œ์˜ Week ํ•„๋“œ(Iteration)๊ฐ€ ์ฃผ์ฐจ ์ผ์ •์„ ๊ด€๋ฆฌํ•˜๋ฏ€๋กœ, ์ฃผ์ฐจ ๋ฒˆํ˜ธ๋ฅผ ํ•˜๋“œ์ฝ”๋”ฉํ•˜์ง€ ์•Š๊ณ  ์—ฌ๊ธฐ์„œ ์ฝ์Šต๋‹ˆ๋‹ค. +`/resolve-iteration` ์ด ์žˆ๋Š” ์ด์œ : Actions ์˜ ๊ธฐ๋ณธ `GITHUB_TOKEN` ์€ org ํ”„๋กœ์ ํŠธ ๋ณด๋“œ๋ฅผ ์ฝ์ง€ ๋ชปํ•ด์„œ(`read:project` ์—†์Œ) App ํ† ํฐ์„ ๊ฐ€์ง„ ์ด Worker ๊ฐ€ ๋Œ€์‹  ์ฝ๋Š”๋‹ค. ์ฃผ์ฐจ๋Š” ๋ณด๋“œ์˜ Week(Iteration) ํ•„๋“œ์—์„œ ์˜ค๋ฏ€๋กœ ํ•˜๋“œ์ฝ”๋”ฉํ•˜์ง€ ์•Š๋Š”๋‹ค. -Actions์˜ ๊ธฐ๋ณธ `GITHUB_TOKEN`์œผ๋กœ๋Š” org ํ”„๋กœ์ ํŠธ ๋ณด๋“œ๋ฅผ ์ฝ์„ ์ˆ˜ ์—†์–ด(`read:project` ์Šค์ฝ”ํ”„ ๋ถ€์žฌ) App ํ† ํฐ์„ ๊ฐ€์ง„ ์ด ์›Œ์ปค๊ฐ€ ์กฐํšŒ๋ฅผ ๋Œ€์‹ ํ•ฉ๋‹ˆ๋‹ค. ์™„๋ฃŒ๋œ ์ฃผ์ฐจ๋Š” `completedIterations`๋กœ ์˜ฎ๊ฒจ๊ฐ€๋ฏ€๋กœ ๋‘ ๋ฐฐ์—ด์„ ๋ชจ๋‘ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. +## ์›นํ›… -**Request:** - -```json -{ "date": "2026-08-08" } -``` - -**Response:** - -```json -{ - "success": true, - "date": "2026-08-08", - "found": true, - "cohort": 8, - "week": 7, - "week_label": "Week 7", - "project_number": 29, - "project_title": "๋ฆฌํŠธ์ฝ”๋“œ ์Šคํ„ฐ๋”” 8๊ธฐ", - "start_date": "2026-08-02", - "end_date": "2026-08-08" -} -``` +`handlers/webhooks.js` ๊ฐ€ leetcode-study ์ €์žฅ์†Œ์˜ ์ด๋ฒคํŠธ๋งŒ ์ฒ˜๋ฆฌํ•œ๋‹ค. -ํ•ด๋‹น ๋‚ ์งœ๋ฅผ ํฌํ•จํ•˜๋Š” ์ฃผ์ฐจ๊ฐ€ ์—†์œผ๋ฉด(๊ธฐ์ˆ˜ ์‚ฌ์ด ํœด์‹๊ธฐ ๋“ฑ) `{ "success": true, "date": "...", "found": false }`๋ฅผ ๋Œ๋ ค์ค๋‹ˆ๋‹ค. +| ์ด๋ฒคํŠธ | ํ•˜๋Š” ์ผ | +| ---------------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| `projects_v2_item` | Week ์„ค์ •์ด ๋ฐ”๋€Œ๋ฉด ๊ฒฝ๊ณ  ๋Œ“๊ธ€์„ ๋‹ฌ๊ฑฐ๋‚˜ ์ง€์šด๋‹ค | +| `pull_request` (`opened`, `reopened`, `synchronize`) | Week ๊ฒ€์‚ฌ, ํ’€์ด ํŒจํ„ด ํƒœ๊น…(`tagPatterns`), ํ•™์Šต ํ˜„ํ™ฉ ๋Œ“๊ธ€(`postLearningStatus`) | +| `issue_comment`, `pull_request_review_comment` (`created`) | `@dalestudy` ๋ฉ˜์…˜์— ์‘๋‹ตํ•œ๋‹ค. `approve`ยท`์Šน์ธ` ์ด๋ฉด PR ์„ ์Šน์ธํ•˜๊ณ , ๊ทธ ๋ฐ–์—๋Š” AI ๋ฆฌ๋ทฐ๋ฅผ ๋‹จ๋‹ค | -### 3. ์›Œํฌํ”Œ๋กœ์šฐ +Week ๊ฒ€์‚ฌ๋Š” ์ œ๋ชฉ์ด `WEEK NN Solutions` ํ˜•ํƒœ์ธ ํ’€์ด PR ๋งŒ ๋Œ€์ƒ์œผ๋กœ ํ•˜๊ณ , ๊ฒฝ๊ณ  ๋Œ“๊ธ€์€ ๋ด‡์ด ๋‹จ ๊ฒƒ๋งŒ ์ง€์šด๋‹ค. -1. Open PR ๋ชฉ๋ก ์กฐํšŒ (GitHub REST API) -2. ํ’€์ด ์ œ์ถœ PR์ด ์•„๋‹Œ PR ์Šคํ‚ต (์ œ๋ชฉ์ด `WEEK NN Solutions` ํ˜•ํƒœ๊ฐ€ ์•„๋‹ˆ๋ฉด ๋ด‡ ํ…Œ์ŠคํŠธยท์ €์žฅ์†Œ ์ •๋ฆฌ PR๋กœ ๊ฐ„์ฃผ) -3. ๊ฐ PR์˜ Week ์„ค์ • ํ™•์ธ (GitHub GraphQL API - Projects v2 ์ ‘๊ทผ ํ•„์š”) -4. Week ์—†์Œ โ†’ ๊ฒฝ๊ณ  ๋Œ“๊ธ€ ์ž‘์„ฑ (์ค‘๋ณต ๋ฐฉ์ง€: Bot์ด ์ž‘์„ฑํ•œ ๊ฒฝ๊ณ  ๋Œ“๊ธ€์ด ์ด๋ฏธ ์žˆ์œผ๋ฉด ์Šคํ‚ต) -5. Week ์žˆ์Œ โ†’ ๊ธฐ์กด ๊ฒฝ๊ณ  ๋Œ“๊ธ€ ์‚ญ์ œ (Bot์ด ์ž‘์„ฑํ•œ Week ๊ฒฝ๊ณ  ๋Œ“๊ธ€๋งŒ) +### AI ์ž‘์—…์„ ๋ณ„๋„ invocation ์œผ๋กœ ๋‚˜๋ˆ„๊ธฐ -### 4. AI ํ•ธ๋“ค๋Ÿฌ Worker ๋ถ„๋ฆฌ ์•„ํ‚คํ…์ฒ˜ - -PR ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›์œผ๋ฉด webhook ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ๋‘ AI ํ•ธ๋“ค๋Ÿฌ(`tagPatterns`, `postLearningStatus`)๋ฅผ **๋ณ„๋„ Worker invocation**์œผ๋กœ ๋ถ„๋ฆฌ ๋””์ŠคํŒจ์น˜ํ•œ๋‹ค. ๊ฐ invocation์€ ๋…๋ฆฝ์ ์ธ Cloudflare subrequest ์˜ˆ์‚ฐ(50)์„ ๊ฐ€์ง€๋ฏ€๋กœ ํŒŒ์ผ์ด ๋งŽ์€ PR์—์„œ๋„ ์˜ˆ์‚ฐ ์ดˆ๊ณผ๋ฅผ ๋ฐฉ์ง€ํ•œ๋‹ค. +Worker ํ•œ ๋ฒˆ์˜ ์‹คํ–‰(invocation)์€ subrequest ๋ฅผ 50 ๊ฐœ๊นŒ์ง€ ์“ธ ์ˆ˜ ์žˆ๋‹ค. ํŒŒ์ผ์ด ๋งŽ์€ PR ์—์„œ ๋„˜์น˜์ง€ ์•Š๋„๋ก, ์›นํ›… ํ•ธ๋“ค๋Ÿฌ๋Š” ๋‘ AI ์ž‘์—…์„ ์ž๊ธฐ ์ž์‹ ์—๊ฒŒ ๋‹ค์‹œ ์š”์ฒญํ•ด ๋”ฐ๋กœ ์‹คํ–‰ํ•œ๋‹ค. ``` GitHub webhook - โ”‚ โ–ผ -[Invocation #1] webhook ํ•ธ๋“ค๋Ÿฌ โ† 50 subrequest ์˜ˆ์‚ฐ - โ”‚ ctx.waitUntil(fetch("/internal/tag-patterns")) โ”€โ” self-fetch๋Š” - โ”‚ ctx.waitUntil(fetch("/internal/learning-status")) โ”€โ”ค ์™ธ๋ถ€ ์š”์ฒญ์ด๋ผ - โ”‚ โ”‚ ์ƒˆ invocation ํŠธ๋ฆฌ๊ฑฐ - โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ [Invocation #2] tagPatterns โ—€โ”€โ”˜ โ† ๋…๋ฆฝ 50 ์˜ˆ์‚ฐ - โ”‚ - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ [Invocation #3] postLearningStatus โ† ๋…๋ฆฝ 50 ์˜ˆ์‚ฐ -``` - -- `INTERNAL_SECRET`๊ณผ `WORKER_URL`์ด ๋ชจ๋‘ ์„ค์ •๋˜์–ด์•ผ ํ™œ์„ฑํ™”๋œ๋‹ค. ๋‘˜ ์ค‘ ํ•˜๋‚˜๋ผ๋„ ์—†์œผ๋ฉด ๊ธฐ์กด์ฒ˜๋Ÿผ ๊ฐ™์€ invocation์—์„œ ์ˆœ์ฐจ ์‹คํ–‰(subrequest ์˜ˆ์‚ฐ ๊ณต์œ )๋˜์–ด ํŒŒ์ผ์ด ๋งŽ์€ PR์—์„œ ์˜ˆ์‚ฐ์„ ์ดˆ๊ณผํ•  ์ˆ˜ ์žˆ๋‹ค. -- ๋‚ด๋ถ€ ์—”๋“œํฌ์ธํŠธ๋Š” `/internal/tag-patterns`, `/internal/learning-status`์ด๋ฉฐ `X-Internal-Secret` ํ—ค๋”๋กœ ์ธ์ฆํ•œ๋‹ค. -- ์ฐธ๊ณ : `tests/subrequest-budget.test.js`๊ฐ€ 5๊ฐœ ํŒŒ์ผ ๋ณ€๊ฒฝ ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ ๊ฐ ํ•ธ๋“ค๋Ÿฌ์˜ fetch ํ˜ธ์ถœ ์ˆ˜(`tagPatterns` 20ํšŒ, `postLearningStatus` 31ํšŒ)๋ฅผ ํšŒ๊ท€ ํ…Œ์ŠคํŠธ๋กœ ๋ฐ•์•„๋‘”๋‹ค. - -## ๋ณด์•ˆ ๋ฐ ๊ถŒํ•œ - -### DaleStudy Organization ์ „์šฉ - -`repo_owner !== 'DaleStudy'` ์š”์ฒญ์€ 403 Forbidden ๋ฐ˜ํ™˜. - -### GitHub App ํ•„์ˆ˜ ๊ถŒํ•œ - -- `contents: read`: PR ์ •๋ณด ์กฐํšŒ -- `issues: write`: ๋Œ“๊ธ€ ์ž‘์„ฑ ๋ฐ ์‚ญ์ œ -- `pull_requests: read & write`: PR ๋ชฉ๋ก/์ƒํƒœ ์กฐํšŒ, ๋ฆฌ๋ทฐ ์ƒ์„ฑ, ๋ณ‘ํ•ฉ ์ˆ˜ํ–‰ -- `organization_projects: read`: Projects v2์˜ Week ํ•„๋“œ ์ ‘๊ทผ (GraphQL API) - -### Secrets ๊ด€๋ฆฌ - -**์ ˆ๋Œ€ ์ปค๋ฐ‹ ๊ธˆ์ง€**: `.env`, `.dev.vars`, `*.pem`, `*.key` - -#### Cloudflare Workers Secrets (ํ”„๋กœ๋•์…˜) - -```bash -wrangler secret put APP_ID # GitHub App ID -wrangler secret put PRIVATE_KEY # GitHub App Private Key (PEM) -``` - -#### ๋กœ์ปฌ ๊ฐœ๋ฐœ (.dev.vars) - -``` -APP_ID=123456 -PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----" -``` - -## ๋ฐฐํฌ - -### Cloudflare Workers - -```bash -wrangler deploy -``` - -### ์ปค์Šคํ…€ ๋„๋ฉ”์ธ - -- Production: https://github.dalestudy.com -- Worker.dev: https://github.dalestudy.workers.dev - -์ž์„ธํ•œ ๋ฐฐํฌ ๊ฐ€์ด๋“œ๋Š” `DEPLOYMENT.md` ์ฐธ๊ณ . - -## GitHub App Webhook ์„ค์ • - -### 1. GitHub App ์„ค์ • ํŽ˜์ด์ง€ ์ ‘๊ทผ - -``` -https://github.com/settings/apps/dalestudy -``` - -### 2. General ํƒญ - Webhook ์„ค์ • - -- **Webhook URL**: `https://github.dalestudy.com/webhooks` -- **Webhook Secret**: (์„ ํƒ์‚ฌํ•ญ) ์•ˆ์ „ํ•œ ๋žœ๋ค ๋ฌธ์ž์—ด -- **โœ… Active**: ์ฒดํฌ - -### 3. Permissions & events ํƒญ - ๊ถŒํ•œ ์„ค์ • - -**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 ์ฒดํฌ) - -### 4. Worker Secrets ์„ค์ • - -```bash -# AI Gateway ์ธ์ฆ ํ† ํฐ (AI ์ฝ”๋“œ ๋ฆฌ๋ทฐ์šฉ, ํ•„์ˆ˜) -wrangler secret put AI_GATEWAY_TOKEN - -# Webhook Secret (์„ ํƒ์‚ฌํ•ญ) -wrangler secret put WEBHOOK_SECRET - -# Internal Dispatch Secret (AI ํ•ธ๋“ค๋Ÿฌ Worker ๋ถ„๋ฆฌ์šฉ, ๊ถŒ์žฅ) -# ์„ค์ •ํ•˜๋ฉด tagPatterns, learningStatus๊ฐ€ ๋ณ„๋„ Worker ํ˜ธ์ถœ๋กœ -# ๋””์ŠคํŒจ์น˜๋˜์–ด ๊ฐ๊ฐ ๋…๋ฆฝ์ ์ธ subrequest ์˜ˆ์‚ฐ์„ ๊ฐ€์ง. -# WORKER_URL๊ณผ ํ•จ๊ป˜ ์„ค์ •๋˜์–ด์•ผ ํ™œ์„ฑํ™”๋œ๋‹ค. -wrangler secret put INTERNAL_SECRET +[#1] webhook ํ•ธ๋“ค๋Ÿฌ โ† 50 ์˜ˆ์‚ฐ + โ”œโ”€ ctx.waitUntil(fetch("/internal/tag-patterns")) โ†’ [#2] tagPatterns โ† ๋…๋ฆฝ 50 ์˜ˆ์‚ฐ + โ””โ”€ ctx.waitUntil(fetch("/internal/learning-status")) โ†’ [#3] postLearningStatus โ† ๋…๋ฆฝ 50 ์˜ˆ์‚ฐ ``` -`WORKER_URL`์€ `wrangler.jsonc`์˜ `vars`์— ์ •์˜๋˜์–ด ์žˆ์–ด ๊ธฐ๋ณธ ๋ฐฐํฌ์—๋Š” ์ถ”๊ฐ€ ์„ค์ •์ด ํ•„์š” ์—†๋‹ค. ์Šคํ…Œ์ด์ง•/๋‹ค๋ฅธ ๊ณ„์ • ๋“ฑ์œผ๋กœ ๋ฐฐํฌํ•  ๋•Œ๋งŒ ๋ฎ์–ด์“ฐ๋ฉด ๋œ๋‹ค. +`INTERNAL_SECRET` ๊ณผ `WORKER_URL` ์ด ๋‘˜ ๋‹ค ์žˆ์–ด์•ผ ์ผœ์ง„๋‹ค. ํ•˜๋‚˜๋ผ๋„ ์—†์œผ๋ฉด ๊ฐ™์€ invocation ์—์„œ ์ฐจ๋ก€๋กœ ์‹คํ–‰๋œ๋‹ค. +`tests/subrequest-budget.test.js` ๊ฐ€ ํŒŒ์ผ 5 ๊ฐœ ์‹œ๋‚˜๋ฆฌ์˜ค์˜ fetch ํšŸ์ˆ˜(`tagPatterns` 20, `postLearningStatus` 31)๋ฅผ ํšŒ๊ท€ ํ…Œ์ŠคํŠธ๋กœ ๊ณ ์ •ํ•œ๋‹ค. -### 5. GitHub App ์„ค์น˜ +## ๋ฐฐํฌ์™€ ์‹œํฌ๋ฆฟ -์ €์žฅ์†Œ์— App์ด ์„ค์น˜๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ: +`main` ์— ๋จธ์ง€๋˜๋ฉด GitHub Actions(`.github/workflows/deploy.yml`)๊ฐ€ ํ…Œ์ŠคํŠธ ํ›„ `wrangler deploy --secrets-file` ๋กœ ๋ฐฐํฌํ•œ๋‹ค. PR ์—์„œ๋Š” `integration.yaml` ์ด ํ…Œ์ŠคํŠธ๋งŒ ๋ˆ๋‹ค. ์›นํ›…์„ ๋ฐ›๋Š” ์„œ๋ฒ„๋ผ PR ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋Š” ๋‘์ง€ ์•Š๋Š”๋‹ค. -``` -https://github.com/apps/dalestudy/installations -``` - -**DaleStudy organization์— ์„ค์น˜**๋˜์–ด ์žˆ์–ด์•ผ ํ•˜๋ฉฐ, **leetcode-study ์ €์žฅ์†Œ ์ ‘๊ทผ ๊ถŒํ•œ** ํ•„์š” +| Worker ์—์„œ ์ฝ๋Š” ์ด๋ฆ„ | ์–ด๋””์„œ ์˜ค๋‚˜ | +| ---------------------- | ----------------------------------------------------------------------------------- | +| `APP_ID`, `WORKER_URL` | `wrangler.jsonc` ์˜ `vars` (๊ณต๊ฐœ ๊ฐ’) | +| `PRIVATE_KEY` | GitHub org secret `APP_PRIVATE_KEY` | +| `AI_GATEWAY_TOKEN` | GitHub org secret `AI_GATEWAY_TOKEN` | +| `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` ์€ ์ปค๋ฐ‹ํ•˜์ง€ ์•Š๋Š”๋‹ค. -์ „์ฒด PR์„ ํ•œ ๋ฒˆ์— ๊ฒ€์‚ฌํ•˜๊ณ  ์‹ถ์„ ๋•Œ: +## GitHub App ์„ค์ • -```bash -curl -X POST https://github.dalestudy.com/check-weeks \ - -H "Content-Type: application/json" \ - -d '{"repo_owner": "DaleStudy", "repo_name": "leetcode-study"}' -``` +https://github.com/organizations/DaleStudy/settings/apps/dalestudy -## ์ค‘์š”ํ•œ ์ œ์•ฝ์‚ฌํ•ญ - -### Cloudflare Workers ํ™˜๊ฒฝ - -- โŒ Node.js ๋ชจ๋“ˆ ์‚ฌ์šฉ ๋ถˆ๊ฐ€ (crypto, buffer ๋“ฑ) -- โœ… Web ํ‘œ์ค€ API๋งŒ ์‚ฌ์šฉ (fetch, Web Crypto API) -- โŒ npm ํŒจํ‚ค์ง€ ๋Œ€๋ถ€๋ถ„ ํ˜ธํ™˜ ์•ˆ ๋จ (@octokit/app ๋“ฑ) -- โœ… ์ˆœ์ˆ˜ JavaScript + Web APIs๋กœ ๊ตฌํ˜„ +- Webhook URL: `https://github.dalestudy.com/webhooks`, Webhook secret ์€ ์ €์žฅ์†Œ secret `WEBHOOK_SECRET` ๊ณผ ๊ฐ™์€ ๊ฐ’. `/webhooks` ๋Š” ์„œ๋ช…(`X-Hub-Signature-256`)์ด ๋งž์ง€ ์•Š๊ฑฐ๋‚˜ `WEBHOOK_SECRET` ์ด ์—†์œผ๋ฉด ๋ฐ›์ง€ ์•Š๋Š”๋‹ค +- Repository permissions: 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 ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค ## ํ…Œ์ŠคํŠธ -์ด ํ”„๋กœ์ ํŠธ๋Š” Cloudflare Workers Vitest integration์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. Vitest๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๊ณ , Worker ๋Ÿฐํƒ€์ž„๊ณผ ๋ฐ”์ธ๋”ฉ์ด ํ•„์š”ํ•œ ํ…Œ์ŠคํŠธ๋Š” Cloudflare ํ…Œ์ŠคํŠธ ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. - -### ํ…Œ์ŠคํŠธ ์‹คํ–‰ - -ํ…Œ์ŠคํŠธ๋Š” `handlers/`(ํ•ธ๋“ค๋Ÿฌ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ), `utils/`(์œ ํ‹ธ๋ฆฌํ‹ฐ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ), `tests/`(Worker runtime smoke test์™€ ๊ต์ฐจ ๋ชจ๋“ˆ ํ…Œ์ŠคํŠธ)๋กœ ๋‚˜๋‰œ๋‹ค. ๋ชจ๋“ˆ ๋ชจํ‚น์€ Vitest์˜ `vi.mock()`์„ ์‚ฌ์šฉํ•˜๊ณ , Worker runtime๊ณผ bindings๊ฐ€ ํ•„์š”ํ•œ ํ…Œ์ŠคํŠธ๋Š” `cloudflare:test`์™€ `cloudflare:workers`๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. - -```bash -# ์˜์กด์„ฑ ์„ค์น˜ -bun install - -# ์ „์ฒด ํ…Œ์ŠคํŠธ ์‹คํ–‰ -bun run test - -# ๊ฐ์‹œ ๋ชจ๋“œ -bun run test:watch - -# ํŠน์ • ํŒŒ์ผ๋งŒ ์‹คํ–‰ -bun run test -- handlers/webhooks.test.js -``` - -Bun ์„ค์น˜: https://bun.sh/docs/installation - -### ํ…Œ์ŠคํŠธ ํŒŒ์ผ ์ž‘์„ฑ ๊ทœ์น™ - -- ํ…Œ์ŠคํŠธ ํŒŒ์ผ์€ ๋Œ€์ƒ ํŒŒ์ผ๊ณผ ๊ฐ™์€ ๋””๋ ‰ํ† ๋ฆฌ์— `*.test.js` ์ด๋ฆ„์œผ๋กœ ๋ฐฐ์น˜ํ•ฉ๋‹ˆ๋‹ค. - - ์˜ˆ: `handlers/webhooks.js` โ†’ `handlers/webhooks.test.js` -- `vitest`์—์„œ ์ œ๊ณตํ•˜๋Š” API(`describe`, `it`, `expect`, `vi`, `beforeEach`)๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. -- ์™ธ๋ถ€ ์˜์กด์„ฑ(`utils/github.js` ๋“ฑ)์€ `vi.mock()`์œผ๋กœ ๋Œ€์ฒดํ•˜๊ณ , `fetch`๋Š” `globalThis.fetch = vi.fn()...`๋กœ ์Šคํ…ํ•ฉ๋‹ˆ๋‹ค. +ํ…Œ์ŠคํŠธ๋Š” ๋Œ€์ƒ ํŒŒ์ผ ์˜†์— `*.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` ๋ฅผ ์ฝ๋Š”๋‹ค). ```javascript -import { describe, it, expect, vi, beforeEach } from "vitest"; - vi.mock("../utils/github.js", () => ({ generateGitHubAppToken: vi.fn().mockResolvedValue("fake-token"), })); - -import { checkWeeks } from "./check-weeks.js"; - -describe("checkWeeks", () => { - beforeEach(() => { - vi.clearAllMocks(); - globalThis.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: () => Promise.resolve([]), - }); - }); - - it("returns 403 for non-DaleStudy organization", async () => { - const request = new Request("https://example.com/check-weeks", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ repo_owner: "OtherOrg", repo_name: "leetcode-study" }), - }); - - const response = await checkWeeks(request, {}); - expect(response.status).toBe(403); - }); -}); ``` -### CI ์ž๋™ ์‹คํ–‰ - -`.github/workflows/integration.yaml`์ด ๋ชจ๋“  Pull Request์™€ `main` ๋ธŒ๋žœ์น˜ ํ‘ธ์‹œ์—์„œ `bun install` ๋‹ค์Œ `bun run test`๋ฅผ ์ž๋™ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•˜๋ฉด PR ์ฒดํฌ๊ฐ€ ์‹คํŒจํ•˜๋ฏ€๋กœ, ๋จธ์ง€ ์ „์— ๋ฐ˜๋“œ์‹œ ํ†ต๊ณผ์‹œ์ผœ์•ผ ํ•ฉ๋‹ˆ๋‹ค. - -## ์ƒˆ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ ๊ฐ€์ด๋“œ - -์ƒˆ๋กœ์šด ์ž๋™ํ™” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•  ๋•Œ ๋‹ค์Œ ๋‹จ๊ณ„๋ฅผ ๋”ฐ๋ฅด์„ธ์š”: - -1. **์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€**: `index.js`์˜ `fetch()` ํ•จ์ˆ˜์— ์ƒˆ๋กœ์šด pathname ๋ผ์šฐํŒ… ์ถ”๊ฐ€ -2. **ํ•ธ๋“ค๋Ÿฌ ํ•จ์ˆ˜ ์ž‘์„ฑ**: ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๋ณ„๋„ ํ•จ์ˆ˜๋กœ ๋ถ„๋ฆฌ (์˜ˆ: `handleCheckAllPrs`) -3. **GitHub App ๊ถŒํ•œ ํ™•์ธ**: ํ•„์š”ํ•œ ๊ถŒํ•œ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๊ณ  ์—†์œผ๋ฉด ์ถ”๊ฐ€ -4. **ํ…Œ์ŠคํŠธ ์ž‘์„ฑ**: ํ•ธ๋“ค๋Ÿฌ ์˜†์— `*.test.js`๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  `bun run test`๋กœ ํ†ต๊ณผ ํ™•์ธ (์œ„ "ํ…Œ์ŠคํŠธ" ์„น์…˜ ์ฐธ๊ณ ) -5. **๋ฌธ์„œ ์—…๋ฐ์ดํŠธ**: AGENTS.md, README.md์— ์ƒˆ ๊ธฐ๋Šฅ ๋ฌธ์„œํ™” -6. **๋กœ์ปฌ ์‹คํ–‰ ํ…Œ์ŠคํŠธ**: `wrangler dev`๋กœ ์‹ค์ œ ์—”๋“œํฌ์ธํŠธ ๋™์ž‘ ํ™•์ธ ํ›„ ๋ฐฐํฌ - -## ์ฝ”๋“œ ์ˆ˜์ • ์‹œ ์ฃผ์˜์‚ฌํ•ญ - -1. **Octokit ์‚ฌ์šฉ ๊ธˆ์ง€** - - Cloudflare Workers์—์„œ ์ž‘๋™ํ•˜์ง€ ์•Š์Œ - - fetch API ์ง์ ‘ ์‚ฌ์šฉ - -2. **Private Key ์ฒ˜๋ฆฌ** - - PKCS8 ๋˜๋Š” PKCS1 ํ˜•์‹ ์ง€์› - - Web Crypto API๋กœ import - -3. **GraphQL ์ฟผ๋ฆฌ ์ฃผ์˜** - - GraphQL ์ฟผ๋ฆฌ์—์„œ ๋ณ€์ˆ˜๋ฅผ ๋ฌธ์ž์—ด ํ…œํ”Œ๋ฆฟ์œผ๋กœ ์ง์ ‘ ์‚ฝ์ž… (GraphQL ๋ณ€์ˆ˜ ๋ฌธ๋ฒ• ์‚ฌ์šฉ ์•ˆ ํ•จ) - - ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ์ด ์ค‘์š” (SQL Injection ์Šคํƒ€์ผ ์ทจ์•ฝ์  ๋ฐฉ์ง€) - -4. **์—๋Ÿฌ ํ•ธ๋“ค๋ง** - - Worker๋Š” ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ 500 ๋ฐ˜ํ™˜ - - ๋กœ๊ทธ๋Š” `wrangler tail`๋กœ ํ™•์ธ - -5. **CORS ํ—ค๋”** - - ๋ชจ๋“  ์‘๋‹ต์— CORS ํ—ค๋” ํฌํ•จ (`Access-Control-Allow-Origin: *`) - -6. **์ฝ”๋“œ ์žฌ์‚ฌ์šฉ** - - GitHub ์ธ์ฆ ๋กœ์ง (`generateGitHubAppToken`, `createJWT` ๋“ฑ)์€ ๋ชจ๋“  ๊ธฐ๋Šฅ์—์„œ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉ - - ์ƒˆ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ ์‹œ ๊ธฐ์กด ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ ํ™œ์šฉ - -7. **์ฝ”๋ฉ˜ํŠธ ์ˆจ๊น€ ๋งˆ์ปค ์ง๋ ฌํ™” ํฌ๋งท ๋ณ€๊ฒฝ** - - ์ฝ”๋ฉ˜ํŠธ์— `` ํ˜•ํƒœ๋กœ ์ˆจ๊ฒจ ์ €์žฅํ•˜๋Š” ๋ฐ์ดํ„ฐ์˜ ์ง๋ ฌํ™” ํฌ๋งท(๊ฐ์ฒดโ†”๋ฐฐ์—ด ๋“ฑ)์„ ๋ฐ”๊ฟ€ ๋•Œ๋Š” **์ •๊ทœ์‹ยท๋ฌธ์„œ ์ฃผ์„ยทํ…Œ์ŠคํŠธ๋ฅผ ๊ฐ™์€ PR์—์„œ ํ•จ๊ป˜ ๊ฐฑ์‹ ** - - ํŒŒ์‹ฑ์ด `Array.isArray` ๊ฐ™์€ ๋ฐฉ์–ด ์ฝ”๋“œ๋กœ ๋นˆ ๊ฐ’์— fallbackํ•˜๋ฉด ํšŒ๊ท€๊ฐ€ ์กฐ์šฉํžˆ ๋ฌปํ˜€ ๋””๋ฒ„๊น…์ด ์–ด๋ ค์›Œ์ง - -## ๊ด€๋ จ ๋ฌธ์„œ +## ์ฃผ์˜์‚ฌํ•ญ -- [Cloudflare Workers Docs](https://developers.cloudflare.com/workers/) -- [GitHub Apps API](https://docs.github.com/en/apps) -- [GitHub GraphQL API](https://docs.github.com/en/graphql) -- [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) +- **Workers ๋Ÿฐํƒ€์ž„์ด๋‹ค.** Node.js ๋ชจ๋“ˆ๊ณผ Octokit ๊ฐ™์€ npm ํŒจํ‚ค์ง€๋Š” ๋Œ€๋ถ€๋ถ„ ๋™์ž‘ํ•˜์ง€ ์•Š๋Š”๋‹ค. `fetch` ์™€ Web Crypto API ๋กœ ์ง์ ‘ ๊ตฌํ˜„ํ•œ๋‹ค. Private Key ๋Š” PKCS8, PKCS1 ๋ชจ๋‘ ๋ฐ›๋Š”๋‹ค (`utils/github.js`). +- **GraphQL ์ฟผ๋ฆฌ์— ๊ฐ’์„ ๋ฌธ์ž์—ด๋กœ ์ง์ ‘ ๋ผ์›Œ ๋„ฃ๋Š”๋‹ค.** GraphQL ๋ณ€์ˆ˜ ๋ฌธ๋ฒ•์„ ์“ฐ์ง€ ์•Š์œผ๋ฏ€๋กœ ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ์„ ๋น ๋œจ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค. +- **๋Œ“๊ธ€ ์ˆจ๊น€ ๋งˆ์ปค(``)์˜ ์ง๋ ฌํ™” ํ˜•์‹์„ ๋ฐ”๊ฟ€ ๋•Œ๋Š”** ์ •๊ทœ์‹, ๋ฌธ์„œ ์ฃผ์„, ํ…Œ์ŠคํŠธ๋ฅผ ๊ฐ™์€ PR ์—์„œ ํ•จ๊ป˜ ๊ณ ์นœ๋‹ค. ํŒŒ์‹ฑ์ด ๋นˆ ๊ฐ’์œผ๋กœ fallback ํ•˜๋ฉด ํšŒ๊ท€๊ฐ€ ์กฐ์šฉํžˆ ๋ฌปํžŒ๋‹ค. +- ์‘๋‹ต์—๋Š” CORS ํ—ค๋”(`Access-Control-Allow-Origin: *`)๊ฐ€ ๋ถ™๊ณ , ์—๋Ÿฌ๋Š” 500 ์œผ๋กœ ๋Œ๋ ค์ค€๋‹ค. ์›์ธ์€ `wrangler tail` ๋กœ ๋ณธ๋‹ค. +- 401 ์ด ๋‚˜๋ฉด `APP_ID` ์™€ `PRIVATE_KEY` ๊ฐ€ ๊ฐ™์€ App ์˜ ๊ฒƒ์ธ์ง€ ํ™•์ธํ•œ๋‹ค. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f4741fc --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 DaleStudy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 6f446fb..acdda5d 100644 --- a/README.md +++ b/README.md @@ -1,269 +1,51 @@ -# DaleStudy GitHub App +# github -DaleStudy ์กฐ์ง์˜ ์ž๋™ํ™” ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•˜๋Š” GitHub App (https://github.com/apps/dalestudy) +[![All Contributors](https://img.shields.io/github/all-contributors/DaleStudy/github?color=ee8449&style=flat-square)](#contributors) -## ํ”„๋กœ์ ํŠธ ๋น„์ „ +๋‹ฌ๋ ˆ ์Šคํ„ฐ๋”” ์กฐ์ง์˜ [GitHub App](https://github.com/apps/dalestudy)์ด ๋ฐ›๋Š” ์›นํ›…๊ณผ ์ž๋™ํ™” ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š” Cloudflare Worker. ๋ฆฌํŠธ์ฝ”๋“œ ์Šคํ„ฐ๋””์˜ Week ์„ค์ • ๊ฒ€์‚ฌ, ํ’€์ด PR ์ผ๊ด„ ์Šน์ธยท๋ณ‘ํ•ฉ, AI ์ฝ”๋“œ ๋ฆฌ๋ทฐ๋ฅผ ๋งก๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. -DaleStudy ์กฐ์ง์˜ ์—ฌ๋Ÿฌ repository์—์„œ ๋ฐ˜๋ณต์ ์ธ ์ž‘์—…๋“ค์„ ์ž๋™ํ™”ํ•˜๋Š” ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ํ”Œ๋žซํผ์ž…๋‹ˆ๋‹ค. +๊ฐœ๋ฐœ์— ํ•„์š”ํ•œ ๋‚ด์šฉ์€ [AGENTS.md](AGENTS.md)์— ์žˆ์Šต๋‹ˆ๋‹ค. -## ํ˜„์žฌ ๊ตฌํ˜„๋œ ๊ธฐ๋Šฅ +## ๊ธฐ์—ฌ (Contribution) -### 1. PR Week ์„ค์ • ์ž๋™ ๊ฒ€์‚ฌ (๋ฆฌํŠธ์ฝ”๋“œ ์Šคํ„ฐ๋””) +๋ณธ ํ”„๋กœ์ ํŠธ๋Š” [All Contributors](https://github.com/all-contributors/all-contributors) ๊ด€๋ก€์— ๋”ฐ๋ผ ๊ธฐ์—ฌ์ž๋ถ„๋“ค์˜ ๊ณตํ—Œ์„ ์ธ์ •ํ•˜๊ณ  ๊ฐ์‚ฌ๋ฅผ ํ‘œํ˜„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. -Fork PR์—์„œ๋„ ์ž‘๋™ํ•˜๋„๋ก GitHub Projects v2์˜ Week ํ•„๋“œ๋ฅผ ์กฐํšŒํ•˜๊ณ , Week ์„ค์ •์ด ๋ˆ„๋ฝ๋œ PR์— ์ž๋™์œผ๋กœ ๊ฒฝ๊ณ  ๋Œ“๊ธ€์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. + + + + + + + + + + + + + + + +
Dale Seo
Dale Seo

๐Ÿ’ป
Soobin Bak
Soobin Bak

๐Ÿ’ป
Evan Suhyeong Lee
Evan Suhyeong Lee

๐Ÿ’ป
Gichan Kim
Gichan Kim

๐Ÿ’ป
KwangHun Lee
KwangHun Lee

๐Ÿ’ป
Hojeong Park
Hojeong Park

๐Ÿ’ป
Jooyoung Julia Lee
Jooyoung Julia Lee

๐Ÿ’ป
-**ํŠธ๋ฆฌ๊ฑฐ ๋ฐฉ์‹:** -- **์‹ค์‹œ๊ฐ„**: GitHub Organization Webhook (Week ์„ค์ • ๋ณ€๊ฒฝ ์ฆ‰์‹œ ๋ฐ˜์‘) -- **์ˆ˜๋™**: REST API ์ง์ ‘ ํ˜ธ์ถœ + + -**๋Œ€์ƒ**: https://github.com/DaleStudy/leetcode-study + -**์„ค์ • ๊ฐ€์ด๋“œ**: `AGENTS.md`์˜ "GitHub Organization Webhook ์„ค์ •" ์„น์…˜ ์ฐธ๊ณ  +## ์‘์› (Support) -## ํ–ฅํ›„ ํ™•์žฅ ๊ณ„ํš +๋‹ฌ๋ ˆ ์Šคํ„ฐ๋””๋กœ ๋ถ€ํ„ฐ ๋ฐ›์€ ๋„์›€์„ ํ›„์›์„ ํ†ตํ•ด์„œ ๋” ๋งŽ์€ ๋ถ„๋“ค์—๊ฒŒ ๋‚˜๋ˆ ์ฃผ์„ธ์š”. ๐Ÿ’ +ํ›„์›์€ ๋‹ฌ๋ ˆ ์Šคํ„ฐ๋””๊ฐ€ ์ปค๋ฎค๋‹ˆํ‹ฐ๋กœ์„œ ์ง€์† ๊ฐ€๋Šฅํ•  ์ˆ˜ ์žˆ๋Š” ์ค‘์š”ํ•œ ๊ธฐ๋ฐ˜์ด ๋ฉ๋‹ˆ๋‹ค. ๐Ÿ™ +์ €์žฅ์†Œ์— ์Šคํƒ€๋ฅผ ์ฃผ์‹œ๋Š” ๊ฒƒ๋„ ๋” ๋งŽ์€ ๋ถ„๋“ค์ด ์ €ํฌ ์ปค๋ฎค๋‹ˆํ‹ฐ๋ฅผ ์ฐพ๋Š”๋ฐ ๋„์›€์ด ๋ฉ๋‹ˆ๋‹ค. โญ -๋‹จ์ผ Worker์— ์—ฌ๋Ÿฌ ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๋‹ค์–‘ํ•œ ์ž๋™ํ™” ์š”๊ตฌ์‚ฌํ•ญ์„ ์ฒ˜๋ฆฌํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค. +

+ + Sponsors + +

-## ๋ฐฐํฌ ๋ฐฉ๋ฒ• +## ๋ผ์ด์„ ์Šค (License) -### 1. Cloudflare ๊ณ„์ • ์„ค์ • +์ด ์ €์žฅ์†Œ์˜ ์†Œ์Šค ์ฝ”๋“œ๋Š” [MIT ๋ผ์ด์„ ์Šค](LICENSE)๋ฅผ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. -1. https://dash.cloudflare.com ํšŒ์›๊ฐ€์ž… -2. Workers ์„น์…˜์œผ๋กœ ์ด๋™ - -### 2. Wrangler CLI ์„ค์น˜ - -```bash -npm install -g wrangler -wrangler login -``` - -### 3. Secrets ์„ค์ • - -GitHub App์˜ credentials๋ฅผ Worker secrets์— ์ €์žฅ: - -```bash -# APP_ID ์„ค์ • -wrangler secret put APP_ID -# ํ”„๋กฌํ”„ํŠธ์—์„œ GitHub App ID ์ž…๋ ฅ - -# PRIVATE_KEY ์„ค์ • -wrangler secret put PRIVATE_KEY -# ํ”„๋กฌํ”„ํŠธ์—์„œ GitHub App Private Key ์ „์ฒด ๋‚ด์šฉ ์ž…๋ ฅ -# (-----BEGIN RSA PRIVATE KEY----- ๋ถ€ํ„ฐ -----END RSA PRIVATE KEY----- ๊นŒ์ง€) -``` - -### 4. Worker ๋ฐฐํฌ - -```bash -wrangler deploy -``` - -๋ฐฐํฌ ์™„๋ฃŒ ํ›„ Custom Domain์„ ํ†ตํ•ด ์ ‘๊ทผ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค: - -``` -https://github.dalestudy.com -``` - -## API ์—”๋“œํฌ์ธํŠธ - -**Base URL**: https://github.dalestudy.com - -### `POST /check-weeks` - -๋ชจ๋“  Open PR์—์„œ Week ์„ค์ •์„ ๊ฒ€์‚ฌํ•˜๊ณ  ์ž๋™์œผ๋กœ ๋Œ“๊ธ€ ์ž‘์„ฑ/์‚ญ์ œ - -**Request:** - -์š”์ฒญ ๋ฐ”๋””์—์„œ `repo_owner`๋ฅผ ์ƒ๋žตํ•˜๋ฉด ์ž๋™์œผ๋กœ `DaleStudy`๊ฐ€ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. - -```json -{ - "repo_name": "leetcode-study" -} -``` - -**Response:** - -```json -{ - "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 } - ] -} -``` - -### `POST /approve-prs` - -์—ด๋ ค์žˆ๋Š” ๋‹ต์•ˆ ์ œ์ถœ PR์„ ์ผ๊ด„ ์Šน์ธํ•ฉ๋‹ˆ๋‹ค. `excludes` ๋ฐฐ์—ด์„ ์‚ฌ์šฉํ•ด ํŠน์ • PR ๋ฒˆํ˜ธ๋ฅผ ์ œ์™ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฏธ ์Šน์ธ๋˜์—ˆ๊ฑฐ๋‚˜ Draft/`maintenance` ๋ผ๋ฒจ์ด ๋ถ™์€ PR์€ ์ž๋™์œผ๋กœ ์Šคํ‚ต๋ฉ๋‹ˆ๋‹ค. - -**Request:** - -```json -{ "repo_name": "leetcode-study", "excludes": [1972] } -``` - -**Response:** - -```json -{ - "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 } - ] -} -``` - -### `POST /merge-prs` - -์—ด๋ ค์žˆ๋Š” PR์„ ์ผ๊ด„ ๋ณ‘ํ•ฉํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ ๋ณ‘ํ•ฉ ๋ฐฉ์‹์€ `squash`์ด๋ฉฐ, `merge_method`๋กœ `merge | squash | rebase` ์ค‘ ์„ ํƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. `excludes` ๋ฐฐ์—ด๋กœ ํŠน์ • PR์„ ์ œ์™ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ตœ์†Œ 1๊ฐœ์˜ ์Šน์ธ ๋ฆฌ๋ทฐ๊ฐ€ ์—†๊ฑฐ๋‚˜ Draft/`maintenance` ๋ผ๋ฒจ์ด ๋ถ™์€ PR์€ ์Šคํ‚ต๋˜๋ฉฐ, GitHub์—์„œ `mergeable_state === "clean"`์ธ PR๋งŒ ๋ณ‘ํ•ฉ๋ฉ๋‹ˆ๋‹ค(`behind`, `dirty`, `unknown` ๋“ฑ์€ ์Šคํ‚ต). `unknown`/`behind` ์ƒํƒœ๋Š” ์ตœ๋Œ€ 1์ดˆ ํ›„ ํ•œ ์ฐจ๋ก€ ์žฌํ™•์ธํ•ฉ๋‹ˆ๋‹ค. - -**Request:** - -```json -{ - "repo_name": "leetcode-study", - "excludes": [1972], - "merge_method": "squash" -} -``` - -**Response:** - -```json -{ - "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" } - ] -} -``` - -## ๊ฐœ๋ฐœ ๋ฐ ํ…Œ์ŠคํŠธ - -### ๋กœ์ปฌ ๊ฐœ๋ฐœ - -```bash -# ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹œ์ž‘ -wrangler dev - -# ๋กœ์ปฌ ์—”๋“œํฌ์ธํŠธ ํ˜ธ์ถœ (๋ณ„๋„ ํ„ฐ๋ฏธ๋„) -curl -X POST http://localhost:8787/check-weeks \ - -H "Content-Type: application/json" \ - -d '{"repo_owner": "DaleStudy", "repo_name": "leetcode-study"}' -``` - -### ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์‹คํ–‰ - -์ด ํ”„๋กœ์ ํŠธ๋Š” **Cloudflare Workers Vitest integration**์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ๋Š” Vitest๋กœ ์‹คํ–‰ํ•˜๊ณ , Worker ๋Ÿฐํƒ€์ž„๊ณผ ๋ฐ”์ธ๋”ฉ์ด ํ•„์š”ํ•œ ํ…Œ์ŠคํŠธ๋Š” Cloudflare ํ…Œ์ŠคํŠธ ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. - -```bash -# ์˜์กด์„ฑ ์„ค์น˜ -bun install - -# ์ „์ฒด ํ…Œ์ŠคํŠธ ์‹คํ–‰ -bun run test - -# ๊ฐ์‹œ ๋ชจ๋“œ -bun run test:watch - -# ํŠน์ • ํŒŒ์ผ๋งŒ ์‹คํ–‰ -bun run test -- handlers/webhooks.test.js -``` - -ํ…Œ์ŠคํŠธ๋Š” ์„ธ ๋””๋ ‰ํ† ๋ฆฌ๋กœ ๋‚˜๋‰˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค: - -- `handlers/*.test.js`: ํ•ธ๋“ค๋Ÿฌ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ -- `utils/*.test.js`: ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ -- `tests/*.test.js`: Worker runtime smoke test์™€ ๊ต์ฐจ ๋ชจ๋“ˆ ํ…Œ์ŠคํŠธ - -๋ชจ๋“ˆ ๋ชจํ‚น์€ Vitest์˜ `vi.mock()`์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. Worker runtime๊ณผ bindings๊ฐ€ ํ•„์š”ํ•œ ํ…Œ์ŠคํŠธ๋Š” `cloudflare:test`์™€ `cloudflare:workers`๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. - -์ž์„ธํ•œ ์ž‘์„ฑ ๊ทœ์น™๊ณผ ์˜ˆ์ œ๋Š” `AGENTS.md`์˜ "ํ…Œ์ŠคํŠธ" ์„น์…˜์„ ์ฐธ๊ณ ํ•˜์„ธ์š”. - -๋ชจ๋“  Pull Request์™€ `main` ๋ธŒ๋žœ์น˜ ํ‘ธ์‹œ์—์„œ `.github/workflows/integration.yaml`์ด `bun install` ๋‹ค์Œ `bun run test`๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. - -### ํ”„๋กœ๋•์…˜ ์—”๋“œํฌ์ธํŠธ ํ˜ธ์ถœ - -```bash -curl -X POST https://github.dalestudy.com/check-weeks \ - -H "Content-Type: application/json" \ - -d '{"repo_owner": "DaleStudy", "repo_name": "leetcode-study"}' -``` - -### ๋กœ๊ทธ ํ™•์ธ - -```bash -wrangler tail -``` - -## ๋ณด์•ˆ - -- DaleStudy organization๋งŒ ํ—ˆ์šฉ -- CORS ํ—ค๋” ์„ค์ • (๋ชจ๋“  origin ํ—ˆ์šฉ) -- GitHub App credentials๋Š” Worker secrets์— ์•ˆ์ „ํ•˜๊ฒŒ ์ €์žฅ -- Rate limiting์€ Cloudflare์—์„œ ์ž๋™ ์ฒ˜๋ฆฌ - -## ๋น„์šฉ - -Cloudflare Workers ๋ฌด๋ฃŒ ํ‹ฐ์–ด: - -- 100,000 requests/day -- 10ms CPU time per request - -๋ฆฌํŠธ์ฝ”๋“œ ์Šคํ„ฐ๋”” PR ์ˆ˜์ค€์ด๋ฉด ๋ฌด๋ฃŒ ํ‹ฐ์–ด๋กœ ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค. - -## ์ƒˆ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ํ•˜๊ธฐ - -์ƒˆ๋กœ์šด ์ž๋™ํ™” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๋ ค๋ฉด: - -1. `index.js`์— ์ƒˆ ์—”๋“œํฌ์ธํŠธ ๋ผ์šฐํŒ… ์ถ”๊ฐ€ -2. ํ•ธ๋“ค๋Ÿฌ ํ•จ์ˆ˜ ๊ตฌํ˜„ (๊ธฐ์กด `handleCheckAllPrs` ์ฐธ๊ณ ) -3. GitHub App ๊ถŒํ•œ ํ™•์ธ ๋ฐ ํ•„์š”์‹œ ์ถ”๊ฐ€ -4. ๋ฌธ์„œ(AGENTS.md, README.md) ์—…๋ฐ์ดํŠธ -5. ๋กœ์ปฌ ํ…Œ์ŠคํŠธ ํ›„ ๋ฐฐํฌ (`wrangler deploy`) - -์ž์„ธํ•œ ๊ฐ€์ด๋“œ๋Š” `AGENTS.md`๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”. - -## ๋ฌธ์ œ ํ•ด๊ฒฐ - -### Worker๊ฐ€ 401 ์—๋Ÿฌ ๋ฐ˜ํ™˜ - -GitHub App credentials ํ™•์ธ: - -```bash -wrangler secret list -``` - -APP_ID์™€ PRIVATE_KEY๊ฐ€ ๋ชจ๋‘ ์„ค์ •๋˜์–ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. - -### Worker๊ฐ€ 403 ์—๋Ÿฌ ๋ฐ˜ํ™˜ - -DaleStudy organization์ด ์•„๋‹Œ ๊ฒฝ์šฐ ์ฐจ๋‹จ๋ฉ๋‹ˆ๋‹ค. - -## ๊ด€๋ จ ๋ฌธ์„œ - -- **AGENTS.md**: AI ์—์ด์ „ํŠธ๋ฅผ ์œ„ํ•œ ์ƒ์„ธ ๊ฐ€์ด๋“œ (๊ฐœ๋ฐœ์ž๋„ ํ•„๋…!) -- **DEPLOYMENT.md**: ์ฒ˜์Œ๋ถ€ํ„ฐ ๋ฐฐํฌํ•˜๋Š” ์ „์ฒด ๊ฐ€์ด๋“œ -- **GitHub App**: https://github.com/organizations/DaleStudy/settings/apps/dalestudy +๋‹ค๋งŒ ๋‹ฌ๋ ˆ ์Šคํ„ฐ๋””์˜ ์ด๋ฆ„๊ณผ ๋กœ๊ณ ๋ฅผ ๋น„๋กฏํ•œ ๋ธŒ๋žœ๋“œ ์ž์‚ฐ์€ ๋ผ์ด์„ ์Šค ๋ฒ”์œ„์— ํฌํ•จ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. diff --git a/index.js b/index.js index ce4ad56..810e166 100644 --- a/index.js +++ b/index.js @@ -37,18 +37,21 @@ export default { const signature = request.headers.get("X-Hub-Signature-256"); const rawBody = await request.text(); - // Secret์ด ์„ค์ •๋˜์–ด ์žˆ์œผ๋ฉด ๊ฒ€์ฆ - if (env.WEBHOOK_SECRET) { - const isValid = await verifyWebhookSignature( - rawBody, - signature, - env.WEBHOOK_SECRET - ); - - if (!isValid) { - console.error("Invalid webhook signature"); - return errorResponse("Invalid signature", 401); - } + // Secret์ด ์—†์œผ๋ฉด ์„œ๋ช…์„ ๊ฒ€์ฆํ•  ์ˆ˜ ์—†์œผ๋ฏ€๋กœ ๋ฐ›์ง€ ์•Š๋Š”๋‹ค + if (!env.WEBHOOK_SECRET) { + console.error("WEBHOOK_SECRET is not configured"); + return errorResponse("Webhook secret is not configured", 500); + } + + const isValid = await verifyWebhookSignature( + rawBody, + signature, + env.WEBHOOK_SECRET + ); + + if (!isValid) { + console.error("Invalid webhook signature"); + return errorResponse("Invalid signature", 401); } // Request ๊ฐ์ฒด ์žฌ์ƒ์„ฑ (body๋ฅผ ๋‹ค์‹œ ์ฝ์„ ์ˆ˜ ์žˆ๋„๋ก) diff --git a/tests/worker-runtime.test.js b/tests/worker-runtime.test.js index f323249..d33a6de 100644 --- a/tests/worker-runtime.test.js +++ b/tests/worker-runtime.test.js @@ -4,10 +4,10 @@ import { describe, expect, it } from "vitest"; import worker from "../index.js"; -async function fetchWorker(input, init = {}) { +async function fetchWorker(input, init = {}, bindings = env) { const request = input instanceof Request ? input : new Request(input, init); const ctx = createExecutionContext(); - const response = await worker.fetch(request, env, ctx); + const response = await worker.fetch(request, bindings, ctx); await waitOnExecutionContext(ctx); @@ -59,4 +59,47 @@ describe("Worker runtime smoke test", () => { it("exposes WORKER_URL from wrangler config in env", () => { expect(env.WORKER_URL).toBe("https://github.dalestudy.workers.dev"); }); + + describe("/webhooks signature", () => { + const secret = "test-webhook-secret"; + const body = JSON.stringify({ zen: "ping", repository: { name: "other-repo" } }); + + async function sign(payload) { + const key = await crypto.subtle.importKey( + "raw", + new TextEncoder().encode(secret), + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"], + ); + const bytes = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(payload)); + return "sha256=" + [...new Uint8Array(bytes)].map((b) => b.toString(16).padStart(2, "0")).join(""); + } + + function webhookRequest(signature) { + const headers = { "Content-Type": "application/json", "X-GitHub-Event": "ping" }; + if (signature) headers["X-Hub-Signature-256"] = signature; + return new Request("https://example.com/webhooks", { method: "POST", headers, body }); + } + + it("rejects webhooks when WEBHOOK_SECRET is not configured", async () => { + const response = await fetchWorker(webhookRequest(await sign(body)), {}, { ...env, WEBHOOK_SECRET: undefined }); + + expect(response.status).toBe(500); + }); + + it("rejects webhooks with a missing or wrong signature", async () => { + const bindings = { ...env, WEBHOOK_SECRET: secret }; + + expect((await fetchWorker(webhookRequest(), {}, bindings)).status).toBe(401); + expect((await fetchWorker(webhookRequest("sha256=" + "0".repeat(64)), {}, bindings)).status).toBe(401); + }); + + it("accepts webhooks signed with WEBHOOK_SECRET", async () => { + const response = await fetchWorker(webhookRequest(await sign(body)), {}, { ...env, WEBHOOK_SECRET: secret }); + + expect(response.status).toBe(200); + }); + }); }); + diff --git a/wrangler.jsonc b/wrangler.jsonc index 9864b1b..be8c081 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -5,6 +5,8 @@ "compatibility_flags": ["global_fetch_strictly_public"], "account_id": "94480614a6df7731f1e4491bdac5c440", "vars": { + // GitHub App(dalestudy)์˜ ID. ๊ณต๊ฐœ ๊ฐ’์ด๋‹ค. + "APP_ID": "1067270", "WORKER_URL": "https://github.dalestudy.workers.dev", }, "observability": {