From d7184da25082b119f6e2772a664ae5a608c67f23 Mon Sep 17 00:00:00 2001 From: Hansol Lim Date: Sun, 27 Sep 2026 06:17:41 +0900 Subject: [PATCH] =?UTF-8?q?feat(cron):=20GitHub=20=EC=8A=A4=EC=BC=80?= =?UTF-8?q?=EC=A4=84=EC=9D=B4=20=EB=B9=A0=EC=A0=B8=EB=8F=84=20Watch=20OneD?= =?UTF-8?q?rive=20=EA=B0=80=20=EB=A7=A4=EC=8B=9C=20=EB=8F=8C=EA=B2=8C=20Ve?= =?UTF-8?q?rcel=20Cron=20=EB=B3=B4=EC=A1=B0=20=ED=8A=B8=EB=A6=AC=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /api/cron/watch-onedrive 를 매시 47분 호출해, 최근 40분 안에 감시 실행이 없으면 watch-onedrive.yml 을 디스패치한다. CRON_SECRET 으로 호출자를 확인하고, GITHUB_DISPATCH_TOKEN 이 없으면 건너뛴다. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013AJufoD68ZCmdcq1fhN2az --- .github/workflows/watch-onedrive.yml | 1 + src/app/api/cron/watch-onedrive/route.ts | 65 ++++++++++++++++++++++++ vercel.json | 8 ++- 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/app/api/cron/watch-onedrive/route.ts diff --git a/.github/workflows/watch-onedrive.yml b/.github/workflows/watch-onedrive.yml index 98c101b..26a60da 100644 --- a/.github/workflows/watch-onedrive.yml +++ b/.github/workflows/watch-onedrive.yml @@ -3,6 +3,7 @@ # 기사 : vault/objects/news-articles 가 바뀌면 Neon articles 미러 갱신 + dbId 를 OneDrive 원본에 역기록 # 빌드 : vault 가 바뀌면 Build With Vault Refresh 를 바뀐 경로 목록과 함께 디스패치(site-data, 원페이저 재생성 판단) # 매시 실행이 OneDrive 리프레시 토큰도 살려 둔다(사이트 런타임도 필요할 때 갱신한다). +# GitHub 스케줄이 밀릴 때를 대비해 Vercel Cron(매시 47분, /api/cron/watch-onedrive)이 최근 40분 안에 실행이 없으면 디스패치한다. # # Repository secrets # DATABASE_URL Neon (토큰, 변경 기준선, 기사 미러) diff --git a/src/app/api/cron/watch-onedrive/route.ts b/src/app/api/cron/watch-onedrive/route.ts new file mode 100644 index 0000000..316f363 --- /dev/null +++ b/src/app/api/cron/watch-onedrive/route.ts @@ -0,0 +1,65 @@ +import { NextResponse } from "next/server"; + +/** + * Vercel Cron -> GitHub `watch-onedrive.yml` 디스패치. GitHub 스케줄(cron)이 밀리거나 빠져도 + * OneDrive vault 변경 감시(기사 DB 반영, 사이트 재생성 디스패치)가 매시 돌게 하는 보조 트리거다. + * + * 최근 40분 안에 감시 실행이 이미 있었으면(GitHub 스케줄이나 수동 실행) 건너뛴다. + * + * 환경변수 + * CRON_SECRET Vercel Cron 이 Authorization: Bearer 로 붙여 보내는 값. 없으면 거부 + * GITHUB_DISPATCH_TOKEN hsol/hsol.info 의 Actions 쓰기 권한이 있는 토큰(fine-grained 권장). 없으면 건너뜀 + * (선택) GITHUB_DISPATCH_REPO(기본 hsol/hsol.info), GITHUB_DISPATCH_REF(기본 dev) + */ +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +const WORKFLOW = "watch-onedrive.yml"; +const RECENT_MS = 40 * 60 * 1000; + +async function gh(path: string, token: string, init: RequestInit = {}): Promise { + return fetch(`https://api.github.com${path}`, { + ...init, + headers: { + Accept: "application/vnd.github+json", + Authorization: `Bearer ${token}`, + "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": "hsol.info-cron", + ...(init.headers as Record | undefined), + }, + cache: "no-store", + }); +} + +export async function GET(req: Request) { + const secret = process.env.CRON_SECRET; + if (!secret || req.headers.get("authorization") !== `Bearer ${secret}`) { + return NextResponse.json({ error: "unauthorized" }, { status: 401 }); + } + const token = process.env.GITHUB_DISPATCH_TOKEN?.trim(); + if (!token) return NextResponse.json({ skipped: "GITHUB_DISPATCH_TOKEN 미설정" }, { status: 200 }); + + const repo = process.env.GITHUB_DISPATCH_REPO || "hsol/hsol.info"; + const ref = process.env.GITHUB_DISPATCH_REF || "dev"; + + const runsRes = await gh(`/repos/${repo}/actions/workflows/${WORKFLOW}/runs?per_page=1`, token); + if (runsRes.ok) { + const runs = (await runsRes.json()) as { workflow_runs?: { created_at: string; event: string }[] }; + const latest = runs.workflow_runs?.[0]; + if (latest && Date.now() - Date.parse(latest.created_at) < RECENT_MS) { + return NextResponse.json({ skipped: "최근 실행 있음", latest }); + } + } + + const res = await gh(`/repos/${repo}/actions/workflows/${WORKFLOW}/dispatches`, token, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ref }), + }); + if (res.status !== 204) { + const detail = await res.text().catch(() => ""); + console.error(`[cron/watch-onedrive] dispatch 실패 HTTP ${res.status} ${detail.slice(0, 300)}`); + return NextResponse.json({ error: "dispatch 실패", status: res.status }, { status: 502 }); + } + return NextResponse.json({ dispatched: WORKFLOW, ref }); +} diff --git a/vercel.json b/vercel.json index 8c9dacf..53be7b1 100644 --- a/vercel.json +++ b/vercel.json @@ -2,5 +2,11 @@ "$schema": "https://openapi.vercel.sh/vercel.json", "framework": "nextjs", "installCommand": "npm ci", - "buildCommand": "npm run build" + "buildCommand": "npm run build", + "crons": [ + { + "path": "/api/cron/watch-onedrive", + "schedule": "47 * * * *" + } + ] }