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 .claude/rules/ci-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ paths:
- `agent-rules.yml` runs `scripts/agent-rules.mjs --check` and the agent hook tests (`node --test '.cursor/hooks/*.test.js'`; a bare directory runs nothing on Node 22) with no install (node and git only). Its `paths` must cover every input and output of that script, and `.cursor/hooks/`.
- `editor-types.yml` reruns `yarn copy:websitetypes` and fails if `website/src/components/Playground/editor-types` changes. It needs the `website` workspace (for deps like `bignumber.js`), which CircleCI's `setup` drops. Its `paths` must cover every input of `scripts/copywebsitetypes.sh`.
- `site-preview.yml` runs one `build` job (one install for the typecheck and the build). It builds the site directly (no Vercel CLI, only the packages it imports via `ci:build:website`, `VERCEL_ENV=preview` to include drafts), restores Docusaurus' webpack cache (only master pushes save it, so PRs share one entry), and fails on any `[WARNING]`/`[ERROR]` line. Broken links are `warn` in `docusaurus.config.ts` so this check catches them without failing Vercel deploys.
- `site-preview.yml` runs `blog-publish.mjs --check`: posts published in the PR must be dated within 3 days by filename.
- Production docs deploys come from Vercel's Git integration only (gated by `vercel-ignore.sh`); there is no Actions deploy workflow. Vercel clones ~10 commits deep, so `website/scripts/deepenGitHistory.cjs` (called from `docusaurus.config.ts`) fetches 800 more for the "Last updated" dates.
- `site-preview.yml` `paths` (`website/**`, `docs/{core,rest,graphql}/**`) must match `SITE_PATHS` in `website/scripts/vercel-ignore.sh`.
- `benchmark-react.yml` caches Playwright browsers keyed on the resolved `playwright` version from `examples/benchmark-react`; bumping playwright invalidates the cache automatically.
Expand Down
1 change: 1 addition & 0 deletions .cursor/rules/ci-config.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ alwaysApply: false
- `agent-rules.yml` runs `scripts/agent-rules.mjs --check` and the agent hook tests (`node --test '.cursor/hooks/*.test.js'`; a bare directory runs nothing on Node 22) with no install (node and git only). Its `paths` must cover every input and output of that script, and `.cursor/hooks/`.
- `editor-types.yml` reruns `yarn copy:websitetypes` and fails if `website/src/components/Playground/editor-types` changes. It needs the `website` workspace (for deps like `bignumber.js`), which CircleCI's `setup` drops. Its `paths` must cover every input of `scripts/copywebsitetypes.sh`.
- `site-preview.yml` runs one `build` job (one install for the typecheck and the build). It builds the site directly (no Vercel CLI, only the packages it imports via `ci:build:website`, `VERCEL_ENV=preview` to include drafts), restores Docusaurus' webpack cache (only master pushes save it, so PRs share one entry), and fails on any `[WARNING]`/`[ERROR]` line. Broken links are `warn` in `docusaurus.config.ts` so this check catches them without failing Vercel deploys.
- `site-preview.yml` runs `blog-publish.mjs --check`: posts published in the PR must be dated within 3 days by filename.
- Production docs deploys come from Vercel's Git integration only (gated by `vercel-ignore.sh`); there is no Actions deploy workflow. Vercel clones ~10 commits deep, so `website/scripts/deepenGitHistory.cjs` (called from `docusaurus.config.ts`) fetches 800 more for the "Last updated" dates.
- `site-preview.yml` `paths` (`website/**`, `docs/{core,rest,graphql}/**`) must match `SITE_PATHS` in `website/scripts/vercel-ignore.sh`.
- `benchmark-react.yml` caches Playwright browsers keyed on the resolved `playwright` version from `examples/benchmark-react`; bumping playwright invalidates the cache automatically.
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/site-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ jobs:
with:
node-version: '26'
cache: 'yarn'
- name: Check published blog posts are dated today
if: github.event_name == 'pull_request'
Comment thread
ntucker marked this conversation as resolved.
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
git fetch --depth=1 origin "$BASE_SHA"
node website/scripts/blog-publish.mjs --check "$BASE_SHA"
- name: Install packages
run: ./scripts/ci-install.sh website
- name: Build package types and the packages the website imports
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"build:skills": "yarn workspace rdc-website build:skills",
"check:vue-examples": "yarn workspace rdc-website check:vue-examples",
"build:agent-rules": "node scripts/agent-rules.mjs",
"blog:publish": "node website/scripts/blog-publish.mjs",
"test": "NODE_ENV=test run jest",
"test:ci": "ANANSI_JEST_TYPECHECK=false yarn test --ci",
"test:coverage": "ANANSI_JEST_TYPECHECK=false yarn test --coverage --selectProjects ReactDOM Node --coverageThreshold='{\"global\":{\"statements\":98,\"branches\":96,\"functions\":90,\"lines\":98}}'",
Expand Down
1 change: 1 addition & 0 deletions website/blog/.claude/rules/blog-posts.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Files: `YYYY-MM-DD-vX.Y-short-description.md`
- Title format: `vX.Y: Feature1, Feature2, Feature3` or `vX.Y Feature-focused Title`
- Include `releases` tag plus relevant feature tags from [tags.yml](./tags.yml)
- `draft: true` for unpublished/WIP posts
- Publish a draft with `yarn blog:publish website/blog/<post>.md` (removes `draft: true`, re-dates the filename to today, fixes links). CI fails posts published with a stale date.

## Package Layers

Expand Down
1 change: 1 addition & 0 deletions website/blog/.cursor/rules/blog-posts.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Files: `YYYY-MM-DD-vX.Y-short-description.md`
- Title format: `vX.Y: Feature1, Feature2, Feature3` or `vX.Y Feature-focused Title`
- Include `releases` tag plus relevant feature tags from [tags.yml](./tags.yml)
- `draft: true` for unpublished/WIP posts
- Publish a draft with `yarn blog:publish website/blog/<post>.md` (removes `draft: true`, re-dates the filename to today, fixes links). CI fails posts published with a stale date.

## Package Layers

Expand Down
156 changes: 156 additions & 0 deletions website/scripts/blog-publish.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
#!/usr/bin/env node
// Blog posts take their date (and URL) from the filename prefix, so a draft
// written weeks before release would publish with its draft date.
//
// yarn blog:publish website/blog/<post>.md
// removes `draft: true`, renames the post to today's date, rewrites its
// /blog/YYYY/MM/DD/slug links repo-wide and stages the post
// node website/scripts/blog-publish.mjs --check <base-ref>
// (CI) fails when a post published since <base-ref> isn't dated within
// MAX_DAYS of today, or is dated any way but its filename
import { execFileSync } from 'node:child_process';
import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
import { basename } from 'node:path';

const BLOG_DIR = 'website/blog';
const MAX_DAYS = 3;
const POST = /^(\d{4})-(\d{2})-(\d{2})-(.+)\.mdx?$/;
const FRONT_MATTER = /^---\r?\n([\s\S]*?)\r?\n---/;
const DRAFT = /^draft:\s*(true|True|TRUE)[^\S\r\n]*(#.*)?(\r?\n|$)/m;

const git = (...args) => execFileSync('git', args, { encoding: 'utf8' });
const today = new Date().toISOString().slice(0, 10);
const urlDate = date => date.replaceAll('-', '/');

// Front matter is read with regexes rather than @docusaurus/utils so CI can
// run this before installing packages.
function frontMatter(source) {
const block = source.match(FRONT_MATTER)?.[1] ?? '';
return { draft: DRAFT.test(block), hasDate: /^date:/m.test(block) };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recognize the YAML forms Docusaurus parses

Fresh evidence beyond the earlier boolean/comment fixes is that valid YAML keys may be quoted or contain whitespace before the colon, such as date : 2026-01-01 or "draft": true; Docusaurus's gray-matter/js-yaml parser accepts these, but both regexes report the fields as absent. Consequently publish() can retain an explicit stale date while renaming and rewriting links for the filename date, and --check can classify removal of a spaced or quoted base draft key as an already-published post and skip date validation. Parse front matter consistently with Docusaurus or recognize these valid key forms.

Useful? React with 👍 / 👎.

}

function publish(arg) {
const file = basename(arg);
const path = `${BLOG_DIR}/${file}`;
const slug = file.match(POST)?.[4];
if (!slug || !existsSync(path)) {
throw new Error(`${arg}: expected ${BLOG_DIR}/YYYY-MM-DD-slug.md`);
}
const source = readFileSync(path, 'utf8');
if (frontMatter(source).hasDate) {
throw new Error(
`${path}: remove its \`date:\` field; the filename dates it`,
);
}
const newPath = `${BLOG_DIR}/${today}${file.slice(today.length)}`;
if (newPath !== path && existsSync(newPath)) {
throw new Error(`${path}: can't rename, ${newPath} already exists`);
}
writeFileSync(
path,
source.replace(FRONT_MATTER, block => block.replace(DRAFT, '')),
);
Comment thread
ntucker marked this conversation as resolved.

if (newPath !== path) {
// Not `git mv`: a new post may not be tracked yet
renameSync(path, newPath);
rewriteLinks(
`/blog/${urlDate(file.slice(0, 10))}/${slug}`,
`/blog/${urlDate(today)}/${slug}`,
);
git('rm', '--cached', '-q', '--ignore-unmatch', '--', path);
}
// Stage after the rewrite so the commit gets the updated self-links
git('add', '--', newPath);
console.log(`published ${newPath}`);
}

function rewriteLinks(oldUrl, newUrl) {
// Stop at the slug's end so siblings like `${slug}-notes` are untouched
const oldUrlPattern = new RegExp(
`${oldUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w.-])`,
'g',
);
let linking = [];
try {
linking = git('grep', '--untracked', '-IlF', oldUrl)
.split('\n')
.filter(Boolean);
} catch (error) {
// git grep exits 1 when nothing links to the post
if (error.status !== 1) throw error;
}
for (const linked of linking) {
const text = readFileSync(linked, 'utf8');
writeFileSync(linked, text.replace(oldUrlPattern, newUrl));
Comment thread
ntucker marked this conversation as resolved.
}
if (!linking.length) return;
// Stage the rewrites so the commit has no broken links. Untracked files are
// left for their author to add (`git add -u` rejects them on newer git)
const tracked = git('ls-files', '-z', '--', ...linking)
.split('\0')
.filter(Boolean);
if (tracked.length) git('add', '--', ...tracked);
}

function check(base) {
let failed = false;
const fail = message => {
console.error(message);
failed = true;
};
// Only added, modified or renamed posts can have gone from draft to published
const changes = git('diff', '--name-status', '-M', base, '--', BLOG_DIR)
.split('\n')
.filter(Boolean)
.map(line => line.split('\t'));
for (const [status, from, to = from] of changes) {
const file = to.slice(BLOG_DIR.length + 1);
if (status === 'D' || file.startsWith('.') || !/\.mdx?$/.test(file)) {
continue;
}
// Docusaurus also builds posts from folders, which this check can't date
if (file.includes('/')) {
fail(`${to}: use a YYYY-MM-DD-slug.md file, not a post folder`);
continue;
}
const { draft, hasDate } = frontMatter(readFileSync(to, 'utf8'));
if (draft) continue;
if (status !== 'A' && !frontMatter(git('show', `${base}:${from}`)).draft) {
continue;
}

if (!POST.test(file) || hasDate) {
fail(
`${to}: name published posts YYYY-MM-DD-slug.md, without a \`date:\` front matter field`,
);
continue;
}
const postDate = file.slice(0, 10);
const days = Math.abs(Date.parse(postDate) - Date.parse(today)) / 864e5;
if (days > MAX_DAYS) {
fail(
`${to}: published with date ${postDate}, but today is ${today}. ` +
`Run \`yarn blog:publish ${to}\` to date it today and fix its links.`,
);
}
}
return failed;
}

const [first, second] = process.argv.slice(2);
if (first === '--check' && second) {
process.exit(check(second) ? 1 : 0);
} else if (first && !first.startsWith('-')) {
try {
publish(first);
} catch (error) {
console.error(error.message);
process.exit(1);
}
} else {
console.error(
'usage: yarn blog:publish <post> | blog-publish.mjs --check <base-ref>',
);
process.exit(2);
}
Loading