Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/watch-onedrive.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 (토큰, 변경 기준선, 기사 미러)
Expand Down
65 changes: 65 additions & 0 deletions src/app/api/cron/watch-onedrive/route.ts
Original file line number Diff line number Diff line change
@@ -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<Response> {
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<string, string> | 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 });
}
8 changes: 7 additions & 1 deletion vercel.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 * * * *"
}
]
}
Loading