|
| 1 | +// Cloudflare Worker entry point. |
| 2 | +// |
| 3 | +// Two responsibilities: |
| 4 | +// 1. fetch() — delegate every request to the Static Assets binding so |
| 5 | +// the site is served from _site/ exactly as before. |
| 6 | +// 2. scheduled() — once per day, push an empty commit to main via the |
| 7 | +// GitHub API. CF Workers Builds (already connected to |
| 8 | +// eSolia/blog.esolia.pro) sees the push and rebuilds. |
| 9 | +// This refreshes time-dependent build artefacts such as |
| 10 | +// `elapseddays` per post (computed in _config.ts). |
| 11 | +// |
| 12 | +// Manual one-time setup outside this file: |
| 13 | +// - Create a fine-grained GitHub PAT scoped to eSolia/blog.esolia.pro only, |
| 14 | +// permission: Contents = read & write. |
| 15 | +// - Bind it to the Worker: |
| 16 | +// deno run -A npm:wrangler@latest secret put GITHUB_TOKEN |
| 17 | +// |
| 18 | +// Minimal Cloudflare Workers types declared inline to avoid pulling in |
| 19 | +// @cloudflare/workers-types and dueling tsconfigs with the Lume Deno setup. |
| 20 | + |
| 21 | +interface Fetcher { |
| 22 | + fetch(request: Request): Promise<Response>; |
| 23 | +} |
| 24 | + |
| 25 | +interface ExecutionContext { |
| 26 | + waitUntil(promise: Promise<unknown>): void; |
| 27 | + passThroughOnException(): void; |
| 28 | +} |
| 29 | + |
| 30 | +interface ScheduledEvent { |
| 31 | + cron: string; |
| 32 | + scheduledTime: number; |
| 33 | + type: "scheduled"; |
| 34 | +} |
| 35 | + |
| 36 | +interface Env { |
| 37 | + ASSETS: Fetcher; |
| 38 | + GITHUB_TOKEN: string; |
| 39 | +} |
| 40 | + |
| 41 | +const REPO_OWNER = "eSolia"; |
| 42 | +const REPO_NAME = "blog.esolia.pro"; |
| 43 | +const REPO_BRANCH = "main"; |
| 44 | +const USER_AGENT = "blog-esolia-pro-rebuild-cron"; |
| 45 | + |
| 46 | +export default { |
| 47 | + fetch(request: Request, env: Env): Promise<Response> { |
| 48 | + return env.ASSETS.fetch(request); |
| 49 | + }, |
| 50 | + |
| 51 | + scheduled(_event: ScheduledEvent, env: Env, ctx: ExecutionContext): void { |
| 52 | + ctx.waitUntil(triggerRebuild(env)); |
| 53 | + }, |
| 54 | +}; |
| 55 | + |
| 56 | +async function triggerRebuild(env: Env): Promise<void> { |
| 57 | + const ghHeaders = { |
| 58 | + "Authorization": `Bearer ${env.GITHUB_TOKEN}`, |
| 59 | + "User-Agent": USER_AGENT, |
| 60 | + "Accept": "application/vnd.github+json", |
| 61 | + "X-GitHub-Api-Version": "2022-11-28", |
| 62 | + }; |
| 63 | + const apiBase = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}`; |
| 64 | + |
| 65 | + const refResp = await fetch(`${apiBase}/git/ref/heads/${REPO_BRANCH}`, { |
| 66 | + headers: ghHeaders, |
| 67 | + }); |
| 68 | + if (!refResp.ok) { |
| 69 | + throw new Error( |
| 70 | + `get ref failed: ${refResp.status} ${await refResp.text()}`, |
| 71 | + ); |
| 72 | + } |
| 73 | + const ref = (await refResp.json()) as { object: { sha: string } }; |
| 74 | + const parentSha = ref.object.sha; |
| 75 | + |
| 76 | + const commitResp = await fetch(`${apiBase}/git/commits/${parentSha}`, { |
| 77 | + headers: ghHeaders, |
| 78 | + }); |
| 79 | + if (!commitResp.ok) { |
| 80 | + throw new Error( |
| 81 | + `get commit failed: ${commitResp.status} ${await commitResp.text()}`, |
| 82 | + ); |
| 83 | + } |
| 84 | + const parent = (await commitResp.json()) as { tree: { sha: string } }; |
| 85 | + |
| 86 | + const createResp = await fetch(`${apiBase}/git/commits`, { |
| 87 | + method: "POST", |
| 88 | + headers: { ...ghHeaders, "Content-Type": "application/json" }, |
| 89 | + body: JSON.stringify({ |
| 90 | + message: |
| 91 | + "chore: nightly rebuild trigger\n\nInfoSec: no security impact — empty commit fired by Worker cron to refresh time-dependent build artefacts.", |
| 92 | + tree: parent.tree.sha, |
| 93 | + parents: [parentSha], |
| 94 | + author: { |
| 95 | + name: "blog-esolia-pro-cron", |
| 96 | + email: "noreply@esolia.pro", |
| 97 | + date: new Date().toISOString(), |
| 98 | + }, |
| 99 | + }), |
| 100 | + }); |
| 101 | + if (!createResp.ok) { |
| 102 | + throw new Error( |
| 103 | + `create commit failed: ${createResp.status} ${await createResp.text()}`, |
| 104 | + ); |
| 105 | + } |
| 106 | + const newCommit = (await createResp.json()) as { sha: string }; |
| 107 | + |
| 108 | + const updateResp = await fetch(`${apiBase}/git/refs/heads/${REPO_BRANCH}`, { |
| 109 | + method: "PATCH", |
| 110 | + headers: { ...ghHeaders, "Content-Type": "application/json" }, |
| 111 | + body: JSON.stringify({ sha: newCommit.sha, force: false }), |
| 112 | + }); |
| 113 | + if (!updateResp.ok) { |
| 114 | + throw new Error( |
| 115 | + `update ref failed: ${updateResp.status} ${await updateResp.text()}`, |
| 116 | + ); |
| 117 | + } |
| 118 | + console.log(`Triggered rebuild via empty commit ${newCommit.sha}`); |
| 119 | +} |
0 commit comments