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
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ If the package's current version is 1.0.0 or greater and does NOT have a prerele
- `"minor"`: New exported APIs, non-breaking signature changes to exported APIs, or more significant changes to internal logic. (If the package has a `<package path>/etc/*.api.md` file, checking its diff is the easiest way to see exported API changes.)
- `"major"`: Breaking changes to exported APIs (removals or breaking signature changes), critical dependency updates, or behavior changes that might be breaking for the consumer. You MUST confirm with the user before choosing `"major"`.
- `"none"`: None of the changes will impact consumers of the package (e.g. the changes are only to non-exported test-specific files or documentation). If you're not certain, prefer `"patch"`.
- There are additional options `prerelease|premajor|preminor|prepatch`, but you should only use one of these if explicitly requested by the user.
- There are additional prerelease options:
- Use `premajor`, `preminor`, or `prepatch` when the user-facing change should start a prerelease instead of a stable release.
- Use `prerelease` only for packages already on a prerelease version, to continue the prerelease sequence.

#### Case 2: Version is 0.x.y and NOT prerelease

Expand All @@ -132,8 +134,9 @@ If the package's major version is 0 and does NOT have a prerelease suffix, this

#### Case 3: Version IS prerelease

ONLY if the package's current version includes a prerelease suffix, the typical options are `<prerelease|none>` (but you MUST respect `disallowedChangeTypes`):
ONLY if the package's current version includes a prerelease suffix, the typical options are `<prerelease|patch|minor|major|none>` (but you MUST respect `disallowedChangeTypes`):

- `"prerelease"`: Any changes that impact consumers of the package
- `"patch"`, `"minor"`, or `"major"`: Promote or advance the package to a stable version according to semver. Use these when the current prerelease already represents the stable release you want to ship (for example, `2.0.0-rc.0` → `2.0.0`).
- `"none"`: None of the changes will impact consumers of the package (e.g. the changes are only to non-exported test-specific files or documentation). If you're not certain, prefer `"prerelease"`.
- There are additional options `premajor|preminor|prepatch`, but you should only use one of these if explicitly requested by the user or all other change types are disallowed.
- There are additional options `premajor|preminor|prepatch`; use one when intentionally starting a new prerelease target from the current prerelease version, or when all other change types are disallowed.
11 changes: 11 additions & 0 deletions change/change-337f29c3-dd36-424e-a0e6-c3798a3cdf2b.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "beachball",
"type": "minor",
"dependentChangeType": "patch",
"comment": "Add prerelease-to-stable promotion and prerelease change prompts.",
"email": "7559015+janechu@users.noreply.github.com"
}
]
}
8 changes: 4 additions & 4 deletions docs/cli/bump.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ $ 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 |
| `--prerelease-prefix` | prerelease prefix (e.g. `beta`); stable packages with major/minor/patch changes bump as prereleases, while existing prerelease packages can promote to stable |
7 changes: 7 additions & 0 deletions docs/cli/change.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,15 @@ Please describe the changes for: some-pkg
Minor - small feature; backwards compatible changes.
None - this change does not affect the published package in any way.
Major - major feature; breaking changes.
Prepatch - start a prerelease patch.
Preminor - start a prerelease minor.
Premajor - start a prerelease major.
```

`Prepatch`, `preminor`, and `premajor` start prerelease versions. For a package that is already on a
prerelease version, use `prerelease` to continue the prerelease sequence or a stable type (`patch`,
`minor`, or `major`) to move to a stable version according to semver.

Next, it asks for a **description** of the change. You can type any text or choose from a list of recent commit messages.

> Tip: These descriptions will be collated into a changelog when the change is published by `beachball publish`, so think about how to describe your change in a way that's helpful and relevant for consumers of the package.
Expand Down
2 changes: 1 addition & 1 deletion docs/cli/publish.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ 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 |
| `--prerelease-prefix` | | | prerelease prefix (e.g. `beta`); stable packages with major/minor/patch changes bump as prereleases, while existing prerelease packages can promote to stable |
| `--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
13 changes: 13 additions & 0 deletions docs/concepts/change-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,23 @@ The available types follow [semantic versioning](https://semver.org/) convention
| `patch` | Bug fixes or internal changes that don't affect exported API signatures | 1.0.0 → 1.0.1 |
| `minor` | New exported APIs, non-breaking changes to exported API signatures, or significant changes to internal logic | 1.0.0 → 1.1.0 |
| `major` | Breaking changes to exported APIs (removals or breaking signature changes), critical dependency updates, or behavior changes that could break consumers | 1.0.0 → 2.0.0 |
| `prepatch` | Start a prerelease for patch-level changes | 1.0.0 → 1.0.1-beta.0 |
| `preminor` | Start a prerelease for minor-level changes | 1.0.0 → 1.1.0-beta.0 |
| `premajor` | Start a prerelease for major-level changes | 1.0.0 → 2.0.0-beta.0 |
| `none` | Changes that don't affect consumers at all (tests, documentation, internal config) | no bump |

When in doubt between `minor`/`patch` or `patch`/`none`, it's generally best to choose the larger change type.

The prerelease examples assume `prereleasePrefix` is set to `beta`. Without a prefix, semver uses a
numeric prerelease identifier such as `1.0.1-0`.

### Prerelease packages

For packages already on a prerelease version, choose `prerelease` to continue the prerelease sequence.
Choose `patch`, `minor`, or `major` to move to a stable version when the current prerelease already
represents that release (for example, 2.0.0-rc.0 → 2.0.0). Choose `prepatch`, `preminor`, or
`premajor` to start a new prerelease target from the current version.

### Zero-version packages (version 0.x.y)

Packages with a major version of 0 are considered unstable per the semver spec. The conventions are slightly different:
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` | | repo | Prerelease prefix, e.g. `"beta"`. Stable packages with major/minor/patch change types will be bumped as prerelease instead. Existing prerelease packages keep normal semver behavior, so major/minor/patch can promote to stable. |
| `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
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,23 @@ describe('bumpPackageInfoVersion', () => {
expect(bumpInfo.modifiedPackages).toContain(name);
});

it.each<[ChangeType, string]>([
['major', '2.0.0'],
['minor', '2.0.0'],
['patch', '2.0.0'],
])(
'promotes prerelease package to stable for changeType %s when prereleasePrefix is set',
(changeType, expectedVersion) => {
const bumpInfo = bumpPackageInfoVersionWrapper({
changeType,
packageInfo: { version: '2.0.0-rc.0' },
options: { prereleasePrefix: 'rc' },
});
expect(bumpInfo.packageInfos[name].version).toBe(expectedVersion);
expect(bumpInfo.modifiedPackages).toContain(name);
}
);

it('bumps to subsequent prerelease version with existing prefix', () => {
const bumpInfo = bumpPackageInfoVersionWrapper({
changeType: 'prerelease',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ describe('getQuestionsForPackage', () => {
{ title: expect.stringContaining('Minor'), value: 'minor' },
{ title: expect.stringContaining('None'), value: 'none' },
{ title: expect.stringContaining('Major'), value: 'major' },
{ title: expect.stringContaining('Prepatch'), value: 'prepatch' },
{ title: expect.stringContaining('Preminor'), value: 'preminor' },
{ title: expect.stringContaining('Premajor'), value: 'premajor' },
],
message: 'Change type',
name: 'type',
Expand Down Expand Up @@ -74,7 +77,9 @@ describe('getQuestionsForPackage', () => {

it('errors if there are no valid change types for package', () => {
const questions = getQuestionsWrapper({
packageInfo: { beachball: { disallowedChangeTypes: ['major', 'minor', 'patch', 'none'] } },
packageInfo: {
beachball: { disallowedChangeTypes: ['major', 'minor', 'patch', 'none', 'premajor', 'preminor', 'prepatch'] },
},
});
expect(questions).toBeUndefined();
expect(logs.mocks.error).toHaveBeenCalledWith('No valid change types available for package "foo"');
Expand All @@ -85,15 +90,23 @@ describe('getQuestionsForPackage', () => {
packageInfo: { beachball: { disallowedChangeTypes: ['major'] } },
});
const choices = (questions![0].choices as prompts.Choice[]).map(c => c.value as ChangeType);
expect(choices).toEqual(['patch', 'minor', 'none']);
expect(choices).toEqual(['patch', 'minor', 'none', 'prepatch', 'preminor', 'premajor']);
});

it('excludes prerelease bump choices if disallowed', () => {
const questions = getQuestionsWrapper({
packageInfo: { beachball: { disallowedChangeTypes: ['prepatch', 'preminor', 'premajor'] } },
});
const choices = (questions![0].choices as prompts.Choice[]).map(c => c.value as ChangeType);
expect(choices).toEqual(['patch', 'minor', 'none', 'major']);
});

it('allows prerelease change for package with prerelease version', () => {
const questions = getQuestionsWrapper({
packageInfo: { version: '1.0.0-beta.1' },
});
const choices = (questions![0].choices as prompts.Choice[]).map(c => c.value as ChangeType);
expect(choices).toEqual(['prerelease', 'patch', 'minor', 'none', 'major']);
expect(choices).toEqual(['prerelease', 'patch', 'minor', 'none', 'major', 'prepatch', 'preminor', 'premajor']);
});

// this is a bit weird as well, but documenting current behavior
Expand All @@ -102,7 +115,7 @@ describe('getQuestionsForPackage', () => {
packageInfo: { version: '1.0.0-beta.1', beachball: { disallowedChangeTypes: ['prerelease'] } },
});
const choices = (questions![0].choices as prompts.Choice[]).map(c => c.value as ChangeType);
expect(choices).toEqual(['patch', 'minor', 'none', 'major']);
expect(choices).toEqual(['patch', 'minor', 'none', 'major', 'prepatch', 'preminor', 'premajor']);
});

it('excludes the change type question when options.type is specified', () => {
Expand All @@ -115,7 +128,9 @@ describe('getQuestionsForPackage', () => {

it('excludes the change type question with only one valid option', () => {
const questions = getQuestionsWrapper({
packageInfo: { beachball: { disallowedChangeTypes: ['major', 'minor', 'none'] } },
packageInfo: {
beachball: { disallowedChangeTypes: ['major', 'minor', 'none', 'premajor', 'preminor', 'prepatch'] },
},
});
expect(questions).toHaveLength(1);
expect(questions![0].name).toBe('comment');
Expand All @@ -125,7 +140,7 @@ describe('getQuestionsForPackage', () => {
const questions = getQuestionsWrapper({
packageInfo: {
version: '1.0.0-beta.1',
beachball: { disallowedChangeTypes: ['major', 'minor', 'patch', 'none'] },
beachball: { disallowedChangeTypes: ['major', 'minor', 'patch', 'none', 'premajor', 'preminor', 'prepatch'] },
},
});
expect(questions).toHaveLength(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ describe('promptForChange', () => {
...defaultParams(),
packageInfos: makePackageInfos({
foo: {},
bar: { beachball: { disallowedChangeTypes: ['major', 'minor', 'patch', 'none'] } },
bar: {
beachball: { disallowedChangeTypes: ['major', 'minor', 'patch', 'none', 'premajor', 'preminor', 'prepatch'] },
},
}),
});

Expand Down
Loading
Loading