diff --git a/astro.config.mjs b/astro.config.mjs index 6a2b4ce1..f0c0aa89 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -181,6 +181,7 @@ export default defineConfig({ items: [{ autogenerate: { directory: 'flutter-concepts' } }], }, { label: 'Roadmap', link: '/roadmap/' }, + { label: 'Changelog', link: '/changelog/' }, ], plugins: [ starlightThemeNova(), @@ -208,6 +209,12 @@ Developer & Agent Interfaces: - Code Push REST API: OpenAPI 3.1 specification at https://api.shorebird.dev/openapi.json, base URL https://api.shorebird.dev/api/v1. Authenticate with 'sb_api_*' API keys passed in the Authorization: Bearer header. - Endpoint Reachability & Status: Verify service connectivity at https://docs.shorebird.dev/system/endpoint-reachability/.`, optionalLinks: [ + { + label: 'Changelog', + url: 'https://docs.shorebird.dev/changelog.md', + description: + 'What shipped to Code Push, the CLI, and the API, newest first', + }, { label: 'OpenAPI specification', url: 'https://api.shorebird.dev/openapi.json', diff --git a/src/data/changelog.ts b/src/data/changelog.ts new file mode 100644 index 00000000..27bc5526 --- /dev/null +++ b/src/data/changelog.ts @@ -0,0 +1,76 @@ +// cspell:words rollouts + +// Shared by the changelog page (`src/pages/changelog.astro`) and its +// agent-facing Markdown twin (`src/pages/changelog.md.ts`), so both always +// list the same entries. + +export const CHANGELOG_TITLE = 'Changelog'; +export const CHANGELOG_DESCRIPTION = + 'Everything we shipped to Code Push, the CLI, and the API.'; + +// The parts of Shorebird a change can be filed under. Fixed on purpose — +// this drives the area filter chips, so it should stay in sync with the +// products we actually ship rather than growing a new value per entry. +export const AREAS = ['Code Push', 'CLI', 'Console', 'API', 'Flutter'] as const; +export type Area = (typeof AREAS)[number]; + +export interface ChangelogEntry { + /** Anchor id and permalink slug for this entry. */ + id: string; + date: string; + version: string; + area: Area; + type: 'New' | 'Fixed' | 'Changed' | 'Deprecated'; + title: string; + summary: string; + bullets: string[]; + code?: string; + docLink?: { label: string; href: string }; +} + +// Hand-written, newest first. Add an entry here when something ships that a +// developer integrating Code Push would want to know about. +export const ENTRIES: ChangelogEntry[] = [ + { + id: 'staged-rollouts-in-one-command', + date: '2026-09-10', + version: '1.7.2', + area: 'Code Push', + type: 'New', + title: 'Staged rollouts in one command', + summary: + 'shorebird patch --track=beta now takes --rollout, so a staged patch can start at 5% without a second command.', + bullets: [ + '--rollout takes a whole percentage from 1 to 100. Omit it and the patch goes to the entire track, exactly as before.', + 'Raise or halt a rollout from the console, or with shorebird patch rollout set 25.', + 'Rollback still applies to the whole track — halting a rollout leaves the previous patch installed.', + ], + code: 'shorebird patch android --track=beta --rollout=5', + docLink: { + label: 'Percentage-based rollouts', + href: '/code-push/guides/percentage-based-rollouts/', + }, + }, +]; + +function formatMonth(iso: string): string { + return new Date(iso).toLocaleDateString('en-US', { + month: 'long', + year: 'numeric', + timeZone: 'UTC', + }); +} + +/** `ENTRIES` grouped under "September 2026"-style headings, newest first. */ +export function groupByMonth( + entries: ChangelogEntry[], +): { label: string; items: ChangelogEntry[] }[] { + const groups: { label: string; items: ChangelogEntry[] }[] = []; + for (const e of entries) { + const label = formatMonth(e.date); + let group = groups.find((g) => g.label === label); + if (!group) groups.push((group = { label, items: [] })); + group.items.push(e); + } + return groups; +} diff --git a/src/pages/changelog.astro b/src/pages/changelog.astro new file mode 100644 index 00000000..e548c510 --- /dev/null +++ b/src/pages/changelog.astro @@ -0,0 +1,620 @@ +--- +// cspell:ignore data-type white-space +import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'; +import { + AREAS, + CHANGELOG_DESCRIPTION, + CHANGELOG_TITLE, + ENTRIES, + groupByMonth, + type ChangelogEntry, +} from '~/data/changelog'; + +const TYPE_LABEL_COLOR: Record = { + New: 'new', + Fixed: 'fixed', + Changed: 'changed', + Deprecated: 'deprecated', +}; + +const groups = groupByMonth(ENTRIES); +--- + + +

+ Everything we shipped to Code Push, the CLI, and the API. Expand an entry + for the full detail, the command, and the doc it belongs to. +

+ +
+ +
+ + { + AREAS.map((a) => ( + + )) + } +
+ + {ENTRIES.length} + {ENTRIES.length === 1 ? 'entry' : 'entries'} + +
+ +
+ { + groups.map((group) => ( +
+

{group.label}

+ {group.items.map((e) => { + const searchText = [ + e.title, + e.summary, + e.area, + e.type, + e.version, + ...e.bullets, + e.code ?? '', + ] + .join(' ') + .toLowerCase(); + return ( +
+
+ +

{e.title}

+

{e.summary}

+
+ +
+ ); + })} +
+ )) + } +
+ + +
+ + + + diff --git a/src/pages/changelog.md.ts b/src/pages/changelog.md.ts new file mode 100644 index 00000000..11a46508 --- /dev/null +++ b/src/pages/changelog.md.ts @@ -0,0 +1,51 @@ +import { + CHANGELOG_DESCRIPTION, + CHANGELOG_TITLE, + ENTRIES, + groupByMonth, + type ChangelogEntry, +} from '~/data/changelog'; + +// The Markdown twin of `/changelog/`. `[...slug].md.ts` only covers content +// collection pages, and the changelog is a standalone `.astro` page, so without +// this route the `` that `Head.astro` +// emits for every page would point at a 404, and `Accept: text/markdown` on +// `/changelog/` would fall back to HTML. +export const prerender = true; + +function entryToMarkdown(e: ChangelogEntry): string { + return [ + `### ${e.title}`, + `**${e.type}** in ${e.area}, version ${e.version}, ${e.date}. Permalink: [/changelog/#${e.id}](/changelog/#${e.id})`, + e.summary, + e.bullets.map((b) => `- ${b}`).join('\n'), + e.code && ['```sh', e.code, '```'].join('\n'), + e.docLink && `Docs: [${e.docLink.label}](${e.docLink.href})`, + ] + .filter(Boolean) + .join('\n\n'); +} + +export function GET() { + const frontmatter = [ + '---', + `title: ${JSON.stringify(CHANGELOG_TITLE)}`, + `description: ${JSON.stringify(CHANGELOG_DESCRIPTION)}`, + '---', + ].join('\n'); + + const body = [ + frontmatter, + `${CHANGELOG_DESCRIPTION} Newest first.`, + ...groupByMonth(ENTRIES).flatMap((group) => [ + `## ${group.label}`, + ...group.items.map(entryToMarkdown), + ]), + ].join('\n\n'); + + // As in `[...slug].md.ts`, this header only applies in `astro dev` and + // `astro preview`; `public/_headers` sets it in production. + return new Response(`${body}\n`, { + headers: { 'Content-Type': 'text/markdown; charset=utf-8' }, + }); +}