diff --git a/.github/workflows/typecheck.yaml b/.github/workflows/check.yaml similarity index 87% rename from .github/workflows/typecheck.yaml rename to .github/workflows/check.yaml index f09b3e2fbc0..5375aecb4f8 100644 --- a/.github/workflows/typecheck.yaml +++ b/.github/workflows/check.yaml @@ -9,7 +9,7 @@ on: - v2 jobs: - typecheck: + check: runs-on: ubuntu-latest steps: @@ -28,5 +28,9 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Check change files + run: pnpm changes:validate + - name: Run typechecks run: pnpm typecheck + diff --git a/AGENTS.md b/AGENTS.md index 8d319229edd..ad95dc8b88d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,7 +37,14 @@ - **Accessible navigation**: Always use proper `` elements for navigation links. Never use JavaScript `onclick` handlers on non-interactive elements like ``, `
`, or `` for navigation. Links should be keyboard accessible and work with screen readers. - **Clean shutdown**: Demo servers should handle `SIGINT` and `SIGTERM` signals to exit cleanly when Ctrl+C is pressed. Close the server and call `process.exit(0)`. -## Changelog Formatting - -- Use `## Unreleased` as the heading for unreleased changes (not `## HEAD`) -- Scripts in `./scripts` are configured to replace `## Unreleased` with version and date on release +## Changes and Releases + +- **Adding changes**: Create `packages/*/.changes/[major|minor|patch].short-description.md` files. See [CONTRIBUTING.md](./CONTRIBUTING.md#adding-a-change-file) for details. +- **Updating changes**: If iterating on an unpublished change with a change file, update it in place rather than creating a new one. +- **Versioning**: Follow semver 0.x.x conventions where breaking changes can happen in minor releases: + - For **v0.x.x packages**: Use "minor" for breaking changes and new features, "patch" for bug fixes. Never use "major" unless explicitly instructed. + - For **v1.x.x+ packages**: Use standard semver - "major" for breaking changes, "minor" for new features, "patch" for bug fixes. + - **Breaking changes are relative to main**: If you introduce a new API in a PR and then change it within the same PR before merging, that's not considered a breaking change. +- **Validating changes**: `pnpm changes:validate` checks that all change files follow the correct naming convention and format. +- **Previewing releases**: `pnpm changes:preview` shows which packages will be released, what the CHANGELOG will look like, the commit message and tags. +- **Versioning releases**: `pnpm changes:version` updates package.json, CHANGELOG.md, creates a git commit and tags. Don't run this unless explicitly instructed to do so. We don't want accidental releases during development. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1ebf5c73ff6..5d9c856fadb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,32 +38,109 @@ All packages are published with TypeScript types along with both ESM and CJS mod Packages live in the [`packages` directory](https://github.com/remix-run/remix/tree/v3/packages). At a minimum, each package includes: +- `.changes/`: Directory containing change files for the next release - `CHANGELOG.md`: A log of what's changed - `package.json`: Package metadata and dependencies - `README.md`: Information about the package - `src/`: The package's source code -When you make changes to a package, please make sure you add a few relevant tests and run the whole test suite to make sure everything still works. Then, add a human-friendly description of your change in the changelog and [make a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request). We will take a look at it as soon as we can. +When you make changes to a package, please make sure you add a few relevant tests and run the whole test suite to make sure everything still works. Then, [add a change file](#adding-a-change-file) describing your changes and [make a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request). We will take a look at it as soon as we can. + +### Adding a Change File + +When making changes to a package, create a markdown file in the package's `.changes/` directory following this naming convention: + +``` +[major|minor|patch].short-description.md +``` + +#### Examples + +- `major.breaking-api-change.md` - Breaking change requiring major version bump +- `minor.add-new-feature.md` - New feature requiring minor version bump +- `patch.fix-bug.md` - Bug fix requiring patch version bump + +#### Content Format + +Write your change as a bullet point (without the leading `-` or `*`). This content will be added to the CHANGELOG during release. + +**Simple change:** + +```markdown +Add support for X feature +``` + +**Multi-line change:** + +```markdown +Add support for X feature + +This is a longer explanation that will be indented +under the main bullet point in the CHANGELOG. +``` + +#### Validation + +Change files are automatically validated in CI. You can also validate them locally: + +```sh +pnpm changes:validate +``` ## Releases -Cutting releases is a 2-step process: +Cutting releases is a multi-step process: + +1. **Preview** - See what will be released +2. **Version** - Update versions and create commit/tags locally +3. **Push** - Push to GitHub (triggers CI to publish to npm) + +### Preview a Release + +To see which packages have changes and preview the release: + +```sh +pnpm changes:preview +``` + +This shows: -- Update versions in package.json and the changelog and create a git tag (tagging) -- Publish new packages to npm and create a GitHub Release (publishing) +- Which packages will be released with version bumps +- CHANGELOG additions +- Git tags that will be created +- Commit message -This repo includes a script for each step. +### Version a Release -To update versions and create a tag, use `pnpm run tag-release `. For example, to create a `minor` release of the `headers` package, run: +When ready to release, update versions and create the commit and tags locally: ```sh -pnpm run tag-release headers minor +pnpm changes:version ``` -To publish the release you just tagged, use `pnpm run publish-release `. For example, if the tag that was created in the previous step was `headers@1.0.0`, you'd run `pnpm run publish-release headers@1.0.0`. +This will: + +- Validate all change files +- Update `package.json` versions +- Update `CHANGELOG.md` files +- Delete processed change files +- Create a git commit +- Create git tags -The publish step runs in GitHub Actions if you just push the tag to GitHub: +If you want to review the file changes before committing: ```sh -git push origin main --tags +pnpm changes:version --no-commit ``` + +This updates the files but skips git operations. After reviewing, the script will show you the exact git commands to run to create the commit and tags. + +### Push the Release + +Push the release commit and tags to GitHub: + +```sh +git push && git push --tags +``` + +GitHub Actions will automatically publish the tagged packages to npm and create GitHub Releases. diff --git a/package.json b/package.json index c5fa90fbc11..6f2369b4b59 100644 --- a/package.json +++ b/package.json @@ -17,15 +17,16 @@ }, "scripts": { "build": "pnpm -r build", + "changes:preview": "node ./scripts/changes-preview.js", + "changes:validate": "node ./scripts/changes-validate.js", + "changes:version": "node ./scripts/changes-version.js", "clean": "git clean -fdX -e '!/.env' .", "codegen": "pnpm -r run codegen", "create-github-release": "node --env-file .env ./scripts/create-github-release.js", "lint": "eslint . --max-warnings=0", "lint:fix": "eslint . --fix", - "tag-release": "node ./scripts/tag-release.js", "test": "pnpm --parallel run test", - "typecheck": "pnpm -r typecheck", - "view-changes": "node ./scripts/view-changes.js" + "typecheck": "pnpm -r typecheck" }, "workspaces": [ "packages/*" diff --git a/packages/async-context-middleware/.changes/README.md b/packages/async-context-middleware/.changes/README.md new file mode 100644 index 00000000000..84a76abac76 --- /dev/null +++ b/packages/async-context-middleware/.changes/README.md @@ -0,0 +1,23 @@ +# Changes Directory + +This directory contains change files that will be processed during releases. + +## Adding a Change + +See the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files. + +## Quick Reference + +Create a file with the pattern: + +``` +[major|minor|patch].short-description.md +``` + +Examples: + +- `major.breaking-api-change.md` +- `minor.add-new-feature.md` +- `patch.fix-bug.md` + +The content will be added to the CHANGELOG during release. diff --git a/packages/compression-middleware/.changes/README.md b/packages/compression-middleware/.changes/README.md new file mode 100644 index 00000000000..84a76abac76 --- /dev/null +++ b/packages/compression-middleware/.changes/README.md @@ -0,0 +1,23 @@ +# Changes Directory + +This directory contains change files that will be processed during releases. + +## Adding a Change + +See the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files. + +## Quick Reference + +Create a file with the pattern: + +``` +[major|minor|patch].short-description.md +``` + +Examples: + +- `major.breaking-api-change.md` +- `minor.add-new-feature.md` +- `patch.fix-bug.md` + +The content will be added to the CHANGELOG during release. diff --git a/packages/cookie/.changes/README.md b/packages/cookie/.changes/README.md new file mode 100644 index 00000000000..84a76abac76 --- /dev/null +++ b/packages/cookie/.changes/README.md @@ -0,0 +1,23 @@ +# Changes Directory + +This directory contains change files that will be processed during releases. + +## Adding a Change + +See the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files. + +## Quick Reference + +Create a file with the pattern: + +``` +[major|minor|patch].short-description.md +``` + +Examples: + +- `major.breaking-api-change.md` +- `minor.add-new-feature.md` +- `patch.fix-bug.md` + +The content will be added to the CHANGELOG during release. diff --git a/packages/fetch-proxy/.changes/README.md b/packages/fetch-proxy/.changes/README.md new file mode 100644 index 00000000000..84a76abac76 --- /dev/null +++ b/packages/fetch-proxy/.changes/README.md @@ -0,0 +1,23 @@ +# Changes Directory + +This directory contains change files that will be processed during releases. + +## Adding a Change + +See the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files. + +## Quick Reference + +Create a file with the pattern: + +``` +[major|minor|patch].short-description.md +``` + +Examples: + +- `major.breaking-api-change.md` +- `minor.add-new-feature.md` +- `patch.fix-bug.md` + +The content will be added to the CHANGELOG during release. diff --git a/packages/fetch-router/.changes/README.md b/packages/fetch-router/.changes/README.md new file mode 100644 index 00000000000..84a76abac76 --- /dev/null +++ b/packages/fetch-router/.changes/README.md @@ -0,0 +1,23 @@ +# Changes Directory + +This directory contains change files that will be processed during releases. + +## Adding a Change + +See the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files. + +## Quick Reference + +Create a file with the pattern: + +``` +[major|minor|patch].short-description.md +``` + +Examples: + +- `major.breaking-api-change.md` +- `minor.add-new-feature.md` +- `patch.fix-bug.md` + +The content will be added to the CHANGELOG during release. diff --git a/packages/file-storage/.changes/README.md b/packages/file-storage/.changes/README.md new file mode 100644 index 00000000000..84a76abac76 --- /dev/null +++ b/packages/file-storage/.changes/README.md @@ -0,0 +1,23 @@ +# Changes Directory + +This directory contains change files that will be processed during releases. + +## Adding a Change + +See the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files. + +## Quick Reference + +Create a file with the pattern: + +``` +[major|minor|patch].short-description.md +``` + +Examples: + +- `major.breaking-api-change.md` +- `minor.add-new-feature.md` +- `patch.fix-bug.md` + +The content will be added to the CHANGELOG during release. diff --git a/packages/form-data-middleware/.changes/README.md b/packages/form-data-middleware/.changes/README.md new file mode 100644 index 00000000000..84a76abac76 --- /dev/null +++ b/packages/form-data-middleware/.changes/README.md @@ -0,0 +1,23 @@ +# Changes Directory + +This directory contains change files that will be processed during releases. + +## Adding a Change + +See the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files. + +## Quick Reference + +Create a file with the pattern: + +``` +[major|minor|patch].short-description.md +``` + +Examples: + +- `major.breaking-api-change.md` +- `minor.add-new-feature.md` +- `patch.fix-bug.md` + +The content will be added to the CHANGELOG during release. diff --git a/packages/form-data-parser/.changes/README.md b/packages/form-data-parser/.changes/README.md new file mode 100644 index 00000000000..84a76abac76 --- /dev/null +++ b/packages/form-data-parser/.changes/README.md @@ -0,0 +1,23 @@ +# Changes Directory + +This directory contains change files that will be processed during releases. + +## Adding a Change + +See the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files. + +## Quick Reference + +Create a file with the pattern: + +``` +[major|minor|patch].short-description.md +``` + +Examples: + +- `major.breaking-api-change.md` +- `minor.add-new-feature.md` +- `patch.fix-bug.md` + +The content will be added to the CHANGELOG during release. diff --git a/packages/fs/.changes/README.md b/packages/fs/.changes/README.md new file mode 100644 index 00000000000..84a76abac76 --- /dev/null +++ b/packages/fs/.changes/README.md @@ -0,0 +1,23 @@ +# Changes Directory + +This directory contains change files that will be processed during releases. + +## Adding a Change + +See the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files. + +## Quick Reference + +Create a file with the pattern: + +``` +[major|minor|patch].short-description.md +``` + +Examples: + +- `major.breaking-api-change.md` +- `minor.add-new-feature.md` +- `patch.fix-bug.md` + +The content will be added to the CHANGELOG during release. diff --git a/packages/headers/.changes/README.md b/packages/headers/.changes/README.md new file mode 100644 index 00000000000..84a76abac76 --- /dev/null +++ b/packages/headers/.changes/README.md @@ -0,0 +1,23 @@ +# Changes Directory + +This directory contains change files that will be processed during releases. + +## Adding a Change + +See the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files. + +## Quick Reference + +Create a file with the pattern: + +``` +[major|minor|patch].short-description.md +``` + +Examples: + +- `major.breaking-api-change.md` +- `minor.add-new-feature.md` +- `patch.fix-bug.md` + +The content will be added to the CHANGELOG during release. diff --git a/packages/html-template/.changes/README.md b/packages/html-template/.changes/README.md new file mode 100644 index 00000000000..84a76abac76 --- /dev/null +++ b/packages/html-template/.changes/README.md @@ -0,0 +1,23 @@ +# Changes Directory + +This directory contains change files that will be processed during releases. + +## Adding a Change + +See the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files. + +## Quick Reference + +Create a file with the pattern: + +``` +[major|minor|patch].short-description.md +``` + +Examples: + +- `major.breaking-api-change.md` +- `minor.add-new-feature.md` +- `patch.fix-bug.md` + +The content will be added to the CHANGELOG during release. diff --git a/packages/interaction/.changes/README.md b/packages/interaction/.changes/README.md new file mode 100644 index 00000000000..84a76abac76 --- /dev/null +++ b/packages/interaction/.changes/README.md @@ -0,0 +1,23 @@ +# Changes Directory + +This directory contains change files that will be processed during releases. + +## Adding a Change + +See the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files. + +## Quick Reference + +Create a file with the pattern: + +``` +[major|minor|patch].short-description.md +``` + +Examples: + +- `major.breaking-api-change.md` +- `minor.add-new-feature.md` +- `patch.fix-bug.md` + +The content will be added to the CHANGELOG during release. diff --git a/packages/lazy-file/.changes/README.md b/packages/lazy-file/.changes/README.md new file mode 100644 index 00000000000..84a76abac76 --- /dev/null +++ b/packages/lazy-file/.changes/README.md @@ -0,0 +1,23 @@ +# Changes Directory + +This directory contains change files that will be processed during releases. + +## Adding a Change + +See the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files. + +## Quick Reference + +Create a file with the pattern: + +``` +[major|minor|patch].short-description.md +``` + +Examples: + +- `major.breaking-api-change.md` +- `minor.add-new-feature.md` +- `patch.fix-bug.md` + +The content will be added to the CHANGELOG during release. diff --git a/packages/logger-middleware/.changes/README.md b/packages/logger-middleware/.changes/README.md new file mode 100644 index 00000000000..84a76abac76 --- /dev/null +++ b/packages/logger-middleware/.changes/README.md @@ -0,0 +1,23 @@ +# Changes Directory + +This directory contains change files that will be processed during releases. + +## Adding a Change + +See the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files. + +## Quick Reference + +Create a file with the pattern: + +``` +[major|minor|patch].short-description.md +``` + +Examples: + +- `major.breaking-api-change.md` +- `minor.add-new-feature.md` +- `patch.fix-bug.md` + +The content will be added to the CHANGELOG during release. diff --git a/packages/method-override-middleware/.changes/README.md b/packages/method-override-middleware/.changes/README.md new file mode 100644 index 00000000000..84a76abac76 --- /dev/null +++ b/packages/method-override-middleware/.changes/README.md @@ -0,0 +1,23 @@ +# Changes Directory + +This directory contains change files that will be processed during releases. + +## Adding a Change + +See the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files. + +## Quick Reference + +Create a file with the pattern: + +``` +[major|minor|patch].short-description.md +``` + +Examples: + +- `major.breaking-api-change.md` +- `minor.add-new-feature.md` +- `patch.fix-bug.md` + +The content will be added to the CHANGELOG during release. diff --git a/packages/mime/.changes/README.md b/packages/mime/.changes/README.md new file mode 100644 index 00000000000..84a76abac76 --- /dev/null +++ b/packages/mime/.changes/README.md @@ -0,0 +1,23 @@ +# Changes Directory + +This directory contains change files that will be processed during releases. + +## Adding a Change + +See the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files. + +## Quick Reference + +Create a file with the pattern: + +``` +[major|minor|patch].short-description.md +``` + +Examples: + +- `major.breaking-api-change.md` +- `minor.add-new-feature.md` +- `patch.fix-bug.md` + +The content will be added to the CHANGELOG during release. diff --git a/packages/multipart-parser/.changes/README.md b/packages/multipart-parser/.changes/README.md new file mode 100644 index 00000000000..84a76abac76 --- /dev/null +++ b/packages/multipart-parser/.changes/README.md @@ -0,0 +1,23 @@ +# Changes Directory + +This directory contains change files that will be processed during releases. + +## Adding a Change + +See the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files. + +## Quick Reference + +Create a file with the pattern: + +``` +[major|minor|patch].short-description.md +``` + +Examples: + +- `major.breaking-api-change.md` +- `minor.add-new-feature.md` +- `patch.fix-bug.md` + +The content will be added to the CHANGELOG during release. diff --git a/packages/node-fetch-server/.changes/README.md b/packages/node-fetch-server/.changes/README.md new file mode 100644 index 00000000000..84a76abac76 --- /dev/null +++ b/packages/node-fetch-server/.changes/README.md @@ -0,0 +1,23 @@ +# Changes Directory + +This directory contains change files that will be processed during releases. + +## Adding a Change + +See the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files. + +## Quick Reference + +Create a file with the pattern: + +``` +[major|minor|patch].short-description.md +``` + +Examples: + +- `major.breaking-api-change.md` +- `minor.add-new-feature.md` +- `patch.fix-bug.md` + +The content will be added to the CHANGELOG during release. diff --git a/packages/remix-dev/.changes/README.md b/packages/remix-dev/.changes/README.md new file mode 100644 index 00000000000..84a76abac76 --- /dev/null +++ b/packages/remix-dev/.changes/README.md @@ -0,0 +1,23 @@ +# Changes Directory + +This directory contains change files that will be processed during releases. + +## Adding a Change + +See the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files. + +## Quick Reference + +Create a file with the pattern: + +``` +[major|minor|patch].short-description.md +``` + +Examples: + +- `major.breaking-api-change.md` +- `minor.add-new-feature.md` +- `patch.fix-bug.md` + +The content will be added to the CHANGELOG during release. diff --git a/packages/response/.changes/README.md b/packages/response/.changes/README.md new file mode 100644 index 00000000000..84a76abac76 --- /dev/null +++ b/packages/response/.changes/README.md @@ -0,0 +1,23 @@ +# Changes Directory + +This directory contains change files that will be processed during releases. + +## Adding a Change + +See the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files. + +## Quick Reference + +Create a file with the pattern: + +``` +[major|minor|patch].short-description.md +``` + +Examples: + +- `major.breaking-api-change.md` +- `minor.add-new-feature.md` +- `patch.fix-bug.md` + +The content will be added to the CHANGELOG during release. diff --git a/packages/route-pattern/.changes/README.md b/packages/route-pattern/.changes/README.md new file mode 100644 index 00000000000..84a76abac76 --- /dev/null +++ b/packages/route-pattern/.changes/README.md @@ -0,0 +1,23 @@ +# Changes Directory + +This directory contains change files that will be processed during releases. + +## Adding a Change + +See the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files. + +## Quick Reference + +Create a file with the pattern: + +``` +[major|minor|patch].short-description.md +``` + +Examples: + +- `major.breaking-api-change.md` +- `minor.add-new-feature.md` +- `patch.fix-bug.md` + +The content will be added to the CHANGELOG during release. diff --git a/packages/session-middleware/.changes/README.md b/packages/session-middleware/.changes/README.md new file mode 100644 index 00000000000..84a76abac76 --- /dev/null +++ b/packages/session-middleware/.changes/README.md @@ -0,0 +1,23 @@ +# Changes Directory + +This directory contains change files that will be processed during releases. + +## Adding a Change + +See the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files. + +## Quick Reference + +Create a file with the pattern: + +``` +[major|minor|patch].short-description.md +``` + +Examples: + +- `major.breaking-api-change.md` +- `minor.add-new-feature.md` +- `patch.fix-bug.md` + +The content will be added to the CHANGELOG during release. diff --git a/packages/session/.changes/README.md b/packages/session/.changes/README.md new file mode 100644 index 00000000000..84a76abac76 --- /dev/null +++ b/packages/session/.changes/README.md @@ -0,0 +1,23 @@ +# Changes Directory + +This directory contains change files that will be processed during releases. + +## Adding a Change + +See the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files. + +## Quick Reference + +Create a file with the pattern: + +``` +[major|minor|patch].short-description.md +``` + +Examples: + +- `major.breaking-api-change.md` +- `minor.add-new-feature.md` +- `patch.fix-bug.md` + +The content will be added to the CHANGELOG during release. diff --git a/packages/static-middleware/.changes/README.md b/packages/static-middleware/.changes/README.md new file mode 100644 index 00000000000..84a76abac76 --- /dev/null +++ b/packages/static-middleware/.changes/README.md @@ -0,0 +1,23 @@ +# Changes Directory + +This directory contains change files that will be processed during releases. + +## Adding a Change + +See the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files. + +## Quick Reference + +Create a file with the pattern: + +``` +[major|minor|patch].short-description.md +``` + +Examples: + +- `major.breaking-api-change.md` +- `minor.add-new-feature.md` +- `patch.fix-bug.md` + +The content will be added to the CHANGELOG during release. diff --git a/packages/tar-parser/.changes/README.md b/packages/tar-parser/.changes/README.md new file mode 100644 index 00000000000..84a76abac76 --- /dev/null +++ b/packages/tar-parser/.changes/README.md @@ -0,0 +1,23 @@ +# Changes Directory + +This directory contains change files that will be processed during releases. + +## Adding a Change + +See the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files. + +## Quick Reference + +Create a file with the pattern: + +``` +[major|minor|patch].short-description.md +``` + +Examples: + +- `major.breaking-api-change.md` +- `minor.add-new-feature.md` +- `patch.fix-bug.md` + +The content will be added to the CHANGELOG during release. diff --git a/scripts/changes-preview.js b/scripts/changes-preview.js new file mode 100644 index 00000000000..d1c3e6fcc8b --- /dev/null +++ b/scripts/changes-preview.js @@ -0,0 +1,71 @@ +import { + validateAllChanges, + formatValidationErrors, + getAllReleases, + generateChangelogContent, + generateCommitMessage, +} from './utils/changes.js' + +/** + * Main preview function + */ +function main() { + let validationResult = validateAllChanges() + + if (validationResult.errorCount > 0) { + console.error('❌ Validation failed\n') + console.error(formatValidationErrors(validationResult)) + console.error() + process.exit(1) + } + + let releases = getAllReleases() + + if (releases.length === 0) { + console.log('📭 No packages have changes to release.\n') + process.exit(0) + } + + console.log('📦 CHANGES') + console.log() + console.log(`${releases.length} package${releases.length === 1 ? '' : 's'} with changes:\n`) + + for (let release of releases) { + console.log( + ` • ${release.packageName}: ${release.currentVersion} → ${release.nextVersion} (${release.bump} bump)`, + ) + for (let change of release.changes) { + console.log(` - ${change.file}`) + } + console.log() + } + + console.log('📝 CHANGELOG PREVIEW') + console.log() + + let now = new Date() + for (let release of releases) { + console.log(`${release.packageName}/CHANGELOG.md:`) + console.log() + console.log(generateChangelogContent(release, now)) + } + + console.log('💾 COMMIT MESSAGE') + console.log() + console.log(generateCommitMessage(releases)) + console.log() + + console.log('🏷️ GIT TAGS') + console.log() + for (let release of releases) { + console.log(`${release.packageName}@${release.nextVersion}`) + } + console.log() + + console.log('🚀 VERSION COMMAND') + console.log() + console.log('pnpm changes:version') + console.log() +} + +main() diff --git a/scripts/changes-validate.js b/scripts/changes-validate.js new file mode 100755 index 00000000000..ac9dca2e830 --- /dev/null +++ b/scripts/changes-validate.js @@ -0,0 +1,23 @@ +import { validateAllChanges, formatValidationErrors } from './utils/changes.js' + +/** + * Validates all change files in the repository + * Exits with code 1 if any validation errors are found + */ +function main() { + console.log('🔍 Validating change files...\n') + + let validationResult = validateAllChanges() + + if (validationResult.errorCount === 0) { + console.log('✅ All change files are valid!\n') + process.exit(0) + } + + console.error('❌ Validation failed\n') + console.error(formatValidationErrors(validationResult)) + console.error() + process.exit(1) +} + +main() diff --git a/scripts/changes-version.js b/scripts/changes-version.js new file mode 100644 index 00000000000..34279f2c5e6 --- /dev/null +++ b/scripts/changes-version.js @@ -0,0 +1,169 @@ +import * as fs from 'node:fs' +import * as path from 'node:path' +import { + validateAllChanges, + formatValidationErrors, + getAllReleases, + generateChangelogContent, + generateCommitMessage, +} from './utils/changes.js' +import { getPackageFile, getPackageDir } from './utils/packages.js' +import { readJson, writeJson, readFile, writeFile } from './utils/fs.js' +import { logAndExec } from './utils/process.js' + +/** + * Updates package.json version + * @param {string} packageName + * @param {string} newVersion + */ +function updatePackageJson(packageName, newVersion) { + let packageJsonPath = getPackageFile(packageName, 'package.json') + let packageJson = readJson(packageJsonPath) + packageJson.version = newVersion + writeJson(packageJsonPath, packageJson) + console.log(` ✓ Updated package.json to ${newVersion}`) +} + +/** + * Updates CHANGELOG.md with new content + * @param {string} packageName + * @param {string} newContent + */ +function updateChangelog(packageName, newContent) { + let changelogPath = getPackageFile(packageName, 'CHANGELOG.md') + let existingChangelog = readFile(changelogPath) + + // Find where to insert (after the heading and intro, before first ## version) + let lines = existingChangelog.split('\n') + let insertIndex = 0 + + // Skip past the initial # heading and introductory text + for (let i = 0; i < lines.length; i++) { + if (lines[i].startsWith('## ')) { + insertIndex = i + break + } + } + + // Insert new content + lines.splice(insertIndex, 0, newContent) + writeFile(changelogPath, lines.join('\n')) + console.log(` ✓ Updated CHANGELOG.md`) +} + +/** + * Deletes all change files (except README.md) + * @param {string} packageName + */ +function deleteChangeFiles(packageName) { + let changesDir = path.join(getPackageDir(packageName), '.changes') + let files = fs.readdirSync(changesDir) + let changeFiles = files.filter((file) => file !== 'README.md' && file.endsWith('.md')) + + for (let file of changeFiles) { + let filePath = path.join(changesDir, file) + fs.unlinkSync(filePath) + } + + console.log(` ✓ Deleted ${changeFiles.length} change file(s)`) +} + +/** + * Main version function + */ +function main() { + let skipCommit = process.argv.includes('--no-commit') + + console.log('🔍 Validating change files...\n') + + let validationResult = validateAllChanges() + if (validationResult.errorCount > 0) { + console.error('❌ Validation failed\n') + console.error(formatValidationErrors(validationResult)) + console.error() + process.exit(1) + } + + let releases = getAllReleases() + if (releases.length === 0) { + console.log('📭 No packages have changes to release.\n') + process.exit(0) + } + + console.log('✅ Validation passed!\n') + console.log('═'.repeat(80)) + console.log(skipCommit ? '📦 UPDATING VERSION' : '📦 PREPARING RELEASE') + console.log('═'.repeat(80)) + console.log() + + let now = new Date() + + // Process each package + for (let release of releases) { + console.log(`📦 ${release.packageName}: ${release.currentVersion} → ${release.nextVersion}`) + + // Update package.json + updatePackageJson(release.packageName, release.nextVersion) + + // Update CHANGELOG.md + let changelogContent = generateChangelogContent(release, now) + updateChangelog(release.packageName, changelogContent) + + // Delete change files + deleteChangeFiles(release.packageName) + + console.log() + } + + if (skipCommit) { + // Success message for --no-commit + console.log('═'.repeat(80)) + console.log('✅ VERSION UPDATED') + console.log('═'.repeat(80)) + console.log() + console.log('Files have been updated. Review the changes, then manually commit and tag:') + console.log() + let commitMessage = generateCommitMessage(releases) + console.log(` git add .`) + console.log(` git commit -m "${commitMessage.split('\n').join('\\n')}"`) + for (let release of releases) { + let tag = `${release.packageName}@${release.nextVersion}` + console.log(` git tag ${tag}`) + } + console.log() + } else { + // Stage all changes + console.log('📋 Staging changes...') + logAndExec('git add .') + console.log() + + // Create commit + let commitMessage = generateCommitMessage(releases) + console.log('💾 Creating commit...') + logAndExec(`git commit -m "${commitMessage.split('\n').join('\\n')}"`) + console.log() + + // Create tags + console.log('🏷️ Creating tags...') + for (let release of releases) { + let tag = `${release.packageName}@${release.nextVersion}` + logAndExec(`git tag ${tag}`) + console.log(` ✓ Created tag: ${tag}`) + } + console.log() + + // Success message + console.log('═'.repeat(80)) + console.log('✅ RELEASE PREPARED') + console.log('═'.repeat(80)) + console.log() + console.log('Release commit and tags have been created locally.') + console.log() + console.log('To push the release, run:') + console.log() + console.log(' git push && git push --tags') + console.log() + } +} + +main() diff --git a/scripts/tag-release.js b/scripts/tag-release.js deleted file mode 100644 index 082b2e5f009..00000000000 --- a/scripts/tag-release.js +++ /dev/null @@ -1,106 +0,0 @@ -import * as cp from 'node:child_process' - -import { readFile, readJson, writeFile, writeJson } from './utils/fs.js' -import { getPackageFile } from './utils/packages.js' -import { logAndExec } from './utils/process.js' -import { getNextVersion } from './utils/semver.js' - -let rawArgs = process.argv.slice(2) - -if (rawArgs.length === 0) { - console.error('Usage:') - console.error(' node tag-release.js ') - console.error(' node tag-release.js [ ...]') - process.exit(1) -} - -/** @typedef {{ packageName: string, releaseType: string }} ReleaseInput */ - -/** @type {ReleaseInput[]} */ -let inputs = [] - -if (rawArgs.length === 2 && !rawArgs[0].includes('@') && !rawArgs[1].includes('@')) { - // node tag-release.js - inputs.push({ packageName: rawArgs[0], releaseType: rawArgs[1] }) -} else { - // node tag-release.js [ ...] - for (let arg of rawArgs) { - let idx = arg.indexOf('@') - if (idx <= 0 || idx === arg.length - 1) { - console.error(`Invalid argument: "${arg}"`) - console.error('Each argument must be in the form ') - process.exit(1) - } - let packageName = arg.slice(0, idx) - let releaseType = arg.slice(idx + 1) - inputs.push({ packageName, releaseType }) - } -} - -// 1) Ensure git staging area is clean -let status = cp.execSync('git status --porcelain').toString() -if (status !== '') { - console.error('Git staging area is not clean') - process.exit(1) -} - -/** @type {{ packageName: string, currentVersion: string, nextVersion: string, tag: string }[]} */ -let releases = [] - -// 2) For each package, compute next version, update files, and stage changes -for (let { packageName, releaseType } of inputs) { - let packageJsonFile = getPackageFile(packageName, 'package.json') - let packageJson = readJson(packageJsonFile) - let currentVersion = packageJson.version - let nextVersion = getNextVersion(currentVersion, releaseType) - let tag = `${packageName}@${nextVersion}` - - console.log(`Tagging release ${tag} ...`) - - // 2a) Update package.json with the new release version - writeJson(packageJsonFile, { ...packageJson, version: nextVersion }) - logAndExec(`git add ${packageJsonFile}`) - - // 2b) Update jsr.json (if applicable) with the new release version - // let jsrJsonFile = getPackageFile(packageName, 'jsr.json'); - // if (fileExists(jsrJsonFile)) { - // let jsrJson = readJson(jsrJsonFile); - // writeJson(jsrJsonFile, { ...jsrJson, version: nextVersion }); - // logAndExec(`git add ${jsrJsonFile}`); - // } - - // 2c) Swap out "## Unreleased" in CHANGELOG.md with the new release version + date - let changelogFile = getPackageFile(packageName, 'CHANGELOG.md') - let changelog = readFile(changelogFile) - let match = /^## Unreleased\n/m.exec(changelog) - if (match) { - let [today] = new Date().toISOString().split('T') - - changelog = - changelog.slice(0, match.index) + - `## v${nextVersion} (${today})\n` + - changelog.slice(match.index + match[0].length) - - writeFile(changelogFile, changelog) - logAndExec(`git add ${changelogFile}`) - } - - releases.push({ packageName, currentVersion, nextVersion, tag }) -} - -// 3) Commit and create one tag per release -let commitTitle = - releases.length === 1 - ? `Release ${releases[0].tag}` - : `Release ${releases.map((r) => r.tag).join(', ')}` -let commitBody = releases - .map((r) => `- ${r.packageName}: ${r.currentVersion} -> ${r.nextVersion}`) - .join('\n') - -logAndExec(`git commit -m "${commitTitle}" -m "${commitBody}"`) - -for (let r of releases) { - logAndExec(`git tag ${r.tag}`) -} - -console.log() diff --git a/scripts/utils/changes.js b/scripts/utils/changes.js index 39f443a09e3..bb1ad6cbbfb 100644 --- a/scripts/utils/changes.js +++ b/scripts/utils/changes.js @@ -1,45 +1,323 @@ -import { getPackageFile } from './packages.js' -import { fileExists, readFile } from './fs.js' +import * as fs from 'node:fs' +import * as path from 'node:path' +import { getAllPackageNames, getPackageDir, getPackageFile } from './packages.js' +import { readJson } from './fs.js' +import { getNextVersion } from './semver.js' -/** @typedef {{ version: string; date?: Date; body: string }} Changes */ -/** @typedef {Record} AllChanges */ +/** + * @typedef {{ + * package: string + * file: string + * error: string + * }} ValidationError + */ -/** @type (packageName: string) => AllChanges | null */ -export function getAllChanges(packageName) { - let changelogFile = getPackageFile(packageName, 'CHANGELOG.md') +/** + * Valid change file name pattern: (major|minor|patch).description.md + * @type {RegExp} + */ +const CHANGE_FILE_PATTERN = /^(major|minor|patch)\..+\.md$/ - if (!fileExists(changelogFile)) { - return null +/** + * @typedef {{ + * errorCount: number + * errorsByPackage: Record + * }} ValidationResult + */ + +/** + * Formats validation errors for display + * @param {ValidationResult} validationResult + * @returns {string} Formatted error message + */ +export function formatValidationErrors(validationResult) { + let lines = [] + + for (let [packageName, packageErrors] of Object.entries(validationResult.errorsByPackage)) { + lines.push(`📦 ${packageName}:`) + for (let error of packageErrors) { + lines.push(` ${error.file}: ${error.error}`) + } + lines.push('') + } + + let packageCount = Object.keys(validationResult.errorsByPackage).length + lines.push( + `Found ${validationResult.errorCount} error${validationResult.errorCount === 1 ? '' : 's'} in ${packageCount} package${packageCount === 1 ? '' : 's'}`, + ) + + return lines.join('\n') +} + +/** + * Validates all change files across all packages + * @returns {ValidationResult} Validation result with error count and errors grouped by package + */ +export function validateAllChanges() { + let packageNames = getAllPackageNames() + /** @type {Record} */ + let errorsByPackage = {} + let errorCount = 0 + + for (let packageName of packageNames) { + let packageErrors = validatePackageChanges(packageName) + if (packageErrors.length > 0) { + errorsByPackage[packageName] = packageErrors + errorCount += packageErrors.length + } + } + + return { errorCount, errorsByPackage } +} + +/** + * Validates change files for a single package + * @param {string} packageName + * @returns {ValidationError[]} + */ +export function validatePackageChanges(packageName) { + let packageDir = getPackageDir(packageName) + let changesDir = path.join(packageDir, '.changes') + let errors = [] + + // Changes directory should exist (with at least README.md) + if (!fs.existsSync(changesDir)) { + errors.push({ + package: packageName, + file: '.changes/', + error: 'Changes directory does not exist', + }) + return errors + } + + // README.md should exist in .changes directory so it persists between releases + let readmePath = path.join(changesDir, 'README.md') + if (!fs.existsSync(readmePath)) { + errors.push({ + package: packageName, + file: '.changes/README.md', + error: 'README.md is missing from .changes directory', + }) + } + + // Read all files in .changes directory + let files = fs.readdirSync(changesDir) + + // Filter out README.md and validate the rest + let changeFiles = files.filter((file) => file !== 'README.md') + + for (let file of changeFiles) { + // Check if it's a markdown file + if (!file.endsWith('.md')) { + errors.push({ + package: packageName, + file, + error: 'Change files must have .md extension', + }) + continue + } + + // Check if it matches the required pattern + if (!CHANGE_FILE_PATTERN.test(file)) { + errors.push({ + package: packageName, + file, + error: + 'Change file name must start with "major.", "minor.", or "patch." (e.g. "minor.add-feature.md")', + }) + continue + } + + // Check if file is not empty + let filePath = path.join(changesDir, file) + let content = fs.readFileSync(filePath, 'utf-8').trim() + + if (content.length === 0) { + errors.push({ + package: packageName, + file, + error: 'Change file cannot be empty', + }) + continue + } + + // Check if first line starts with a bullet point + let firstLine = content.split('\n')[0].trim() + if (firstLine.startsWith('- ') || firstLine.startsWith('* ')) { + errors.push({ + package: packageName, + file, + error: + 'Change file should not start with a bullet point (- or *). The bullet will be added automatically in the CHANGELOG. Just write the text directly.', + }) + continue + } + } + + return errors +} + +/** + * Gets all change files for a package + * @param {string} packageName + * @returns {{ file: string; bump: 'major' | 'minor' | 'patch'; content: string }[]} + */ +export function getPackageChangeFiles(packageName) { + let packageDir = getPackageDir(packageName) + let changesDir = path.join(packageDir, '.changes') + + if (!fs.existsSync(changesDir)) { + return [] + } + + let files = fs.readdirSync(changesDir) + let changeFiles = files.filter((file) => file !== 'README.md' && file.endsWith('.md')) + + return changeFiles + .filter((file) => CHANGE_FILE_PATTERN.test(file)) + .map((file) => { + let filePath = path.join(changesDir, file) + let content = fs.readFileSync(filePath, 'utf-8').trim() + let bump = /** @type {'major' | 'minor' | 'patch'} */ (file.split('.')[0]) + + return { + file, + bump, + content, + } + }) +} + +/** + * Gets all packages that have change files + * @returns {string[]} + */ +export function getPackagesWithChanges() { + let packageNames = getAllPackageNames() + return packageNames.filter((packageName) => { + let changeFiles = getPackageChangeFiles(packageName) + return changeFiles.length > 0 + }) +} + +/** + * @typedef {{ + * packageName: string + * currentVersion: string + * nextVersion: string + * bump: 'major' | 'minor' | 'patch' + * changes: Array<{ file: string; content: string }> + * }} PackageRelease + */ + +/** + * Determines the highest severity bump type + * @param {Array<'major' | 'minor' | 'patch'>} bumps + * @returns {'major' | 'minor' | 'patch'} + */ +export function getHighestBump(bumps) { + if (bumps.includes('major')) return 'major' + if (bumps.includes('minor')) return 'minor' + return 'patch' +} + +/** + * Formats a changelog entry from change file content + * @param {string} content + * @returns {string} + */ +export function formatChangelogEntry(content) { + let lines = content.trim().split('\n') + + if (lines.length === 1) { + return `- ${lines[0]}` + } + + // Multi-line: first line is bullet, rest are indented + let [firstLine, ...restLines] = lines + let formatted = [`- ${firstLine}`] + + for (let line of restLines) { + // Add proper indentation for continuation lines + formatted.push(line ? ` ${line}` : '') } - let changelog = readFile(changelogFile) - let parser = /^## ([a-z\d\.\-]+)(?: \(([^)]+)\))?$/gim + return formatted.join('\n') +} + +/** + * Generates changelog content for a package release + * @param {PackageRelease} release + * @param {Date} date + * @returns {string} + */ +export function generateChangelogContent(release, date) { + let dateStr = date.toISOString().split('T')[0] // YYYY-MM-DD + let lines = [] + + lines.push(`## v${release.nextVersion} (${dateStr})`) + lines.push('') - /** @type {AllChanges} */ - let result = {} + // Sort changes alphabetically by filename for consistency + let sortedChanges = [...release.changes].sort((a, b) => a.file.localeCompare(b.file)) - let match - while ((match = parser.exec(changelog))) { - let [_, versionString, dateString] = match - let lastIndex = parser.lastIndex - let version = versionString.startsWith('v') ? versionString.slice(1) : versionString - let date = dateString ? new Date(dateString) : undefined - let nextMatch = parser.exec(changelog) - let body = changelog.slice(lastIndex, nextMatch ? nextMatch.index : undefined).trim() - result[version] = { version, date, body } - parser.lastIndex = lastIndex + for (let change of sortedChanges) { + lines.push(formatChangelogEntry(change.content)) + lines.push('') } - return result + return lines.join('\n') } -/** @type (packageName: string, version: string) => Changes | null */ -export function getChanges(packageName, version) { - let allChanges = getAllChanges(packageName) +/** + * Gets all releases that would be prepared + * @returns {PackageRelease[]} + */ +export function getAllReleases() { + let packageNames = getPackagesWithChanges() + let releases = [] - if (allChanges !== null) { - return allChanges[version] ?? null + for (let packageName of packageNames) { + let packageJsonPath = getPackageFile(packageName, 'package.json') + let packageJson = readJson(packageJsonPath) + let currentVersion = packageJson.version + + let changeFiles = getPackageChangeFiles(packageName) + let bumps = changeFiles.map((cf) => cf.bump) + let bump = getHighestBump(bumps) + let nextVersion = getNextVersion(currentVersion, bump) + + releases.push({ + packageName, + currentVersion, + nextVersion, + bump, + changes: changeFiles.map((cf) => ({ + file: cf.file, + content: cf.content, + })), + }) } - return null + return releases +} + +/** + * Generates the commit message for all releases + * @param {PackageRelease[]} releases + * @returns {string} + */ +export function generateCommitMessage(releases) { + // Subject line + let subject = + releases.length === 1 + ? `Release ${releases[0].packageName}@${releases[0].nextVersion}` + : `Release ${releases.map((r) => `${r.packageName}@${r.nextVersion}`).join(', ')}` + + // Body with version changes + let body = releases + .map((r) => `- ${r.packageName}: ${r.currentVersion} -> ${r.nextVersion}`) + .join('\n') + + return `${subject}\n\n${body}` } diff --git a/scripts/view-changes.js b/scripts/view-changes.js deleted file mode 100644 index 7a85dba7164..00000000000 --- a/scripts/view-changes.js +++ /dev/null @@ -1,44 +0,0 @@ -import { getChanges } from './utils/changes.js' -import { getAllPackageNames, packageExists } from './utils/packages.js' - -/** @type {(packageName: string, changes: import('./utils/changes.js').Changes) => void} */ -function printPackageChanges(packageName, changes) { - console.log(`📦 ${packageName}`) - console.log('─'.repeat(50)) - console.log(changes.body) - console.log() -} - -let packageName = process.argv[2] - -if (packageName) { - if (!packageExists(packageName)) { - console.error(`Error: Package "${packageName}" not found in ./packages`) - process.exit(1) - } - - let changes = getChanges(packageName, 'Unreleased') - - if (changes) { - printPackageChanges(packageName, changes) - } else { - console.log(`No pending changes found for package "${packageName}"`) - console.log() - } -} else { - let hasChanges = false - - let packageNames = getAllPackageNames() - packageNames.forEach((packageName) => { - let changes = getChanges(packageName, 'Unreleased') - if (changes) { - hasChanges = true - printPackageChanges(packageName, changes) - } - }) - - if (!hasChanges) { - console.log('No packages have pending changes') - console.log() - } -}