Public: anyone can read this; the crew can change it.

An empty Atom feed says when it was last true, not 1970: /changelog.atom and the events feed carry the running build's time #8480

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/8460-an-empty-atom-feed-says-when-it-was-last → main 2 commits fork

  • Merged. scout/8460-an-empty-atom-feed-says-when-it-was-last was merged into main as 85f900d.

Review

Sign in to review this change.
5 files changed +68 −3
src/web/feeds.ts +3 −2
@@ -24,6 +24,7 @@ import { shippedAnswer } from "../release/answer.ts";
2424 import { notePhase, takeSessionTime, timePhase, timePhaseAsync } from "../core/route_timing.ts";
2525 import type { Ctx } from "../http/context.ts";
2626 import { notFound } from "../http/respond.ts";
27 import { STARTED } from "./started.ts";
2728
2829 const FEED_KINDS = new Set([
2930 "push",
@@ -165,7 +166,7 @@ export function activityAtom(ctx: Ctx): Response {
165166 </entry>`;
166167 });
167168
168 const updated = events[0]?.createdAt ?? new Date(0).toISOString();
169 const updated = events[0]?.createdAt ?? STARTED;
169170 const body = `<?xml version="1.0" encoding="utf-8"?>
170171 <feed xmlns="http://www.w3.org/2005/Atom">
171172 <id>${xml(base)}/activity.atom</id>
@@ -214,7 +215,7 @@ export async function changelogAtom(ctx: Ctx): Promise<Response> {
214215 <link rel="alternate" type="text/html" href="${xml(link)}"/>
215216 </entry>`;
216217 });
217 const updated = live[0]?.at ?? new Date(0).toISOString();
218 const updated = live[0]?.at ?? STARTED;
218219 const body = `<?xml version="1.0" encoding="utf-8"?>
219220 <feed xmlns="http://www.w3.org/2005/Atom">
220221 <id>${xml(origin)}/changelog.atom</id>
src/web/pages/waves.ts +2 −1
@@ -9,6 +9,7 @@
99 import { canAdmin, canRead, getRepo } from "../../core/repos.ts";
1010 import { notFound } from "../../http/respond.ts";
1111 import { holds, stamp, xml } from "../feeds.ts";
12 import { STARTED } from "../started.ts";
1213 import { notePhase, takeSessionTime, timePhase, timePhaseAsync } from "../../core/route_timing.ts";
1314 import { repoDir } from "../../git/repos.ts";
1415 import type { Ctx } from "../../http/context.ts";
@@ -138,7 +139,7 @@ export async function wavesAtom(ctx: Ctx): Promise<Response> {
138139 <id>${xml(base)}/waves.atom</id>
139140 <title>${xml(`${repo.ownerName}/${repo.name} waves`)}</title>
140141 <subtitle>${xml(repo.description === "" ? "Waves" : repo.description)}</subtitle>
141 <updated>${xml(newest ? updatedAt(newest.date) : new Date(0).toISOString())}</updated>
142 <updated>${xml(newest ? updatedAt(newest.date) : STARTED)}</updated>
142143 <link rel="self" type="application/atom+xml" href="${xml(base)}/waves.atom"/>
143144 <link rel="alternate" type="text/html" href="${xml(base)}/waves"/>
144145 <generator>forge</generator>
src/web/started.ts added +2
@@ -0,0 +1,2 @@
1 /** When this process began serving: taken once, so an empty feed's `<updated>` is stable and its ETag holds. */
2 export const STARTED = new Date().toISOString();
test/changelog-atom.test.ts +19
@@ -75,3 +75,22 @@ describe("GET /changelog.atom", () => {
7575 expect(html).toMatch(/rel="alternate"\s+type="application\/atom\+xml"[^>]*href="\/changelog\.atom"/);
7676 });
7777 });
78
79 describe("GET /changelog.atom on an empty forge", () => {
80 test("says when the process started, not 1970, and the same twice", async () => {
81 const dataDir = await mkdtemp(join(tmpdir(), "forge-changelog-atom-empty-"));
82 const app = createApp({ dataDir, port: 0, forgeRepo: "alice/forge" });
83 const servers = await serve(app);
84 try {
85 const get = async () => (await (await fetch(`${servers.url}/changelog.atom`)).text()).match(/<updated>([^<]*)<\/updated>/)![1]!;
86 const first = await get();
87 expect(first).not.toContain("1970-01-01");
88 expect(Date.parse(first)).toBeGreaterThan(Date.parse("2026-01-01"));
89 expect(await get()).toBe(first);
90 } finally {
91 servers.stop();
92 app.close();
93 await rm(dataDir, { recursive: true, force: true });
94 }
95 }, 30_000);
96 });
test/empty-feeds.test.ts added +42
@@ -0,0 +1,42 @@
1 /** An empty repository's activity and waves feeds say when the process started, not 1970, and the same twice. */
2 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
3 import { mkdtemp, rm } from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createRepo } from "../src/actions/repos.ts";
7 import { createApp, type App } from "../src/app.ts";
8 import { createUser } from "../src/core/users.ts";
9 import { serve, type Servers } from "../src/http/server.ts";
10
11 describe("empty feeds", () => {
12 let dataDir: string;
13 let app: App;
14 let servers: Servers;
15
16 beforeAll(async () => {
17 dataDir = await mkdtemp(join(tmpdir(), "forge-empty-feeds-"));
18 app = createApp({ dataDir, port: 0 });
19 servers = await serve(app);
20 const u = await createUser(app.db, { origin: "seed", trusted: true, username: "alice", email: "[email protected]", password: "a-good-passphrase", adminIfFirst: true });
21 if (!u.ok) throw new Error(u.error);
22 const made = await createRepo(app, u.value, { name: "r", visibility: "public", description: "d" });
23 if (!made.ok) throw new Error(String(made.error));
24 });
25
26 afterAll(async () => {
27 servers.stop();
28 app.close();
29 await rm(dataDir, { recursive: true, force: true });
30 });
31
32 for (const feed of ["activity.atom", "waves.atom"]) {
33 test(`${feed} is dated by the process start`, async () => {
34 const get = async () =>
35 (await (await fetch(`${servers.url}/alice/r/${feed}`)).text()).match(/<updated>([^<]*)<\/updated>/)![1]!;
36 const first = await get();
37 expect(first).not.toContain("1970-01-01");
38 expect(Date.parse(first)).toBeGreaterThan(Date.parse("2026-01-01"));
39 expect(await get()).toBe(first);
40 });
41 }
42 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs