From 1ef942d047558bde9eb95cd6978fffb7b90d4928 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 02:30:41 +0000 Subject: [PATCH 1/4] plugin: add a changelog for the radius plugin and require an entry per version (ENG-2633) plugins/radius/CHANGELOG.md backfills 0.0.2 to 0.2.2 from the versions as they landed on main, written for developers who use the skills. The docs release notes will be built from it. validate_plugin.py fails when the manifest version has no `## ()` entry, when headings are malformed, or when versions or dates are not newest first. The changelog is not release content (skills, manifest, README), so it needs no plugin version bump. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012M6NrUBhuq32wQaTAVbWn7 --- AGENTS.md | 2 +- README.md | 3 ++- plugins/radius/CHANGELOG.md | 53 +++++++++++++++++++++++++++++++++++++ scripts/validate_plugin.py | 18 +++++++++++++ 4 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 plugins/radius/CHANGELOG.md diff --git a/AGENTS.md b/AGENTS.md index 96a5cb6..ff31783 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,7 @@ From the repository root, run whichever of these apply to what you changed. Ever - Every PR that changes `packages/cli` or `packages/sdk` adds a changeset (`pnpm changeset`, or a file in `.changeset/`). Never edit `CHANGELOG.md` or package versions by hand. - Document an SDK or CLI change in `packages/docs` in the same PR, and audit the skills under `plugins/radius/skills` that cover it. - A skill may name a new SDK or CLI API only once that version is on npm. State the minimum package version in the skill. -- Bump `plugins/radius/.claude-plugin/plugin.json` in any PR that changes a skill, the plugin manifest or the plugin README: patch for corrections, minor for new capabilities. The plugin is not part of Changesets. +- Bump `plugins/radius/.claude-plugin/plugin.json` in any PR that changes a skill, the plugin manifest or the plugin README: patch for corrections, minor for new capabilities. The plugin is not part of Changesets. Each bump adds a `## ()` entry to `plugins/radius/CHANGELOG.md`; the docs release notes are built from it, so write the first sentence of each change for developers who use the skills. - In skill examples, write prices as decimal strings such as `'0.001'`. Claude substitutes `$0` and other positional placeholders even inside code fences. - No eval case, test or example you run may spend from a funded wallet. The funded testnet proof (`pnpm eval:payments:live`) runs only when a person asks for it. - Never print, log or commit private keys, `RADIUS_*` secrets or API keys. diff --git a/README.md b/README.md index 9e8d5a1..f4cd8f2 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,8 @@ The [Radius Claude Code plugin](plugins/radius) contains the `radius-dev`, `x402 [`.claude-plugin/marketplace.json`](.claude-plugin/marketplace.json). These files live outside `packages/*`. The plugin has its own version in [`plugins/radius/.claude-plugin/plugin.json`](plugins/radius/.claude-plugin/plugin.json) -and does not enter the npm Changesets release flow. The marketplace's +and does not enter the npm Changesets release flow. Each version bump adds an entry to +[`plugins/radius/CHANGELOG.md`](plugins/radius/CHANGELOG.md), which the docs release notes are built from. The marketplace's `metadata.version` describes the catalog, not the plugin; the marketplace entry does not duplicate the plugin version. diff --git a/plugins/radius/CHANGELOG.md b/plugins/radius/CHANGELOG.md new file mode 100644 index 0000000..0efe000 --- /dev/null +++ b/plugins/radius/CHANGELOG.md @@ -0,0 +1,53 @@ +# Radius plugin changelog + +The `radius` Claude Code plugin and its skills (`radius-dev`, `x402`, `sell-api`, `dripping-faucet`). +The version is `plugins/radius/.claude-plugin/plugin.json`. Every version bump adds an entry here, +newest first, as `## ()` with the date it reached `main`. The docs release +notes (docs.radiustech.xyz/release-notes) are built from this file: write each change's first +sentence for developers who use the skills. + +## 0.2.2 (2026-10-26) + +- The buyer install commands in the `x402` and `radius-dev` skills install both x402 peer packages, `@x402/core` and `@x402/evm`. `@x402/evm` is an optional peer that npm and pnpm skip, which left `radius-sdk/client` unable to load. The `sell-api` skill names `@x402/core` for sellers, and the plugin README says which peer each entry point needs. + +## 0.2.1 (2026-10-21) + +- The `sell-api` skill says Radius SBC offers use EIP-3009, not Permit2, and that MPP support is the `evm` charge method only. + +## 0.2.0 (2026-10-16) + +- **Breaking:** the plugin is now `radius` in the `tools` marketplace. Install it with `claude plugin marketplace add radiustechsystems/tools` and `claude plugin install radius@tools`. It replaces `radius-dev@radius-cli`; an existing install needs a one-time migration ([commands](https://docs.radiustech.xyz/build/coding-assistants#claude-code)). Skill names are unchanged, and slash commands take the `radius` prefix, for example `/radius:sell-api`. +- The plugin README shows how to install the skills in Codex with `npx skills add`. + +## 0.1.2 (2026-10-15) + +- The `sell-api` proxy snippet drops `Content-Encoding` and `Content-Length` when Node's `fetch` has already decoded a compressed origin response, and passes the request's `signal` so a client disconnect aborts the origin request. + +## 0.1.1 (2026-10-15) + +- The `sell-api` skill covers a Hono proxy on Node in front of an origin (with a Dockerfile), checks that free routes stay free, and ends with a minimum verify checklist. Its gotchas add the `duplex` option a stream body needs on Node, and the immutable-headers error a raw `fetch()` response hit after settlement under `@hono/node-server`. + +## 0.1.0 (2026-10-15) + +- New `sell-api` skill: charge for an API with `radius-sdk`. It detects the framework, installs and configures the SDK, runs the server locally, verifies the `402` with `radius-cli sell check`, and hands off deployment, with references for frameworks, protocols, networks and facilitators, and settlement. +- The `x402` skill covers buying and the protocol, and points sellers to `sell-api`. + +## 0.0.6 (2026-10-14) + +- Links and install commands point at the renamed repository, github.com/radiustechsystems/tools. + +## 0.0.5 (2026-10-13) + +- The `radius-dev` skill requires Node.js 22 or later for `radius-sdk`. + +## 0.0.4 (2026-10-13) + +- The skills use `radius-cli wallet pay` and `--threshold`, the new names for `wallet x402` and `--x402-threshold` in radius-cli 0.4.0. + +## 0.0.3 (2026-10-07) + +- The `radius-dev` and `x402` skills build with `radius-sdk` (`radius-sdk/client` for buyers, `radius-sdk/hono` for sellers) and `radius-cli`, with viem for other EVM work, instead of hand-written viem payment code. + +## 0.0.2 (2026-10-02) + +- The `radius-dev`, `x402` and `dripping-faucet` skills move into this repository as the `radius-dev` Claude Code plugin, with eval cases that run in CI. diff --git a/scripts/validate_plugin.py b/scripts/validate_plugin.py index 6afe953..9aa514d 100644 --- a/scripts/validate_plugin.py +++ b/scripts/validate_plugin.py @@ -45,6 +45,23 @@ def check_release_version(base_ref, current_version): ) +def check_changelog(current_version): + # The docs release notes are built from this file, so every released version needs an entry. + changelog = PLUGIN / "CHANGELOG.md" + assert changelog.is_file(), "plugin CHANGELOG.md missing" + content = changelog.read_text(encoding="utf-8") + entries = re.findall(r"^## (\S+) \((\d{4}-\d{2}-\d{2})\)$", content, re.MULTILINE) + headings = re.findall(r"^## .*$", content, re.MULTILINE) + assert len(entries) == len(headings), "plugin CHANGELOG.md headings must read `## ()`" + versions = [version_tuple(version) for version, _ in entries] + assert versions == sorted(set(versions), reverse=True), "plugin CHANGELOG.md versions must be unique, newest first" + dates = [date for _, date in entries] + assert dates == sorted(dates, reverse=True), "plugin CHANGELOG.md dates must be newest first" + assert current_version in [version for version, _ in entries], ( + f"plugin {current_version} has no entry in plugins/radius/CHANGELOG.md" + ) + + def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--base-ref", help="require a plugin version bump for release content changed since this ref") @@ -61,6 +78,7 @@ def main(): version_tuple(manifest["version"]) assert (PLUGIN / "README.md").is_file(), "plugin README missing" assert manifest["license"] == "MIT" + check_changelog(manifest["version"]) if args.base_ref: check_release_version(args.base_ref, manifest["version"]) From 022e955852faf727a073e57eaf87ec55f7fcf7d8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 02:42:38 +0000 Subject: [PATCH 2/4] docs: build release notes from the network notes and the SDK, CLI and skills changelogs (ENG-2633) scripts/generate-release-notes.ts writes docs/pages/release-notes.mdx and an Atom feed, public/release-notes.xml (both gitignored), newest first, from: - release-notes/network.md: the Radius Network notes, moved from the page - packages/sdk and packages/cli CHANGELOG.md (Changesets): a version is listed once its @ tag exists, dated by the tag, with npm and GitHub release links; Major/Minor/Patch become Breaking changes, New and Fixes, and "Updated dependencies" becomes one "Uses ..." line - plugins/radius/CHANGELOG.md: the Radius skills Each release is a `## : ` heading, so its anchor is a stable link to share. Each change's first sentence is bolded as its headline, as written. The page description names the latest release, so link previews and llms.txt show what is new. The generator fails when a checkout has no release tags; docs CI now fetches history and tags like docs-deploy, runs the generator tests, and runs on CLI changelog changes. prebuild, predev, docs:validate and docs:markdown-audit run the generator. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012M6NrUBhuq32wQaTAVbWn7 --- .github/workflows/docs.yml | 12 +- packages/docs/.gitignore | 4 +- packages/docs/.prettierignore | 2 + packages/docs/CLAUDE.md | 19 +- packages/docs/README.md | 24 +- packages/docs/package.json | 9 +- .../network.md} | 14 +- .../scripts/generate-release-notes.test.ts | 149 +++++++ .../docs/scripts/generate-release-notes.ts | 413 ++++++++++++++++++ 9 files changed, 618 insertions(+), 28 deletions(-) rename packages/docs/{docs/pages/release-notes.mdx => release-notes/network.md} (93%) create mode 100644 packages/docs/scripts/generate-release-notes.test.ts create mode 100644 packages/docs/scripts/generate-release-notes.ts diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 6a8c3e3..6220d13 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,7 +1,8 @@ name: Docs # Checks packages/docs on every pull request and push to main that can change the built site: the docs -# themselves, the SDK they import, the skills they publish under /skills/, and the lockfile. +# themselves, the SDK they import, the skills they publish under /skills/, the changelogs the release +# notes are built from, and the lockfile. # Same-repo pull requests also get a preview: a Worker version uploaded under the alias pr-. # Production deploys are not done here; see docs-deploy.yml. on: @@ -9,6 +10,7 @@ on: paths: - packages/docs/** - packages/sdk/** + - packages/cli/CHANGELOG.md - plugins/radius/** - .claude-plugin/** - pnpm-lock.yaml @@ -18,6 +20,7 @@ on: paths: - packages/docs/** - packages/sdk/** + - packages/cli/CHANGELOG.md - plugins/radius/** - .claude-plugin/** - pnpm-lock.yaml @@ -43,6 +46,10 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + # Release notes list a radius-sdk / radius-cli version once its release tag exists, dated by + # the tag; docs-deploy.yml checks out the same way. + fetch-depth: 0 + fetch-tags: true - name: Set up pnpm uses: pnpm/action-setup@v6 @@ -75,6 +82,9 @@ jobs: - name: Test flat skill generation run: pnpm test:skills + - name: Test release notes generation + run: pnpm test:release-notes + - name: Markdown audit run: pnpm docs:markdown-audit diff --git a/packages/docs/.gitignore b/packages/docs/.gitignore index 32d36f2..e819379 100644 --- a/packages/docs/.gitignore +++ b/packages/docs/.gitignore @@ -4,8 +4,10 @@ dist/ .vocs/ tmp/ -# Generated at build time (scripts/generate-skills-md.ts, scripts/generate-sitemap.ts) +# Generated at build time (scripts/generate-skills-md.ts, scripts/generate-sitemap.ts, +# scripts/generate-release-notes.ts) public/skills/ +docs/pages/release-notes.mdx public/sitemap.txt public/*.xml diff --git a/packages/docs/.prettierignore b/packages/docs/.prettierignore index 2123c44..e082a87 100644 --- a/packages/docs/.prettierignore +++ b/packages/docs/.prettierignore @@ -6,3 +6,5 @@ tmp/ public/**/*.md public/**/*.txt public/**/*.xml +# Generated by scripts/generate-release-notes.ts +docs/pages/release-notes.mdx diff --git a/packages/docs/CLAUDE.md b/packages/docs/CLAUDE.md index beda292..0cf3026 100644 --- a/packages/docs/CLAUDE.md +++ b/packages/docs/CLAUDE.md @@ -126,8 +126,9 @@ packages/docs/ │ ├── constants.ts # Network constants used in MDX and generated Markdown │ └── remark-llm-output.ts # Output-only remark plugin for agent Markdown ├── worker/index.ts # Worker entry: /api/try/* ("Try Radius"), Markdown negotiation, agent logging -├── public/ # Static assets (logos, robots.txt, _redirects, _headers, generated skills/sitemap) -├── scripts/ # Build utilities (skills, sitemap, tx cost, postbuild mirror, validators) +├── public/ # Static assets (logos, robots.txt, _redirects, _headers, generated skills/sitemap/feed) +├── release-notes/network.md # Radius Network release notes, by hand (see Release notes) +├── scripts/ # Build utilities (skills, sitemap, release notes, tx cost, postbuild mirror, validators) ├── vocs.config.ts # Site configuration ├── wrangler.jsonc # Worker, static assets, TRY_PAY_TO, logs, Analytics Engine ├── CLAUDE.md # This file @@ -165,7 +166,7 @@ Network values (chain IDs, RPC URLs, asset names and decimals, contract addresse ## Deployment -The site is the `radius-docs` Cloudflare Worker (`wrangler.jsonc`). Workers static assets serve `dist/public`, including `public/_redirects` and `public/_headers` (which marks every `workers.dev` host, previews included, `noindex`); `worker/index.ts` runs only for the paths in `assets.run_worker_first`. Under `/api/try/*` it is the backend of the interactive "Try Radius" components (`docs/components/try/`, used on `/try`), a testnet faucet proxy (the faucet API sends no CORS headers) and priced testnet routes built with `radius-sdk/hono`. Payments go to `TRY_PAY_TO` in `wrangler.jsonc`. Every other path except scripts, styles and images (excluded in `run_worker_first`) also reaches it, because Cloudflare Web Analytics only counts browsers that run its script. It serves pages and agent-facing files (`*.md`, `llms.txt`, `llms-full.txt`, `robots.txt`, sitemaps) from static assets, serves a page's Markdown twin to requests whose `Accept` header prefers `text/markdown`, and records agent requests twice: an `agent fetch` line in Workers Logs (kept 3 to 7 days, for debugging) and a data point in the `docs_agent_fetches` Analytics Engine dataset (for weekly KPIs; `worker/index.ts` documents its columns, which are append-only, including the host to filter previews out). A request is recorded when it negotiated Markdown or has no `Sec-Fetch-Mode` header, which every current browser sends, including on the page's own fetch of its `.md` twin (PageActions prefetches it on every page view). Neither record holds an IP address, query string or body. New static asset folders or file types go in the `run_worker_first` exclusions, so they stay asset-first: every Worker invocation is billed. Run it locally with `pnpm build && pnpm wrangler dev`; `vocs dev` serves the pages only, so the components' API calls fail there. +The site is the `radius-docs` Cloudflare Worker (`wrangler.jsonc`). Workers static assets serve `dist/public`, including `public/_redirects` and `public/_headers` (which marks every `workers.dev` host, previews included, `noindex`); `worker/index.ts` runs only for the paths in `assets.run_worker_first`. Under `/api/try/*` it is the backend of the interactive "Try Radius" components (`docs/components/try/`, used on `/try`), a testnet faucet proxy (the faucet API sends no CORS headers) and priced testnet routes built with `radius-sdk/hono`. Payments go to `TRY_PAY_TO` in `wrangler.jsonc`. Every other path except scripts, styles and images (excluded in `run_worker_first`) also reaches it, because Cloudflare Web Analytics only counts browsers that run its script. It serves pages and agent-facing files (`*.md`, `llms.txt`, `llms-full.txt`, `robots.txt`, sitemaps, `release-notes.xml`) from static assets, serves a page's Markdown twin to requests whose `Accept` header prefers `text/markdown`, and records agent requests twice: an `agent fetch` line in Workers Logs (kept 3 to 7 days, for debugging) and a data point in the `docs_agent_fetches` Analytics Engine dataset (for weekly KPIs; `worker/index.ts` documents its columns, which are append-only, including the host to filter previews out). A request is recorded when it negotiated Markdown or has no `Sec-Fetch-Mode` header, which every current browser sends, including on the page's own fetch of its `.md` twin (PageActions prefetches it on every page view). Neither record holds an IP address, query string or body. New static asset folders or file types go in the `run_worker_first` exclusions, so they stay asset-first: every Worker invocation is billed. Run it locally with `pnpm build && pnpm wrangler dev`; `vocs dev` serves the pages only, so the components' API calls fail there. The `docs.radiustech.xyz` custom domain is managed in the Cloudflare dashboard, not in `wrangler.jsonc`: listing it there makes `wrangler deploy` call the custom-domains API, which needs zone and DNS permissions the CI token deliberately lacks. @@ -174,6 +175,18 @@ The `docs.radiustech.xyz` custom domain is managed in the Cloudflare dashboard, --- +## Release notes + +`docs/pages/release-notes.mdx` and the Atom feed `public/release-notes.xml` are generated (and gitignored) by `scripts/generate-release-notes.ts`, which `prebuild`, `predev`, `docs:validate` and `docs:markdown-audit` run. Do not edit the page; edit its sources: + +- **Radius Network:** `release-notes/network.md`, by hand. `## ()` per release; the text is MDX. +- **radius-sdk, radius-cli:** their `CHANGELOG.md`, written by Changesets. A version is listed once its `@` git tag exists, dated by the tag, so the docs never announce a version that is not on npm. The generator fails when a checkout has no such tags: CI checks out with `fetch-depth: 0` and `fetch-tags: true`. +- **Radius skills:** `plugins/radius/CHANGELOG.md`, one `## ()` entry per plugin version bump. + +Each release is a `## : ` heading, so its anchor (`#2026-10-15-radius-sdk-060`) is a stable link to share. Each change's first sentence is bolded as its headline, and the page description names the latest release for link previews and `llms.txt`. Production picks up new entries on the next docs deploy: after every npm release, or a docs-only deploy run by hand. + +--- + ## Content Pages Three areas, each with its own sidebar in `vocs.config.ts`; the area list is in `docs/areas.ts`. The top nav and a Discover / Build / Reference switcher at the top of the desktop sidebar (`SidebarHeader` slot, `docs/components/AreaSwitcher.tsx`) link them; on mobile, Vocs shows the areas as a menu drop-down. Vocs picks the sidebar whose key is the longest prefix of the page path. Moved URLs redirect in `public/_redirects`. diff --git a/packages/docs/README.md b/packages/docs/README.md index 26c0bfe..a289cca 100644 --- a/packages/docs/README.md +++ b/packages/docs/README.md @@ -6,23 +6,23 @@ Source of [docs.radiustech.xyz](https://docs.radiustech.xyz), built with [Vocs]( Run from `packages/docs` (Node 22.15+, pnpm). Build the SDK once first: `pnpm --filter radius-sdk build` (the docs resolve it from its `dist/`). -| Command | Description | -| -------------------------- | ------------------------------------------------------------------------------ | -| `pnpm dev` | Development server at `http://localhost:5173` (pages only, no `/api/try`) | -| `pnpm build` | Generate skills and sitemap, build to `dist/public`, mirror Markdown twins | -| `pnpm wrangler dev` | After a build: serve the site and the Worker (`/api/try/*`) like production | -| `pnpm typecheck:docs` | Type-check the site, scripts and Worker | -| `pnpm docs:validate` | Frontmatter, links, headings and JSON blocks in `docs/pages` | -| `pnpm docs:markdown-audit` | Fail if an MDX component would be left unrendered in agent Markdown | -| `pnpm docs:agent-check` | After a build: fail on unparsed callouts or `.md` links with no file | -| `pnpm docs:sitemap-check` | After a build: validate `sitemap.xml` | -| `pnpm fetch-cost` | Refresh the fee constants in `docs/constants.ts` (the deploy workflow does it) | +| Command | Description | +| -------------------------- | -------------------------------------------------------------------------------- | +| `pnpm dev` | Development server at `http://localhost:5173` (pages only, no `/api/try`) | +| `pnpm build` | Generate skills, release notes and sitemap, build to `dist/public`, mirror twins | +| `pnpm wrangler dev` | After a build: serve the site and the Worker (`/api/try/*`) like production | +| `pnpm typecheck:docs` | Type-check the site, scripts and Worker | +| `pnpm docs:validate` | Frontmatter, links, headings and JSON blocks in `docs/pages` | +| `pnpm docs:markdown-audit` | Fail if an MDX component would be left unrendered in agent Markdown | +| `pnpm docs:agent-check` | After a build: fail on unparsed callouts or `.md` links with no file | +| `pnpm docs:sitemap-check` | After a build: validate `sitemap.xml` | +| `pnpm fetch-cost` | Refresh the fee constants in `docs/constants.ts` (the deploy workflow does it) | CI (`.github/workflows/docs.yml`) runs format, lint, typecheck, the validators and the build on every pull request that touches the docs, the SDK or the skills, and uploads a preview version of the Worker for same-repo pull requests. ## Hosting and releases -The site is a Cloudflare Worker (`wrangler.jsonc`): Workers static assets serve `dist/public`, including `public/_redirects` and `public/_headers` (`noindex` on `workers.dev` hosts, so previews and the pre-cutover site stay out of search results), and `worker/index.ts` handles `/api/try/*`, the backend of the interactive "Try Radius" components (a testnet faucet proxy and priced testnet routes built with `radius-sdk/hono`). Pages and the agent-facing files (`*.md`, `llms.txt`, `llms-full.txt`, `robots.txt`, sitemaps) also pass through the Worker, which serves them from static assets, gives a page's Markdown twin to requests that ask for `text/markdown`, and records agent requests in Workers Logs and the `docs_agent_fetches` Analytics Engine dataset: browser analytics never see agents. Scripts, styles and images stay asset-first. +The site is a Cloudflare Worker (`wrangler.jsonc`): Workers static assets serve `dist/public`, including `public/_redirects` and `public/_headers` (`noindex` on `workers.dev` hosts, so previews and the pre-cutover site stay out of search results), and `worker/index.ts` handles `/api/try/*`, the backend of the interactive "Try Radius" components (a testnet faucet proxy and priced testnet routes built with `radius-sdk/hono`). Pages and the agent-facing files (`*.md`, `llms.txt`, `llms-full.txt`, `robots.txt`, sitemaps, the `release-notes.xml` feed) also pass through the Worker, which serves them from static assets, gives a page's Markdown twin to requests that ask for `text/markdown`, and records agent requests in Workers Logs and the `docs_agent_fetches` Analytics Engine dataset: browser analytics never see agents. Scripts, styles and images stay asset-first. Production follows releases, not `main`, so the docs never describe a package version that is not on npm. `.github/workflows/docs-deploy.yml` builds and deploys, then moves the `docs-live` tag to the deployed commit: diff --git a/packages/docs/package.json b/packages/docs/package.json index 7d8a970..a711647 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -7,18 +7,21 @@ "generate-skills": "tsx scripts/generate-skills-md.ts", "test:skills": "node --import tsx --test scripts/generate-skills-md.test.ts", "generate-sitemap": "tsx scripts/generate-sitemap.ts", + "generate-release-notes": "tsx scripts/generate-release-notes.ts", + "test:release-notes": "node --import tsx --test scripts/generate-release-notes.test.ts", "fetch-cost": "tsx scripts/fetch-transaction-cost.ts", + "predev": "tsx scripts/generate-release-notes.ts", "dev": "vocs dev", - "prebuild": "tsx scripts/generate-skills-md.ts && tsx scripts/generate-sitemap.ts", + "prebuild": "tsx scripts/generate-skills-md.ts && tsx scripts/generate-release-notes.ts && tsx scripts/generate-sitemap.ts", "build": "vocs build", "preview": "vocs preview", "format:check": "prettier --check .", "format:write": "prettier --write .", "lint": "eslint .", "lint:fix": "eslint --fix .", - "docs:validate": "tsx scripts/validate-docs-v2.ts", + "docs:validate": "tsx scripts/generate-release-notes.ts && tsx scripts/validate-docs-v2.ts", "postbuild": "tsx scripts/postbuild-static-markdown.ts", - "docs:markdown-audit": "vocs markdown-audit", + "docs:markdown-audit": "tsx scripts/generate-release-notes.ts && vocs markdown-audit", "docs:agent-check": "tsx scripts/validate-agent-markdown.ts", "docs:sitemap-check": "tsx scripts/validate-sitemap.ts", "typecheck:docs": "tsc --noEmit && tsc --noEmit -p tsconfig.scripts.json && tsc --noEmit -p worker/tsconfig.json" diff --git a/packages/docs/docs/pages/release-notes.mdx b/packages/docs/release-notes/network.md similarity index 93% rename from packages/docs/docs/pages/release-notes.mdx rename to packages/docs/release-notes/network.md index 8483bbb..594f560 100644 --- a/packages/docs/docs/pages/release-notes.mdx +++ b/packages/docs/release-notes/network.md @@ -1,11 +1,9 @@ ---- -title: Release notes -description: Read the latest Radius release notes, including product updates, feature launches, fixes, and important developer changes. ---- - -# Release notes [Latest product and developer changes] - -Latest changes to the Radius Network. New features, improvements and bug fixes will be posted here. + ## 2026-09-28 (099a1b5b) diff --git a/packages/docs/scripts/generate-release-notes.test.ts b/packages/docs/scripts/generate-release-notes.test.ts new file mode 100644 index 0000000..1d714df --- /dev/null +++ b/packages/docs/scripts/generate-release-notes.test.ts @@ -0,0 +1,149 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { test } from 'node:test'; +import { + anchor, + collectReleases, + describe, + emphasizeHeadline, + escapeMdx, + firstSentence, + parseChangesetsVersion, + parseNetwork, + parsePackage, + parseSkills, + renderFeed, + renderPage, + sortReleases, +} from './generate-release-notes'; + +const repoRoot = join(import.meta.dirname, '..', '..', '..'); +const read = (path: string) => readFileSync(join(repoRoot, path), 'utf8'); + +test('the first sentence ends outside code and abbreviations', () => { + assert.equal(firstSentence('Use `a.b. c` here, e.g. now. Then more.'), 'Use `a.b. c` here, e.g. now.'); + assert.equal(firstSentence('New `radius-sdk/server` entry point:'), undefined); + assert.equal(emphasizeHeadline('One. Two.\n more'), '**One.** Two.\n more'); + assert.equal(emphasizeHeadline('**Breaking:** kept as written.'), '**Breaking:** kept as written.'); +}); + +test('MDX syntax is escaped outside code only', () => { + assert.equal(escapeMdx('a {b} `{d} `'), 'a \\{b\\} <c> `{d} `'); + assert.equal(escapeMdx('```ts\nconst x = {a: 1} < 2\n```'), '```ts\nconst x = {a: 1} < 2\n```'); +}); + +test('anchors match Vocs heading ids', () => { + assert.equal(anchor('2026-10-15: radius-sdk 0.6.0'), '2026-10-15-radius-sdk-060'); + assert.equal(anchor('2026-09-28: Radius Network'), '2026-09-28-radius-network'); +}); + +const changelog = `# radius-cli + +## 0.2.0 + +### Major Changes + +- abc1234: Removed the old command. It is gone. + +### Minor Changes + +- def5678: Add \`thing\`. It does more. + - **Breaking:** a sub-change. +- 1234567: Add \`other\`. + +### Patch Changes + +- 7654321: Fix a crash. +- Updated dependencies [7654321] + - radius-sdk@0.6.0 + +## 0.1.0 + +### Patch Changes + +- Updated dependencies [aaaaaaa] + - radius-sdk@0.5.0 +`; + +test('Changesets sections map to breaking, new and fix; dependency bumps are pulled out', () => { + const body = changelog.split('## 0.2.0')[1]!.split('## 0.1.0')[0]!; + const { changes, dependencies } = parseChangesetsVersion(body); + assert.deepEqual( + changes.map((change) => change.kind), + ['breaking', 'breaking', 'new', 'fix'], + ); + assert.equal(changes[1]!.text, 'Add `thing`. It does more.\n - **Breaking:** a sub-change.'); + assert.deepEqual(dependencies, ['radius-sdk@0.6.0']); +}); + +test('only tagged package versions are listed, dated by their tag', () => { + const releases = parsePackage('radius-cli', changelog, new Map([['radius-cli@0.1.0', '2026-09-01']])); + assert.equal(releases.length, 1); + assert.equal(releases[0]!.title, 'radius-cli 0.1.0'); + assert.equal(releases[0]!.date, '2026-09-01'); + assert.match(releases[0]!.body, /Uses `radius-sdk@0\.5\.0`\./); + assert.match(releases[0]!.body, /releases\/tag\/radius-cli%400\.1\.0/); +}); + +test('network and skills sections parse; malformed headings fail', () => { + const network = parseNetwork( + '\n\n## 2026-09-24 (10eea1f9)\n\n### Improvements\n\n- Faster. More.\n\n### API / RPC changes\n\n#### `eth_call` change\n\n- **Before:** x\n', + ); + assert.equal(network[0]!.date, '2026-09-24'); + assert.deepEqual(network[0]!.headlines, ['Faster.', 'eth_call change']); + assert.throws(() => parseNetwork('## September (abc)\n'), /heading must be/); + + const skills = parseSkills('# Title\n\n## 0.2.0 (2026-10-16)\n\n- **Breaking:** renamed.\n- Added. More.\n'); + assert.equal(skills[0]!.title, 'Radius skills 0.2.0'); + assert.deepEqual(skills[0]!.headlines, ['Breaking: renamed.', 'Added.']); + assert.throws(() => parseSkills('## 0.2.0\n'), /heading must be/); +}); + +test('releases sort newest first, network before packages on the same day', () => { + const releases = sortReleases([ + { title: 'Radius skills 0.1.0', date: '2026-10-15', body: '', headlines: [] }, + { title: 'radius-sdk 0.6.0', date: '2026-10-15', body: '', headlines: [] }, + { title: 'Radius Network', date: '2026-10-15', body: '', headlines: [] }, + { title: 'radius-cli 0.5.0', date: '2026-10-16', body: '', headlines: [] }, + ]); + assert.deepEqual( + releases.map((release) => release.title), + ['radius-cli 0.5.0', 'Radius Network', 'radius-sdk 0.6.0', 'Radius skills 0.1.0'], + ); +}); + +test('the description names the latest release within 160 characters', () => { + const description = describe([ + { title: 'radius-sdk 1.0.0', date: '2026-11-01', body: '', headlines: ['x'.repeat(300)] }, + ]); + assert.ok(description.startsWith('Latest: radius-sdk 1.0.0 (2026-11-01).')); + assert.ok(description.length <= 160); +}); + +test('the real sources build a page and feed with every released version', () => { + const tags = new Map(); + for (const [name, path] of [ + ['radius-sdk', 'packages/sdk/CHANGELOG.md'], + ['radius-cli', 'packages/cli/CHANGELOG.md'], + ] as const) { + for (const [, version] of read(path).matchAll(/^## (\S+)$/gm)) tags.set(`${name}@${version}`, '2026-10-01'); + } + const releases = collectReleases(read, tags); + const page = renderPage(releases); + const feed = renderFeed(releases); + for (const tag of tags.keys()) { + const title = tag.replace('@', ' '); + assert.ok(page.includes(`: ${title}\n`), `${title} missing from the page`); + assert.ok(feed.includes(`${title}`), `${title} missing from the feed`); + } + const version = JSON.parse(read('plugins/radius/.claude-plugin/plugin.json')).version; + assert.ok(page.includes(`: Radius skills ${version}\n`)); + assert.ok(page.includes(': Radius Network\n')); + assert.doesNotMatch(page, /Updated dependencies/); + assert.equal((feed.match(//g) ?? []).length, releases.length); +}); + +test('a checkout without release tags fails instead of dropping the packages', () => { + assert.throws(() => collectReleases(read, new Map()), /no radius-sdk@ git tags found/); +}); diff --git a/packages/docs/scripts/generate-release-notes.ts b/packages/docs/scripts/generate-release-notes.ts new file mode 100644 index 0000000..2349a3d --- /dev/null +++ b/packages/docs/scripts/generate-release-notes.ts @@ -0,0 +1,413 @@ +#!/usr/bin/env tsx +/** + * generate-release-notes.ts + * + * Builds the release notes page and its Atom feed from four sources, newest first: + * + * - release-notes/network.md: Radius Network notes, written by hand + * - packages/sdk/CHANGELOG.md and packages/cli/CHANGELOG.md: written by Changesets. A version is + * listed only once its `@` git tag exists (the release workflow pushes it after + * the npm publish), and dated by that tag + * - plugins/radius/CHANGELOG.md: the Radius skills, `## ()` per version + * + * Output (both gitignored): + * - docs/pages/release-notes.mdx, one `## : ` section per release, so every + * release has a stable anchor to share. Its description names the latest release, for link + * previews and llms.txt. + * - public/release-notes.xml, an Atom feed with one entry per release. + * + * Usage: + * pnpm generate-release-notes (also run by prebuild, predev, docs:validate and docs:markdown-audit) + */ + +import { execFileSync } from 'node:child_process'; +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +// --------------------------------------------------------------------------- +// Config +// --------------------------------------------------------------------------- + +const PROJECT_ROOT = join(import.meta.dirname, '..'); +const REPO_ROOT = join(PROJECT_ROOT, '..', '..'); +const SITE_BASE_URL = process.env.SITE_BASE_URL ?? 'https://docs.radiustech.xyz'; +const PAGE_URL = `${SITE_BASE_URL}/release-notes`; +const FEED_URL = `${SITE_BASE_URL}/release-notes.xml`; +const REPOSITORY = 'https://github.com/radiustechsystems/tools'; + +const PACKAGES = [ + { name: 'radius-sdk', changelog: 'packages/sdk/CHANGELOG.md' }, + { name: 'radius-cli', changelog: 'packages/cli/CHANGELOG.md' }, +] as const; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export type ChangeKind = 'breaking' | 'new' | 'fix'; + +export interface Change { + kind: ChangeKind; + /** Markdown, first line unindented, continuation lines as written (two-space indent). */ + text: string; +} + +export interface Release { + /** Display name: `Radius Network`, `radius-sdk 0.6.0`, `Radius skills 0.2.2`. */ + title: string; + /** YYYY-MM-DD */ + date: string; + /** Markdown under the heading. */ + body: string; + /** One line per change, plain text, for the feed summary and the page description. */ + headlines: string[]; +} + +// --------------------------------------------------------------------------- +// Markdown helpers +// --------------------------------------------------------------------------- + +/** The first sentence of a line, outside inline code, or undefined when it has no sentence end. */ +export function firstSentence(line: string): string | undefined { + let inCode = false; + for (let i = 0; i < line.length; i++) { + const char = line[i]; + if (char === '`') inCode = !inCode; + if (inCode || !'.!?'.includes(char!)) continue; + const next = line[i + 1]; + if (next !== undefined && next !== ' ') continue; + // Abbreviations such as "e.g." and "i.e." do not end a sentence. + if (/(?:^|\s)(?:e\.g|i\.e|vs|etc)$/i.test(line.slice(0, i))) continue; + return line.slice(0, i + 1); + } + return undefined; +} + +/** Bolds a change's first sentence (its headline), unless it already carries emphasis. */ +export function emphasizeHeadline(text: string): string { + const [first = '', ...rest] = text.split('\n'); + if (first.includes('**')) return text; + const sentence = firstSentence(first); + const bold = sentence ? `**${sentence}**${first.slice(sentence.length)}` : `**${first}**`; + return [bold, ...rest].join('\n'); +} + +/** Plain text of a change's headline: no code ticks, emphasis or link targets. */ +export function plainHeadline(text: string): string { + const first = text.split('\n')[0] ?? ''; + return (firstSentence(first) ?? first) + .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') + .replace(/[`*]/g, '') + .trim(); +} + +/** Escapes text that MDX would read as JSX or expressions, outside inline code and code fences. */ +export function escapeMdx(markdown: string): string { + let inFence = false; + return markdown + .split('\n') + .map((line) => { + if (/^\s*```/.test(line)) { + inFence = !inFence; + return line; + } + if (inFence) return line; + return line + .split(/(`[^`]*`)/) + .map((part) => (part.startsWith('`') ? part : part.replace(/[{}]/g, '\\$&').replace(/): string { + return `${release.date}: ${release.title}`; +} + +// --------------------------------------------------------------------------- +// Sources +// --------------------------------------------------------------------------- + +/** Splits a document into `## ` sections: [heading text, body]. */ +function sections(markdown: string): [string, string][] { + const parts = markdown.split(/^## /m).slice(1); + return parts.map((part) => { + const newline = part.indexOf('\n'); + return [part.slice(0, newline).trim(), part.slice(newline + 1).trim()]; + }); +} + +/** Top-level list items of a block, with their indented continuation lines. */ +function listItems(block: string): string[] { + const items: string[] = []; + for (const line of block.split('\n')) { + if (line.startsWith('- ')) items.push(line.slice(2)); + else if (items.length > 0) items[items.length - 1] += `\n${line}`; + } + return items.map((item) => item.replace(/\s+$/, '')); +} + +/** + * Headlines of a network release: each `####` heading (an API change) and the first sentence of + * each top-level bullet outside them. + */ +function networkHeadlines(body: string): string[] { + const headlines: string[] = []; + let inApiChange = false; + for (const line of body.split('\n')) { + if (line.startsWith('#### ')) { + inApiChange = true; + headlines.push(plainHeadline(line.slice(5))); + } else if (line.startsWith('### ')) { + inApiChange = false; + } else if (line.startsWith('- ') && !inApiChange) { + headlines.push(plainHeadline(line.slice(2))); + } + } + return headlines.filter(Boolean); +} + +/** Network notes: `## ()` sections, kept as written. */ +export function parseNetwork(markdown: string): Release[] { + return sections(markdown.replace(//g, '')).map(([title, body]) => { + const match = title.match(/^(\d{4}-\d{2}-\d{2}) \(([0-9a-f]+)\)$/); + if (!match) throw new Error(`release-notes/network.md: heading must be "## ()": ${title}`); + return { + title: 'Radius Network', + date: match[1]!, + body: `Network build \`${match[2]}\`.\n\n${body}`, + headlines: networkHeadlines(body), + }; + }); +} + +const KIND_OF_SECTION: Record = { + 'Major Changes': 'breaking', + 'Minor Changes': 'new', + 'Patch Changes': 'fix', +}; + +const KIND_HEADING: Record = { breaking: 'Breaking changes', new: 'New', fix: 'Fixes' }; + +/** One version of a Changesets CHANGELOG.md: its changes and the workspace packages it updated. */ +export function parseChangesetsVersion(body: string): { changes: Change[]; dependencies: string[] } { + const changes: Change[] = []; + const dependencies: string[] = []; + for (const [title, block] of body + .split(/^### /m) + .slice(1) + .map((part) => { + const newline = part.indexOf('\n'); + return [part.slice(0, newline).trim(), part.slice(newline + 1)] as const; + })) { + const kind = KIND_OF_SECTION[title]; + if (!kind) throw new Error(`unknown changelog section: ${title}`); + for (const item of listItems(block)) { + if (item.startsWith('Updated dependencies')) { + dependencies.push(...[...item.matchAll(/^\s+- (\S+@\S+)$/gm)].map((match) => match[1]!)); + continue; + } + // ": "; continuation lines are indented two spaces under the item. + const text = item.replace(/^[0-9a-f]{7,40}: /, ''); + changes.push({ kind: kind === 'new' && /\*\*Breaking:?\*\*/.test(text) ? 'breaking' : kind, text }); + } + } + return { changes, dependencies }; +} + +function changeList(changes: Change[]): string { + return changes.map((change) => `- ${escapeMdx(emphasizeHeadline(change.text))}`).join('\n'); +} + +/** Changesets CHANGELOG.md versions that have a release tag. */ +export function parsePackage(name: string, markdown: string, tagDates: Map): Release[] { + const releases: Release[] = []; + for (const [version, body] of sections(markdown)) { + const tag = `${name}@${version}`; + const date = tagDates.get(tag); + if (!date) continue; // Not published yet (the Version Packages PR), or tags not fetched. + const { changes, dependencies } = parseChangesetsVersion(body); + const blocks = [ + `[npm](https://www.npmjs.com/package/${name}/v/${version}) · [GitHub release](${REPOSITORY}/releases/tag/${encodeURIComponent(tag)})`, + ]; + for (const kind of ['breaking', 'new', 'fix'] as const) { + const ofKind = changes.filter((change) => change.kind === kind); + if (ofKind.length > 0) blocks.push(`### ${KIND_HEADING[kind]}\n\n${changeList(ofKind)}`); + } + if (dependencies.length > 0) blocks.push(`Uses ${dependencies.map((dep) => `\`${dep}\``).join(', ')}.`); + releases.push({ + title: `${name} ${version}`, + date, + body: blocks.join('\n\n'), + headlines: + changes.length > 0 ? changes.map((change) => plainHeadline(change.text)) : [`Uses ${dependencies.join(', ')}.`], + }); + } + return releases; +} + +/** plugins/radius/CHANGELOG.md: `## ()` sections of bullets. */ +export function parseSkills(markdown: string): Release[] { + return sections(markdown).map(([title, body]) => { + const match = title.match(/^(\d+\.\d+\.\d+) \((\d{4}-\d{2}-\d{2})\)$/); + if (!match) throw new Error(`plugins/radius/CHANGELOG.md: heading must be "## ()": ${title}`); + const items = listItems(body); + return { + title: `Radius skills ${match[1]}`, + date: match[2]!, + body: [ + '`claude plugin update radius@tools` · [Install the skills](/build/coding-assistants)', + items.map((item) => `- ${escapeMdx(emphasizeHeadline(item))}`).join('\n'), + ].join('\n\n'), + headlines: items.map(plainHeadline), + }; + }); +} + +/** Newest first; on the same day, the network, then the SDK, the CLI and the skills. */ +export function sortReleases(releases: Release[]): Release[] { + const rank = (release: Release) => + ['Radius Network', 'radius-sdk', 'radius-cli', 'Radius skills'].findIndex((prefix) => + release.title.startsWith(prefix), + ); + return [...releases].sort((a, b) => b.date.localeCompare(a.date) || rank(a) - rank(b)); +} + +// --------------------------------------------------------------------------- +// Output +// --------------------------------------------------------------------------- + +const PRODUCTS = ['Radius Network', 'radius-sdk', 'radius-cli', 'Radius skills'] as const; + +/** Meta description: the latest release and its first change, within 160 characters. */ +export function describe(releases: Release[]): string { + const latest = releases[0]; + if (!latest) return 'Radius release notes: the network, radius-sdk, radius-cli and the Radius skills.'; + const description = `Latest: ${latest.title} (${latest.date}). ${latest.headlines[0] ?? ''}`.trim(); + if (description.length <= 160) return description; + return `${description.slice(0, 157).replace(/\s+\S*$/, '')}...`; +} + +export function renderPage(releases: Release[]): string { + const latestRows = PRODUCTS.map((product) => { + const release = releases.find((candidate) => candidate.title.startsWith(product)); + if (!release) return undefined; + // The network has builds, not versions. + const version = release.title.slice(product.length).trim() || `build ${release.body.match(/`([0-9a-f]+)`/)?.[1]}`; + return `| ${product} | [${version}](#${anchor(heading(release))}) | ${release.date} |`; + }).filter(Boolean); + + return [ + '---', + 'title: Release notes', + `description: ${JSON.stringify(describe(releases))}`, + '---', + '', + '{/* Generated by scripts/generate-release-notes.ts. Edit release-notes/network.md or a changelog instead. */}', + '', + '# Release notes [What changed in the Radius Network, radius-sdk, radius-cli and the Radius skills]', + '', + 'Every release, newest first. Each heading is a link you can share. Follow new releases with the', + `[Atom feed](${FEED_URL}), or read this page as [Markdown](${SITE_BASE_URL}/release-notes.md).`, + '', + '| Product | Latest | Released |', + '| --- | --- | --- |', + ...latestRows, + '', + ...releases.flatMap((release) => [`## ${heading(release)}`, '', release.body, '']), + ].join('\n'); +} + +function escapeXml(text: string): string { + return text.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +} + +export function renderFeed(releases: Release[]): string { + const updated = `${releases[0]?.date ?? '1970-01-01'}T00:00:00Z`; + const entries = releases.map((release) => { + const link = `${PAGE_URL}#${anchor(heading(release))}`; + const id = `tag:${new URL(SITE_BASE_URL).host},${release.date}:${release.title.replace(/ /g, '-')}`; + return [ + ' ', + ` ${escapeXml(release.title)}`, + ` `, + ` ${escapeXml(id)}`, + ` ${release.date}T00:00:00Z`, + ` ${escapeXml(release.headlines.join('\n'))}`, + ` ${escapeXml(release.body.replace(/\]\(\//g, `](${SITE_BASE_URL}/`))}`, + ' ', + ].join('\n'); + }); + return [ + '', + '', + ' Radius release notes', + ' The Radius Network, radius-sdk, radius-cli and the Radius skills.', + ` `, + ` `, + ` ${PAGE_URL}`, + ` ${updated}`, + ' Radius', + ...entries, + '', + '', + ].join('\n'); +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +/** `@` tag -> YYYY-MM-DD it was created. */ +function readTagDates(): Map { + const output = execFileSync( + 'git', + ['for-each-ref', '--format=%(refname:short) %(creatordate:short)', 'refs/tags/radius-*'], + { cwd: REPO_ROOT, encoding: 'utf8' }, + ); + return new Map( + output + .split('\n') + .filter(Boolean) + .map((line) => line.split(' ') as [string, string]), + ); +} + +export function collectReleases(read: (path: string) => string, tagDates: Map): Release[] { + const releases = [ + ...parseNetwork(read('packages/docs/release-notes/network.md')), + ...PACKAGES.flatMap((pkg) => { + const markdown = read(pkg.changelog); + const listed = parsePackage(pkg.name, markdown, tagDates); + if (listed.length === 0 && sections(markdown).length > 0) { + throw new Error( + `no ${pkg.name}@ git tags found: fetch tags (actions/checkout fetch-tags: true) so released versions are listed`, + ); + } + return listed; + }), + ...parseSkills(read('plugins/radius/CHANGELOG.md')), + ]; + return sortReleases(releases); +} + +function main() { + const releases = collectReleases((path) => readFileSync(join(REPO_ROOT, path), 'utf8'), readTagDates()); + const page = join(PROJECT_ROOT, 'docs/pages/release-notes.mdx'); + const feed = join(PROJECT_ROOT, 'public/release-notes.xml'); + mkdirSync(dirname(feed), { recursive: true }); + writeFileSync(page, renderPage(releases)); + writeFileSync(feed, renderFeed(releases)); + console.log(` ✓ ${releases.length} releases → docs/pages/release-notes.mdx, public/release-notes.xml`); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main(); From 16c67fcd285151c5f0a04642cc5e0d2fb0aa14e8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 02:42:38 +0000 Subject: [PATCH 3/4] docs: make release notes easy to share and follow (ENG-2633) - Every page links the Atom feed () for feed readers and agents; _headers serves it as application/atom+xml. - The Worker records feed fetches with the other agent files, so docs_agent_fetches shows who follows release notes. - A link to a heading scrolls it below both fixed bars. Vocs offsets headings for the top nav only, so the "On this page" bar covered a shared release on phones and desktops. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012M6NrUBhuq32wQaTAVbWn7 --- packages/docs/docs/pages/_root.css | 6 ++++++ packages/docs/public/_headers | 4 ++++ packages/docs/vocs.config.ts | 9 +++++++++ packages/docs/worker/index.ts | 4 ++-- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/docs/docs/pages/_root.css b/packages/docs/docs/pages/_root.css index 5239b07..8410446 100644 --- a/packages/docs/docs/pages/_root.css +++ b/packages/docs/docs/pages/_root.css @@ -135,6 +135,12 @@ h1 { font-weight: 600; } +/* A link to a heading (a shared release, a section) scrolls it below both fixed bars: Vocs offsets + * headings for the top nav only, so the "On this page" bar covered them. */ +[data-v-content] :is(h2, h3, h4)[id] { + scroll-margin-top: 2.5rem; +} + /* Inline code only; code blocks are `pre > code`. */ [data-v-content] :not(pre) > code { color: #eb6359; diff --git a/packages/docs/public/_headers b/packages/docs/public/_headers index edf95c8..4e0e885 100644 --- a/packages/docs/public/_headers +++ b/packages/docs/public/_headers @@ -2,3 +2,7 @@ # only docs.radiustech.xyz should be indexed. https://:worker.:account.workers.dev/* X-Robots-Tag: noindex + +# The release notes feed (scripts/generate-release-notes.ts) is Atom, not generic XML. +/release-notes.xml + Content-Type: application/atom+xml; charset=utf-8 diff --git a/packages/docs/vocs.config.ts b/packages/docs/vocs.config.ts index c0cd6f9..24602f6 100644 --- a/packages/docs/vocs.config.ts +++ b/packages/docs/vocs.config.ts @@ -397,6 +397,15 @@ function headImpl(path: string, context: HeadContext, data: HeadData): HeadResul return { ...base, + // Feed autodiscovery on every page: readers and agents find release notes from any docs URL. + link: [ + { + rel: 'alternate', + type: 'application/atom+xml', + title: 'Radius release notes', + href: `${data.siteUrl}/release-notes.xml`, + }, + ], script: jsonLd.map((entry) => ({ type: 'application/ld+json', innerHTML: JSON.stringify(entry).replace(/ c.json({ message: 'Hello, world!' })); app.notFound((c) => c.json({ error: 'not found' }, 404)); -// Files written for agents. -const AGENT_FILE = /\.md$|^\/(?:llms\.txt|llms-full\.txt|robots\.txt|sitemap\.xml|sitemap\.txt)$/; +// Files written for agents and feed readers. +const AGENT_FILE = /\.md$|^\/(?:llms\.txt|llms-full\.txt|robots\.txt|sitemap\.xml|sitemap\.txt|release-notes\.xml)$/; // A page URL: no file extension in the last segment (`/`, `/build/`, `/build/x402`). const PAGE = /(?:^|\/)[^/.]*$/; From e8b54d6a89fa6f700f79c498dade74c0a8f55cfe Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 14:14:35 +0000 Subject: [PATCH 4/4] docs: list a minor change marked "Breaking ..." with the breaking changes (ENG-2633) radius-sdk 0.7.0 marks its peer-dependency move "**Breaking for installs:**", which the "**Breaking:**"-only rule left under New. Any bold text starting with "Breaking" now counts. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012M6NrUBhuq32wQaTAVbWn7 --- packages/docs/scripts/generate-release-notes.test.ts | 5 ++++- packages/docs/scripts/generate-release-notes.ts | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/docs/scripts/generate-release-notes.test.ts b/packages/docs/scripts/generate-release-notes.test.ts index 1d714df..6fee2f3 100644 --- a/packages/docs/scripts/generate-release-notes.test.ts +++ b/packages/docs/scripts/generate-release-notes.test.ts @@ -51,6 +51,9 @@ const changelog = `# radius-cli - def5678: Add \`thing\`. It does more. - **Breaking:** a sub-change. - 1234567: Add \`other\`. +- 2345678: Move a dependency. + + **Breaking for installs:** add it yourself. ### Patch Changes @@ -71,7 +74,7 @@ test('Changesets sections map to breaking, new and fix; dependency bumps are pul const { changes, dependencies } = parseChangesetsVersion(body); assert.deepEqual( changes.map((change) => change.kind), - ['breaking', 'breaking', 'new', 'fix'], + ['breaking', 'breaking', 'new', 'breaking', 'fix'], ); assert.equal(changes[1]!.text, 'Add `thing`. It does more.\n - **Breaking:** a sub-change.'); assert.deepEqual(dependencies, ['radius-sdk@0.6.0']); diff --git a/packages/docs/scripts/generate-release-notes.ts b/packages/docs/scripts/generate-release-notes.ts index 2349a3d..6915a1d 100644 --- a/packages/docs/scripts/generate-release-notes.ts +++ b/packages/docs/scripts/generate-release-notes.ts @@ -218,7 +218,9 @@ export function parseChangesetsVersion(body: string): { changes: Change[]; depen } // ": "; continuation lines are indented two spaces under the item. const text = item.replace(/^[0-9a-f]{7,40}: /, ''); - changes.push({ kind: kind === 'new' && /\*\*Breaking:?\*\*/.test(text) ? 'breaking' : kind, text }); + // A minor change that says it breaks something (`**Breaking:**`, `**Breaking for installs:**`) is + // listed with the breaking changes. + changes.push({ kind: kind === 'new' && /\*\*Breaking\b[^*]*\*\*/.test(text) ? 'breaking' : kind, text }); } } return { changes, dependencies };