diff --git a/.claude/skills/release-notes/SKILL.md b/.claude/skills/release-notes/SKILL.md new file mode 100644 index 00000000000..c4deebe020c --- /dev/null +++ b/.claude/skills/release-notes/SKILL.md @@ -0,0 +1,177 @@ +--- +description: Polish the scaffolded release notes MDX files generated by `yarn releaseNotes` — rewrites the Changelog section, groups by component, and fills in the intro/description. +--- + +# Release Notes Skill + +Polish the scaffolded release-notes files generated by `scripts/changelog.js`. + +## Setup + +`GITHUB_TOKEN` with repo read access must be set (used by the changelog script when it looks up PR authors). + +## Background + +`yarn releaseNotes` now: +- Buckets commits by library (React Aria Components, React Spectrum v3, Spectrum 2) using the per-package diffs. +- Detects the next version per library by scanning the corresponding releases directory. +- Writes one scaffold MDX file per non-empty bucket with the standard frontmatter, version heading, intro placeholder, a `## Changelog` populated with raw commit lines, and a `## Released packages` placeholder. + +Target paths: +- React Aria Components → `packages/dev/s2-docs/pages/react-aria/releases/vX-Y-Z.mdx` +- Spectrum 2 → `packages/dev/s2-docs/pages/s2/releases/vX-Y-Z.mdx` + +A commit that touches both `@react-aria/*` and `@react-spectrum/*` is written into both library files — intentional. + +## Steps + +### 1. Run the script and collect the file paths + +```bash +yarn releaseNotes +``` + +Output: +``` +✓ Wrote packages/dev/s2-docs/pages/react-aria/releases/v1-18-0.mdx (42 commits) +✓ Wrote packages/dev/s2-docs/pages/s2/releases/v1-4-0.mdx (18 commits) +✓ Wrote packages/dev/docs/pages/releases/2026-05-22.mdx (8 commits) +``` + +If a target file already exists the script prints a `⚠` line and dumps that bucket's commits to stdout instead. Hand-merge into the existing file. + +If the user has already run the script, work from the files it created — do not re-run it. + +### 2. Identify pre-release packages + +Scan `packages/` for `package.json` files whose version contains `alpha`, `beta`, or `rc`. Note the component names — commits that touch only these packages should be called out as Under Construction inside the relevant file's intro paragraph or a dedicated section if the team prefers. + +### 2.5. Record the expected commit count for each file + +Each scaffold file contains a comment directly above `## Changelog`: + +``` +{/* Commits: N */} +## Changelog +``` + +Before rewriting, read this value and store it as the expected count for that file. You will verify against it after the rewrite. + +### 3. For each scaffolded file, rewrite the `## Changelog` body in place + +Do **not** touch the frontmatter, version heading, or `## Released packages` block (the team fills the latter in by hand from `yarn bumpVersions`). + +For each commit line in the existing `## Changelog`: + +**Rewrite the message.** Original format: `Type (Scope): Summary - [@user](url) - [PR](url)` → target `Summary - [@user](url) - [PR](url)`. + +First, evaluate whether the commit subject is a clear, user-facing description. If it is vague or developer-internal (e.g. "fix typo", "address review comments", "nit", "updates", "wip", "cleanup"), fetch the PR body, changed files, and the rspbot ts-diff comment for more context: + +```bash +gh pr view --json title,body,files +gh pr view --json comments --jq '.comments[] | select(.author.login == "rspbot") | .body' +``` + +The rspbot comment contains a TypeScript diff of the public API surface — new/changed/removed exports and props. If present, treat it as the authoritative source for: +- Prop names and their exact spelling (already in TypeScript syntax — wrap in backticks directly) +- Whether a change is public API (exported) vs. internal-only +- How many distinct API surfaces a single PR introduced (drives the one-bullet-per-API-surface rule below) + +Use the PR description and changed file paths to write a more informative summary. If the PR has no useful description either, use the file paths to infer the affected area (e.g. files under `packages/@react-aria/table/` → Table-related fix). + +If `gh pr view` exits non-zero (PR deleted, no access, closed without merge, etc.), do **not** skip the commit. Keep the original commit subject and mark it for manual review: + +``` +- [NEEDS REVIEW] - +``` + +Then apply the standard formatting rules: + +- Strip the type prefix (`fix:`, `feat:`, `docs:`, etc.). +- Keep as a single grammatically correct sentence. +- Verbs first-person present tense, no subject (e.g. "Adds support for..." not "Added" or "I added"). +- Wrap camelCase/code terms in backticks: `onClick`, `isDisabled`, `onPress`. +- Do NOT use backticks for component names. +- Replace: RAC → React Aria, V3 → React Spectrum. +- Strip editorial hedging — remove words like *inadvertently*, *accidentally*, *incorrectly*, *mistakenly* from bug descriptions. Just describe what changed. +- When a bug fix enables something users previously couldn't do reliably, frame it as a user capability rather than an internal fix. Prefer "Allow `formatOptions` to be passed as an inline object without resetting user input" over "Fix resetting the typed value when `formatOptions` is passed as an inline object". +- Capitalize component names: Accordion, Autocomplete, Badge, Breadcrumbs, Buttons, Calendar, Checkbox, CheckboxGroup, Collections, ColorArea, ColorField, ColorPicker, ColorSlider, ColorSwatch, ColorSwatchPicker, ColorWheel, ComboBox, Date and Time, DateField, DatePicker, DateRangePicker, Dialog, Disclosure, DisclosureGroup, Drag and Drop, DropZone, FileTrigger, Form, InlineAlert, Link, Listbox, ListView, Menu, Meter, Modal, NotificationBadge, NumberField, Picker, ProgressBar, ProgressCircle, RadioGroup, RangeCalendar, SearchField, Select, Slider, StatusLight, Switch, Table, Tabs, TagGroup, TextArea, TextField, TimeField, Toast, ToggleButton, ToggleButtonGroup, Tooltip, Tree, Virtualizer. + +**Drop internal/infrastructure commits.** Before writing a bullet, ask: *"Would a user of this library notice this change in their own code or in the browser?"* If no, drop the entry and add a drop-note comment instead. Common cases to drop: +- Build-tool or bundler fixes (e.g. lightningcss compatibility, Parcel internals) +- TypeScript fixes for non-public or internal files (e.g. `page.css` type stubs) +- Internal tooling, lint migrations, script changes + +**Check whether a bug fix is a follow-up for a change introduced in this same release.** When processing a commit that is labeled `fix:` or is otherwise clearly a bug fix, fetch the PR body and title: + +```bash +gh pr view --json title,body +``` + +Scan the title and body for references to other PRs — patterns like `#1234`, `PR #1234`, `follow-up to #1234`, `follow up for #1234`, `introduced in #1234`, `regression from #1234`, `broken by #1234`, `related to #1234`. If you find one or more referenced PR numbers, check whether any of those numbers appear in the current release's commit list (the flat list in `## Changelog` before your rewrite). + +- **Referenced PR is in this release** → the bug was introduced and fixed within the same release cycle, so users on the previous release never experienced it. Drop this fix bullet using `follow-up refinement of [PR](url)` as the reason (see §3.5 drop notes). Do not mention the bug in the original PR's bullet — the net user-visible effect is just the original change working correctly. +- **Referenced PR is not in this release (or no cross-reference found)** → the bug is a real prod regression that users on the current release could hit. Keep it as a standalone bullet, following the standard framing and tense rules. + +This check matters because surfacing a "fix" for something that was never actually released as broken misleads users about what they should expect. When in doubt (ambiguous reference, cross-repo link), keep the bullet and treat it as a real fix. + +**Do not drop docs commits without checking the files.** A commit prefixed `docs:` or containing "docs" in the message is not automatically internal. Before dropping, fetch `gh pr view --json body,files` and scan: +- **New user-facing guides or how-tos on component pages** (e.g. a "Client side routing" section added to `Link.mdx`, a migration guide, an accessibility pattern explanation): include if a library user would find it directly actionable. Write the bullet as "Add [topic] guide to [Component] documentation". +- **Starter template changes** (`starters/docs/src/`, `starters/tailwind/src/`): these power the docs site examples, not standalone copyable files — treat changes here as docs site fixes. +- **Internal docs** (agent skills, CONTRIBUTING.md, build documentation, internal READMEs): drop as internal tooling. + +Use the standard `internal tooling` drop-note format (see §3.5) for any dropped commit. + +**One bullet per new API surface.** When a single PR introduces multiple distinct public APIs — new components, new props, new callback arguments — write one bullet per API rather than one bullet per PR. Fetch the PR body (`gh pr view --json body,files`) and scan the description and changed file paths for each new exported symbol or prop. Each deserves its own line. + +**Group by component.** Replace the flat list under `## Changelog` with sub-headings: + +``` +## Changelog + +### General Changes +- ... + +### Autocomplete +- ... + +### ComboBox +- ... +``` + +- Each component gets its own sub-heading — never group multiple components under a combined heading like "Checkbox, RadioGroup, Switch". If a single PR affects multiple components, write one bullet per component, each naming only that component, and place each under its own sub-heading. Do not name sibling components inside the bullet text — e.g. write "Add drag and drop support" under **ListView**, **Table**, and **TreeView** separately, not "Add drag and drop support to ListView, Table, and TreeView" under a single heading. +- When a PR affects multiple components but with *different* behavior per component, write distinct descriptions for each rather than copying the same bullet. Fetch the PR body and changed files to understand which component got which change — e.g. a `--trigger-width` fix that adds standalone support to Popover and also fixes Group-wrapped inputs in ComboBox needs two different bullets with two different descriptions. +- Sub-headings in alphabetical order, with "General Changes" first. +- Use `-` (not `*`) for list items inside sub-headings, matching the existing release-notes style (see `packages/dev/s2-docs/pages/react-aria/releases/v1-17-0.mdx` for reference). +- Do NOT bold sub-heading text. + +### 3.5. Verify commit count + +After rewriting the `## Changelog` body, count the total number of `-` bullet lines across all component sections in the file. Compare to the expected count from step 2.5. + +- **Count matches:** continue to step 4. +- **Count is less:** diff the original flat commit list against the bullets you wrote to find which entries are missing. Either place them in the correct section, or leave a drop note explaining why. Do not leave commits silently missing. Drop note forms: + All drop notes must include the PR link, author handle, and author profile URL so the entry can be reinstated quickly if needed. Format: + + `{/* dropped: [@author](https://github.com/author) - [PR](url) — reason */}` + + Reason values: + - `merged into above` — use **only** when the bullet directly above literally describes what this PR did (i.e. you rewrote the bullet to incorporate this PR's changes). Do not use this if the bullet above doesn't mention the content of this PR. + - `follow-up refinement of [PR](url)` — use for iterative polish PRs (design feedback, followups, small adjustments) whose user-facing change is fully captured by the initial feature PR's bullet. + - `internal tooling` — use for commits with no user-facing impact (lint migration, formatting, yarn upgrade, internal scripts, test-only changes). +- **Count is greater:** this is fine if a multi-component PR was duplicated across sections (intentional per the grouping rules). + +### 4. Fill in `description` and intro paragraph + +Read the rewritten changelog and write: +- The `description` export (in S2-docs files) or the `description:` frontmatter line (in v3 files) — 1–2 sentences highlighting the most notable changes. +- The intro paragraph below the `# vX.Y.Z` (or `# Month DD, YYYY Release`) heading — slightly longer, links to relevant component docs where helpful. + +### 5. Leave `## Released packages` alone + +The team pastes `yarn bumpVersions` output here before publishing. + +## Notes + +- Do not duplicate-dedupe across files. A PR that legitimately touched both RAC and S2 should appear in both files. +- Do not add new top-level sections or rename existing ones — match the structure of the most recent release in each directory. diff --git a/.cursor/rules/add-subheadings.mdc b/.cursor/rules/add-subheadings.mdc deleted file mode 100644 index d723ca3dfd5..00000000000 --- a/.cursor/rules/add-subheadings.mdc +++ /dev/null @@ -1,22 +0,0 @@ ---- -alwaysApply: false ---- - -# Step 3: Add sub-headings - -Within the Enhancements, Fixes, and Under Construction categories, group commits by UI component under sub-headings. - -### Sub-heading rules: -- Use sub-headings ONLY for: - - Enhancements - - Fixes - - Under Construction -- Do NOT create sub-headings for: - - Documentation - - To Be Categorized - - S2 -- Sub-headings should be in alphabetical order -- Use "Miscellaneous" as a sub-heading for commits that do not belong to a specific component -- Each category can have its own Miscellaneous sub-heading -- Write sub-headings and commits as unordered lists using a hyphen (-) -- Do NOT bold the sub-heading text diff --git a/.cursor/rules/categorize-commits.mdc b/.cursor/rules/categorize-commits.mdc deleted file mode 100644 index 466190cfadf..00000000000 --- a/.cursor/rules/categorize-commits.mdc +++ /dev/null @@ -1,36 +0,0 @@ ---- -alwaysApply: false ---- - -# Step 1: Categorize commits - -Sort ALL commit messages into one of six main categories. The main categories are the following: -- Enhancements -- Fixes -- Documentation -- Under Construction -- To Be Categorized -- S2 - -Before categorizing commits into other groups, check whether each commit should be classified as “Under Construction.” -- Follow the steps below in order: - 1. Identify pre-release packages - - Use a command such as grep to scan the repository for package versions that include prerelease identifiers (e.g., alpha, beta, rc) - 2. Extract component keywords from commit messages - - Parse each commit message to identify possible component names - - Normalize these keywords (e.g., lowercase, remove punctuation) for easier comparison. - 3. Compare extracted keywords with pre-release packages - - If any keyword matches a package in the list, mark the commit as Under Construction. - 4. Check for explicit prerelease keywords in commit text - - If the commit message directly includes alpha, beta, or rc, classify it as Under Construction, regardless of package matches. - -Next, categorize the remaining commits not categorized as "Under Construction". Use the following keywords to determine the category: -| Keyword | Category | -|----------------------------|----------| -| feat | Enhancements| -| fix | Fixes | -| docs | Documentation | -| chore, revert, bump, build | To Be Categorized | -| S2 | S2 | - -Do not duplicate commits. In terms of priority, it should be Under Construction > S2 > To Be Categorized > Enhancements > Fixes > Documentation diff --git a/.cursor/rules/rewrite-commit-message.mdc b/.cursor/rules/rewrite-commit-message.mdc deleted file mode 100644 index a2d68af8858..00000000000 --- a/.cursor/rules/rewrite-commit-message.mdc +++ /dev/null @@ -1,27 +0,0 @@ ---- -alwaysApply: false ---- - -You are a expert technical writer for front-end development. - -# Step 2: Rewrite commit messages - -Original format: Type (Scope): Summary of changes - [@username](link to username) - [PR](link to PR) -New Format: Summary of changes - [@username](link to username) - [PR](link to PR) - -### General Guidelines: -- Keep the summary as a single, grammatically correct sentence -- Verbs should be first person present tense but do NOT include the subject (e.g. I) -- The message should be concise and easy to read -- Wrap any camelCase or code-like terms (e.g. onClick, onAction, isDisabled) in backticks (``) - - Do NOT use backticks for component names -- Replace specific terms: - - RAC -> React Aria - - V3 -> React Spectrum -- ALWAYS capitalize UI component names - - Example: - - toast -> Toast - - inline alert -> InlineAlert - -### Component Names to Capitalize: -Accordion, Autocomplete, Badge, Breadcrumbs, Buttons, Calendar, Checkbox, CheckboxGroup, Collections, ColorArea, ColorField, ColorPicker, ColorSlider, ColorSwatch, ColorSwatchPicker, ColorWheel, ComboBox, Date and Time, DateField, DatePicker, DateRangePicker, Dialog, Disclosure, DisclosureGroup, Drag and Drop, DropZone, FileTrigger, Form, InlineAlert, Link, Listbox, ListView, Menu, Meter, Modal, NotificationBadge, NumberField, Picker, ProgressBar, ProgressCircle, RadioGroup, RangeCalendar, SearchField, Select, Slider, StatusLight, Switch, Table, Tabs, TagGroup, TextArea, TextField, TimeField, Toast, ToggleButton, ToggleButtonGroup, Tooltip, Tree, Virtualizer. diff --git a/scripts/changelog.js b/scripts/changelog.js index 58e177be6c2..2bce12dfc9b 100644 --- a/scripts/changelog.js +++ b/scripts/changelog.js @@ -1,12 +1,194 @@ const exec = require('child_process').execSync; const spawn = require('child_process').spawnSync; const fs = require('fs'); +const path = require('path'); const Octokit = require('@octokit/rest'); const octokit = new Octokit({ auth: `token ${process.env.GITHUB_TOKEN}` }); +const LIBRARY_ORDER = ['React Aria Components', 'Spectrum 2']; + +const LIBRARY_CONFIG = { + 'React Aria Components': { + releasesDir: 'packages/dev/s2-docs/pages/react-aria/releases', + tag: 'React Aria', + docsDir: 'packages/dev/s2-docs/pages/react-aria' + }, + 'Spectrum 2': { + releasesDir: 'packages/dev/s2-docs/pages/s2/releases', + tag: 'S2', + docsDir: 'packages/dev/s2-docs/pages/s2' + } +}; + +function packageToLibrary(name) { + // Skip this package + if (name.startsWith('@spectrum-icons/')) { + return null; + } + if ( + name.startsWith('@internationalized/') || + name === 'react-stately' || + name.startsWith('@react-stately/') || + name === 'react-aria' || + name.startsWith('@react-aria/') || + name === 'react-aria-components' || + name === 'tailwindcss-react-aria-components' || + name.startsWith('@react-types/') + ) { + return 'React Aria Components'; + } + if (name === '@react-spectrum/s2') { + return 'Spectrum 2'; + } + // V3 has no LIBRARY_CONFIG entry. Its commits are collected but only printed + // as a warning, never written to a file. See the v3Bucket handling at the end of run(). + if (name === '@adobe/react-spectrum' || name.startsWith('@react-spectrum/')) { + return 'React Spectrum'; + } + return null; +} + +function nextVersionFilename(releasesDir) { + let entries = fs.readdirSync(releasesDir); + let versions = []; + for (let entry of entries) { + let m = entry.match(/^v(\d+)-(\d+)-(\d+)\.mdx$/); + if (m) { + versions.push([Number(m[1]), Number(m[2]), Number(m[3])]); + } + } + if (versions.length === 0) { + return {filename: 'v1-0-0.mdx', version: '1.0.0'}; + } + versions.sort((a, b) => b[0] - a[0] || b[1] - a[1] || b[2] - a[2]); + let [major, minor] = versions[0]; + let nextMinor = minor + 1; + // Always produces a minor bump (x.N+1.0) + return {filename: `v${major}-${nextMinor}-0.mdx`, version: `${major}.${nextMinor}.0`}; +} + +function todayYMD() { + let d = new Date(); + let y = d.getFullYear(); + let m = String(d.getMonth() + 1).padStart(2, '0'); + let day = String(d.getDate()).padStart(2, '0'); + return `${y}-${m}-${day}`; +} + +function prettyDate(ymd) { + let [y, m, d] = ymd.split('-').map(Number); + let months = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' + ]; + return `${months[m - 1]} ${d}, ${y}`; +} + +function licenseHeader(year) { + return `{/* Copyright ${year} Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. */}`; +} + +function scaffoldS2Docs({version, dateYMD, tag, body, commitCount}) { + let year = new Date().getFullYear(); + return `${licenseHeader(year)} + +import {InstallCommand} from '../../../src/InstallCommand'; + +import {Layout} from '../../../src/Layout'; +export default Layout; + +import docs from 'docs:@react-spectrum/s2'; + +export const hideNav = true; +export const section = 'Releases'; +export const tags = ['release', '${tag}']; +export const date = '${prettyDate(dateYMD)}'; +export const title = 'v${version}'; +export const description = '[TODO: 1–2 sentence highlights summary]'; +export const isSubpage = true; + +# v${version} + +[TODO: intro paragraph] + +{/* Commits: ${commitCount} */} +## Changelog + +${body} + +## Released packages + +\`\`\` +[TODO: paste output of yarn bumpVersions here] +\`\`\` +`; +} + +function resolveTag(pkgName, pkgVersion) { + let exact = `${pkgName}@${pkgVersion}`; + let check = spawn('git', ['rev-parse', '--verify', exact], {encoding: 'utf8'}); + if (check.status === 0) { + return exact; + } + // Exact tag doesn't exist (e.g. a patch bump without a new tag) — fall back to the + // most recent tag for this package using git's version sort. + let list = spawn('git', ['tag', '-l', '--sort=version:refname', `${pkgName}@*`], { + encoding: 'utf8' + }); + let tags = list.stdout.trim().split('\n').filter(Boolean); + return tags.length > 0 ? tags[tags.length - 1] : null; +} + +async function renderCommitLine(commit) { + let message = ''; + let user = ''; + let pr; + + // commit fields: [0] hash, [1] ISO date, [2] author name, [3] subject (from --pretty format) + let m = commit[3].match(/(.*?) \(#(\d+)\)$/); + + if (m) { + let prId = m[2]; + message = m[1]; + + try { + let res = await octokit.request('GET /repos/adobe/react-spectrum/pulls/{pull}', {pull: prId}); + user = `[@${res.data.user.login}](${res.data.user.html_url})`; + pr = `https://github.com/adobe/react-spectrum/pull/${prId}`; + } catch (e) { + console.warn( + `⚠ Could not fetch PR #${prId} from GitHub (${e.message}) — falling back to git author` + ); + user = commit[2]; + } + } else { + message = commit[3]; + user = commit[2]; + } + + return `* ${message} - ${user}` + (pr ? ` - [PR](${pr})` : ''); +} + run(); async function run() { @@ -15,68 +197,153 @@ async function run() { .split(require('os').EOL) .filter(Boolean) .map(x => JSON.parse(x)); - let commits = new Map(); - // Diff each package individually. Some packages might have been skipped during last release, - // so we cannot simply look at the last tag on the whole repo. - for (let name in packages) { - let filePath = packages[name].location + '/package.json'; + let commitsByLibrary = new Map(); + let libraryTags = new Map(); + + for (let {location} of packages) { + let filePath = location + '/package.json'; let pkg = JSON.parse(fs.readFileSync(filePath, 'utf8')); - if (!pkg.private) { - // Diff this package since the last published version, according to the package.json. - // The release script creates a tag for each package version. - let tag = `${pkg.name}@${pkg.version}`; - - let args = [ - 'log', - `${tag}..HEAD`, - '--pretty="%H%x00%aI%x00%an%x00%s"', - packages[name].location, - - // filter out non-code changes - ':!**/test/**', - ':!**/stories/**', - ':!**/chromatic/**' - ]; - - let res = spawn('git', args, {encoding: 'utf8'}); - if (res.stdout.length === 0) { - continue; + if (pkg.private) { + continue; + } + + let library = packageToLibrary(pkg.name); + if (!library) { + if (!pkg.name.startsWith('@spectrum-icons/')) { + console.warn(`⚠ No library mapping for ${pkg.name} — skipped`); } + continue; + } + + let tag = resolveTag(pkg.name, pkg.version); + if (!tag) { + console.warn(`⚠ No tag found for ${pkg.name}@${pkg.version} — skipped`); + continue; + } + + // Track the first tag seen per library as the anchor for git log ranges. + if (!libraryTags.has(library)) { + libraryTags.set(library, tag); + } + + let args = [ + 'log', + `${tag}..HEAD`, + '--pretty="%H%x00%aI%x00%an%x00%s"', + location, + + // filter out non-code changes + ':!**/test/**', + ':!**/stories/**', + ':!**/chromatic/**' + ]; + + let res = spawn('git', args, {encoding: 'utf8'}); + if (res.stdout.length === 0) { + continue; + } + + if (!commitsByLibrary.has(library)) { + commitsByLibrary.set(library, new Map()); + } + let bucket = commitsByLibrary.get(library); - for (let line of res.stdout.split('\n')) { - if (line === '') { - continue; - } + for (let line of res.stdout.split('\n')) { + if (line === '') { + continue; + } - let info = line.replace(/^"|"$/g, '').split('\0'); - commits.set(info[0], info); + let info = line.replace(/^"|"$/g, '').split('\0'); + if (info[3] === 'Publish') { + // skip Publish commits + continue; } + bucket.set(info[0], info); // keyed by hash — deduplicates commits touching multiple packages } } - let sortedCommits = [...commits.values()].sort((a, b) => (a[1] < b[1] ? -1 : 1)); + // Scan docs directories for docs-only commits + // These live in private packages skipped by the main loop above. + for (let [library, config] of Object.entries(LIBRARY_CONFIG)) { + if (!config.docsDir) continue; + let tag = libraryTags.get(library); + if (!tag) continue; - for (let commit of sortedCommits) { - let message = ''; - let user = ''; - let pr; + let args = [ + 'log', + `${tag}..HEAD`, + '--pretty="%H%x00%aI%x00%an%x00%s"', + config.docsDir, + ':!**/releases/**' // don't include the release note files themselves + ]; - //look for commits with pr # - let m = commit[3].match(/(.*?) \(#(\d+)\)$/); + let res = spawn('git', args, {encoding: 'utf8'}); + if (!res.stdout || res.stdout.length === 0) continue; - if (m) { - let prId = m[2]; - message = m[1]; + if (!commitsByLibrary.has(library)) { + commitsByLibrary.set(library, new Map()); + } + let bucket = commitsByLibrary.get(library); - let res = await octokit.request('GET /repos/adobe/react-spectrum/pulls/{pull}', {pull: prId}); - user = `[@${res.data.user.login}](${res.data.user.html_url})`; - pr = `https://github.com/adobe/react-spectrum/pull/${prId}`; - } else { - // not a pr so just print what we know from the commit - message = commit[3]; - user = commit[2]; + for (let line of res.stdout.split('\n')) { + if (line === '') continue; + let info = line.replace(/^"|"$/g, '').split('\0'); + if (info[3] === 'Publish') continue; + bucket.set(info[0], info); // keyed by hash — deduplicates with source commits + } + } + + // Handle commits under RAC and S2 + for (let library of LIBRARY_ORDER) { + let bucket = commitsByLibrary.get(library); + if (!bucket || bucket.size === 0) { + continue; + } + + let sorted = [...bucket.values()].sort((a, b) => (a[1] < b[1] ? -1 : 1)); + let lines = await Promise.all(sorted.map(renderCommitLine)); + let body = lines.join('\n'); + + let config = LIBRARY_CONFIG[library]; + let dir = config.releasesDir; + let outPath; + let content; + + let {filename, version} = nextVersionFilename(dir); + outPath = path.join(dir, filename); + content = scaffoldS2Docs({ + version, + dateYMD: todayYMD(), + tag: config.tag, + body, + commitCount: lines.length + }); + + if (fs.existsSync(outPath)) { + console.warn( + `⚠ ${outPath} already exists — skipping (${bucket.size} commits not written). Printing to stdout instead:\n` + ); + console.log(`# ${library}\n`); + console.log(body); + console.log(); + continue; + } + + fs.writeFileSync(outPath, content); + console.log(`✓ Wrote ${outPath} (${bucket.size} commits)`); + } + + // Handle commits in V3 + let v3Bucket = commitsByLibrary.get('React Spectrum'); + if (v3Bucket && v3Bucket.size > 0) { + console.warn( + `\nℹ ${v3Bucket.size} React Spectrum (v3) commit(s) were not written to a file. Review them manually if needed:\n` + ); + let sorted = [...v3Bucket.values()].sort((a, b) => (a[1] < b[1] ? -1 : 1)); + for (let commit of sorted) { + console.warn(` ${commit[3]}`); } - console.log(`* ${message} - ${user}` + (pr ? ` - [PR](${pr})` : '')); + console.warn(); } }