Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --all option for change command #891

Merged
merged 4 commits into from
Jul 13, 2023
Merged
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
7 changes: 7 additions & 0 deletions change/beachball-9bdf704a-61ed-4b77-8dfa-26bc583976fa.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Add --all option for `change` command",
"packageName": "beachball",
"email": "elcraig@microsoft.com",
"dependentChangeType": "patch"
}
74 changes: 57 additions & 17 deletions docs/cli/change.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,43 @@ $ beachball change

### Options

See the [options page](./options).
Some [general options](./options) including `--branch` and `--scope` also apply for this command.

### Walkthrough
| Option | Alias | Default | Description |
| ------------- | ----- | -------------------- | --------------------------------------------------------------------------------- |
| `--all` | | false | Generate change files for all packages |
| `--message` | `-m` | (interactive prompt) | Description for all change files |
| `--no-commit` | | false | Stage the change files rather than committing |
| `--package` | | (changed packages) | Generate change files for these packages (option can be specified multiple times) |
| `--type` | | (interactive prompt) | Type for all the change files (must be valid for each package) |

### Examples

Basic interactive prompt (see [walkthrough](#prompt-walkthrough) for details):

```
beachball change
```

Skip the interactive prompt by specifying a message and type for all changed packages:

```
beachball change --type patch --message 'some message'
```

Generate change file for specific package(s), regardless of changes, and even if a change file already exists for the package in this branch. Each package must be specified with a separate `--package` option. (You can also use the `--message` and `--type` options here.)

```
beachball change --package foo --package bar
```

Generate change files for all packages, regardless of changes. This would most often be used for build config updates which only touch a shared config file, but actually impact the output of all packages.

```
beachball change --all --type patch --message 'update build output settings'
```

### Prompt walkthrough

If you have changes that are not committed yet (i.e. `git status` reports changes), then `beachball change` will warn you about these:

Expand All @@ -31,28 +65,34 @@ There are uncommitted changes in your repository. Please commit these files firs

Make sure to commit _all_ changes before proceeding with the `change` command.

Now we'll commit the changes we made and run `beachball change` again:
After committing, run `beachball change`:

```
$ beachball change
Defaults to "origin/master"
Checking for changes against "origin/master"

Please describe the changes for: single
? Describe changes (type or choose one) ›
adding a new file
Validating options and change files...
Checking for changes against "origin/main"
Found changes in the following packages:
some-pkg
```

First, it will ask for a **description** of the change. You can enter any text, but `beachball` will also provide a list of recent commit messages to choose from.
For each package, the prompt will start by asking for a change **type**. This should be chosen based on [semantic versioning rules](https://semver.org/) because it determines how to update the package version. If the change doesn't affect the published package at all (e.g. you just updated some comments), choose `none`.

> 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.
```
Please describe the changes for: some-pkg
? Change type › - Use arrow-keys. Return to submit.
❯ Patch - bug fixes; no backwards incompatible changes.
Minor - small feature; backwards compatible changes.
None - this change does not affect the published package in any way.
Major - major feature; breaking changes.
```

Next, the form will ask for a change **type**. This should be chosen based on [semantic versioning rules](https://semver.org/) because it determines how to update the package version. If the change doesn't affect the published package at all (e.g. you just updated some comments), choose `none`.
Next, it asks for a **description** of the change. You can type any text or choose from a list of recent commit messages.

```bash
? Change type › - Use arrow-keys. Return to submit.
❯ Patch - bug fixes; no backwards incompatible changes.
Minor - small feature; backwards compatible changes.
None - this change does not affect the published package in any way.
Major - major feature; breaking changes.
> 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.

```
Please describe the changes for: some-pkg
? Describe changes (type or choose one) ›
adding a new file
```
17 changes: 6 additions & 11 deletions docs/cli/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,14 @@ For the latest full list of supported options, see `CliOptions` [in this file](h

These apply to most CLI commands.

| Option | Alias | Default | Description |
| ---------- | ---------- | ----------------- | ------------------------- |
| `--branch` | `-b` | `'origin/master'` | target branch from origin |
| `--help` | `-?`, `-h` | | show help message |
| Option | Alias | Default | Description |
| ------------ | ----- | ---------------------------------------------------------------------------- | ------------------------- |
| `--branch` | `-b` | Detected default branch in default remote, falling back to `'origin/master'` | target branch from origin |
| `--no-fetch` | | | Disable fetching |

## Change options
## `change` options

These options are applicable to the `change` command.

| Option | Alias | Default | Description |
| ----------- | ----- | -------------------- | -------------------------------------------------------------- |
| `--message` | `-m` | (interactive prompt) | Description for all change files |
| `--type` | | (interactive prompt) | Type for all the change files (must be valid for each package) |
See the [`change` page](./change).

## Bumping and publishing options

Expand Down
9 changes: 9 additions & 0 deletions src/__e2e__/getChangedPackages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,4 +192,13 @@ describe('getChangedPackages', () => {
expect(changedPackagesB).toStrictEqual([]);
expect(changedPackagesRoot).toStrictEqual(['@workspace-a/foo']);
});

it('returns all packages with --all option', () => {
repositoryFactory = new RepositoryFactory('monorepo');
const repo = repositoryFactory.cloneRepository();
const options = { all: true, fetch: false, path: repo.rootPath, branch: defaultBranchName } as BeachballOptions;
const packageInfos = getPackageInfos(repo.rootPath);

expect(getChangedPackages(options, packageInfos).sort()).toStrictEqual(['a', 'b', 'bar', 'baz', 'foo']);
});
});
59 changes: 43 additions & 16 deletions src/changefile/getChangedPackages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,51 @@ function getMatchingPackageInfo(
}

/**
* Gets all the changed package names, regardless of the change files
* Determines whether the package is included in the list of potentially-changed published packages,
* based on private flags and scopedPackages.
*/
function isPackageIncluded(
packageInfo: PackageInfo | undefined,
scopedPackages: string[]
): { isIncluded: boolean; reason: string } {
const reason = !packageInfo
? 'no corresponding package found'
: packageInfo.private
? `${packageInfo.name} is private`
: packageInfo.combinedOptions.shouldPublish === false
? `${packageInfo.name} has beachball.shouldPublish=false`
: !scopedPackages.includes(packageInfo.name)
? `${packageInfo.name} is out of scope`
: ''; // not ignored

return { isIncluded: !reason, reason };
}

/**
* Gets all the changed package names, regardless of the change files.
* If `options.all` is set, returns all the packages in scope, regardless of whether they've changed.
*/
function getAllChangedPackages(options: BeachballOptions, packageInfos: PackageInfos): string[] {
const { branch, path: cwd, verbose } = options;
const { branch, path: cwd, verbose, all } = options;

const verboseLog = (msg: string) => verbose && console.log(msg);
const logIgnored = (file: string, reason: string) => verboseLog(` - ~~${file}~~ (${reason})`);
const logIncluded = (file: string) => verboseLog(` - ${file}`);

const scopedPackages = getScopedPackages(options, packageInfos);

// If --all is set, return all the packages in scope rather than looking at which files changed
if (all) {
verboseLog('--all option was provided, so including all packages that are in scope (regardless of changes)');
return Object.values(packageInfos)
.filter(pkg => {
const { isIncluded, reason } = isPackageIncluded(pkg, scopedPackages);
verboseLog(isIncluded ? ` - ${pkg.name}` : ` - ~~${pkg.name}~~ (${reason.replace(`${pkg.name} `, '')})`);
return isIncluded;
})
.map(pkg => pkg.name);
}

const changes = [...(getChanges(branch, cwd) || []), ...(getStagedChanges(cwd) || [])];
verboseLog(`Found ${count(changes.length, 'changed file')} in branch "${branch}" (before filtering)`);

Expand All @@ -66,26 +102,17 @@ function getAllChangedPackages(options: BeachballOptions, packageInfos: PackageI
// and whether that package is in scope and not private
const includedPackages = new Set<string>();
let fileCount = 0;
const scopedPackages = getScopedPackages(options, packageInfos);
const packageInfosByPath: { [packageAbsNormalizedPath: string]: PackageInfo } = {};
for (const info of Object.values(packageInfos)) {
packageInfosByPath[path.normalize(path.dirname(info.packageJsonPath))] = info;
}
for (const moddedFile of nonIgnoredChanges) {
const packageInfo = getMatchingPackageInfo(moddedFile, cwd, packageInfosByPath);

const omitReason = !packageInfo
? 'no corresponding package found'
: packageInfo.private
? `${packageInfo.name} is private`
: packageInfo.combinedOptions.shouldPublish === false
? `${packageInfo.name} has beachball.shouldPublish=false`
: !scopedPackages.includes(packageInfo.name)
? `${packageInfo.name} is out of scope`
: ''; // not ignored

if (omitReason) {
logIgnored(moddedFile, omitReason);
const { isIncluded, reason } = isPackageIncluded(packageInfo, scopedPackages);

if (!isIncluded) {
logIgnored(moddedFile, reason);
} else {
includedPackages.add(packageInfo!.name);
fileCount++;
Expand All @@ -101,7 +128,7 @@ function getAllChangedPackages(options: BeachballOptions, packageInfos: PackageI
}

/**
* Gets all the changed packages, accounting for change files
* Gets all the changed packages which do not already have a change file
*/
export function getChangedPackages(options: BeachballOptions, packageInfos: PackageInfos): string[] {
const { path: cwd, branch } = options;
Expand Down
3 changes: 1 addition & 2 deletions src/commands/change.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import { getChangedPackages } from '../changefile/getChangedPackages';
import { getPackageGroups } from '../monorepo/getPackageGroups';

export async function change(options: BeachballOptions): Promise<void> {
const { branch, path: cwd } = options;
const { package: specificPackage } = options;
const { branch, path: cwd, package: specificPackage } = options;

const packageInfos = getPackageInfos(cwd);
const packageGroups = getPackageGroups(packageInfos, cwd, options.groups);
Expand Down
9 changes: 6 additions & 3 deletions src/packageManager/listPackageVersions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,19 @@ export async function listPackageVersionsByTag(
packageInfos: PackageInfo[],
tag: string | undefined,
options: NpmOptions
): Promise<{ [pkg: string]: string | undefined }> {
): Promise<{ [pkg: string]: string }> {
const limit = pLimit(NPM_CONCURRENCY);
const versions: { [pkg: string]: string | undefined } = {};
const versions: { [pkg: string]: string } = {};

await Promise.all(
packageInfos.map(pkg =>
limit(async () => {
const info = await getNpmPackageInfo(pkg.name, options);
const npmTag = tag || pkg.combinedOptions.tag || pkg.combinedOptions.defaultNpmTag;
versions[pkg.name] = (npmTag && info['dist-tags']?.[npmTag]) || undefined;
const version = npmTag && info['dist-tags']?.[npmTag];
if (version) {
versions[pkg.name] = version;
}
})
)
);
Expand Down
4 changes: 3 additions & 1 deletion src/validation/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ export function validate(

const packageInfos = getPackageInfos(options.path);

if (typeof options.package === 'string' && !packageInfos[options.package]) {
if (options.all && options.package) {
logValidationError('Cannot specify both "all" and "package" options');
} else if (typeof options.package === 'string' && !packageInfos[options.package]) {
logValidationError(`package "${options.package}" was not found`);
} else {
const invalidPackages = Array.isArray(options.package)
Expand Down