summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoe Mou <dev@mou.fo>2026-08-11 08:37:25 -0400
committerJoe Mou <dev@mou.fo>2026-08-11 08:38:30 -0400
commitab52f48e50d0dd242ad5b0cfbee9c458946cd427 (patch)
treebeb9519f163cd1ee2b229c8811b4de636c76078b
parent2b4a69720b53480d82cf296ac5dbe4614d9470b9 (diff)
Show the build's commit in the page footer
Workers can't shell out at request time, so the commit has to be baked in before wrangler bundles the sources. compile-views.ts grows a second job and becomes scripts/build.ts: alongside the precompiled views it writes src/commit.generated.ts with `git describe --tags --always --dirty`, which yields a bare short hash until the repo is tagged and marks builds made from an unclean working copy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
-rw-r--r--cgithub/.gitignore1
-rw-r--r--cgithub/CLAUDE.md2
-rw-r--r--cgithub/package.json4
-rw-r--r--cgithub/scripts/build.ts52
-rw-r--r--cgithub/scripts/compile-views.ts29
-rw-r--r--cgithub/src/app.ts3
-rw-r--r--cgithub/src/worker.ts2
-rw-r--r--cgithub/views/layout.eta2
8 files changed, 60 insertions, 35 deletions
diff --git a/cgithub/.gitignore b/cgithub/.gitignore
index 5a5e404..b8fc600 100644
--- a/cgithub/.gitignore
+++ b/cgithub/.gitignore
@@ -1,3 +1,4 @@
/node_modules/
/.wrangler/
+/src/commit.generated.ts
/src/views.generated.ts
diff --git a/cgithub/CLAUDE.md b/cgithub/CLAUDE.md
index 37774cc..0ef4b47 100644
--- a/cgithub/CLAUDE.md
+++ b/cgithub/CLAUDE.md
@@ -24,7 +24,7 @@ pnpm test
- `src/worker.ts` - Cloudflare Workers entry (config in `wrangler.jsonc`)
- `src/scraper.ts` - GitHub HTML fetching and JSON extraction logic
-**Templates:** `views/*.eta` - Eta templates with layout inheritance (`layout.eta` as base). On Workers, runtime code generation is disallowed, so `scripts/compile-views.ts` precompiles templates to `src/views.generated.ts` (gitignored, regenerated by the `dev`/`deploy` scripts).
+**Templates:** `views/*.eta` - Eta templates with layout inheritance (`layout.eta` as base). On Workers, runtime code generation is disallowed, so `scripts/build.ts` precompiles templates to `src/views.generated.ts`. The same script records the build's `git describe` output in `src/commit.generated.ts` for the page footer. Both are gitignored and regenerated by the `dev`/`deploy` scripts.
**Static files:** `public/static/*`, served by Workers Static Assets.
diff --git a/cgithub/package.json b/cgithub/package.json
index 337fabc..7673612 100644
--- a/cgithub/package.json
+++ b/cgithub/package.json
@@ -5,8 +5,8 @@
"license": "MIT",
"type": "module",
"scripts": {
- "dev": "node scripts/compile-views.ts && wrangler dev",
- "deploy": "node scripts/compile-views.ts && wrangler deploy",
+ "dev": "node scripts/build.ts && wrangler dev",
+ "deploy": "node scripts/build.ts && wrangler deploy",
"test": "node --test --test-concurrency=4 src/scraper.test.ts"
},
"dependencies": {
diff --git a/cgithub/scripts/build.ts b/cgithub/scripts/build.ts
new file mode 100644
index 0000000..766e5e8
--- /dev/null
+++ b/cgithub/scripts/build.ts
@@ -0,0 +1,52 @@
+// Generates the sources that can't exist at runtime on Workers:
+//
+// src/views.generated.ts - views/*.eta precompiled into plain functions.
+// Workers disallow runtime code generation (`new Function`), so Eta cannot
+// compile templates there at request time.
+// src/commit.generated.ts - the commit this build was made from, for the
+// page footer. Workers can't shell out to git at request time either.
+import { execFileSync } from "node:child_process";
+import { Eta } from "eta";
+import fs from "node:fs";
+import path from "node:path";
+
+const root = path.join(import.meta.dirname, "..");
+const viewsDir = path.join(root, "views");
+const eta = new Eta({ views: viewsDir });
+
+const names = fs
+ .readdirSync(viewsDir)
+ .filter((name) => name.endsWith(".eta"))
+ .sort();
+
+let out = `// @ts-nocheck
+// Generated by scripts/build.ts -- do not edit.
+export const views = {
+`;
+for (const name of names) {
+ const source = fs.readFileSync(path.join(viewsDir, name), "utf8");
+ const body = eta.compileToString(source);
+ out += ` ${JSON.stringify(name)}: function (it, options) {\n${body}\n },\n`;
+}
+out += "};\n";
+
+fs.writeFileSync(path.join(root, "src", "views.generated.ts"), out);
+console.log(`Compiled ${names.length} templates to src/views.generated.ts`);
+
+// Untagged repositories describe as a bare short hash; --dirty marks a build
+// made from an unclean working copy.
+let commit = "unknown";
+try {
+ commit = execFileSync("git", ["describe", "--tags", "--always", "--dirty"], {
+ cwd: root,
+ encoding: "utf8",
+ }).trim();
+} catch {
+ // Building from a source copy without git history.
+}
+
+fs.writeFileSync(
+ path.join(root, "src", "commit.generated.ts"),
+ `// Generated by scripts/build.ts -- do not edit.\nexport const commit = ${JSON.stringify(commit)};\n`,
+);
+console.log(`Wrote commit ${commit} to src/commit.generated.ts`);
diff --git a/cgithub/scripts/compile-views.ts b/cgithub/scripts/compile-views.ts
deleted file mode 100644
index 785efc1..0000000
--- a/cgithub/scripts/compile-views.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-// Precompiles views/*.eta into plain functions for the Cloudflare Workers
-// build (src/views.generated.ts). Workers disallow runtime code generation
-// (`new Function`), so Eta cannot compile templates there at request time.
-import { Eta } from "eta";
-import fs from "node:fs";
-import path from "node:path";
-
-const root = path.join(import.meta.dirname, "..");
-const viewsDir = path.join(root, "views");
-const eta = new Eta({ views: viewsDir });
-
-const names = fs
- .readdirSync(viewsDir)
- .filter((name) => name.endsWith(".eta"))
- .sort();
-
-let out = `// @ts-nocheck
-// Generated by scripts/compile-views.ts -- do not edit.
-export const views = {
-`;
-for (const name of names) {
- const source = fs.readFileSync(path.join(viewsDir, name), "utf8");
- const body = eta.compileToString(source);
- out += ` ${JSON.stringify(name)}: function (it, options) {\n${body}\n },\n`;
-}
-out += "};\n";
-
-fs.writeFileSync(path.join(root, "src", "views.generated.ts"), out);
-console.log(`Compiled ${names.length} templates to src/views.generated.ts`);
diff --git a/cgithub/src/app.ts b/cgithub/src/app.ts
index 5bed45d..0218918 100644
--- a/cgithub/src/app.ts
+++ b/cgithub/src/app.ts
@@ -1,6 +1,7 @@
import type { Eta } from "eta";
import { type Context, Hono } from "hono";
import type { StatusCode } from "hono/utils/http-status";
+import { commit } from "./commit.generated.ts";
import {
getGitHubBlob,
getGitHubCommits,
@@ -47,7 +48,7 @@ export function createApp(eta: Eta) {
// Our URLs mirror GitHub's, so the page we scraped is the same path there.
const { pathname, search } = new URL(c.req.url);
const githubUrl = `https://github.com${pathname}${search}`;
- return c.html(eta.render(template, { ...data, githubUrl }));
+ return c.html(eta.render(template, { ...data, githubUrl, commit }));
}
// For fragments fetched asynchronously by client-side JS (see public/static/refs.js):
diff --git a/cgithub/src/worker.ts b/cgithub/src/worker.ts
index 024b01a..7bf9100 100644
--- a/cgithub/src/worker.ts
+++ b/cgithub/src/worker.ts
@@ -5,7 +5,7 @@ import { createApp } from "./app.ts";
import { views } from "./views.generated.ts";
// Workers disallow runtime code generation, so templates are precompiled by
-// scripts/compile-views.ts and preloaded into Eta's cache here. With
+// scripts/build.ts and preloaded into Eta's cache here. With
// cache: true, Eta serves them from the cache and never compiles.
const eta = new Eta({ cache: true });
diff --git a/cgithub/views/layout.eta b/cgithub/views/layout.eta
index f803caf..ca6fff2 100644
--- a/cgithub/views/layout.eta
+++ b/cgithub/views/layout.eta
@@ -34,7 +34,7 @@ git clone git@github.com:<%= it.repo.owner %>/<%= it.repo.name %>.git</code></pr
<%~ it.body %>
- <footer>Generated by cgithub · <a href="<%= it.githubUrl %>" rel="noreferrer">View original</a></footer>
+ <footer>Generated by cgithub commit <code><%= it.commit %></code> · <a href="<%= it.githubUrl %>" rel="noreferrer">View original</a></footer>
<script>
document.addEventListener("keydown", function(e) {