From 45ec51e7f5693feeb34298854d366af8018f4a5e Mon Sep 17 00:00:00 2001 From: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:20:20 -0400 Subject: [PATCH 1/4] ci: deploy with github actions --- .github/workflows/deploy.yml | 41 ++++++++++++++++++++++++++++++++++++ AGENTS.md | 35 ++++++++---------------------- README.md | 23 ++++---------------- wrangler.jsonc | 2 ++ 4 files changed, 56 insertions(+), 45 deletions(-) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..3b28cff --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,41 @@ +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 }} + run: | + jq -n --arg private_key "$PRIVATE_KEY" --arg ai "$AI_GATEWAY_TOKEN" --arg internal "$INTERNAL_SECRET" \ + '{PRIVATE_KEY: $private_key, AI_GATEWAY_TOKEN: $ai, INTERNAL_SECRET: $internal}' > "$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..effa62c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -95,13 +95,9 @@ curl -X POST http://localhost:8787/check-weeks \ ### ๋ฐฐํฌ ```bash -# Worker ๋ฐฐํฌ +# Worker ์ˆ˜๋™ ๋ฐฐํฌ (๋ณดํ†ต์€ main ๋จธ์ง€ ์‹œ GitHub Actions ๊ฐ€ ํ•œ๋‹ค) 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 @@ -316,12 +312,9 @@ GitHub webhook **์ ˆ๋Œ€ ์ปค๋ฐ‹ ๊ธˆ์ง€**: `.env`, `.dev.vars`, `*.pem`, `*.key` -#### Cloudflare Workers Secrets (ํ”„๋กœ๋•์…˜) +#### Secrets (ํ”„๋กœ๋•์…˜) -```bash -wrangler secret put APP_ID # GitHub App ID -wrangler secret put PRIVATE_KEY # GitHub App Private Key (PEM) -``` +`main` ์— ๋จธ์ง€๋˜๋ฉด GitHub Actions(`.github/workflows/deploy.yml`)๊ฐ€ ํ…Œ์ŠคํŠธ ํ›„ `wrangler deploy --secrets-file` ๋กœ ๋ฐฐํฌํ•œ๋‹ค. Worker secret(`PRIVATE_KEY`, `AI_GATEWAY_TOKEN`, `INTERNAL_SECRET`)์€ GitHub secret(`APP_PRIVATE_KEY`, `AI_GATEWAY_TOKEN`, `INTERNAL_SECRET`)์—์„œ ๋ฐฐํฌ์™€ ํ•จ๊ป˜ ๋„ฃ์œผ๋ฏ€๋กœ `wrangler secret put` ์œผ๋กœ ๋”ฐ๋กœ ๋ฐ”๊พธ์ง€ ์•Š๋Š”๋‹ค โ€” ๋‹ค์Œ ๋ฐฐํฌ ๋•Œ GitHub ๊ฐ’์œผ๋กœ ๋ฎ์ธ๋‹ค. `APP_ID` ๋Š” ๊ณต๊ฐœ ๊ฐ’์ด๋ผ `wrangler.jsonc` ์˜ `vars` ์— ์žˆ๋‹ค. #### ๋กœ์ปฌ ๊ฐœ๋ฐœ (.dev.vars) @@ -334,16 +327,13 @@ PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----" ### Cloudflare Workers -```bash -wrangler deploy -``` +`main` ์— ๋จธ์ง€๋˜๋ฉด GitHub Actions ๊ฐ€ ๋ฐฐํฌํ•œ๋‹ค. ์‹œํฌ๋ฆฟ์€ ์œ„ "Secrets (ํ”„๋กœ๋•์…˜)" ์ฐธ๊ณ . ### ์ปค์Šคํ…€ ๋„๋ฉ”์ธ - Production: https://github.dalestudy.com - Worker.dev: https://github.dalestudy.workers.dev -์ž์„ธํ•œ ๋ฐฐํฌ ๊ฐ€์ด๋“œ๋Š” `DEPLOYMENT.md` ์ฐธ๊ณ . ## GitHub App Webhook ์„ค์ • @@ -377,19 +367,12 @@ https://github.com/settings/apps/dalestudy ### 4. Worker Secrets ์„ค์ • -```bash -# AI Gateway ์ธ์ฆ ํ† ํฐ (AI ์ฝ”๋“œ ๋ฆฌ๋ทฐ์šฉ, ํ•„์ˆ˜) -wrangler secret put AI_GATEWAY_TOKEN - -# Webhook Secret (์„ ํƒ์‚ฌํ•ญ) -wrangler secret put WEBHOOK_SECRET +GitHub secret ์— ๋„ฃ์œผ๋ฉด ๋ฐฐํฌํ•  ๋•Œ Worker ๋กœ ๋“ค์–ด๊ฐ„๋‹ค (`.github/workflows/deploy.yml`). -# Internal Dispatch Secret (AI ํ•ธ๋“ค๋Ÿฌ Worker ๋ถ„๋ฆฌ์šฉ, ๊ถŒ์žฅ) -# ์„ค์ •ํ•˜๋ฉด tagPatterns, learningStatus๊ฐ€ ๋ณ„๋„ Worker ํ˜ธ์ถœ๋กœ -# ๋””์ŠคํŒจ์น˜๋˜์–ด ๊ฐ๊ฐ ๋…๋ฆฝ์ ์ธ subrequest ์˜ˆ์‚ฐ์„ ๊ฐ€์ง. -# WORKER_URL๊ณผ ํ•จ๊ป˜ ์„ค์ •๋˜์–ด์•ผ ํ™œ์„ฑํ™”๋œ๋‹ค. -wrangler secret put INTERNAL_SECRET -``` +- `APP_PRIVATE_KEY` (org secret): GitHub App Private Key โ†’ Worker `PRIVATE_KEY` +- `AI_GATEWAY_TOKEN` (org secret): AI Gateway ์ธ์ฆ ํ† ํฐ (AI ์ฝ”๋“œ ๋ฆฌ๋ทฐ์šฉ, ํ•„์ˆ˜) +- `INTERNAL_SECRET` (์ €์žฅ์†Œ secret): AI ํ•ธ๋“ค๋Ÿฌ Worker ๋ถ„๋ฆฌ์šฉ. ์„ค์ •ํ•˜๋ฉด tagPatterns, learningStatus๊ฐ€ ๋ณ„๋„ Worker ํ˜ธ์ถœ๋กœ ๋””์ŠคํŒจ์น˜๋˜์–ด ๊ฐ๊ฐ ๋…๋ฆฝ์ ์ธ subrequest ์˜ˆ์‚ฐ์„ ๊ฐ€์ง„๋‹ค. `WORKER_URL`๊ณผ ํ•จ๊ป˜ ์žˆ์–ด์•ผ ํ™œ์„ฑํ™”๋œ๋‹ค. +- `WEBHOOK_SECRET` (์„ ํƒ): ์„ค์ •ํ•˜๋ฉด webhook ์„œ๋ช…์„ ๊ฒ€์ฆํ•œ๋‹ค. ์“ฐ๋ ค๋ฉด GitHub secret ์„ ๋งŒ๋“ค๊ณ  `deploy.yml` ์˜ secrets ํŒŒ์ผ์— ์ถ”๊ฐ€ํ•œ๋‹ค. `WORKER_URL`์€ `wrangler.jsonc`์˜ `vars`์— ์ •์˜๋˜์–ด ์žˆ์–ด ๊ธฐ๋ณธ ๋ฐฐํฌ์—๋Š” ์ถ”๊ฐ€ ์„ค์ •์ด ํ•„์š” ์—†๋‹ค. ์Šคํ…Œ์ด์ง•/๋‹ค๋ฅธ ๊ณ„์ • ๋“ฑ์œผ๋กœ ๋ฐฐํฌํ•  ๋•Œ๋งŒ ๋ฎ์–ด์“ฐ๋ฉด ๋œ๋‹ค. diff --git a/README.md b/README.md index 6f446fb..4830536 100644 --- a/README.md +++ b/README.md @@ -38,26 +38,11 @@ npm install -g wrangler wrangler login ``` -### 3. Secrets ์„ค์ • +### 3. ๋ฐฐํฌ -GitHub App์˜ credentials๋ฅผ Worker secrets์— ์ €์žฅ: +`main` ์— ๋จธ์ง€๋˜๋ฉด GitHub Actions(`.github/workflows/deploy.yml`)๊ฐ€ ํ…Œ์ŠคํŠธ ํ›„ `wrangler deploy --secrets-file` ๋กœ ๋ฐฐํฌํ•œ๋‹ค. Worker secret(`PRIVATE_KEY`, `AI_GATEWAY_TOKEN`, `INTERNAL_SECRET`)์€ GitHub secret(`APP_PRIVATE_KEY`, `AI_GATEWAY_TOKEN`, `INTERNAL_SECRET`)์—์„œ ๋ฐฐํฌ์™€ ํ•จ๊ป˜ ๋„ฃ์œผ๋ฏ€๋กœ `wrangler secret put` ์œผ๋กœ ๋”ฐ๋กœ ๋ฐ”๊พธ์ง€ ์•Š๋Š”๋‹ค โ€” ๋‹ค์Œ ๋ฐฐํฌ ๋•Œ GitHub ๊ฐ’์œผ๋กœ ๋ฎ์ธ๋‹ค. `APP_ID` ๋Š” ๊ณต๊ฐœ ๊ฐ’์ด๋ผ `wrangler.jsonc` ์˜ `vars` ์— ์žˆ๋‹ค. -```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 -``` +์ˆ˜๋™์œผ๋กœ ๋ฐฐํฌํ•ด์•ผ ํ•  ๋•Œ๋งŒ `wrangler deploy` ๋ฅผ ์“ด๋‹ค. ๋ฐฐํฌ ์™„๋ฃŒ ํ›„ Custom Domain์„ ํ†ตํ•ด ์ ‘๊ทผ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค: @@ -256,7 +241,7 @@ GitHub App credentials ํ™•์ธ: wrangler secret list ``` -APP_ID์™€ PRIVATE_KEY๊ฐ€ ๋ชจ๋‘ ์„ค์ •๋˜์–ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. +`APP_ID`(`wrangler.jsonc`)์™€ `PRIVATE_KEY`(GitHub secret `APP_PRIVATE_KEY`)๊ฐ€ ๋ชจ๋‘ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ### Worker๊ฐ€ 403 ์—๋Ÿฌ ๋ฐ˜ํ™˜ 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": { From 38bd43d0bc5e9868d1ae937574a24d9240b3062a Mon Sep 17 00:00:00 2001 From: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:25:45 -0400 Subject: [PATCH 2/4] docs: simplify AGENTS.md and align README with other repos --- .all-contributorsrc | 77 +++++++ AGENTS.md | 541 ++++++-------------------------------------- README.md | 277 +++-------------------- 3 files changed, 179 insertions(+), 716 deletions(-) create mode 100644 .all-contributorsrc 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/AGENTS.md b/AGENTS.md index effa62c..703dc2f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,519 +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 ์ˆ˜๋™ ๋ฐฐํฌ (๋ณดํ†ต์€ main ๋จธ์ง€ ์‹œ GitHub Actions ๊ฐ€ ํ•œ๋‹ค) -wrangler deploy - -# 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 } - ] -} ``` - -#### `POST /merge-prs` - -์—ด๋ ค์žˆ๋Š” PR์„ ์ผ๊ด„ ๋ณ‘ํ•ฉํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ ๋ณ‘ํ•ฉ ๋ฐฉ์‹์€ `squash`์ด๋ฉฐ `merge_method` ๊ฐ’์œผ๋กœ `merge | squash | rebase` ์ค‘ ์„ ํƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. `excludes`๋กœ ํŠน์ • PR์„ ์ œ์™ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์Šน์ธ ๋ฆฌ๋ทฐ๊ฐ€ ์—†๋Š” PR, Draft PR, GitHub `mergeable_state !== "clean"` PR์€ ์Šคํ‚ต๋˜๋ฉฐ `unknown`/`behind` ์ƒํƒœ๋Š” ์ตœ๋Œ€ 1์ดˆ ํ›„ ํ•œ ๋ฒˆ ๋” ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. - -**Request:** - -```json -{ - "repo_name": "leetcode-study", - "merge_method": "squash", - "excludes": [1972] -} +index.js ์—”๋“œํฌ์ธํŠธ ๋ผ์šฐํŒ…๋งŒ ํ•œ๋‹ค (POST ๋งŒ ๋ฐ›๋Š”๋‹ค) +handlers/ ๊ธฐ๋Šฅ๋ณ„ ํ•ธ๋“ค๋Ÿฌ์™€ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ(*.test.js) +utils/ ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ์™€ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ (github.js: App ์ธ์ฆ, prActions.js: ์Šน์ธยท๋ณ‘ํ•ฉ ๊ณตํ†ต ๋กœ์ง) +tests/ Worker runtime smoke test, subrequest ์˜ˆ์‚ฐ, ๊ต์ฐจ ๋ชจ๋“ˆ ํ…Œ์ŠคํŠธ +wrangler.jsonc Worker ์„ค์ •๊ณผ ๊ณต๊ฐœ ๊ฐ’(vars) ``` -**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" } - ] -} -``` +์ƒˆ ๊ธฐ๋Šฅ์€ `handlers/` ์— ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๋งŒ๋“ค๊ณ  `index.js` ์— ๊ฒฝ๋กœ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค. ํ•„์š”ํ•œ GitHub App ๊ถŒํ•œ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๊ณ , ํ•ธ๋“ค๋Ÿฌ ์˜†์— ํ…Œ์ŠคํŠธ๋ฅผ ๋‘”๋‹ค. -#### `POST /resolve-iteration` +## ์—”๋“œํฌ์ธํŠธ -์ฃผ์–ด์ง„ ๋‚ ์งœ๊ฐ€ ์–ด๋А ๊ธฐ์ˆ˜์˜ ๋ช‡ ์ฃผ์ฐจ์ธ์ง€ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. ๋ฆฌํŠธ์ฝ”๋“œ ์Šคํ„ฐ๋””๋Š” ๊ธฐ์ˆ˜๋งˆ๋‹ค ํ”„๋กœ์ ํŠธ ๋ณด๋“œ๋ฅผ ์ƒˆ๋กœ ๋งŒ๋“ค๊ณ  ๋ณด๋“œ์˜ Week ํ•„๋“œ(Iteration)๊ฐ€ ์ฃผ์ฐจ ์ผ์ •์„ ๊ด€๋ฆฌํ•˜๋ฏ€๋กœ, ์ฃผ์ฐจ ๋ฒˆํ˜ธ๋ฅผ ํ•˜๋“œ์ฝ”๋”ฉํ•˜์ง€ ์•Š๊ณ  ์—ฌ๊ธฐ์„œ ์ฝ์Šต๋‹ˆ๋‹ค. +`repo_owner` ๋ฅผ ๋ฐ›๋Š” ์—”๋“œํฌ์ธํŠธ๋Š” `DaleStudy` ๊ฐ€ ์•„๋‹ˆ๋ฉด 403 ์ด๋‹ค (์ƒ๋žตํ•˜๋ฉด `DaleStudy`). `/check-weeks` ๋Š” ์ €์žฅ์†Œ๋„ leetcode-study ๋งŒ ๋ฐ›๋Š”๋‹ค. -Actions์˜ ๊ธฐ๋ณธ `GITHUB_TOKEN`์œผ๋กœ๋Š” org ํ”„๋กœ์ ํŠธ ๋ณด๋“œ๋ฅผ ์ฝ์„ ์ˆ˜ ์—†์–ด(`read:project` ์Šค์ฝ”ํ”„ ๋ถ€์žฌ) App ํ† ํฐ์„ ๊ฐ€์ง„ ์ด ์›Œ์ปค๊ฐ€ ์กฐํšŒ๋ฅผ ๋Œ€์‹ ํ•ฉ๋‹ˆ๋‹ค. ์™„๋ฃŒ๋œ ์ฃผ์ฐจ๋Š” `completedIterations`๋กœ ์˜ฎ๊ฒจ๊ฐ€๋ฏ€๋กœ ๋‘ ๋ฐฐ์—ด์„ ๋ชจ๋‘ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. +| ๊ฒฝ๋กœ | ํ•˜๋Š” ์ผ | +| ------------------------- | --------------------------------------------------------------------------------------------------------------- | +| `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` ์ธ์ฆ) | -**Request:** +์Šน์ธยท๋ณ‘ํ•ฉ์˜ ๊ณตํ†ต ๊ทœ์น™์€ `utils/prActions.js` ์— ์žˆ๋‹ค. ์š”์•ฝํ•˜๋ฉด Week ๊ฐ€ ๋‹ค๋ฅด๊ฑฐ๋‚˜ Status ๊ฐ€ `Solving` ์ธ PR, Draft PR ์€ ๊ฑด๋„ˆ๋›ฐ๊ณ , ์Šน์ธ์€ CI ๊ฐ€ ํ†ต๊ณผํ•œ PR ์—๋งŒ, ๋ณ‘ํ•ฉ์€ ์Šน์ธ ๋ฆฌ๋ทฐ๊ฐ€ ์žˆ๊ณ  `mergeable_state` ๊ฐ€ `clean` ์ธ PR ์—๋งŒ ํ•œ๋‹ค. -```json -{ "date": "2026-08-08" } -``` +`/resolve-iteration` ์ด ์žˆ๋Š” ์ด์œ : Actions ์˜ ๊ธฐ๋ณธ `GITHUB_TOKEN` ์€ org ํ”„๋กœ์ ํŠธ ๋ณด๋“œ๋ฅผ ์ฝ์ง€ ๋ชปํ•ด์„œ(`read:project` ์—†์Œ) App ํ† ํฐ์„ ๊ฐ€์ง„ ์ด Worker ๊ฐ€ ๋Œ€์‹  ์ฝ๋Š”๋‹ค. ์ฃผ์ฐจ๋Š” ๋ณด๋“œ์˜ Week(Iteration) ํ•„๋“œ์—์„œ ์˜ค๋ฏ€๋กœ ํ•˜๋“œ์ฝ”๋”ฉํ•˜์ง€ ์•Š๋Š”๋‹ค. -**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" -} -``` +## ์›นํ›… -ํ•ด๋‹น ๋‚ ์งœ๋ฅผ ํฌํ•จํ•˜๋Š” ์ฃผ์ฐจ๊ฐ€ ์—†์œผ๋ฉด(๊ธฐ์ˆ˜ ์‚ฌ์ด ํœด์‹๊ธฐ ๋“ฑ) `{ "success": true, "date": "...", "found": false }`๋ฅผ ๋Œ๋ ค์ค๋‹ˆ๋‹ค. +`handlers/webhooks.js` ๊ฐ€ leetcode-study ์ €์žฅ์†Œ์˜ ์ด๋ฒคํŠธ๋งŒ ์ฒ˜๋ฆฌํ•œ๋‹ค. -### 3. ์›Œํฌํ”Œ๋กœ์šฐ +| ์ด๋ฒคํŠธ | ํ•˜๋Š” ์ผ | +| ---------------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| `projects_v2_item` | Week ์„ค์ •์ด ๋ฐ”๋€Œ๋ฉด ๊ฒฝ๊ณ  ๋Œ“๊ธ€์„ ๋‹ฌ๊ฑฐ๋‚˜ ์ง€์šด๋‹ค | +| `pull_request` (`opened`, `reopened`, `synchronize`) | Week ๊ฒ€์‚ฌ, ํ’€์ด ํŒจํ„ด ํƒœ๊น…(`tagPatterns`), ํ•™์Šต ํ˜„ํ™ฉ ๋Œ“๊ธ€(`postLearningStatus`) | +| `issue_comment`, `pull_request_review_comment` (`created`) | `@dalestudy` ๋ฉ˜์…˜์— ์‘๋‹ตํ•œ๋‹ค. `approve`ยท`์Šน์ธ` ์ด๋ฉด PR ์„ ์Šน์ธํ•˜๊ณ , ๊ทธ ๋ฐ–์—๋Š” AI ๋ฆฌ๋ทฐ๋ฅผ ๋‹จ๋‹ค | -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 ๊ฒฝ๊ณ  ๋Œ“๊ธ€๋งŒ) +Week ๊ฒ€์‚ฌ๋Š” ์ œ๋ชฉ์ด `WEEK NN Solutions` ํ˜•ํƒœ์ธ ํ’€์ด PR ๋งŒ ๋Œ€์ƒ์œผ๋กœ ํ•˜๊ณ , ๊ฒฝ๊ณ  ๋Œ“๊ธ€์€ ๋ด‡์ด ๋‹จ ๊ฒƒ๋งŒ ์ง€์šด๋‹ค. -### 4. AI ํ•ธ๋“ค๋Ÿฌ Worker ๋ถ„๋ฆฌ ์•„ํ‚คํ…์ฒ˜ +### AI ์ž‘์—…์„ ๋ณ„๋„ invocation ์œผ๋กœ ๋‚˜๋ˆ„๊ธฐ -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` - -#### Secrets (ํ”„๋กœ๋•์…˜) - -`main` ์— ๋จธ์ง€๋˜๋ฉด GitHub Actions(`.github/workflows/deploy.yml`)๊ฐ€ ํ…Œ์ŠคํŠธ ํ›„ `wrangler deploy --secrets-file` ๋กœ ๋ฐฐํฌํ•œ๋‹ค. Worker secret(`PRIVATE_KEY`, `AI_GATEWAY_TOKEN`, `INTERNAL_SECRET`)์€ GitHub secret(`APP_PRIVATE_KEY`, `AI_GATEWAY_TOKEN`, `INTERNAL_SECRET`)์—์„œ ๋ฐฐํฌ์™€ ํ•จ๊ป˜ ๋„ฃ์œผ๋ฏ€๋กœ `wrangler secret put` ์œผ๋กœ ๋”ฐ๋กœ ๋ฐ”๊พธ์ง€ ์•Š๋Š”๋‹ค โ€” ๋‹ค์Œ ๋ฐฐํฌ ๋•Œ GitHub ๊ฐ’์œผ๋กœ ๋ฎ์ธ๋‹ค. `APP_ID` ๋Š” ๊ณต๊ฐœ ๊ฐ’์ด๋ผ `wrangler.jsonc` ์˜ `vars` ์— ์žˆ๋‹ค. - -#### ๋กœ์ปฌ ๊ฐœ๋ฐœ (.dev.vars) - +[#1] webhook ํ•ธ๋“ค๋Ÿฌ โ† 50 ์˜ˆ์‚ฐ + โ”œโ”€ ctx.waitUntil(fetch("/internal/tag-patterns")) โ†’ [#2] tagPatterns โ† ๋…๋ฆฝ 50 ์˜ˆ์‚ฐ + โ””โ”€ ctx.waitUntil(fetch("/internal/learning-status")) โ†’ [#3] postLearningStatus โ† ๋…๋ฆฝ 50 ์˜ˆ์‚ฐ ``` -APP_ID=123456 -PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----" -``` - -## ๋ฐฐํฌ - -### Cloudflare Workers - -`main` ์— ๋จธ์ง€๋˜๋ฉด GitHub Actions ๊ฐ€ ๋ฐฐํฌํ•œ๋‹ค. ์‹œํฌ๋ฆฟ์€ ์œ„ "Secrets (ํ”„๋กœ๋•์…˜)" ์ฐธ๊ณ . - -### ์ปค์Šคํ…€ ๋„๋ฉ”์ธ - -- Production: https://github.dalestudy.com -- Worker.dev: https://github.dalestudy.workers.dev - - -## 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 ์„ค์ • - -GitHub secret ์— ๋„ฃ์œผ๋ฉด ๋ฐฐํฌํ•  ๋•Œ Worker ๋กœ ๋“ค์–ด๊ฐ„๋‹ค (`.github/workflows/deploy.yml`). - -- `APP_PRIVATE_KEY` (org secret): GitHub App Private Key โ†’ Worker `PRIVATE_KEY` -- `AI_GATEWAY_TOKEN` (org secret): AI Gateway ์ธ์ฆ ํ† ํฐ (AI ์ฝ”๋“œ ๋ฆฌ๋ทฐ์šฉ, ํ•„์ˆ˜) -- `INTERNAL_SECRET` (์ €์žฅ์†Œ secret): AI ํ•ธ๋“ค๋Ÿฌ Worker ๋ถ„๋ฆฌ์šฉ. ์„ค์ •ํ•˜๋ฉด tagPatterns, learningStatus๊ฐ€ ๋ณ„๋„ Worker ํ˜ธ์ถœ๋กœ ๋””์ŠคํŒจ์น˜๋˜์–ด ๊ฐ๊ฐ ๋…๋ฆฝ์ ์ธ subrequest ์˜ˆ์‚ฐ์„ ๊ฐ€์ง„๋‹ค. `WORKER_URL`๊ณผ ํ•จ๊ป˜ ์žˆ์–ด์•ผ ํ™œ์„ฑํ™”๋œ๋‹ค. -- `WEBHOOK_SECRET` (์„ ํƒ): ์„ค์ •ํ•˜๋ฉด webhook ์„œ๋ช…์„ ๊ฒ€์ฆํ•œ๋‹ค. ์“ฐ๋ ค๋ฉด GitHub secret ์„ ๋งŒ๋“ค๊ณ  `deploy.yml` ์˜ secrets ํŒŒ์ผ์— ์ถ”๊ฐ€ํ•œ๋‹ค. - -`WORKER_URL`์€ `wrangler.jsonc`์˜ `vars`์— ์ •์˜๋˜์–ด ์žˆ์–ด ๊ธฐ๋ณธ ๋ฐฐํฌ์—๋Š” ์ถ”๊ฐ€ ์„ค์ •์ด ํ•„์š” ์—†๋‹ค. ์Šคํ…Œ์ด์ง•/๋‹ค๋ฅธ ๊ณ„์ • ๋“ฑ์œผ๋กœ ๋ฐฐํฌํ•  ๋•Œ๋งŒ ๋ฎ์–ด์“ฐ๋ฉด ๋œ๋‹ค. - -### 5. GitHub App ์„ค์น˜ -์ €์žฅ์†Œ์— App์ด ์„ค์น˜๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ: +`INTERNAL_SECRET` ๊ณผ `WORKER_URL` ์ด ๋‘˜ ๋‹ค ์žˆ์–ด์•ผ ์ผœ์ง„๋‹ค. ํ•˜๋‚˜๋ผ๋„ ์—†์œผ๋ฉด ๊ฐ™์€ invocation ์—์„œ ์ฐจ๋ก€๋กœ ์‹คํ–‰๋œ๋‹ค. +`tests/subrequest-budget.test.js` ๊ฐ€ ํŒŒ์ผ 5 ๊ฐœ ์‹œ๋‚˜๋ฆฌ์˜ค์˜ fetch ํšŸ์ˆ˜(`tagPatterns` 20, `postLearningStatus` 31)๋ฅผ ํšŒ๊ท€ ํ…Œ์ŠคํŠธ๋กœ ๊ณ ์ •ํ•œ๋‹ค. -``` -https://github.com/apps/dalestudy/installations -``` - -**DaleStudy organization์— ์„ค์น˜**๋˜์–ด ์žˆ์–ด์•ผ ํ•˜๋ฉฐ, **leetcode-study ์ €์žฅ์†Œ ์ ‘๊ทผ ๊ถŒํ•œ** ํ•„์š” +## ๋ฐฐํฌ์™€ ์‹œํฌ๋ฆฟ -## ์ˆ˜๋™ ํ˜ธ์ถœ (์„ ํƒ์‚ฌํ•ญ) +`main` ์— ๋จธ์ง€๋˜๋ฉด GitHub Actions(`.github/workflows/deploy.yml`)๊ฐ€ ํ…Œ์ŠคํŠธ ํ›„ `wrangler deploy --secrets-file` ๋กœ ๋ฐฐํฌํ•œ๋‹ค. PR ์—์„œ๋Š” `integration.yaml` ์ด ํ…Œ์ŠคํŠธ๋งŒ ๋ˆ๋‹ค. ์›นํ›…์„ ๋ฐ›๋Š” ์„œ๋ฒ„๋ผ PR ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋Š” ๋‘์ง€ ์•Š๋Š”๋‹ค. -์ „์ฒด PR์„ ํ•œ ๋ฒˆ์— ๊ฒ€์‚ฌํ•˜๊ณ  ์‹ถ์„ ๋•Œ: +| 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` | (์„ ํƒ) ์žˆ์œผ๋ฉด ์›นํ›… ์„œ๋ช…์„ ๊ฒ€์ฆํ•œ๋‹ค. ์ง€๊ธˆ์€ ์„ค์ •๋ผ ์žˆ์ง€ ์•Š๋‹ค | -```bash -curl -X POST https://github.dalestudy.com/check-weeks \ - -H "Content-Type: application/json" \ - -d '{"repo_owner": "DaleStudy", "repo_name": "leetcode-study"}' -``` +GitHub secret ์€ ๋ฐฐํฌํ•  ๋•Œ๋งˆ๋‹ค Worker secret ์œผ๋กœ ๋“ค์–ด๊ฐ€๋ฏ€๋กœ `wrangler secret put` ์œผ๋กœ ๋”ฐ๋กœ ๋ฐ”๊พธ์ง€ ์•Š๋Š”๋‹ค. ๋‹ค์Œ ๋ฐฐํฌ ๋•Œ GitHub ๊ฐ’์œผ๋กœ ๋ฎ์ธ๋‹ค. +๋กœ์ปฌ์—์„œ๋Š” `.dev.vars` ์— ๊ฐ™์€ ์ด๋ฆ„์œผ๋กœ ๋„ฃ๋Š”๋‹ค. `.dev.vars`, `*.pem` ์€ ์ปค๋ฐ‹ํ•˜์ง€ ์•Š๋Š”๋‹ค. -## ์ค‘์š”ํ•œ ์ œ์•ฝ์‚ฌํ•ญ +## GitHub App ์„ค์ • -### Cloudflare Workers ํ™˜๊ฒฝ +https://github.com/organizations/DaleStudy/settings/apps/dalestudy -- โŒ Node.js ๋ชจ๋“ˆ ์‚ฌ์šฉ ๋ถˆ๊ฐ€ (crypto, buffer ๋“ฑ) -- โœ… Web ํ‘œ์ค€ API๋งŒ ์‚ฌ์šฉ (fetch, Web Crypto API) -- โŒ npm ํŒจํ‚ค์ง€ ๋Œ€๋ถ€๋ถ„ ํ˜ธํ™˜ ์•ˆ ๋จ (@octokit/app ๋“ฑ) -- โœ… ์ˆœ์ˆ˜ JavaScript + Web APIs๋กœ ๊ตฌํ˜„ +- Webhook URL: `https://github.dalestudy.com/webhooks` +- 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/README.md b/README.md index 4830536..36af14a 100644 --- a/README.md +++ b/README.md @@ -1,254 +1,45 @@ -# 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์— ์—ฌ๋Ÿฌ ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๋‹ค์–‘ํ•œ ์ž๋™ํ™” ์š”๊ตฌ์‚ฌํ•ญ์„ ์ฒ˜๋ฆฌํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค. - -## ๋ฐฐํฌ ๋ฐฉ๋ฒ• - -### 1. Cloudflare ๊ณ„์ • ์„ค์ • - -1. https://dash.cloudflare.com ํšŒ์›๊ฐ€์ž… -2. Workers ์„น์…˜์œผ๋กœ ์ด๋™ - -### 2. Wrangler CLI ์„ค์น˜ - -```bash -npm install -g wrangler -wrangler login -``` - -### 3. ๋ฐฐํฌ - -`main` ์— ๋จธ์ง€๋˜๋ฉด GitHub Actions(`.github/workflows/deploy.yml`)๊ฐ€ ํ…Œ์ŠคํŠธ ํ›„ `wrangler deploy --secrets-file` ๋กœ ๋ฐฐํฌํ•œ๋‹ค. Worker secret(`PRIVATE_KEY`, `AI_GATEWAY_TOKEN`, `INTERNAL_SECRET`)์€ GitHub secret(`APP_PRIVATE_KEY`, `AI_GATEWAY_TOKEN`, `INTERNAL_SECRET`)์—์„œ ๋ฐฐํฌ์™€ ํ•จ๊ป˜ ๋„ฃ์œผ๋ฏ€๋กœ `wrangler secret put` ์œผ๋กœ ๋”ฐ๋กœ ๋ฐ”๊พธ์ง€ ์•Š๋Š”๋‹ค โ€” ๋‹ค์Œ ๋ฐฐํฌ ๋•Œ GitHub ๊ฐ’์œผ๋กœ ๋ฎ์ธ๋‹ค. `APP_ID` ๋Š” ๊ณต๊ฐœ ๊ฐ’์ด๋ผ `wrangler.jsonc` ์˜ `vars` ์— ์žˆ๋‹ค. - -์ˆ˜๋™์œผ๋กœ ๋ฐฐํฌํ•ด์•ผ ํ•  ๋•Œ๋งŒ `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`(`wrangler.jsonc`)์™€ `PRIVATE_KEY`(GitHub secret `APP_PRIVATE_KEY`)๊ฐ€ ๋ชจ๋‘ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. - -### Worker๊ฐ€ 403 ์—๋Ÿฌ ๋ฐ˜ํ™˜ - -DaleStudy organization์ด ์•„๋‹Œ ๊ฒฝ์šฐ ์ฐจ๋‹จ๋ฉ๋‹ˆ๋‹ค. - -## ๊ด€๋ จ ๋ฌธ์„œ - -- **AGENTS.md**: AI ์—์ด์ „ํŠธ๋ฅผ ์œ„ํ•œ ์ƒ์„ธ ๊ฐ€์ด๋“œ (๊ฐœ๋ฐœ์ž๋„ ํ•„๋…!) -- **DEPLOYMENT.md**: ์ฒ˜์Œ๋ถ€ํ„ฐ ๋ฐฐํฌํ•˜๋Š” ์ „์ฒด ๊ฐ€์ด๋“œ -- **GitHub App**: https://github.com/organizations/DaleStudy/settings/apps/dalestudy +

+ + Sponsors + +

From f6fc3ab66cda2a35ef6bf6c0f32aa10600c8cfdf Mon Sep 17 00:00:00 2001 From: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:33:02 -0400 Subject: [PATCH 3/4] docs: add MIT license --- LICENSE | 21 +++++++++++++++++++++ README.md | 6 ++++++ 2 files changed, 27 insertions(+) create mode 100644 LICENSE 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 36af14a..acdda5d 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,9 @@ Sponsors

+ +## ๋ผ์ด์„ ์Šค (License) + +์ด ์ €์žฅ์†Œ์˜ ์†Œ์Šค ์ฝ”๋“œ๋Š” [MIT ๋ผ์ด์„ ์Šค](LICENSE)๋ฅผ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. + +๋‹ค๋งŒ ๋‹ฌ๋ ˆ ์Šคํ„ฐ๋””์˜ ์ด๋ฆ„๊ณผ ๋กœ๊ณ ๋ฅผ ๋น„๋กฏํ•œ ๋ธŒ๋žœ๋“œ ์ž์‚ฐ์€ ๋ผ์ด์„ ์Šค ๋ฒ”์œ„์— ํฌํ•จ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. From b35e0c4175e9c4d13c96e3e3a12cc6d20f7df92c Mon Sep 17 00:00:00 2001 From: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:34:35 -0400 Subject: [PATCH 4/4] fix: require webhook signatures --- .github/workflows/deploy.yml | 5 ++-- AGENTS.md | 16 ++++++------ index.js | 27 ++++++++++++--------- tests/worker-runtime.test.js | 47 ++++++++++++++++++++++++++++++++++-- 4 files changed, 71 insertions(+), 24 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3b28cff..f7cff37 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -29,9 +29,10 @@ jobs: 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" \ - '{PRIVATE_KEY: $private_key, AI_GATEWAY_TOKEN: $ai, INTERNAL_SECRET: $internal}' > "$RUNNER_TEMP/secrets.json" + 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 diff --git a/AGENTS.md b/AGENTS.md index 703dc2f..3d847fc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -75,13 +75,13 @@ GitHub webhook `main` ์— ๋จธ์ง€๋˜๋ฉด GitHub Actions(`.github/workflows/deploy.yml`)๊ฐ€ ํ…Œ์ŠคํŠธ ํ›„ `wrangler deploy --secrets-file` ๋กœ ๋ฐฐํฌํ•œ๋‹ค. PR ์—์„œ๋Š” `integration.yaml` ์ด ํ…Œ์ŠคํŠธ๋งŒ ๋ˆ๋‹ค. ์›นํ›…์„ ๋ฐ›๋Š” ์„œ๋ฒ„๋ผ PR ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋Š” ๋‘์ง€ ์•Š๋Š”๋‹ค. -| 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` | (์„ ํƒ) ์žˆ์œผ๋ฉด ์›นํ›… ์„œ๋ช…์„ ๊ฒ€์ฆํ•œ๋‹ค. ์ง€๊ธˆ์€ ์„ค์ •๋ผ ์žˆ์ง€ ์•Š๋‹ค | +| 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` ์€ ์ปค๋ฐ‹ํ•˜์ง€ ์•Š๋Š”๋‹ค. @@ -90,7 +90,7 @@ GitHub secret ์€ ๋ฐฐํฌํ•  ๋•Œ๋งˆ๋‹ค Worker secret ์œผ๋กœ ๋“ค์–ด๊ฐ€๋ฏ€๋กœ `wran https://github.com/organizations/DaleStudy/settings/apps/dalestudy -- Webhook URL: `https://github.dalestudy.com/webhooks` +- 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 ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค 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); + }); + }); }); +