Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Implementation plan: rename `canary` → `prerelease`, simplify ChangeType

This document tracked the plan as it was implemented.

## Decisions (open questions resolved)

1. `prereleasePrefix` defaults to `"prerelease"` (applied in the `prerelease`
command itself rather than in `getDefaultOptions`, to keep `configList`
output minimal). So `beachball prerelease` works with zero configuration.
2. With `identifierBase: false` and the same target version already published,
`beachball prerelease` errors with a message recommending `identifierBase: '0'`.

## High-level changes

- Rename command `canary` → `prerelease`. No alias for `canary`.
- Remove `canaryName` option. `prereleasePrefix` takes over its role (the
suffix used by the prerelease command, e.g. `"beta"`, `"canary"`, `"pr30"`).
- `prereleasePrefix` no longer affects `bump`/`publish`.
- Shrink `ChangeType` to `'patch' | 'minor' | 'major' | 'none'`.
- Migration: change files / CLI types `premajor|preminor|prepatch` are coerced
to `major|minor|patch` with a warning; `prerelease` is a hard error.
- `bump`/`publish` strip an existing prerelease component from
`package.json#version` before applying the change file's bump (assumption E).
- New `getPrereleaseVersion(...)` pure helper; `beachball prerelease` queries
`listPackageVersions` for the highest existing counter.

## Checklist

- [x] Update `ChangeType` in `src/types/ChangeInfo.ts`
- [x] Migration logic for old types in `src/changefile/readChangeFiles.ts`
(warn + coerce `pre*`, error on `prerelease`); also handles
`dependentChangeType`
- [x] CLI `--type` and `--dependent-change-type` migration in
`src/options/getCliOptions.ts` (same coercion + error)
- [x] `bumpPackageInfoVersion`: drop `prereleasePrefix`/`identifierBase` params,
strip prerelease component before `semver.inc` (handles assumption E)
- [x] New `src/bump/getPrereleaseVersion.ts` pure helper + unit tests
- [x] Rename `src/commands/canary.ts` → `src/commands/prerelease.ts`; rewrite
to use `listPackageVersions` + `getPrereleaseVersion`
- [x] `src/cli.ts`: replace `canary` case with `prerelease`
- [x] `src/types/BeachballOptions.ts`: drop `canaryName`
- [x] `src/options/getCliOptions.ts`: drop `canaryName`; derive `tag` from
`prereleasePrefix` for the `prerelease` command
- [x] `src/options/getDefaultOptions.ts`: drop `canaryName` default (default
for `prereleasePrefix` is applied at command level, not here)
- [x] `src/commands/configGet.ts`: drop `canaryName: true`
- [x] `src/help.ts`: replace `--canary-name` docs with `prerelease` section;
remove `--prerelease-prefix` from `bump`/`publish` sections
- [x] `src/changefile/getQuestionsForPackage.ts`: drop "Prerelease" choice
- [x] Changelog rendering: drop `pre*`/`prerelease` `groupNames` entries;
prune snapshot
- [x] Update existing tests (configList snapshot, getCliOptions, bumpInMemory,
bumpPackageInfoVersion, changeTypes, getQuestionsForPackage,
updateRelatedChangeType, writeChangelog, e2e bump)
- [x] Add new tests: - `__tests__/bump/getPrereleaseVersion.test.ts` - `__functional__/commands/prerelease.test.ts` - migration tests in `__functional__/changefile/readChangeFiles.test.ts` - legacy `--type` / `--dependent-change-type` tests in
`__functional__/options/getCliOptions.test.ts`
- [x] Docs: create `docs/cli/prerelease.md`; add to sidebar in
`docs/.vuepress/config.ts`; remove `--prerelease-prefix` rows from
`docs/cli/bump.md` and `docs/cli/publish.md`; rewrite the
`prereleasePrefix` row in `docs/overview/configuration.md`; add
"Prereleases" section to `docs/concepts/change-types.md`. Built with
`yarn docs:build`.
- [x] Generate Beachball change file (major)
- [x] Run `yarn build`, `yarn test`, `yarn lint`
- [ ] `parallel_validation`

## Notes/discoveries

- The single repo fixture has only the package `foo`; tests use that name.
- `createCommandContext` is `@deprecated` and triggers `etc/no-deprecated`;
command implementations and their tests use
`// eslint-disable-next-line etc/no-deprecated -- ...` to suppress.
- `cliOptions.command === 'prerelease'` overrides `--tag`, mirroring previous
canary behavior. `--prerelease-prefix` outside of the `prerelease` command
does NOT affect tag.
- `bumpInMemory.test.ts` previously had four `prereleasePrefix`-driven
scenarios; all are removed since `bump` no longer produces prereleases.
Replaced with a focused test for prerelease → release promotion.
- `updateRelatedChangeType` tests previously expected `preminor` as a fallback
when `minor` was disallowed; with pre\* types removed, the algorithm now
falls back to `patch` instead.
- `writeChangelog` "includes pre\* changes" tests are obsolete; replaced with
one test exercising `major`/`minor`/`patch` headers.
7 changes: 7 additions & 0 deletions change/beachball-e78cf575-a41a-4ddb-a3ba-ff80340e848a.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"packageName": "beachball",
"type": "major",
"dependentChangeType": "patch",
"comment": "Rename `canary` command to `prerelease`, repurpose `prereleasePrefix` (and remove `canaryName`), and shrink `ChangeType` to `patch | minor | major | none`. Old `pre*` change types in change files are auto-coerced (with a warning); `prerelease` change type is now an error. `bump`/`publish` no longer produce prerelease versions and now strip any prerelease component from the current version before bumping (prerelease → release promotion).",
"email": "198982749+Copilot@users.noreply.github.com"
}
1 change: 1 addition & 0 deletions docs/.vuepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export default defineUserConfig({
'/cli/change',
'/cli/check',
'/cli/config',
'/cli/prerelease',
'/cli/publish',
'/cli/sync',
],
Expand Down
7 changes: 3 additions & 4 deletions docs/cli/bump.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ $ beachball bump

[General options](./options) also apply for this command.

| Option | Description |
| --------------------- | -------------------------------------------------------------------------------- |
| `--keep-change-files` | don't delete the change files from disk after bumping |
| `--prerelease-prefix` | prerelease prefix (e.g. `beta`) for packages that will receive a prerelease bump |
| Option | Description |
| --------------------- | ----------------------------------------------------- |
| `--keep-change-files` | don't delete the change files from disk after bumping |
55 changes: 55 additions & 0 deletions docs/cli/prerelease.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
tags:
- cli
category: doc
---

# `prerelease`

Publishes a prerelease version (such as a canary, beta, or per-PR release) for the current change set, **without committing changes back to git or deleting change files**.

```bash
$ beachball prerelease
```

This command is useful for sharing prereleases of in-progress work — for example, a per-PR build that consumers can install to test changes before merging.

## How it works

For each package that has a change file (or that becomes modified due to dependency bumps), `beachball prerelease`:

1. Looks up the change type from the change files (just like `bump` and `publish`).
2. Computes the **target release version** by applying the change type to the current `package.json` version, after stripping any existing prerelease component. (For example, `0.2.0-beta.0` with a `minor` change type produces a target of `0.3.0`.)
3. Queries the npm registry for existing published versions that match `<target>-<prereleasePrefix>.<n>`, finds the highest counter `N`, and publishes `<target>-<prereleasePrefix>.<N+1>`.
4. Publishes to npm under the dist-tag matching `prereleasePrefix` (e.g. `beta`).

If no matching prerelease versions have been published yet, the counter starts from [`identifierBase`](#options).

The change files and `package.json` versions are **not** committed back to git. This means subsequent prerelease runs from the same change set will produce incrementing prerelease versions — useful for iterating on a PR without polluting git history.

## Workflow example

| Step | Command | Result |
| ---- | ---------------------------------------------------------------- | ------------------------------------------------------------------- |
| 1 | A developer creates a change file for a `minor` change to `foo`. | `change/foo-….json` exists; `foo` is at version `1.2.3`. |
| 2 | CI runs `beachball prerelease --prerelease-prefix pr30`. | `foo@1.3.0-pr30.0` is published with dist-tag `pr30`. |
| 3 | The developer pushes another commit to the PR; CI re-runs. | `foo@1.3.0-pr30.1` is published. |
| 4 | The PR is merged; main runs `beachball publish`. | `foo@1.3.0` is published with dist-tag `latest`, normal git commit. |

Step 4 above demonstrates the **prerelease-to-release promotion** behavior of `bump`/`publish`: when a package's current `package.json` version contains a prerelease component (such as if `prerelease` ran on the same branch as `publish` would later run), the prerelease component is stripped before applying the bump, so the user gets the intuitive target release version.

## Options

[General options](./options) also apply for this command.

<!-- prettier-ignore -->
| Option | Default | Description |
| ------ | ------- | ----------- |
| `--prerelease-prefix` | `'prerelease'` | Suffix used for the prerelease version, e.g. `"beta"` produces `1.2.3-beta.0`. The same value is used as the npm dist-tag. |
| `--identifier-base` | `'0'` | Starting counter for prereleases when no matching versions are published yet. `'0'` (default) starts at `.0`, `'1'` starts at `.1`, or `false` omits the numeric counter entirely (in which case re-running on the same target version will error). |

## Migrating from `beachball canary`

This command replaces the old `beachball canary` command. The previous `canaryName` option has been removed; use `prereleasePrefix` instead.

In addition, `bump` and `publish` no longer support producing prerelease versions directly. If you previously used `prereleasePrefix` with `bump`/`publish`, switch to `beachball prerelease` for prerelease publishing.
1 change: 0 additions & 1 deletion docs/cli/publish.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ Publishing automates all the bumping and synchronizing of package versions in th
| `--git-tags`, `--no-git-tags` | | `true` (`--git-tags`) | whether to create git tags for published package versions |
| `--keep-change-files` | | | don't delete the change files from disk after bumping |
| `--message` | `-m` | `'applying package updates'` | custom commit message |
| `--prerelease-prefix` | | | prerelease prefix (e.g. `beta`) for packages that will receive a prerelease bump |
| `--publish`, `--no-publish` | | `true` (`--publish`) | whether to publish to the npm registry |
| `--push`, `--no-push` | | `true` (`--push`) | whether to commit changes and push them back to the git remote |
| `--registry` | `-r` | `'https://registry.npmjs.org'` | npm registry for publishing |
Expand Down
6 changes: 6 additions & 0 deletions docs/concepts/change-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ Packages with a major version of 0 are considered unstable per the semver spec.

Some repos or packages may restrict which change types are allowed using the [`disallowedChangeTypes`](../overview/configuration#options) config option. For example, `major` bumps are often disallowed to ensure coordination of major release efforts. Any disallowed options will be omitted from the interactive prompt, and a change file or `--type` argument that uses a disallowed type will cause an error.

## Prereleases

To publish a prerelease version (such as a canary, beta, or per-PR build), use the [`beachball prerelease`](../cli/prerelease) command. There is no `prerelease` change type — instead, choose one of `patch`, `minor`, `major`, or `none` based on the impact of your changes, and let `beachball prerelease` handle the prerelease versioning.

> **Note:** Older Beachball versions accepted `premajor`, `preminor`, `prepatch`, and `prerelease` as change types. These have been removed; existing change files using `premajor`/`preminor`/`prepatch` are auto-migrated to `major`/`minor`/`patch` (with a deprecation warning), and change files using `prerelease` will produce an error so they can be recreated with the appropriate type.

## Tips for reviewers

Change files show up as part of the PR diff, making it easy to verify the change type during code review. Common things to watch for:
Expand Down
2 changes: 1 addition & 1 deletion docs/overview/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ For the latest full list of supported options, see `RepoOptions` [in this file](
| `ignorePatterns` | `string[]` | | repo | Ignore changes in files matching these glob patterns ([see notes][6]) |
| `npmReadConcurrency` | number | 5 | repo | Maximum concurrency for fetching package versions from the registry (see `concurrency` for write operations) |
| `package` | `string` | | repo | Specifies which package the command relates to (overrides change detection based on `git diff`) |
| `prereleasePrefix` | `string` | | repo | Prerelease prefix, e.g. `"beta"`. Note that if this is specified, packages with change type major/minor/patch will be bumped as prerelease instead. |
| `prereleasePrefix` | `string` | `'prerelease'` | repo | Suffix used by the [`prerelease`](../cli/prerelease) command for prerelease versions, e.g. `"beta"` produces versions like `1.2.3-beta.0`. |
| `packStyle` | `'sequential' \| 'layer'` | `'sequential'` | repo | With `packToPath`, how to organize the tgz files. `'sequential'` uses numeric prefixes to ensure topological ordering. `'layer'` groups the packages into numbered subfolders based on dependency tree layers. |
| `packToPath` | `string` | | repo | Instead of publishing to npm, pack packages to tgz files under the specified path. |
| `publish` | `boolean` | `true` | repo | Whether to publish to npm registry |
Expand Down
35 changes: 12 additions & 23 deletions packages/beachball/src/__e2e__/bump.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,41 +405,30 @@ describe('bump command', () => {
expect(changelogJson3).toBeNull();
});

// Prerelease scenarios are covered in more detail in bumpInMemory.test.ts
it('bumps to prerelease and uses prerelease version for dependents', async () => {
// The previous prerelease-via-bump scenario was removed when prerelease publishing was moved
// to the dedicated `beachball prerelease` command. The prerelease -> release promotion behavior
// (where bumping a package currently on a prerelease version produces the next release) is
// covered by bumpInMemory.test.ts and bumpPackageInfoVersion.test.ts.
it('promotes a prerelease version to a release version when bumping', async () => {
const monorepo: RepoFixture['folders'] = {
packages: {
'pkg-1': { version: '1.0.0' },
'pkg-2': { version: '1.0.0', dependencies: { 'pkg-1': '1.0.0' } },
'pkg-3': { version: '1.0.0', devDependencies: { 'pkg-2': '1.0.0' } },
'pkg-4': { version: '1.0.0', peerDependencies: { 'pkg-3': '1.0.0' } },
'pkg-5': { version: '1.0.0', optionalDependencies: { 'pkg-4': '1.0.0' } },
'pkg-1': { version: '1.0.0-beta.0' },
'pkg-2': { version: '1.0.0', dependencies: { 'pkg-1': '1.0.0-beta.0' } },
},
};
repositoryFactory = new RepositoryFactory({ folders: monorepo });
repo = repositoryFactory.cloneRepository();

const { options, parsedOptions } = getOptions({
bumpDeps: true,
prereleasePrefix: 'beta',
});
generateChangeFiles([{ packageName: 'pkg-1', type: 'prerelease', dependentChangeType: 'prerelease' }], options);
const { options, parsedOptions } = getOptions({ bumpDeps: true });
generateChangeFiles([{ packageName: 'pkg-1', type: 'patch' }], options);
repo.push();

await bumpWrapper(parsedOptions);

const packageInfos = getPackageInfos(parsedOptions);

const newVersion = '1.0.1-beta.0';
expect(packageInfos['pkg-1'].version).toBe(newVersion);
expect(packageInfos['pkg-2'].version).toBe(newVersion);
expect(packageInfos['pkg-3'].version).toBe(newVersion);
expect(packageInfos['pkg-4'].version).toBe(newVersion);

expect(packageInfos['pkg-2'].dependencies!['pkg-1']).toBe(newVersion);
expect(packageInfos['pkg-3'].devDependencies!['pkg-2']).toBe(newVersion);
expect(packageInfos['pkg-4'].peerDependencies!['pkg-3']).toBe(newVersion);
expect(packageInfos['pkg-5'].optionalDependencies!['pkg-4']).toBe(newVersion);
expect(packageInfos['pkg-1'].version).toBe('1.0.1');
expect(packageInfos['pkg-2'].version).toBe('1.0.1');
expect(packageInfos['pkg-2'].dependencies!['pkg-1']).toBe('1.0.1');

const changeFiles = getChangeFiles(options);
expect(changeFiles).toHaveLength(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,46 @@ describe('readChangeFiles', () => {
expect(logs.mocks.warn).not.toHaveBeenCalled();
});

it.each<['premajor' | 'preminor' | 'prepatch', 'major' | 'minor' | 'patch']>([
['premajor', 'major'],
['preminor', 'minor'],
['prepatch', 'patch'],
])('coerces legacy change type %s to %s with a warning', (legacyType, replacement) => {
tempRoot = createTestFileStructureType('monorepo');
const { options, packageInfos, scopedPackages } = getOptionsAndPackages();

generateChangeFiles([{ packageName: 'foo', type: legacyType as 'patch' }], options);
const changeSet = readChangeFiles(options, packageInfos, scopedPackages);

expect(changeSet).toHaveLength(1);
expect(changeSet[0].change.type).toBe(replacement);
expect(logs.mocks.warn).toHaveBeenCalledWith(
expect.stringContaining(`legacy change type "${legacyType}", which has been renamed to "${replacement}"`)
);
});

it('errors on legacy change type "prerelease"', () => {
tempRoot = createTestFileStructureType('monorepo');
const { options, packageInfos, scopedPackages } = getOptionsAndPackages();

generateChangeFiles([{ packageName: 'foo', type: 'prerelease' as 'patch' }], options);

expect(() => readChangeFiles(options, packageInfos, scopedPackages)).toThrow(
/change type "prerelease", which is no longer supported/
);
});

it('coerces legacy dependentChangeType "preminor" to "minor" with a warning', () => {
tempRoot = createTestFileStructureType('monorepo');
const { options, packageInfos, scopedPackages } = getOptionsAndPackages();

generateChangeFiles([{ packageName: 'foo', type: 'minor', dependentChangeType: 'preminor' as 'minor' }], options);
const changeSet = readChangeFiles(options, packageInfos, scopedPackages);

expect(changeSet[0].change.dependentChangeType).toBe('minor');
expect(logs.mocks.warn).toHaveBeenCalledWith(expect.stringContaining('legacy dependentChangeType "preminor"'));
});

it('runs transform.changeFiles functions if provided', () => {
const editedComment = 'Edited comment for testing';
tempRoot = createTestFileStructureType('monorepo');
Expand Down
Loading
Loading