Skip to content

feat(build-cli): New vnext command "modify fluid-deps" #24376

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

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
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
59 changes: 55 additions & 4 deletions build-tools/packages/build-cli/docs/vnext.md
Original file line number Diff line number Diff line change
@@ -4,19 +4,20 @@
Vnext commands are new implementations of standard flub commands using new infrastructure.

* [`flub vnext check latestVersions`](#flub-vnext-check-latestversions)
* [`flub vnext modify fluid-deps`](#flub-vnext-modify-fluid-deps)

## `flub vnext check latestVersions`

Determines if an input version matches a latest minor release version. Intended to be used in the Fluid Framework CI pipeline only.

```
USAGE
$ flub vnext check latestVersions --releaseGroup <value> --version <value> [-v | --quiet]
$ flub vnext check latestVersions -g <value> --version <value> [-v | --quiet]

FLAGS
--releaseGroup=<value> (required) The name of a release group.
--version=<value> (required) The version to check. When running in CI, this value corresponds to the pipeline
trigger branch.
-g, --releaseGroup=<value> (required) The name of a release group.
--version=<value> (required) The version to check. When running in CI, this value corresponds to the
pipeline trigger branch.

LOGGING FLAGS
-v, --verbose Enable verbose logging.
@@ -31,3 +32,53 @@ DESCRIPTION
```

_See code: [src/commands/vnext/check/latestVersions.ts](https://github.com/microsoft/FluidFramework/blob/main/build-tools/packages/build-cli/src/commands/vnext/check/latestVersions.ts)_

## `flub vnext modify fluid-deps`

Update the dependency version that a release group has on another release group. That is, if one or more packages in the release group depend on package A in another release group, then this command will update the dependency range on package A and all other packages in that release group.

```
USAGE
$ flub vnext modify fluid-deps --on <value> [-v | --quiet] [-g <value>...] [--prerelease] [-d ^|~|]

FLAGS
-d, --dependencyRange=<option> [default: ^] Controls the type of dependency that is used when updating packages. Use
"" (the empty string) to indicate exact dependencies. Note that dependencies on
pre-release versions will always be exact.
<options: ^|~|>
-g, --releaseGroup=<value>... A release group whose packages will be updated. This can be specified multiple times
to updates dependencies for multiple release groups.
--on=<value> (required) A release group that contains dependent packages. Packages that depend on
packages in this release group will be updated.
--prerelease Update to the latest prerelease version, which might be an earlier release than
latest.

LOGGING FLAGS
-v, --verbose Enable verbose logging.
--quiet Disable all logging.

DESCRIPTION
Update the dependency version that a release group has on another release group. That is, if one or more packages in
the release group depend on package A in another release group, then this command will update the dependency range on
package A and all other packages in that release group.

To learn more see the detailed documentation at
https://github.com/microsoft/FluidFramework/blob/main/build-tools/packages/build-cli/docs/bumpDetails.md

EXAMPLES
Update 'client' dependencies on packages in the 'build-tools' release group to the latest release version.

$ flub vnext modify fluid-deps -g client --on build-tools

Update 'client' dependencies on packages in the 'server' release group to the latest version. Include pre-release
versions.

$ flub vnext modify fluid-deps -g client --on build-tools --prerelease

Update 'client' dependencies on packages in the 'server' release group to the latest version. Include pre-release
versions.

$ flub vnext modify fluid-deps -g client --on server
```

_See code: [src/commands/vnext/modify/fluid-deps.ts](https://github.com/microsoft/FluidFramework/blob/main/build-tools/packages/build-cli/src/commands/vnext/modify/fluid-deps.ts)_
Original file line number Diff line number Diff line change
@@ -34,11 +34,6 @@ export default class LatestVersionsCommand extends BaseCommandWithBuildProject<
hidden: true,
multiple: true,
}),
searchPath: Flags.string({
description: "The path to build project. Used for testing.",
hidden: true,
multiple: false,
}),
...BaseCommandWithBuildProject.flags,
} as const;

214 changes: 214 additions & 0 deletions build-tools/packages/build-cli/src/commands/vnext/modify/fluid-deps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

import {
type IPackage,
getAllDependencies,
setDependencyRange,
} from "@fluid-tools/build-infrastructure";
import {
RangeOperator,
type RangeOperatorWithVersion,
RangeOperators,
} from "@fluid-tools/version-tools";
import { type Logger } from "@fluidframework/build-tools";
import { Flags } from "@oclif/core";
import type { PackageName } from "@rushstack/node-core-library";
import latestVersion from "latest-version";
import chalk from "picocolors";
import * as semver from "semver";
import { releaseGroupNameFlag, testModeFlag } from "../../../flags.js";
import { BaseCommandWithBuildProject } from "../../../library/index.js";

/**
* Update the dependency version that a release group has on another release group. That is, if one or more packages in
* the release group depend on package A in another release group, then this command will update the dependency range on
* package A and all other packages in that release group.
*/
export default class ModifyFluidDepsCommand extends BaseCommandWithBuildProject<
typeof ModifyFluidDepsCommand
> {
static readonly description =
"Update the dependency version that a release group has on another release group. That is, if one or more packages in the release group depend on package A in another release group, then this command will update the dependency range on package A and all other packages in that release group.\n\nTo learn more see the detailed documentation at https://github.com/microsoft/FluidFramework/blob/main/build-tools/packages/build-cli/docs/bumpDetails.md";

static readonly flags = {
releaseGroup: releaseGroupNameFlag({
required: false,
multiple: true,
description:
"A release group whose packages will be updated. This can be specified multiple times to updates dependencies for multiple release groups.",
}),
on: releaseGroupNameFlag({
required: true,
char: undefined,
description:
"A release group that contains dependent packages. Packages that depend on packages in this release group will be updated.",
}),
prerelease: Flags.boolean({
description:
"Update to the latest prerelease version, which might be an earlier release than latest.",
}),
dependencyRange: Flags.custom<RangeOperator>({
char: "d",
description:
'Controls the type of dependency that is used when updating packages. Use "" (the empty string) to indicate exact dependencies. Note that dependencies on pre-release versions will always be exact.',
default: "^",
options: [...RangeOperators],
})(),
testMode: testModeFlag,
...BaseCommandWithBuildProject.flags,
} as const;

static readonly examples = [
{
description:
"Update 'client' dependencies on packages in the 'build-tools' release group to the latest release version.",
command: "<%= config.bin %> <%= command.id %> -g client --on build-tools",
},
{
description:
"Update 'client' dependencies on packages in the 'server' release group to the latest version. Include pre-release versions.",
command: "<%= config.bin %> <%= command.id %> -g client --on build-tools --prerelease",
},
{
description:
"Update 'client' dependencies on packages in the 'server' release group to the latest version. Include pre-release versions.",
command: "<%= config.bin %> <%= command.id %> -g client --on server",
},
];

/**
* Runs the `modify fluid-deps` command.
*/
public async run(): Promise<void> {
const { flags } = this;

const buildProject = this.getBuildProject(flags.searchPath);
const releaseGroups =
flags.releaseGroup === undefined
? [...buildProject.releaseGroups.values()]
: flags.releaseGroup.map((rg) => {
const found = buildProject.releaseGroups.get(rg);
if (found === undefined) {
this.error(`Release group not found: '${flags.releaseGroup}'`);
}
return found;
});
const packagesToUpdate = releaseGroups.flatMap((rg) => rg.packages);
const dependencyReleaseGroup = buildProject.releaseGroups.get(flags.on);
if (dependencyReleaseGroup === undefined) {
this.error(`Release group not found: '${flags.on}'`);
}

if (flags.testMode) {
this.log(chalk.yellowBright(`Running in test mode. No changes will be made.`));
}

// Get all the deps of the release groups being updated
const depsToUpdate = getAllDependencies(buildProject, packagesToUpdate);

if (!depsToUpdate.releaseGroups.includes(dependencyReleaseGroup)) {
this.error(
`Selected release groups have no dependencies on '${dependencyReleaseGroup}'`,
);
}

this.logHr();
this.log(
`Updating dependencies on '${chalk.blue(dependencyReleaseGroup.name)}' in the '${chalk.blue(releaseGroups.map((rg) => rg.name).join(", "))}' release group`,
);
this.log(`Prerelease: ${flags.prerelease ? chalk.green("yes") : "no"}`);
this.logHr();
this.log("");

const latestDepVersions = await getLatestPackageVersions(
dependencyReleaseGroup.packages,
flags.prerelease,
this.logger,
);

const versionSet = new Set(latestDepVersions.values());
if (versionSet.size > 1) {
this.error(
`Found multiple versions in dependencies - expected only one. Versions: ${[...versionSet].join(", ")}`,
);
}
const newVersion = [...versionSet][0];
this.info(`Found updated version ${newVersion}`);

const newRange =
`${flags.prerelease ? "" : flags.dependencyRange}${newVersion}` as RangeOperatorWithVersion;
await setDependencyRange(packagesToUpdate, dependencyReleaseGroup.packages, newRange);

const workspaces = new Set(releaseGroups.map((rg) => rg.workspace));
for (const ws of workspaces) {
try {
// eslint-disable-next-line no-await-in-loop
await ws.install(/* updateLockfile */ true);
this.info(`Installed dependencies for workspace '${ws.name}'`);
} catch (error) {
this.warning(
`Error installing dependencies for workspace '${ws.name}' - you should install dependencies manually.`,
);
// eslint-disable-next-line prefer-template
this.verbose((error as Error).message + "\n\n" + (error as Error).stack);
continue;
}
}
}
}

/**
* Checks the npm registry for the latest version of the provided packages.
*/
async function getLatestPackageVersions(
dependencies: IPackage[],
prerelease: boolean,
log?: Logger,
): Promise<Map<PackageName, string>> {
/**
* A map of packages to their latest version.
*/
const packageVersions: Map<PackageName, string> = new Map();

// Get the new version for each package based on the update type
for (const { name: pkgName, private: isPrivate } of dependencies) {
if (isPrivate) {
// skip private packages
continue;
}
let latest: string;
let dev: string;

try {
// eslint-disable-next-line no-await-in-loop
[latest, dev] = await Promise.all([
latestVersion(pkgName, {
version: "latest",
}),
latestVersion(pkgName, {
version: "dev",
}),
]);
} catch (error: unknown) {
log?.warning(error as Error);
continue;
}

// If we're allowing pre-release, use the version that has the 'dev' dist-tag in npm. Warn if it is lower than the 'latest'.
if (prerelease) {
packageVersions.set(pkgName, dev);
if (semver.gt(latest, dev)) {
log?.warning(
`The 'latest' dist-tag is version ${latest}, which is greater than the 'dev' dist-tag version, ${dev}. Is this expected?`,
);
}
} else {
packageVersions.set(pkgName, latest);
}
}

return packageVersions;
}
2 changes: 1 addition & 1 deletion build-tools/packages/build-cli/src/flags.ts
Original file line number Diff line number Diff line change
@@ -55,8 +55,8 @@ export const releaseGroupFlag = Flags.custom<ReleaseGroup>({
* A re-usable CLI flag to parse release group names.
*/
export const releaseGroupNameFlag = Flags.custom<ReleaseGroupName>({
required: true,
description: "The name of a release group.",
char: "g",
parse: async (input) => {
return input as ReleaseGroupName;
},
9 changes: 9 additions & 0 deletions build-tools/packages/build-cli/src/library/commands/base.ts
Original file line number Diff line number Diff line change
@@ -280,6 +280,15 @@ export abstract class BaseCommand<T extends typeof Command>
export abstract class BaseCommandWithBuildProject<
T extends typeof Command,
> extends BaseCommand<T> {
static readonly flags = {
searchPath: Flags.string({
description: "The path to build project. Used for testing.",
hidden: true,
multiple: false,
}),
...BaseCommand.flags,
} as const;

private _buildProject: IBuildProject | undefined;

/**
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@

```ts

import type { InterdependencyRange } from '@fluid-tools/version-tools';
import type { Opaque } from 'type-fest';
import type { PackageJson as PackageJson_2 } from 'type-fest';
import { SemVer } from 'semver';
@@ -239,6 +240,9 @@ export interface Reloadable {
reload(): void;
}

// @public
export function setDependencyRange<P extends IPackage>(packagesToUpdate: Iterable<P>, dependencies: Iterable<P>, dependencyRange: InterdependencyRange): Promise<void>;

// @public
export function setVersion<J extends PackageJson>(packages: IPackage<J>[], version: SemVer): Promise<void>;

1 change: 1 addition & 0 deletions build-tools/packages/build-infrastructure/src/index.ts
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@
export {
getAllDependencies,
loadBuildProject,
setDependencyRange,
} from "./buildProject.js";
export {
type ReleaseGroupDefinition,