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
43 changes: 43 additions & 0 deletions .github/workflows/site-checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Site checks

on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: site-checks-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
site-checks:
name: Blog manifest and site build
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.3.0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Test manifest generation across timezones
run: node --test scripts/generate-blog-manifests.test.mjs
- name: Check committed blog manifests
run: pnpm blog:manifest:check
- name: Build site
run: pnpm run build
8 changes: 7 additions & 1 deletion scripts/generate-blog-manifests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,13 @@ function canonicalDate(value, field, sourceFile) {
if (typeof value !== 'string' && !(value instanceof Date)) {
throw new Error(`${sourceFile}: frontmatter field ${field} must be a date string`);
}
const date = value instanceof Date ? value : new Date(value);
// English calendar dates have no timezone. Parse them in UTC so a manifest
// generated locally is identical to one checked by the deployment runner.
const dateInput = typeof value === 'string'
&& /^[a-z]{3,9}\s+\d{1,2},?\s+\d{4}$/i.test(value.trim())
? `${value.trim()} UTC`
: value;
const date = value instanceof Date ? value : new Date(dateInput);
if (Number.isNaN(date.getTime())) {
throw new Error(`${sourceFile}: frontmatter field ${field} is not a valid date`);
}
Expand Down
63 changes: 63 additions & 0 deletions scripts/generate-blog-manifests.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { copyFile, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import test from 'node:test';

const root = dirname(dirname(fileURLToPath(import.meta.url)));
const timezones = ['UTC', 'Asia/Shanghai', 'America/Los_Angeles'];

function generate(directory, mode, timezone) {
return spawnSync(process.execPath, [join(directory, 'scripts/generate-blog-manifests.mjs'), mode], {
cwd: directory,
env: { ...process.env, TZ: timezone },
encoding: 'utf8',
});
}

function succeeds(result) {
assert.equal(result.status, 0, result.stderr || result.stdout);
}

test('committed manifests match article metadata in every supported timezone', () => {
for (const timezone of timezones) succeeds(generate(root, '--check', timezone));
});

test('calendar dates remain stable and stale metadata is rejected', async (t) => {
const fixture = await mkdtemp(join(tmpdir(), 'blog-manifest-'));
t.after(() => rm(fixture, { recursive: true, force: true }));
await mkdir(join(fixture, 'scripts'));
await copyFile(join(root, 'scripts/generate-blog-manifests.mjs'), join(fixture, 'scripts/generate-blog-manifests.mjs'));
await symlink(join(root, 'node_modules'), join(fixture, 'node_modules'), 'dir');
const article = `---
title: Calendar date fixture
description: A timezone regression fixture
pubDate: Aug 9 2026
updatedDate: 'Apr 07 2026'
---
Fixture body.
`;
for (const locale of ['en', 'cn']) {
const directory = join(fixture, 'src/content/blog', locale);
await mkdir(directory, { recursive: true });
await writeFile(join(directory, 'calendar.md'), article);
await writeFile(join(directory, 'iso.md'), article.replace('Aug 9 2026', '2026-08-09').replace("'Apr 07 2026'", "'2026-04-07'"));
await writeFile(join(directory, 'draft.md'), article.replace('title:', 'draft: true\ntitle:'));
}
succeeds(generate(fixture, '--write', 'Asia/Shanghai'));
for (const timezone of timezones) succeeds(generate(fixture, '--check', timezone));
for (const locale of ['en', 'cn']) {
const manifest = JSON.parse(await readFile(join(fixture, 'src/content/blog', locale, 'posts.json'), 'utf8'));
assert.equal(manifest.posts.length, 2, 'drafts must stay excluded');
for (const post of manifest.posts) {
assert.equal(post.pubDate, '2026-08-09');
assert.equal(post.updatedAt, '2026-04-07');
}
}
await writeFile(join(fixture, 'src/content/blog/en/calendar.md'), article.replace('Calendar date fixture', 'Updated title'));
const stale = generate(fixture, '--check', 'UTC');
assert.equal(stale.status, 1);
assert.match(stale.stderr, /en\/posts\.json is stale/);
});
Loading
Loading