diff --git a/docs-ui/README.md b/docs-ui/README.md
index 41b9921413..b943f1ce7c 100644
--- a/docs-ui/README.md
+++ b/docs-ui/README.md
@@ -13,3 +13,28 @@ yarn start
## Deployment
Deployments are done automatically when a PR is merged into the `master` branch. We host the website using Github pages.
+
+## Maintaining Component Changelogs
+
+After a `@backstage/ui` release, sync the component changelogs to keep documentation up-to-date:
+
+```bash
+yarn sync:changelog
+```
+
+This script:
+
+- Parses `packages/ui/CHANGELOG.md` for new versions
+- Extracts entries tagged with "Affected components: ..."
+- Updates `src/utils/changelog.ts` with new entries
+- Handles both component-specific and general package changes
+
+After running, review the changes in `src/utils/changelog.ts` and commit them.
+
+**Preview changes before writing:**
+
+```bash
+yarn sync:changelog:dry-run
+```
+
+Running this gives you a summary of what would be written, without actually adding or changing any files.
diff --git a/docs-ui/package.json b/docs-ui/package.json
index d05fdf81bb..319c0e130f 100644
--- a/docs-ui/package.json
+++ b/docs-ui/package.json
@@ -8,6 +8,8 @@
"lint": "next lint",
"prestart": "yarn sync:css",
"start": "next dev",
+ "sync:changelog": "node scripts/sync-changelog.mjs",
+ "sync:changelog:dry-run": "node scripts/sync-changelog.mjs --dry-run",
"sync:css": "node scripts/sync-css.js",
"sync:css:watch": "node scripts/sync-css.js --watch"
},
@@ -39,6 +41,7 @@
"storybook": "^8.6.12"
},
"devDependencies": {
+ "@octokit/rest": "^22.0.1",
"@shikijs/transformers": "^3.13.0",
"@types/mdx": "^2.0.13",
"@types/node": "^20",
@@ -48,6 +51,7 @@
"eslint": "^8",
"eslint-config-next": "15.3.4",
"lightningcss": "^1.28.2",
- "typescript": "^5"
+ "typescript": "^5",
+ "unified": "^11.0.4"
}
}
diff --git a/docs-ui/scripts/sync-changelog.mjs b/docs-ui/scripts/sync-changelog.mjs
new file mode 100644
index 0000000000..5eab3616dd
--- /dev/null
+++ b/docs-ui/scripts/sync-changelog.mjs
@@ -0,0 +1,991 @@
+#!/usr/bin/env node
+/* eslint-disable no-restricted-syntax */
+
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import { unified } from 'unified';
+import remarkParse from 'remark-parse';
+import { Octokit } from '@octokit/rest';
+import { execSync } from 'child_process';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+/**
+ * Compare two semantic versions.
+ * Returns: 1 if versionA > versionB, -1 if versionA < versionB, 0 if equal
+ * Handles pre-release versions (e.g., "0.9.0-next.2")
+ */
+function compareVersions(versionA, versionB) {
+ // Strip 'v' prefix if present
+ versionA = versionA.replace(/^v/, '');
+ versionB = versionB.replace(/^v/, '');
+
+ // Split into [major.minor.patch, prerelease]
+ const [versionABase, versionAPre] = versionA.split('-');
+ const [versionBBase, versionBPre] = versionB.split('-');
+
+ // Compare base versions
+ const versionAParts = versionABase.split('.').map(Number);
+ const versionBParts = versionBBase.split('.').map(Number);
+
+ for (
+ let i = 0;
+ i < Math.max(versionAParts.length, versionBParts.length);
+ i++
+ ) {
+ const versionAPart = versionAParts[i] || 0;
+ const versionBPart = versionBParts[i] || 0;
+
+ if (versionAPart > versionBPart) return 1;
+ if (versionAPart < versionBPart) return -1;
+ }
+
+ // If base versions equal, compare pre-release
+ if (!versionAPre && !versionBPre) return 0;
+ if (!versionAPre) return 1; // versionA is release, versionB is pre-release
+ if (!versionBPre) return -1; // versionA is pre-release, versionB is release
+
+ // Both have pre-release, compare them
+ return versionAPre.localeCompare(versionBPre);
+}
+
+/**
+ * Check if versionA is newer than versionB
+ */
+function isNewerVersion(versionA, versionB) {
+ return compareVersions(versionA, versionB) > 0;
+}
+
+/**
+ * Normalize version to base version (remove pre-release suffix)
+ * "0.9.0-next.3" ā "0.9.0"
+ * "0.9.0" ā "0.9.0"
+ */
+function normalizeVersion(version) {
+ return version.split('-')[0];
+}
+
+/**
+ * Read the existing changelog.ts and find the highest version number
+ * by scanning the changelogs/ directory for version files
+ */
+function getLastSyncedVersion(changelogPath) {
+ try {
+ // Scan changelogs/ directory for latest version file
+ const changelogsDir = path.join(path.dirname(changelogPath), 'changelogs');
+
+ if (!fs.existsSync(changelogsDir)) {
+ return null;
+ }
+
+ // Read all version files (v0.1.0.ts, v0.2.0.ts, etc.)
+ const files = fs
+ .readdirSync(changelogsDir)
+ .filter(f => f.match(/^v\d+\.\d+\.\d+\.ts$/))
+ .map(f => f.replace(/^v/, '').replace(/\.ts$/, ''));
+
+ if (files.length === 0) {
+ return null;
+ }
+
+ // Find highest version
+ let highest = files[0];
+ for (const version of files) {
+ if (compareVersions(version, highest) > 0) {
+ highest = version;
+ }
+ }
+
+ return highest;
+ } catch (error) {
+ console.error(`Error reading changelog file: ${error.message}`);
+ return null;
+ }
+}
+
+/**
+ * Convert PascalCase component name to kebab-case slug
+ * Avatar -> avatar
+ * ButtonIcon -> button-icon
+ * SearchField -> searchfield (special case per existing data)
+ */
+function pascalToKebab(str) {
+ // Special cases observed in changelog.ts
+ const specialCases = {
+ SearchField: 'searchfield',
+ TextField: 'textfield',
+ DataTable: 'datatable',
+ ScrollArea: 'scrollarea',
+ };
+
+ if (specialCases[str]) {
+ return specialCases[str];
+ }
+
+ // General conversion: insert hyphen before uppercase letters
+ return str
+ .replace(/([A-Z])/g, '-$1')
+ .toLowerCase()
+ .replace(/^-/, ''); // Remove leading hyphen
+}
+
+/**
+ * Map component name to valid Component type slug
+ * Returns null if component not recognized
+ */
+function mapComponentName(name, validComponents) {
+ const kebab = pascalToKebab(name.trim());
+ const kebabLower = kebab.toLowerCase();
+
+ // Check if it's a valid component
+ const valid = validComponents.find(c => c.toLowerCase() === kebabLower);
+
+ return valid || null;
+}
+
+/**
+ * Get valid component names from types.ts Component type
+ */
+function getValidComponents(changelogPath) {
+ // Read from types.ts instead of changelog.ts
+ const typesPath = changelogPath.replace('changelog.ts', 'types.ts');
+ const content = fs.readFileSync(typesPath, 'utf-8');
+
+ // Extract Component type union
+ const typeMatch = content.match(/export type Component =([^;]+);/s);
+ if (!typeMatch) {
+ throw new Error('Could not find Component type definition');
+ }
+
+ // Extract string literals from the union
+ const components = typeMatch[1]
+ .match(/['"]([^'"]+)['"]/g)
+ .map(s => s.replace(/['"]/g, ''));
+
+ return components;
+}
+
+/**
+ * Parse CHANGELOG.md and extract entries newer than sinceVersion
+ */
+async function parseChangelogMd(changelogPath, sinceVersion, validComponents) {
+ const content = fs.readFileSync(changelogPath, 'utf-8');
+ const tree = unified().use(remarkParse).parse(content);
+
+ const entries = [];
+ let currentVersion = null;
+ let currentSection = null; // 'Minor Changes', 'Patch Changes', 'Major Changes'
+ let processedCount = 0;
+
+ // Walk through the markdown AST
+ async function walk(node, depth = 0) {
+ // Version headers (## 0.9.0)
+ if (node.type === 'heading' && node.depth === 2) {
+ const versionText = extractText(node).trim();
+ // Validate that this is actually a version number (X.Y.Z or X.Y.Z-pre.N)
+ // Skip headings that are just markdown content within changelog entries
+ if (/^\d+\.\d+\.\d+/.test(versionText)) {
+ currentVersion = versionText;
+ currentSection = null;
+ }
+ return;
+ }
+
+ // Section headers (### Minor Changes)
+ if (node.type === 'heading' && node.depth === 3) {
+ const sectionText = extractText(node);
+ currentSection = sectionText.trim();
+ return;
+ }
+
+ // List items (- 539cf26: description)
+ if (node.type === 'listItem' && currentVersion && currentSection) {
+ // Only process if version is newer than sinceVersion
+ if (!sinceVersion || isNewerVersion(currentVersion, sinceVersion)) {
+ const entry = await parseListItem(
+ node,
+ currentVersion,
+ currentSection,
+ validComponents,
+ content,
+ );
+ if (entry) {
+ entries.push(entry);
+ processedCount++;
+ // Progress logging every 50 entries
+ if (processedCount % 50 === 0) {
+ process.stdout.write(`\r Processed ${processedCount} entries...`);
+ }
+ }
+ }
+ }
+
+ // Recurse into children
+ if (node.children) {
+ for (const child of node.children) {
+ await walk(child, depth + 1);
+ }
+ }
+ }
+
+ await walk(tree);
+
+ // Clear progress line
+ if (processedCount > 0) {
+ process.stdout.write(`\r Processed ${processedCount} entries - done!\n`);
+ }
+
+ return entries;
+}
+
+/**
+ * Extract plain text from a markdown node
+ */
+function extractText(node) {
+ if (node.type === 'text') {
+ return node.value;
+ }
+ if (node.children) {
+ return node.children.map(extractText).join('');
+ }
+ return '';
+}
+
+/**
+ * Extract raw markdown content from a node using position offsets
+ * This preserves all formatting: code blocks, lists, bold/italic, etc.
+ */
+function extractMarkdown(node, sourceText) {
+ if (!node.position) {
+ return extractText(node); // Fallback to text extraction
+ }
+
+ const start = node.position.start.offset;
+ const end = node.position.end.offset;
+
+ if (start === undefined || end === undefined) {
+ return extractText(node);
+ }
+
+ return sourceText.slice(start, end).trim();
+}
+
+/**
+ * Parse a list item to extract changelog entry
+ * NOTE: PR numbers are NOT fetched here - they're fetched later only for new entries
+ */
+async function parseListItem(
+ node,
+ version,
+ section,
+ validComponents,
+ sourceText,
+) {
+ // Extract the full text content for SHA parsing
+ const fullText = extractText(node);
+
+ // Parse commit SHA and description
+ // Format: "- 539cf26: description..."
+ const match = fullText.match(/^-?\s*([a-f0-9]+):\s*(.+)/s);
+ if (!match) {
+ return null;
+ }
+
+ const commitSha = match[1];
+
+ // Extract full markdown content from the list item
+ let fullMarkdown = extractMarkdown(node, sourceText);
+
+ // Remove the commit SHA prefix from markdown
+ let description = fullMarkdown.replace(/^-?\s*[a-f0-9]+:\s*/, '').trim();
+
+ // Auto-detect components from "Affected components:" line
+ let components = [];
+ const componentMatch = description.match(/Affected components?:\s*([^\n]+)/i);
+ if (componentMatch) {
+ const componentNames = componentMatch[1]
+ .split(',')
+ .map(name => name.trim())
+ .filter(Boolean);
+
+ components = componentNames
+ .map(name => mapComponentName(name, validComponents))
+ .filter(Boolean);
+
+ // Optionally strip "Affected components:" line from description
+ description = description
+ .replace(/\n*Affected components?:[ \t]*[^\n]+/i, '')
+ .trim();
+ }
+
+ const prs = []; // Will be populated later by fetchPRNumbers()
+
+ // Infer type from section and description
+ const type = inferChangeType(section, description, version);
+
+ return {
+ version: normalizeVersion(version),
+ section,
+ commitSha,
+ description,
+ components,
+ prs,
+ type,
+ };
+}
+
+/**
+ * Infer change type from section and description
+ */
+function inferChangeType(section, description, version) {
+ const isPre1 = version.startsWith('0.');
+
+ // Check description keywords first
+ if (description.match(/^(New|Add(ed)?)\s/i)) {
+ return 'new';
+ }
+ if (description.includes('BREAKING')) {
+ return 'breaking';
+ }
+
+ // Infer from section
+ if (section === 'Minor Changes') {
+ return isPre1 ? 'breaking' : undefined;
+ } else if (section === 'Major Changes') {
+ return 'breaking';
+ } else if (section === 'Patch Changes') {
+ return 'fix';
+ }
+
+ return undefined;
+}
+
+/**
+ * Generate new changelog entries and update changelog.ts
+ */
+/**
+ * Fetch PR numbers for entries that need them
+ * Only fetches for entries with empty prs array (new entries)
+ */
+async function fetchPRNumbers(entries, dryRun = false) {
+ const entriesNeedingPRs = entries.filter(
+ e => e.prs.length === 0 && e.commitSha,
+ );
+
+ if (entriesNeedingPRs.length === 0) {
+ return;
+ }
+
+ console.log(
+ `\nš Fetching PR numbers for ${entriesNeedingPRs.length} new entries...`,
+ );
+
+ let fetchedCount = 0;
+ const startTime = Date.now();
+
+ for (const entry of entriesNeedingPRs) {
+ const prNumber = await findPRNumber(entry.commitSha);
+ if (prNumber) {
+ entry.prs.push(prNumber);
+ }
+
+ fetchedCount++;
+ // Progress every 10 lookups
+ if (fetchedCount % 10 === 0 || fetchedCount === entriesNeedingPRs.length) {
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
+ const rate = ((fetchedCount / (Date.now() - startTime)) * 1000).toFixed(
+ 1,
+ );
+ process.stdout.write(
+ `\r Fetched ${fetchedCount}/${entriesNeedingPRs.length} PR numbers (${elapsed}s, ~${rate} req/s)`,
+ );
+ }
+ }
+
+ const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
+ const foundCount = entriesNeedingPRs.filter(e => e.prs.length > 0).length;
+ console.log(
+ `\n ā Found ${foundCount}/${entriesNeedingPRs.length} PR numbers in ${totalTime}s`,
+ );
+}
+
+/**
+ * Generate per-version changelog files in changelogs/ directory
+ */
+async function generateVersionFiles(entries, changelogsDir, dryRun = false) {
+ // Group entries by normalized version
+ const byVersion = {};
+ entries.forEach(entry => {
+ const version = entry.version;
+ if (!byVersion[version]) {
+ byVersion[version] = [];
+ }
+ byVersion[version].push(entry);
+ });
+
+ const versionFiles = [];
+ const skippedVersions = [];
+ const generatedVersions = [];
+
+ for (const [version, versionEntries] of Object.entries(byVersion)) {
+ const fileName = `v${version}.ts`;
+ const filePath = path.join(changelogsDir, fileName);
+ const varName = `changelog_${version.replace(/\./g, '_')}`;
+
+ // Check if file already exists - skip if it does
+ if (fs.existsSync(filePath)) {
+ skippedVersions.push(version);
+ // Still add to versionFiles array so main changelog.ts can import it
+ versionFiles.push({
+ version,
+ fileName,
+ filePath,
+ varName,
+ content: null, // No content needed for existing files
+ skipped: true,
+ });
+ continue;
+ }
+
+ // Generate TypeScript code for this version
+ const entryObjects = versionEntries
+ .map(entry => {
+ const componentsStr = `[${entry.components
+ .map(c => `'${c}'`)
+ .join(', ')}]`;
+ const prsStr = `[${entry.prs.map(pr => `'${pr}'`).join(', ')}]`;
+ const typeStr = entry.type ? `type: '${entry.type}',` : '';
+ const shaStr = entry.commitSha
+ ? `commitSha: '${entry.commitSha}',`
+ : '';
+
+ // Escape description for template literal
+ const descEscaped = entry.description
+ .replace(/\\/g, '\\\\')
+ .replace(/`/g, '\\`')
+ .replace(/\${/g, '\\${');
+
+ return ` {
+ components: ${componentsStr},
+ version: '${entry.version}',
+ prs: ${prsStr},
+ description: \`${descEscaped}\`,
+ ${typeStr}
+ ${shaStr}
+ }`;
+ })
+ .join(',\n');
+
+ const fileContent = `import type { ChangelogProps } from '../types';
+
+export const ${varName}: ChangelogProps[] = [
+${entryObjects}
+];
+`;
+
+ generatedVersions.push(version);
+ versionFiles.push({
+ version,
+ fileName,
+ filePath,
+ varName,
+ content: fileContent,
+ skipped: false,
+ });
+
+ if (!dryRun) {
+ fs.writeFileSync(filePath, fileContent, 'utf-8');
+ }
+ }
+
+ // Log summary
+ if (skippedVersions.length > 0) {
+ console.log(
+ `\nāļø Skipped ${
+ skippedVersions.length
+ } existing version files: ${skippedVersions
+ .sort((a, b) => compareVersions(a, b))
+ .join(', ')}`,
+ );
+ }
+ if (generatedVersions.length > 0) {
+ console.log(
+ `\n⨠${dryRun ? 'Would generate' : 'Generated'} ${
+ generatedVersions.length
+ } new version files: ${generatedVersions
+ .sort((a, b) => compareVersions(a, b))
+ .join(', ')}`,
+ );
+ }
+
+ return versionFiles;
+}
+
+/**
+ * Generate main changelog.ts that imports and spreads all version files
+ */
+async function generateMainChangelog(
+ versionFiles,
+ changelogPath,
+ dryRun = false,
+) {
+ // Get the changelogs directory path
+ const changelogsDir = path.join(path.dirname(changelogPath), 'changelogs');
+
+ // Read all version files from the changelogs directory
+ const allVersionFiles = new Map();
+
+ // First, add all files from the directory (if it exists)
+ if (fs.existsSync(changelogsDir)) {
+ const files = fs.readdirSync(changelogsDir);
+ files.forEach(file => {
+ if (file.match(/^v\d+\.\d+\.\d+\.ts$/)) {
+ const version = file.replace(/^v/, '').replace(/\.ts$/, '');
+ const varName = `changelog_${version.replace(/\./g, '_')}`;
+ allVersionFiles.set(version, {
+ version,
+ fileName: file,
+ varName,
+ });
+ }
+ });
+ }
+
+ // Then, add/update with files from this run (in case new ones were generated)
+ versionFiles.forEach(vf => {
+ allVersionFiles.set(vf.version, {
+ version: vf.version,
+ fileName: vf.fileName,
+ varName: vf.varName,
+ });
+ });
+
+ // Sort versions in descending order
+ const sortedVersions = Array.from(allVersionFiles.values()).sort((a, b) =>
+ compareVersions(b.version, a.version),
+ );
+
+ const imports = sortedVersions
+ .map(
+ v =>
+ `import { ${v.varName} } from './changelogs/${v.fileName.replace(
+ '.ts',
+ '',
+ )}';`,
+ )
+ .join('\n');
+
+ const spreads = sortedVersions.map(v => ` ...${v.varName},`).join('\n');
+
+ const content = `export * from './types';
+${imports}
+
+export const changelog = [
+${spreads}
+];
+`;
+
+ console.log(
+ `\nš Main changelog.ts will import all ${sortedVersions.length} version files`,
+ );
+
+ if (!dryRun) {
+ fs.writeFileSync(changelogPath, content, 'utf-8');
+ }
+
+ return content;
+}
+
+/**
+ * Check if an entry already exists in changelog.ts
+ */
+function isDuplicate(entry, existingContent) {
+ // Simple check: look for version + description substring
+ const descStart = entry.description.substring(0, 50);
+ const pattern = `version: '${entry.version}'`;
+
+ const versionIndex = existingContent.indexOf(pattern);
+ if (versionIndex === -1) return false;
+
+ // Check if description appears near this version
+ const contextWindow = existingContent.substring(
+ Math.max(0, versionIndex - 200),
+ versionIndex + 500,
+ );
+
+ return contextWindow.includes(descStart);
+}
+
+/**
+ * Detect GitHub authentication method
+ * Priority: gh CLI ā GITHUB_TOKEN env ā unauthenticated
+ */
+function getGitHubAuth() {
+ // 1. Try gh CLI (preferred - uses keyring auth)
+ try {
+ // Check specifically for github.com authentication
+ const output = execSync('gh auth status -h github.com 2>&1', {
+ encoding: 'utf-8',
+ });
+ if (output.includes('Logged in to github.com')) {
+ console.log('ā Using gh CLI authentication');
+ return { method: 'gh-cli' };
+ }
+ } catch {
+ // gh not installed or not authenticated to github.com
+ }
+
+ // 2. Try GITHUB_TOKEN env var
+ if (process.env.GITHUB_TOKEN) {
+ console.log('ā Using GITHUB_TOKEN authentication');
+ return { method: 'token', token: process.env.GITHUB_TOKEN };
+ }
+
+ // 3. Fallback to unauthenticated (60 req/hour limit)
+ console.warn('ā ļø Using unauthenticated GitHub API (60 requests/hour)');
+ console.warn(' For higher limits: authenticate gh CLI or set GITHUB_TOKEN');
+ return { method: 'unauthenticated' };
+}
+
+// Initialize auth and Octokit
+const githubAuth = getGitHubAuth();
+const octokit =
+ githubAuth.method === 'token'
+ ? new Octokit({ auth: githubAuth.token })
+ : new Octokit();
+
+// Cache for PR lookups
+const prCache = new Map();
+
+/**
+ * Expand a truncated SHA to full SHA using local git repository
+ * Falls back to GitHub API if git rev-parse fails
+ */
+async function expandCommitSha(shortSha) {
+ try {
+ // First try local git repository (fastest, works for all commits)
+ // We need to run this from the backstage monorepo root
+ const repoRoot = path.join(__dirname, '../../');
+
+ try {
+ const fullSha = execSync(`git rev-parse ${shortSha}`, {
+ encoding: 'utf-8',
+ cwd: repoRoot,
+ stdio: ['pipe', 'pipe', 'pipe'],
+ }).trim();
+
+ if (fullSha && fullSha.length === 40) {
+ return fullSha;
+ }
+ } catch (gitError) {
+ // Handle ambiguous SHA - parse git's error output to find commit candidates
+ if (gitError.stderr && gitError.stderr.includes('ambiguous')) {
+ const stderr = gitError.stderr.toString();
+ // Extract commit SHAs from git's hint output
+ // Format: "hint: 83c100e6accfb commit 2025-10-22 - message"
+ const commitMatch = stderr.match(/hint:\s+([a-f0-9]+)\s+commit\s+/);
+ if (commitMatch && commitMatch[1]) {
+ // Found the commit SHA from the ambiguous candidates
+ const candidateSha = commitMatch[1];
+ // Verify this is a valid commit
+ try {
+ const fullSha = execSync(`git rev-parse ${candidateSha}`, {
+ encoding: 'utf-8',
+ cwd: repoRoot,
+ }).trim();
+ if (fullSha && fullSha.length === 40) {
+ return fullSha;
+ }
+ } catch {
+ // Candidate didn't work, continue to GitHub API
+ }
+ }
+ }
+ // Commit not in local repo, try GitHub API
+ }
+ } catch (error) {
+ // Error setting up git command, try GitHub API
+ }
+
+ try {
+ // Fallback to GitHub API
+ if (githubAuth.method === 'gh-cli') {
+ const result = execSync(
+ `gh api repos/backstage/backstage/commits/${shortSha} --jq '.sha' 2>/dev/null`,
+ { encoding: 'utf-8' },
+ ).trim();
+ return result || null;
+ } else {
+ // Use Octokit
+ const { data: commit } = await octokit.rest.repos.getCommit({
+ owner: 'backstage',
+ repo: 'backstage',
+ ref: shortSha,
+ });
+ return commit.sha;
+ }
+ } catch (error) {
+ // Commit not found or other error - return null silently
+ return null;
+ }
+}
+
+/**
+ * Find PR number for a commit SHA using GitHub API
+ */
+async function findPRNumber(commitSha) {
+ // Check cache first
+ if (prCache.has(commitSha)) {
+ return prCache.get(commitSha);
+ }
+
+ try {
+ // If SHA is truncated (7 chars), expand it first
+ let fullSha = commitSha;
+ if (commitSha.length === 7) {
+ fullSha = await expandCommitSha(commitSha);
+ if (!fullSha) {
+ // Commit not found
+ console.warn(`ā ļø Commit not found: ${commitSha}`);
+ prCache.set(commitSha, null);
+ return null;
+ }
+ }
+
+ let prNumber = null;
+
+ // Use gh CLI if available (faster, uses existing auth)
+ if (githubAuth.method === 'gh-cli') {
+ try {
+ const result = execSync(
+ `gh api repos/backstage/backstage/commits/${fullSha}/pulls --jq '.[0].number' 2>/dev/null`,
+ { encoding: 'utf-8' },
+ ).trim();
+ prNumber = result || null;
+ } catch (ghError) {
+ // Handle gh CLI errors gracefully - commit exists but has no PR
+ prNumber = null;
+ }
+ } else {
+ // Otherwise use Octokit (token or unauthenticated)
+ const { data: prs } =
+ await octokit.rest.repos.listPullRequestsAssociatedWithCommit({
+ owner: 'backstage',
+ repo: 'backstage',
+ commit_sha: fullSha,
+ });
+ prNumber = prs.length > 0 ? prs[0].number.toString() : null;
+ }
+
+ // Cache the result
+ prCache.set(commitSha, prNumber);
+ return prNumber;
+ } catch (error) {
+ if (error.message?.includes('rate limit')) {
+ console.error('ā ļø GitHub API rate limit exceeded');
+ console.error(
+ ' Authenticate gh CLI or set GITHUB_TOKEN for higher limits',
+ );
+ } else {
+ console.warn(`ā ļø Error finding PR for ${commitSha}: ${error.message}`);
+ }
+ prCache.set(commitSha, null);
+ return null;
+ }
+}
+
+/**
+ * Main sync function
+ */
+async function main() {
+ const args = process.argv.slice(2);
+ const dryRun = args.includes('--dry-run');
+
+ const changelogTsPath = path.join(__dirname, '../src/utils/changelog.ts');
+ const changelogMdPath = path.join(
+ __dirname,
+ '../../packages/ui/CHANGELOG.md',
+ );
+
+ // Validate files exist
+ if (!fs.existsSync(changelogTsPath)) {
+ throw new Error(`changelog.ts not found at: ${changelogTsPath}`);
+ }
+ if (!fs.existsSync(changelogMdPath)) {
+ throw new Error(`CHANGELOG.md not found at: ${changelogMdPath}`);
+ }
+
+ console.log('š Syncing UI component changelogs...\n');
+
+ // Get last synced version
+ const lastVersion = getLastSyncedVersion(changelogTsPath);
+ console.log(`Last synced version: ${lastVersion || '(none)'}`);
+
+ // Get valid components
+ const validComponents = getValidComponents(changelogTsPath);
+ console.log(`Valid components: ${validComponents.length}`);
+
+ // Parse CHANGELOG.md
+ console.log('\nš Parsing CHANGELOG.md...');
+ const allEntries = await parseChangelogMd(
+ changelogMdPath,
+ lastVersion,
+ validComponents,
+ );
+ console.log(`Found ${allEntries.length} total entries since ${lastVersion}`);
+
+ // Read existing changelog content for duplicate detection
+ const existingContent = fs.readFileSync(changelogTsPath, 'utf-8');
+
+ // Filter to only new, non-duplicate entries
+ const relevantEntries = allEntries.filter(e => {
+ const hasComponents = e.components.length > 0 || e.components.length === 0;
+ const notDuplicate = !isDuplicate(e, existingContent);
+ return hasComponents && notDuplicate;
+ });
+
+ const duplicatesCount = allEntries.length - relevantEntries.length;
+ if (duplicatesCount > 0) {
+ console.log(`Skipped ${duplicatesCount} duplicate entries`);
+ }
+
+ console.log(
+ `Relevant entries (with or without components): ${relevantEntries.length}`,
+ );
+
+ if (relevantEntries.length === 0) {
+ console.log('\nā
No new entries to sync');
+ return;
+ }
+
+ // Fetch PR numbers for new entries (lazy fetch - only for entries that will be written)
+ await fetchPRNumbers(relevantEntries, dryRun);
+
+ // Show summary
+ console.log('\nš New entries by component:');
+ const byComponent = {};
+ relevantEntries.forEach(entry => {
+ if (entry.components.length === 0) {
+ byComponent['(general)'] = (byComponent['(general)'] || 0) + 1;
+ } else {
+ entry.components.forEach(comp => {
+ byComponent[comp] = (byComponent[comp] || 0) + 1;
+ });
+ }
+ });
+ Object.entries(byComponent).forEach(([comp, count]) => {
+ console.log(` - ${comp}: ${count} ${count === 1 ? 'entry' : 'entries'}`);
+ });
+
+ // Warn about unknown components
+ const unknownComponents = [];
+ allEntries.forEach(entry => {
+ const fullText = entry.description;
+ const componentMatch = fullText.match(
+ /Affected components?:[ \t]*([^\n]+)/i,
+ );
+ if (componentMatch) {
+ const names = componentMatch[1].split(',').map(n => n.trim());
+ names.forEach(name => {
+ if (!mapComponentName(name, validComponents)) {
+ unknownComponents.push(name);
+ }
+ });
+ }
+ });
+
+ if (unknownComponents.length > 0) {
+ console.log('\nā ļø Unknown components (skipped):');
+ [...new Set(unknownComponents)].forEach(name => {
+ console.log(` - ${name}`);
+ });
+ }
+
+ // Create changelogs directory if it doesn't exist
+ const changelogsDir = path.join(__dirname, '../src/utils/changelogs');
+ if (!fs.existsSync(changelogsDir) && !dryRun) {
+ fs.mkdirSync(changelogsDir, { recursive: true });
+ }
+
+ // Generate version files
+ console.log(
+ `\n${
+ dryRun ? 'š Dry run - would generate' : 'āļø Generating'
+ } version files...`,
+ );
+
+ const versionFiles = await generateVersionFiles(
+ relevantEntries,
+ changelogsDir,
+ dryRun,
+ );
+
+ // Generate main changelog.ts
+ const mainContent = await generateMainChangelog(
+ versionFiles,
+ changelogTsPath,
+ dryRun,
+ );
+
+ if (!dryRun) {
+ console.log('\nā
Changelog sync complete!');
+ const newFiles = versionFiles.filter(vf => !vf.skipped).length;
+ const skippedFiles = versionFiles.filter(vf => vf.skipped).length;
+ if (newFiles > 0) {
+ console.log(
+ ` Generated ${newFiles} new version file${newFiles === 1 ? '' : 's'}`,
+ );
+ }
+ if (skippedFiles > 0) {
+ console.log(
+ ` Preserved ${skippedFiles} existing version file${
+ skippedFiles === 1 ? '' : 's'
+ }`,
+ );
+ }
+ console.log(` Updated ${changelogTsPath}`);
+ } else {
+ console.log('\nš Sample version file content:\n');
+ // Find first non-skipped version file for sample
+ const sampleFile = versionFiles.find(vf => !vf.skipped && vf.content);
+ if (sampleFile) {
+ const sample = sampleFile.content.split('\n').slice(0, 30).join('\n');
+ console.log(sample);
+ console.log('...\n');
+ } else {
+ console.log('(No new version files to generate)\n');
+ }
+
+ console.log('\nš Main changelog.ts content:\n');
+ console.log(mainContent);
+
+ const newFiles = versionFiles.filter(vf => !vf.skipped).length;
+ const skippedFiles = versionFiles.filter(vf => vf.skipped).length;
+
+ console.log('\nā
Dry run complete - no files were modified');
+ if (newFiles > 0) {
+ console.log(
+ ` Run without --dry-run to generate ${newFiles} new version file${
+ newFiles === 1 ? '' : 's'
+ }`,
+ );
+ }
+ if (skippedFiles > 0) {
+ console.log(
+ ` ${skippedFiles} existing version file${
+ skippedFiles === 1 ? '' : 's'
+ } will be preserved`,
+ );
+ }
+ }
+}
+
+// Run main if executed directly
+if (import.meta.url === `file://${process.argv[1]}`) {
+ main().catch(error => {
+ console.error('ā Error:', error.message);
+ process.exit(1);
+ });
+}
diff --git a/docs-ui/src/components/Changelog/index.tsx b/docs-ui/src/components/Changelog/index.tsx
index 224e23d668..d7a547b901 100644
--- a/docs-ui/src/components/Changelog/index.tsx
+++ b/docs-ui/src/components/Changelog/index.tsx
@@ -52,14 +52,15 @@ export function Changelog() {
${bumpEntries
.map(e => {
const prs =
- e.prs.length > 0 &&
- e.prs
- .map(
- pr =>
- `[#${pr}](https://github.com/backstage/backstage/pull/${pr})`,
- )
- .join(', ');
- return `- ${e.description} ${prs}`;
+ e.prs.length > 0
+ ? e.prs
+ .map(
+ pr =>
+ `[#${pr}](https://github.com/backstage/backstage/pull/${pr})`,
+ )
+ .join(', ')
+ : '';
+ return `- ${e.description}${prs ? ` ${prs}` : ''}`;
})
.join('\n')}`;
})
diff --git a/docs-ui/src/components/ChangelogComponent/index.tsx b/docs-ui/src/components/ChangelogComponent/index.tsx
index f8832b0731..ff86433268 100644
--- a/docs-ui/src/components/ChangelogComponent/index.tsx
+++ b/docs-ui/src/components/ChangelogComponent/index.tsx
@@ -17,14 +17,17 @@ export const ChangelogComponent = ({ component }: { component: Component }) => {
${componentChangelog
?.map(change => {
const prs =
- change.prs.length > 0 &&
- change.prs
- .map(
- pr =>
- `[#${pr}](https://github.com/backstage/backstage/pull/${pr})`,
- )
- .join(', ');
- return `- \`${change.version}\` - ${change.description} ${prs}`;
+ change.prs.length > 0
+ ? change.prs
+ .map(
+ pr =>
+ `[#${pr}](https://github.com/backstage/backstage/pull/${pr})`,
+ )
+ .join(', ')
+ : '';
+ return `- \`${change.version}\` - ${change.description}${
+ prs ? ` ${prs}` : ''
+ }`;
})
.join('\n')}`}
/>
diff --git a/docs-ui/src/utils/changelog.ts b/docs-ui/src/utils/changelog.ts
index 05a06fdb9f..dafc974a51 100644
--- a/docs-ui/src/utils/changelog.ts
+++ b/docs-ui/src/utils/changelog.ts
@@ -1,538 +1,14 @@
-export type Component =
- | 'avatar'
- | 'box'
- | 'button'
- | 'button-link'
- | 'heading'
- | 'text'
- | 'button-icon'
- | 'icon'
- | 'tabs'
- | 'menu'
- | 'textfield'
- | 'datatable'
- | 'select'
- | 'collapsible'
- | 'accordion'
- | 'checkbox'
- | 'container'
- | 'link'
- | 'tooltip'
- | 'scrollarea'
- | 'flex'
- | 'switch'
- | 'grid'
- | 'searchfield'
- | 'radio-group'
- | 'card'
- | 'skeleton'
- | 'header'
- | 'passwordfield';
+export * from './types';
+import { changelog_0_6_0 } from './changelogs/v0.6.0';
+import { changelog_0_5_0 } from './changelogs/v0.5.0';
+import { changelog_0_4_0 } from './changelogs/v0.4.0';
+import { changelog_0_3_0 } from './changelogs/v0.3.0';
+import { changelog_0_2_0 } from './changelogs/v0.2.0';
-export type Version = `${number}.${number}.${number}`;
-
-export interface ChangelogProps {
- components: Component[];
- description: string;
- version: Version;
- prs: string[];
- type?: 'breaking' | 'new' | 'fix';
-}
-
-export const changelog: ChangelogProps[] = [
- {
- components: ['tooltip'],
- version: '0.6.0',
- description: 'New `Tooltip` component.',
- prs: ['30461'],
- type: 'new',
- },
- {
- components: ['card'],
- version: '0.6.0',
- description: 'New `Card` component.',
- prs: ['30467'],
- type: 'new',
- },
- {
- components: ['header'],
- version: '0.6.0',
- description: 'New `Header` component.',
- prs: ['30410', '30459', '30493'],
- type: 'new',
- },
- {
- components: ['button', 'button-link', 'button-icon'],
- version: '0.6.0',
- description: 'Improve `Button`, `ButtonIcon` and `ButtonLink` styling.',
- prs: ['30448'],
- type: 'fix',
- },
- {
- components: ['text', 'heading'],
- version: '0.6.0',
- description:
- 'Update return types for `Heading` & `Text` components for React 19.',
- prs: ['30440'],
- type: 'fix',
- },
- {
- components: ['button', 'button-link', 'button-icon'],
- version: '0.6.0',
- description:
- 'New `tertiary` variant to `Button`, `ButtonIcon` and `ButtonLink`.',
- prs: ['30453'],
- type: 'new',
- },
- {
- components: ['skeleton'],
- version: '0.6.0',
- description: 'New `Skeleton` component.',
- prs: ['30466'],
- type: 'new',
- },
- {
- components: ['button-icon'],
- version: '0.6.0',
- description: 'Rename `IconButton` to `ButtonIcon`.',
- prs: ['30297'],
- type: 'breaking',
- },
- {
- components: ['button-link'],
- version: '0.6.0',
- description:
- 'New `ButtonLink`, which replaces the previous render prop pattern on `Button` and `IconButton`.',
- prs: ['30297'],
- type: 'new',
- },
- {
- components: ['button', 'button-link', 'button-icon'],
- version: '0.6.0',
- description: 'Remove the `render` prop from all button-related components.',
- prs: ['30297'],
- type: 'breaking',
- },
- {
- components: [],
- version: '0.6.0',
- description: 'We are consolidating all css files into a single styles.css.',
- prs: ['30325'],
- type: 'fix',
- },
- {
- components: ['searchfield'],
- version: '0.6.0',
- description: 'New `SearchField` component.',
- prs: ['30357'],
- type: 'new',
- },
- {
- components: ['radio-group'],
- version: '0.6.0',
- description: 'New `RadioGroup` + `Radio` component.',
- prs: ['30327'],
- type: 'new',
- },
- {
- components: ['textfield'],
- version: '0.6.0',
- description: 'Added placeholder prop back to `TextField` component.',
- prs: ['30286'],
- type: 'fix',
- },
- {
- components: ['textfield'],
- version: '0.5.0',
- description: '`TextField` now has multiple label sizes.',
- prs: ['30249'],
- type: 'breaking',
- },
- {
- components: ['textfield'],
- version: '0.5.0',
- description:
- '`TextField` can now hide label and description while keeping them available for screen readers.',
- prs: ['30249'],
- type: 'breaking',
- },
- {
- components: ['textfield'],
- version: '0.5.0',
- description:
- 'We are removing the `render` prop in favour of the `as` prop on `Heading` and `Text`.',
- prs: ['30291'],
- type: 'breaking',
- },
- {
- components: ['textfield'],
- version: '0.5.0',
- description: 'Move `TextField` component to use react Aria under the hood.',
- prs: ['30286'],
- type: 'breaking',
- },
- {
- components: ['button', 'button-link'],
- version: '0.5.0',
- description:
- 'Added a render prop to the `Button` component to use it as a link.',
- prs: ['30165'],
- type: 'new',
- },
- {
- components: ['heading'],
- version: '0.5.0',
- description: 'Fix styling for the title4 prop on the Heading component.',
- prs: ['30167'],
- type: 'fix',
- },
- {
- components: ['flex'],
- version: '0.5.0',
- description:
- 'Add `min-width: 0;` by default on Flex components to support truncated text.',
- prs: ['30168'],
- type: 'fix',
- },
- {
- components: ['button', 'button-link', 'button-icon'],
- version: '0.5.0',
- description:
- '`Button`, `ButtonLink`, `ButtonIcon` now default to size `small` instead of `medium`',
- prs: ['30085', '30097'],
- type: 'breaking',
- },
- {
- components: ['grid'],
- version: '0.5.0',
- description: 'Rename Grid component to ``',
- prs: ['30013'],
- type: 'breaking',
- },
- {
- components: ['flex', 'container', 'grid', 'box'],
- version: '0.5.0',
- description: 'Fixes spacing props on layout components',
- prs: ['30013'],
- type: 'fix',
- },
- {
- components: ['switch'],
- version: '0.5.0',
- description: 'New `Switch` component',
- prs: ['30251'],
- type: 'new',
- },
- {
- components: ['tabs'],
- version: '0.4.0',
- description: 'New `Tabs` component',
- prs: ['29996'],
- type: 'new',
- },
- {
- components: ['text', 'heading'],
- version: '0.4.0',
- description: 'Add `truncate` prop to `Text` and `Heading`',
- prs: ['29988'],
- type: 'new',
- },
- {
- components: ['menu'],
- version: '0.4.0',
- description: 'Add combobox option to `Menu`',
- prs: ['29986'],
- type: 'new',
- },
- {
- components: ['textfield'],
- version: '0.4.0',
- description: 'Add icon prop on `TextField`',
- prs: ['29820'],
- type: 'new',
- },
- {
- components: ['button', 'button-icon'],
- version: '0.4.0',
- description: 'Improve icon props on `Button` and `IconButton`',
- prs: ['29667'],
- },
- {
- components: ['text', 'heading'],
- version: '0.4.0',
- description:
- 'Improve the way we treat custom render on `Text` and `Heading`',
- prs: ['29989'],
- },
- {
- components: ['menu'],
- version: '0.4.0',
- description: 'Improve `Menu` styles',
- prs: ['29986'],
- },
- {
- components: ['textfield'],
- version: '0.4.0',
- description: 'Improve `TextField` styles',
- prs: ['29974'],
- },
- {
- components: ['textfield'],
- version: '0.4.0',
- description: 'Improve clear button on `TextField`',
- prs: ['29878'],
- },
- {
- components: ['flex'],
- version: '0.4.0',
- description: 'Fix spacing props on all layout components',
- prs: ['30013'],
- },
- {
- components: [],
- version: '0.4.0',
- description: 'Fix - Pin Base UI version',
- prs: ['29782'],
- },
- {
- components: ['select'],
- version: '0.4.0',
- description: 'Fix - Clicking `Select` label moves focus to trigger',
- prs: ['29755'],
- },
- {
- components: ['datatable'],
- version: '0.4.0',
- description: 'Fix `DataTable.Pagination` count issue',
- prs: ['29688'],
- },
- {
- components: ['datatable'],
- version: '0.3.0',
- description: 'Add `DataTable` component',
- prs: ['29484', '29603'],
- type: 'new',
- },
- {
- components: ['select'],
- version: '0.3.0',
- description: 'Add `Select` component',
- prs: ['29440'],
- type: 'new',
- },
- {
- components: ['avatar'],
- version: '0.3.0',
- description: 'Add `Avatar` component',
- prs: ['29594'],
- type: 'new',
- },
- {
- components: ['collapsible'],
- version: '0.3.0',
- description: 'Add `Collapsible` component',
- prs: ['29617'],
- type: 'new',
- },
- {
- components: ['textfield'],
- version: '0.3.0',
- description: 'Add `TextField` component instead of `Field` + `Input`',
- prs: ['29364'],
- type: 'new',
- },
- {
- components: ['datatable'],
- version: '0.3.0',
- description: 'Add `TableCellProfile`',
- prs: ['29600'],
- type: 'new',
- },
- {
- components: [],
- version: '0.3.0',
- description: 'Add breakpoint hooks - `up()` and `down()`',
- prs: ['29564'],
- type: 'new',
- },
- {
- components: [],
- version: '0.3.0',
- description: 'Add gray scale css tokens',
- prs: ['29543'],
- type: 'new',
- },
- {
- components: [],
- version: '0.3.0',
- description:
- 'Update CSS styling API using `[data-___]` instead of class names for props',
- prs: ['29560'],
- },
- {
- components: ['checkbox'],
- version: '0.3.0',
- description: 'Update `Checkbox` dark mode',
- prs: ['29544'],
- },
- {
- components: ['container'],
- version: '0.3.0',
- description: 'Update `Container` styles',
- prs: ['29475'],
- },
- {
- components: ['menu'],
- version: '0.3.0',
- description: 'Update `Menu` styles',
- prs: ['29351'],
- },
- {
- components: ['select'],
- version: '0.3.0',
- description: 'Fix `Select` styles on small sizes + with long option names',
- prs: ['29545'],
- },
- {
- components: ['link'],
- version: '0.3.0',
- description: 'Fix render prop on `Link`',
- prs: ['29247'],
- },
- {
- components: ['textfield', 'select'],
- version: '0.3.0',
- description: 'Remove `Field` from `TextField` + `Select`',
- prs: ['29482'],
- },
- {
- components: ['text', 'heading'],
- version: '0.3.0',
- description: 'Update `textDecoration` to `none` on `Text` / `Heading`',
- prs: ['29357'],
- },
- {
- components: ['text', 'heading'],
- version: '0.3.0',
- description: 'Update `textDecoration` to `none` on `Text` / `Heading`',
- prs: ['29357'],
- },
- {
- components: [],
- version: '0.3.0',
- description: 'Docs - Use stories from Storybook for all examples in Nextjs',
- prs: ['29306'],
- },
- {
- components: [],
- version: '0.3.0',
- description: 'Docs - Add release page (this one š¤)',
- prs: ['29461'],
- },
- {
- components: [],
- version: '0.3.0',
- description: 'Docs - Add docs for Menu, Link',
- prs: ['29576'],
- },
- {
- components: [],
- version: '0.3.0',
- description: 'Fix CSS watch mode',
- prs: ['29352'],
- },
- {
- components: ['menu'],
- version: '0.2.0',
- description: 'New `Menu` component',
- prs: ['29151'],
- type: 'new',
- },
- {
- components: ['button-icon'],
- version: '0.2.0',
- description: 'New `IconButton` component',
- prs: ['29239'],
- type: 'new',
- },
- {
- components: ['scrollarea'],
- version: '0.2.0',
- description: 'New `ScrollArea` component',
- prs: ['29240'],
- type: 'new',
- },
- {
- components: ['button', 'checkbox'],
- version: '0.2.0',
- description: 'Improve `Button` & `Checkbox` styles',
- prs: ['29127', '28789'],
- },
- {
- components: ['text'],
- version: '0.2.0',
- description: 'Improve `Text` styles',
- prs: ['29200'],
- },
- {
- components: ['icon'],
- version: '0.2.0',
- description: 'Renamed `CanonProvider` to `IconProvider`',
- prs: ['29002'],
- },
- {
- components: ['icon'],
- version: '0.2.0',
- description: 'Added about 40+ new icons',
- prs: ['29264'],
- },
- {
- components: [],
- version: '0.2.0',
- description: 'Simplified styling into a unique styles.css file',
- prs: ['29199'],
- },
- {
- components: [],
- version: '0.2.0',
- description: 'Added global styles to Backstage',
- prs: ['29137'],
- type: 'new',
- },
- {
- components: [],
- version: '0.2.0',
- description: 'Update global CSS tokens',
- prs: ['28804'],
- },
- {
- components: ['flex'],
- version: '0.2.0',
- description: 'Merge Stack + Inline into Flex',
- prs: ['28634'],
- },
- {
- components: ['button'],
- version: '0.2.0',
- description: 'Improve `Button` types',
- prs: ['29205'],
- },
- {
- components: [],
- version: '0.2.0',
- description: 'Move font weight and family back to each components',
- prs: ['28972'],
- },
- {
- components: [],
- version: '0.2.0',
- description: 'Fix custom values in spacing props',
- prs: ['28770'],
- },
- {
- components: [],
- version: '0.2.0',
- description: 'Multiple updates on the docs site',
- prs: ['28760'],
- },
+export const changelog = [
+ ...changelog_0_6_0,
+ ...changelog_0_5_0,
+ ...changelog_0_4_0,
+ ...changelog_0_3_0,
+ ...changelog_0_2_0,
];
diff --git a/docs-ui/src/utils/changelogs/v0.2.0.ts b/docs-ui/src/utils/changelogs/v0.2.0.ts
new file mode 100644
index 0000000000..b90cec0309
--- /dev/null
+++ b/docs-ui/src/utils/changelogs/v0.2.0.ts
@@ -0,0 +1,98 @@
+import type { ChangelogProps } from '../types';
+
+export const changelog_0_2_0: ChangelogProps[] = [
+ {
+ components: ['menu'],
+ version: '0.2.0',
+ description: 'New `Menu` component',
+ prs: ['29151'],
+ type: 'new',
+ },
+ {
+ components: ['button-icon'],
+ version: '0.2.0',
+ description: 'New `IconButton` component',
+ prs: ['29239'],
+ type: 'new',
+ },
+ {
+ components: ['scrollarea'],
+ version: '0.2.0',
+ description: 'New `ScrollArea` component',
+ prs: ['29240'],
+ type: 'new',
+ },
+ {
+ components: ['button', 'checkbox'],
+ version: '0.2.0',
+ description: 'Improve `Button` & `Checkbox` styles',
+ prs: ['29127', '28789'],
+ },
+ {
+ components: ['text'],
+ version: '0.2.0',
+ description: 'Improve `Text` styles',
+ prs: ['29200'],
+ },
+ {
+ components: ['icon'],
+ version: '0.2.0',
+ description: 'Renamed `CanonProvider` to `IconProvider`',
+ prs: ['29002'],
+ },
+ {
+ components: ['icon'],
+ version: '0.2.0',
+ description: 'Added about 40+ new icons',
+ prs: ['29264'],
+ },
+ {
+ components: [],
+ version: '0.2.0',
+ description: 'Simplified styling into a unique styles.css file',
+ prs: ['29199'],
+ },
+ {
+ components: [],
+ version: '0.2.0',
+ description: 'Added global styles to Backstage',
+ prs: ['29137'],
+ type: 'new',
+ },
+ {
+ components: [],
+ version: '0.2.0',
+ description: 'Update global CSS tokens',
+ prs: ['28804'],
+ },
+ {
+ components: ['flex'],
+ version: '0.2.0',
+ description: 'Merge Stack + Inline into Flex',
+ prs: ['28634'],
+ },
+ {
+ components: ['button'],
+ version: '0.2.0',
+ description: 'Improve `Button` types',
+ prs: ['29205'],
+ },
+ {
+ components: [],
+ version: '0.2.0',
+ description: 'Move font weight and family back to each components',
+ prs: ['28972'],
+ },
+ {
+ components: [],
+ version: '0.2.0',
+ description: 'Fix custom values in spacing props',
+ prs: ['28770'],
+ },
+ {
+ components: [],
+ version: '0.2.0',
+ description: 'Multiple updates on the docs site',
+ prs: ['28760'],
+ },
+];
diff --git a/docs-ui/src/utils/changelogs/v0.3.0.ts b/docs-ui/src/utils/changelogs/v0.3.0.ts
new file mode 100644
index 0000000000..750ad292cf
--- /dev/null
+++ b/docs-ui/src/utils/changelogs/v0.3.0.ts
@@ -0,0 +1,139 @@
+import type { ChangelogProps } from '../types';
+
+export const changelog_0_3_0: ChangelogProps[] = [
+ {
+ components: ['datatable'],
+ version: '0.3.0',
+ description: 'Add `DataTable` component',
+ prs: ['29484', '29603'],
+ type: 'new',
+ },
+ {
+ components: ['select'],
+ version: '0.3.0',
+ description: 'Add `Select` component',
+ prs: ['29440'],
+ type: 'new',
+ },
+ {
+ components: ['avatar'],
+ version: '0.3.0',
+ description: 'Add `Avatar` component',
+ prs: ['29594'],
+ type: 'new',
+ },
+ {
+ components: ['collapsible'],
+ version: '0.3.0',
+ description: 'Add `Collapsible` component',
+ prs: ['29617'],
+ type: 'new',
+ },
+ {
+ components: ['textfield'],
+ version: '0.3.0',
+ description: 'Add `TextField` component instead of `Field` + `Input`',
+ prs: ['29364'],
+ type: 'new',
+ },
+ {
+ components: ['datatable'],
+ version: '0.3.0',
+ description: 'Add `TableCellProfile`',
+ prs: ['29600'],
+ type: 'new',
+ },
+ {
+ components: [],
+ version: '0.3.0',
+ description: 'Add breakpoint hooks - `up()` and `down()`',
+ prs: ['29564'],
+ type: 'new',
+ },
+ {
+ components: [],
+ version: '0.3.0',
+ description: 'Add gray scale css tokens',
+ prs: ['29543'],
+ type: 'new',
+ },
+ {
+ components: [],
+ version: '0.3.0',
+ description:
+ 'Update CSS styling API using `[data-___]` instead of class names for props',
+ prs: ['29560'],
+ },
+ {
+ components: ['checkbox'],
+ version: '0.3.0',
+ description: 'Update `Checkbox` dark mode',
+ prs: ['29544'],
+ },
+ {
+ components: ['container'],
+ version: '0.3.0',
+ description: 'Update `Container` styles',
+ prs: ['29475'],
+ },
+ {
+ components: ['menu'],
+ version: '0.3.0',
+ description: 'Update `Menu` styles',
+ prs: ['29351'],
+ },
+ {
+ components: ['select'],
+ version: '0.3.0',
+ description: 'Fix `Select` styles on small sizes + with long option names',
+ prs: ['29545'],
+ },
+ {
+ components: ['link'],
+ version: '0.3.0',
+ description: 'Fix render prop on `Link`',
+ prs: ['29247'],
+ },
+ {
+ components: ['textfield', 'select'],
+ version: '0.3.0',
+ description: 'Remove `Field` from `TextField` + `Select`',
+ prs: ['29482'],
+ },
+ {
+ components: ['text', 'heading'],
+ version: '0.3.0',
+ description: 'Update `textDecoration` to `none` on `Text` / `Heading`',
+ prs: ['29357'],
+ },
+ {
+ components: ['text', 'heading'],
+ version: '0.3.0',
+ description: 'Update `textDecoration` to `none` on `Text` / `Heading`',
+ prs: ['29357'],
+ },
+ {
+ components: [],
+ version: '0.3.0',
+ description: 'Docs - Use stories from Storybook for all examples in Nextjs',
+ prs: ['29306'],
+ },
+ {
+ components: [],
+ version: '0.3.0',
+ description: 'Docs - Add release page (this one š¤)',
+ prs: ['29461'],
+ },
+ {
+ components: [],
+ version: '0.3.0',
+ description: 'Docs - Add docs for Menu, Link',
+ prs: ['29576'],
+ },
+ {
+ components: [],
+ version: '0.3.0',
+ description: 'Fix CSS watch mode',
+ prs: ['29352'],
+ },
+];
diff --git a/docs-ui/src/utils/changelogs/v0.4.0.ts b/docs-ui/src/utils/changelogs/v0.4.0.ts
new file mode 100644
index 0000000000..91cd641f4d
--- /dev/null
+++ b/docs-ui/src/utils/changelogs/v0.4.0.ts
@@ -0,0 +1,87 @@
+import type { ChangelogProps } from '../types';
+
+export const changelog_0_4_0: ChangelogProps[] = [
+ {
+ components: ['tabs'],
+ version: '0.4.0',
+ description: 'New `Tabs` component',
+ prs: ['29996'],
+ type: 'new',
+ },
+ {
+ components: ['text', 'heading'],
+ version: '0.4.0',
+ description: 'Add `truncate` prop to `Text` and `Heading`',
+ prs: ['29988'],
+ type: 'new',
+ },
+ {
+ components: ['menu'],
+ version: '0.4.0',
+ description: 'Add combobox option to `Menu`',
+ prs: ['29986'],
+ type: 'new',
+ },
+ {
+ components: ['textfield'],
+ version: '0.4.0',
+ description: 'Add icon prop on `TextField`',
+ prs: ['29820'],
+ type: 'new',
+ },
+ {
+ components: ['button', 'button-icon'],
+ version: '0.4.0',
+ description: 'Improve icon props on `Button` and `IconButton`',
+ prs: ['29667'],
+ },
+ {
+ components: ['text', 'heading'],
+ version: '0.4.0',
+ description:
+ 'Improve the way we treat custom render on `Text` and `Heading`',
+ prs: ['29989'],
+ },
+ {
+ components: ['menu'],
+ version: '0.4.0',
+ description: 'Improve `Menu` styles',
+ prs: ['29986'],
+ },
+ {
+ components: ['textfield'],
+ version: '0.4.0',
+ description: 'Improve `TextField` styles',
+ prs: ['29974'],
+ },
+ {
+ components: ['textfield'],
+ version: '0.4.0',
+ description: 'Improve clear button on `TextField`',
+ prs: ['29878'],
+ },
+ {
+ components: ['flex'],
+ version: '0.4.0',
+ description: 'Fix spacing props on all layout components',
+ prs: ['30013'],
+ },
+ {
+ components: [],
+ version: '0.4.0',
+ description: 'Fix - Pin Base UI version',
+ prs: ['29782'],
+ },
+ {
+ components: ['select'],
+ version: '0.4.0',
+ description: 'Fix - Clicking `Select` label moves focus to trigger',
+ prs: ['29755'],
+ },
+ {
+ components: ['datatable'],
+ version: '0.4.0',
+ description: 'Fix `DataTable.Pagination` count issue',
+ prs: ['29688'],
+ },
+];
diff --git a/docs-ui/src/utils/changelogs/v0.5.0.ts b/docs-ui/src/utils/changelogs/v0.5.0.ts
new file mode 100644
index 0000000000..a48cb96dcc
--- /dev/null
+++ b/docs-ui/src/utils/changelogs/v0.5.0.ts
@@ -0,0 +1,86 @@
+import type { ChangelogProps } from '../types';
+
+export const changelog_0_5_0: ChangelogProps[] = [
+ {
+ components: ['textfield'],
+ version: '0.5.0',
+ description: '`TextField` now has multiple label sizes.',
+ prs: ['30249'],
+ type: 'breaking',
+ },
+ {
+ components: ['textfield'],
+ version: '0.5.0',
+ description:
+ '`TextField` can now hide label and description while keeping them available for screen readers.',
+ prs: ['30249'],
+ type: 'breaking',
+ },
+ {
+ components: ['textfield'],
+ version: '0.5.0',
+ description:
+ 'We are removing the `render` prop in favour of the `as` prop on `Heading` and `Text`.',
+ prs: ['30291'],
+ type: 'breaking',
+ },
+ {
+ components: ['textfield'],
+ version: '0.5.0',
+ description: 'Move `TextField` component to use react Aria under the hood.',
+ prs: ['30286'],
+ type: 'breaking',
+ },
+ {
+ components: ['button', 'button-link'],
+ version: '0.5.0',
+ description:
+ 'Added a render prop to the `Button` component to use it as a link.',
+ prs: ['30165'],
+ type: 'new',
+ },
+ {
+ components: ['heading'],
+ version: '0.5.0',
+ description: 'Fix styling for the title4 prop on the Heading component.',
+ prs: ['30167'],
+ type: 'fix',
+ },
+ {
+ components: ['flex'],
+ version: '0.5.0',
+ description:
+ 'Add `min-width: 0;` by default on Flex components to support truncated text.',
+ prs: ['30168'],
+ type: 'fix',
+ },
+ {
+ components: ['button', 'button-link', 'button-icon'],
+ version: '0.5.0',
+ description:
+ '`Button`, `ButtonLink`, `ButtonIcon` now default to size `small` instead of `medium`',
+ prs: ['30085', '30097'],
+ type: 'breaking',
+ },
+ {
+ components: ['grid'],
+ version: '0.5.0',
+ description: 'Rename Grid component to ``',
+ prs: ['30013'],
+ type: 'breaking',
+ },
+ {
+ components: ['flex', 'container', 'grid', 'box'],
+ version: '0.5.0',
+ description: 'Fixes spacing props on layout components',
+ prs: ['30013'],
+ type: 'fix',
+ },
+ {
+ components: ['switch'],
+ version: '0.5.0',
+ description: 'New `Switch` component',
+ prs: ['30251'],
+ type: 'new',
+ },
+];
diff --git a/docs-ui/src/utils/changelogs/v0.6.0.ts b/docs-ui/src/utils/changelogs/v0.6.0.ts
new file mode 100644
index 0000000000..d892f2a78d
--- /dev/null
+++ b/docs-ui/src/utils/changelogs/v0.6.0.ts
@@ -0,0 +1,105 @@
+import type { ChangelogProps } from '../types';
+
+export const changelog_0_6_0: ChangelogProps[] = [
+ {
+ components: ['tooltip'],
+ version: '0.6.0',
+ description: 'New `Tooltip` component.',
+ prs: ['30461'],
+ type: 'new',
+ },
+ {
+ components: ['card'],
+ version: '0.6.0',
+ description: 'New `Card` component.',
+ prs: ['30467'],
+ type: 'new',
+ },
+ {
+ components: ['header'],
+ version: '0.6.0',
+ description: 'New `Header` component.',
+ prs: ['30410', '30459', '30493'],
+ type: 'new',
+ },
+ {
+ components: ['button', 'button-link', 'button-icon'],
+ version: '0.6.0',
+ description: 'Improve `Button`, `ButtonIcon` and `ButtonLink` styling.',
+ prs: ['30448'],
+ type: 'fix',
+ },
+ {
+ components: ['text', 'heading'],
+ version: '0.6.0',
+ description:
+ 'Update return types for `Heading` & `Text` components for React 19.',
+ prs: ['30440'],
+ type: 'fix',
+ },
+ {
+ components: ['button', 'button-link', 'button-icon'],
+ version: '0.6.0',
+ description:
+ 'New `tertiary` variant to `Button`, `ButtonIcon` and `ButtonLink`.',
+ prs: ['30453'],
+ type: 'new',
+ },
+ {
+ components: ['skeleton'],
+ version: '0.6.0',
+ description: 'New `Skeleton` component.',
+ prs: ['30466'],
+ type: 'new',
+ },
+ {
+ components: ['button-icon'],
+ version: '0.6.0',
+ description: 'Rename `IconButton` to `ButtonIcon`.',
+ prs: ['30297'],
+ type: 'breaking',
+ },
+ {
+ components: ['button-link'],
+ version: '0.6.0',
+ description:
+ 'New `ButtonLink`, which replaces the previous render prop pattern on `Button` and `IconButton`.',
+ prs: ['30297'],
+ type: 'new',
+ },
+ {
+ components: ['button', 'button-link', 'button-icon'],
+ version: '0.6.0',
+ description: 'Remove the `render` prop from all button-related components.',
+ prs: ['30297'],
+ type: 'breaking',
+ },
+ {
+ components: [],
+ version: '0.6.0',
+ description: 'We are consolidating all css files into a single styles.css.',
+ prs: ['30325'],
+ type: 'fix',
+ },
+ {
+ components: ['searchfield'],
+ version: '0.6.0',
+ description: 'New `SearchField` component.',
+ prs: ['30357'],
+ type: 'new',
+ },
+ {
+ components: ['radio-group'],
+ version: '0.6.0',
+ description: 'New `RadioGroup` + `Radio` component.',
+ prs: ['30327'],
+ type: 'new',
+ },
+ {
+ components: ['textfield'],
+ version: '0.6.0',
+ description: 'Added placeholder prop back to `TextField` component.',
+ prs: ['30286'],
+ type: 'fix',
+ },
+];
diff --git a/docs-ui/src/utils/types.ts b/docs-ui/src/utils/types.ts
new file mode 100644
index 0000000000..f2f773ebc2
--- /dev/null
+++ b/docs-ui/src/utils/types.ts
@@ -0,0 +1,46 @@
+export type Component =
+ | 'avatar'
+ | 'box'
+ | 'button'
+ | 'button-link'
+ | 'heading'
+ | 'text'
+ | 'button-icon'
+ | 'icon'
+ | 'tabs'
+ | 'menu'
+ | 'textfield'
+ | 'datatable'
+ | 'select'
+ | 'collapsible'
+ | 'accordion'
+ | 'checkbox'
+ | 'container'
+ | 'link'
+ | 'tooltip'
+ | 'scrollarea'
+ | 'flex'
+ | 'switch'
+ | 'grid'
+ | 'searchfield'
+ | 'radio-group'
+ | 'card'
+ | 'skeleton'
+ | 'header'
+ | 'header-page'
+ | 'password-field'
+ | 'table'
+ | 'visually-hidden'
+ | 'dialog'
+ | 'tag-group';
+
+export type Version = `${number}.${number}.${number}`;
+
+export interface ChangelogProps {
+ components: Component[];
+ description: string;
+ version: Version;
+ prs: string[];
+ type?: 'breaking' | 'new' | 'fix';
+ commitSha?: string;
+}
diff --git a/docs-ui/yarn.lock b/docs-ui/yarn.lock
index 480f7592f8..da7cc2b620 100644
--- a/docs-ui/yarn.lock
+++ b/docs-ui/yarn.lock
@@ -999,6 +999,130 @@ __metadata:
languageName: node
linkType: hard
+"@octokit/auth-token@npm:^6.0.0":
+ version: 6.0.0
+ resolution: "@octokit/auth-token@npm:6.0.0"
+ checksum: 10/a30f5c4c984964b57193de5b6f67169f74e4779fedbe716157dd3558dd9de3ca5c105cae521b7bd8ce1ae180773a2ef01afe2306ad5a329f4fd291eba2b7c7d1
+ languageName: node
+ linkType: hard
+
+"@octokit/core@npm:^7.0.6":
+ version: 7.0.6
+ resolution: "@octokit/core@npm:7.0.6"
+ dependencies:
+ "@octokit/auth-token": "npm:^6.0.0"
+ "@octokit/graphql": "npm:^9.0.3"
+ "@octokit/request": "npm:^10.0.6"
+ "@octokit/request-error": "npm:^7.0.2"
+ "@octokit/types": "npm:^16.0.0"
+ before-after-hook: "npm:^4.0.0"
+ universal-user-agent: "npm:^7.0.0"
+ checksum: 10/852d41fc3150d2a891156427dd0575c77889f1c7a109894ee541594e3fd47c0d4e0a93fee22966c507dfd6158b522e42846c2ac46b9d896078194c95fa81f4ae
+ languageName: node
+ linkType: hard
+
+"@octokit/endpoint@npm:^11.0.2":
+ version: 11.0.2
+ resolution: "@octokit/endpoint@npm:11.0.2"
+ dependencies:
+ "@octokit/types": "npm:^16.0.0"
+ universal-user-agent: "npm:^7.0.2"
+ checksum: 10/0d088747baf94eafbba69da23ba840b40cd3f5d0bfbc51c692ff9d9d78de6d81f06366e6e30df8c1783355be826c27d38ab9ab0708396af8f430b06cfa29db35
+ languageName: node
+ linkType: hard
+
+"@octokit/graphql@npm:^9.0.3":
+ version: 9.0.3
+ resolution: "@octokit/graphql@npm:9.0.3"
+ dependencies:
+ "@octokit/request": "npm:^10.0.6"
+ "@octokit/types": "npm:^16.0.0"
+ universal-user-agent: "npm:^7.0.0"
+ checksum: 10/7b16f281f8571dce55280b3986fbb8d15465a7236164a5f6497ded7597ff9ee95d5796924555b979903fe8c6706fe6be1b3e140d807297f85ac8edeadc28f9fe
+ languageName: node
+ linkType: hard
+
+"@octokit/openapi-types@npm:^27.0.0":
+ version: 27.0.0
+ resolution: "@octokit/openapi-types@npm:27.0.0"
+ checksum: 10/5cd2cdf4e41fdf522e15e3d53f3ece8380d98dda9173a6fc905828fb2c33e8733d5f5d2a757ae3a572525f4749748e66cb40e7939372132988d8eb4ba978d54f
+ languageName: node
+ linkType: hard
+
+"@octokit/plugin-paginate-rest@npm:^14.0.0":
+ version: 14.0.0
+ resolution: "@octokit/plugin-paginate-rest@npm:14.0.0"
+ dependencies:
+ "@octokit/types": "npm:^16.0.0"
+ peerDependencies:
+ "@octokit/core": ">=6"
+ checksum: 10/57ddd857528dad9c02431bc6254c2374c06057872cf9656a4a88b162ebe1c2bc9f34fbec360f2ccff72c940f29b120758ce14e8135bd027223d381eb1b8b6579
+ languageName: node
+ linkType: hard
+
+"@octokit/plugin-request-log@npm:^6.0.0":
+ version: 6.0.0
+ resolution: "@octokit/plugin-request-log@npm:6.0.0"
+ peerDependencies:
+ "@octokit/core": ">=6"
+ checksum: 10/8a79973b1429bfead9113c4117f418aaef5ff368795daded3415ba14623d97d5fc08d1e822dbd566ecc9f041119e1a48a11853a9c48d9eb1caa62baa79c17f83
+ languageName: node
+ linkType: hard
+
+"@octokit/plugin-rest-endpoint-methods@npm:^17.0.0":
+ version: 17.0.0
+ resolution: "@octokit/plugin-rest-endpoint-methods@npm:17.0.0"
+ dependencies:
+ "@octokit/types": "npm:^16.0.0"
+ peerDependencies:
+ "@octokit/core": ">=6"
+ checksum: 10/e9d9ad4d9755cc7fb82fdcbfa870ddea8a432180f0f76c8469095557fd1e26f8caea8cae58401209be17c4f3d8cc48c0e16a3643e37e48f4d23c39e058bf2c55
+ languageName: node
+ linkType: hard
+
+"@octokit/request-error@npm:^7.0.2":
+ version: 7.0.2
+ resolution: "@octokit/request-error@npm:7.0.2"
+ dependencies:
+ "@octokit/types": "npm:^16.0.0"
+ checksum: 10/8edfaca9f5271115b090f470ebfe1006841ee152dd52cb39786e026236e2c13545aa3cf3187b7b1de717167696a42f91d8fa2f870bf9be0832fb2b11d11a741d
+ languageName: node
+ linkType: hard
+
+"@octokit/request@npm:^10.0.6":
+ version: 10.0.6
+ resolution: "@octokit/request@npm:10.0.6"
+ dependencies:
+ "@octokit/endpoint": "npm:^11.0.2"
+ "@octokit/request-error": "npm:^7.0.2"
+ "@octokit/types": "npm:^16.0.0"
+ fast-content-type-parse: "npm:^3.0.0"
+ universal-user-agent: "npm:^7.0.2"
+ checksum: 10/b8c5cd43d3225c8c3b803e889ff251808ae180c953ca92642522843123dafcd84d919b20ca2969b6e4080ee3c2197a99afda15b5649ee51d0715f8249a5c9ca4
+ languageName: node
+ linkType: hard
+
+"@octokit/rest@npm:^22.0.1":
+ version: 22.0.1
+ resolution: "@octokit/rest@npm:22.0.1"
+ dependencies:
+ "@octokit/core": "npm:^7.0.6"
+ "@octokit/plugin-paginate-rest": "npm:^14.0.0"
+ "@octokit/plugin-request-log": "npm:^6.0.0"
+ "@octokit/plugin-rest-endpoint-methods": "npm:^17.0.0"
+ checksum: 10/ec2e94cfa8766716faeb3ca18527d9af746482d35aaa6e4265a30cb669ae3f31f4ebb6235edebe5ae62bc2cec2b8e88902584f698df2e7cabac3a15fd27da665
+ languageName: node
+ linkType: hard
+
+"@octokit/types@npm:^16.0.0":
+ version: 16.0.0
+ resolution: "@octokit/types@npm:16.0.0"
+ dependencies:
+ "@octokit/openapi-types": "npm:^27.0.0"
+ checksum: 10/03d5cfc29556a9b53eae8beb1bf15c0b704dc722db2c51b53f093f3c3ee6c1d8e20b682be8117a3a17034b458be7746d1b22aaefb959ceb5152ad7589b39e2c9
+ languageName: node
+ linkType: hard
+
"@pkgjs/parseargs@npm:^0.11.0":
version: 0.11.0
resolution: "@pkgjs/parseargs@npm:0.11.0"
@@ -1946,6 +2070,13 @@ __metadata:
languageName: node
linkType: hard
+"before-after-hook@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "before-after-hook@npm:4.0.0"
+ checksum: 10/9fd52bc0c3cca0fb115e04dacbeeaacff38fa23e1af725d62392254c31ef433b15da60efcba61552e44d64e26f25ea259f72dba005115924389e88d2fd56e19f
+ languageName: node
+ linkType: hard
+
"better-opn@npm:^3.0.2":
version: 3.0.2
resolution: "better-opn@npm:3.0.2"
@@ -2385,6 +2516,7 @@ __metadata:
"@mdx-js/loader": "npm:^3.1.0"
"@mdx-js/react": "npm:^3.1.0"
"@next/mdx": "npm:15.3.4"
+ "@octokit/rest": "npm:^22.0.1"
"@remixicon/react": "npm:^4.6.0"
"@shikijs/transformers": "npm:^3.13.0"
"@storybook/react": "npm:^8.6.12"
@@ -2409,6 +2541,7 @@ __metadata:
shiki: "npm:^3.13.0"
storybook: "npm:^8.6.12"
typescript: "npm:^5"
+ unified: "npm:^11.0.4"
languageName: unknown
linkType: soft
@@ -3178,6 +3311,13 @@ __metadata:
languageName: node
linkType: hard
+"fast-content-type-parse@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "fast-content-type-parse@npm:3.0.0"
+ checksum: 10/8616a8aa6c9b4f8f4f3c90eaa4e7bfc2240cfa6f41f0eef5b5aa2b2c8b38bd9ad435f1488b6d817ffd725c54651e2777b882ae9dd59366e71e7896f1ec11d473
+ languageName: node
+ linkType: hard
+
"fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3":
version: 3.1.3
resolution: "fast-deep-equal@npm:3.1.3"
@@ -6763,7 +6903,7 @@ __metadata:
languageName: node
linkType: hard
-"unified@npm:^11.0.0":
+"unified@npm:^11.0.0, unified@npm:^11.0.4":
version: 11.0.5
resolution: "unified@npm:11.0.5"
dependencies:
@@ -6864,6 +7004,13 @@ __metadata:
languageName: node
linkType: hard
+"universal-user-agent@npm:^7.0.0, universal-user-agent@npm:^7.0.2":
+ version: 7.0.3
+ resolution: "universal-user-agent@npm:7.0.3"
+ checksum: 10/c497e85f8b11eb8fa4dce584d7a39cc98710164959f494cafc3c269b51abb20fff269951838efd7424d15f6b3d001507f3cb8b52bb5676fdb642019dfd17e63e
+ languageName: node
+ linkType: hard
+
"unrs-resolver@npm:^1.6.2":
version: 1.11.1
resolution: "unrs-resolver@npm:1.11.1"
diff --git a/packages/ui/README.md b/packages/ui/README.md
index 1a0fb84a0c..271b78fca4 100644
--- a/packages/ui/README.md
+++ b/packages/ui/README.md
@@ -16,3 +16,26 @@ yarn add @backstage/ui
- [Backstage UI Documentation](https://ui.backstage.io)
- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md)
- [Backstage Documentation](https://backstage.io/docs)
+
+## Writing Changesets for Components
+
+When creating changesets for component-specific changes, add component metadata to help maintain documentation:
+
+```markdown
+---
+'@backstage/ui': patch
+---
+
+Fixed size prop handling for Avatar component.
+
+Affected components: Avatar
+```
+
+**Guidelines:**
+
+- **Component names**: Use PascalCase as they appear in imports (Avatar, ButtonIcon, SearchField)
+- **Multiple components**: `Affected components: Button, ButtonLink, ButtonIcon`
+- **General changes**: Omit the metadata line (build changes, package-level updates)
+- **Placement**: The line can appear anywhere in the description
+
+The changelog sync tool will parse these tags and update the documentation site automatically.