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

(build-tools): base command with build project context #23257

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 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
1 change: 1 addition & 0 deletions build-tools/feeds/internal-build.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@fluid-tools/version-tools
@fluidframework/bundle-size-tools
@fluidframework/build-tools
@fluid-tools/build-infrastructure
@fluid-tools/build-cli
1 change: 1 addition & 0 deletions build-tools/feeds/internal-test.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@fluid-tools/version-tools
@fluidframework/bundle-size-tools
@fluidframework/build-tools
@fluid-tools/build-infrastructure
@fluid-tools/build-cli
1 change: 1 addition & 0 deletions build-tools/feeds/public.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@fluid-tools/version-tools
@fluidframework/bundle-size-tools
@fluidframework/build-tools
@fluid-tools/build-infrastructure
@fluid-tools/build-cli
1 change: 1 addition & 0 deletions build-tools/packages/build-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ USAGE
* [`flub run`](docs/run.md) - Generate a report from input bundle stats collected through the collect bundleStats command.
* [`flub transform`](docs/transform.md) - Transform commands are used to transform code, docs, etc. into alternative forms.
* [`flub typetests`](docs/typetests.md) - Updates configuration for type tests in package.json files. If the previous version changes after running preparation, then npm install must be run before building.
* [`flub vnext`](docs/vnext.md) - Determines if an input version matches a latest minor release version. Intended to be used in the Fluid Framework CI pipeline only.
zhenmichael marked this conversation as resolved.
Show resolved Hide resolved

<!-- commandsstop -->
<!-- prettier-ignore-stop -->
Expand Down
32 changes: 32 additions & 0 deletions build-tools/packages/build-cli/docs/vnext.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
`flub vnext`
============

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

* [`flub vnext check latestVersions VERSION RELEASE_GROUP`](#flub-vnext-check-latestversions-version-release_group)

## `flub vnext check latestVersions VERSION RELEASE_GROUP`

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 VERSION RELEASE_GROUP [-v | --quiet]

ARGUMENTS
VERSION The version to check. When running in CI, this value corresponds to the pipeline trigger branch.
RELEASE_GROUP The name of a release group.

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

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

This command is used in CI to determine if a pipeline was triggered by a release branch with the latest minor version
of a major version.
```

_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)_
1 change: 1 addition & 0 deletions build-tools/packages/build-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
},
"dependencies": {
"@andrewbranch/untar.js": "^1.0.3",
"@fluid-tools/build-infrastructure": "workspace:~",
"@fluid-tools/version-tools": "workspace:~",
"@fluidframework/build-tools": "workspace:~",
"@fluidframework/bundle-size-tools": "workspace:~",
Expand Down
25 changes: 25 additions & 0 deletions build-tools/packages/build-cli/src/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
* Licensed under the MIT License.
*/

import type {
IBuildProject,
IReleaseGroup,
ReleaseGroupName,
} from "@fluid-tools/build-infrastructure";
import { MonoRepo, Package } from "@fluidframework/build-tools";
import { Args } from "@oclif/core";
import { PackageName } from "@rushstack/node-core-library";
Expand Down Expand Up @@ -39,6 +44,26 @@ export const findPackageOrReleaseGroup = (
);
};

/**
* Creates a CLI argument for release group names. It's a factory function so that commands can override the
* properties more easily when using the argument.
*/
export const releaseGroupArg = Args.custom({
name: "release_group",
required: true,
description: "The name of a release group.",
});

/**
* Takes a release group name and searches the build project for it.
*/
export const findReleaseGroup = (
name: string,
buildProject: IBuildProject,
): IReleaseGroup | undefined => {
return buildProject.releaseGroups.get(name as ReleaseGroupName);
};
zhenmichael marked this conversation as resolved.
Show resolved Hide resolved

/**
* Creates a CLI argument for semver versions. It's a factory function so that commands can override the properties more
* easily when using the argument.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

import { isInternalVersionScheme } from "@fluid-tools/version-tools";
import * as semver from "semver";
import { findReleaseGroup, releaseGroupArg, semverArg } from "../../../args.js";
import {
BaseCommandWithBuildProject,
getVersionsFromTags,
sortVersions,
} from "../../../library/index.js";

type MajorVersion = number;

export default class LatestVersionsCommand extends BaseCommandWithBuildProject<
typeof LatestVersionsCommand
> {
static readonly summary =
"Determines if an input version matches a latest minor release version. Intended to be used in the Fluid Framework CI pipeline only.";

static readonly description =
"This command is used in CI to determine if a pipeline was triggered by a release branch with the latest minor version of a major version.";

static readonly args = {
version: semverArg({
required: true,
description:
"The version to check. When running in CI, this value corresponds to the pipeline trigger branch.",
}),
release_group: releaseGroupArg({ required: true }),
} as const;

public async run(): Promise<void> {
const { args } = this;
const buildProject = this.getBuildProject();
const releaseGroup = findReleaseGroup(args.release_group, buildProject);

const versionInput = args.version;

if (releaseGroup === undefined) {
this.error(`Package not found: ${args.release_group}`);
}
zhenmichael marked this conversation as resolved.
Show resolved Hide resolved

const git = await buildProject.getGitRepository();
const versions = await getVersionsFromTags(releaseGroup, git);

if (!versions) {
this.error(`No versions found for ${releaseGroup.name}`);
}

// Filter out pre-release versions
const stableVersions = versions.filter((v) => {
return !isInternalVersionScheme(v.version);
});

// Sort the semver versions ordered from highest to lowest
const sortedByVersion = sortVersions(stableVersions, "version");

const inputMajorVersion: MajorVersion = semver.major(versionInput.version);

for (const v of sortedByVersion) {
const majorVersion: MajorVersion = semver.major(v.version);

// Since sortedByVersion is sorted, the first encountered version is the highest one
if (majorVersion === inputMajorVersion) {
if (v.version === versionInput.version) {
// Check if the input version is the latest version for the major version
this.log(
`Version ${versionInput.version} is the latest version for major version ${majorVersion}`,
);
this.log(`##vso[task.setvariable variable=shouldDeploy;isoutput=true]true`);
this.log(
`##vso[task.setvariable variable=majorVersion;isoutput=true]${majorVersion}`,
);
return;
}

// If versions do not match on first major version encounter, then the input version is not the latest
this.log(
`##[warning]skipping deployment stage. input version ${versionInput.version} does not match the latest version ${v.version}`,
);
this.log(`##vso[task.setvariable variable=shouldDeploy;isoutput=true]false`);
this.log(`##vso[task.setvariable variable=majorVersion;isoutput=true]${majorVersion}`);
return;
}
}

// Error if no major version corresponds to input version
this.log(
`##[warning]No major version found corresponding to input version ${versionInput.version}`,
);
this.log(`##vso[task.setvariable variable=shouldDeploy;isoutput=true]false`);
this.log(
`##vso[task.setvariable variable=majorVersion;isoutput=true]${inputMajorVersion}`,
);
}
}
31 changes: 31 additions & 0 deletions build-tools/packages/build-cli/src/library/commands/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Command, Flags, Interfaces } from "@oclif/core";
import type { PrettyPrintableError } from "@oclif/core/errors";
import chalk from "picocolors";

import { IBuildProject, loadBuildProject } from "@fluid-tools/build-infrastructure";
import { CommandLogger } from "../../logging.js";
import { Context } from "../context.js";
import { indentString } from "../text.js";
Expand Down Expand Up @@ -282,3 +283,33 @@ export abstract class BaseCommand<T extends typeof Command>
}
}
}

export abstract class BaseCommandWithBuildProject<
tylerbutler marked this conversation as resolved.
Show resolved Hide resolved
T extends typeof Command,
> extends BaseCommand<T> {
private _buildProject: IBuildProject | undefined;

/**
* This method is deprecated and should only be called in BaseCommand instances.
*
* @deprecated This method should only be called in BaseCommand instances.
*/
public getContext(): never {
zhenmichael marked this conversation as resolved.
Show resolved Hide resolved
throw new Error("getContext method should only be called in BaseCommand instances");
}

/**
* Gets the build project for the current command. The build project is loaded from the closest build root to searchPath.
*
* @param searchPath - The path to search for the build project.
* @returns The build project.
*/
public getBuildProject(searchPath?: string): IBuildProject {
zhenmichael marked this conversation as resolved.
Show resolved Hide resolved
if (this._buildProject === undefined) {
const root = searchPath ?? process.cwd();
this._buildProject = loadBuildProject(root);
}

return this._buildProject;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

export { BaseCommand } from "./base.js";
export { BaseCommandWithBuildProject } from "./base.js";
export { unscopedPackageNameString } from "./constants.js";
export {
GenerateEntrypointsCommand,
Expand Down
47 changes: 47 additions & 0 deletions build-tools/packages/build-cli/src/library/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import * as semver from "semver";
import { SimpleGit, SimpleGitOptions, simpleGit } from "simple-git";
import type { SetRequired } from "type-fest";

import type { IReleaseGroup } from "@fluid-tools/build-infrastructure";
import { parseISO } from "date-fns";
import { CommandLogger } from "../logging.js";
import { ReleaseGroup } from "../releaseGroups.js";
Expand Down Expand Up @@ -43,6 +44,52 @@ function getVersionFromTag(tag: string): string | undefined {
return ver.version;
}

/**
* Get all the versions for a release group. This function only considers the tags in the repo.
*
* @param git - The git repository.
* @param releaseGroup - The release group to get versions for.
* @returns List of version details, or undefined if one could not be found.
*/
export async function getVersionsFromTags(
zhenmichael marked this conversation as resolved.
Show resolved Hide resolved
releaseGroup: IReleaseGroup,
git: Readonly<SimpleGit>,
): Promise<VersionDetails[] | undefined> {
const prefix = PackageName.getUnscopedName(releaseGroup.name);

zhenmichael marked this conversation as resolved.
Show resolved Hide resolved
const tags = await git.tags(["--list", `${prefix}_v*`]);

const versions = new Map<string, Date>();

for (const tag of tags.all) {
const ver = getVersionFromTag(tag);
if (ver !== undefined && ver !== "" && ver !== null) {
// eslint-disable-next-line no-await-in-loop
const rawDate = await git.show([
// Suppress the diff output
"-s",
// ISO 8601 format
"--format=%cI",
// the git ref - a tag in this case
tag,
]);
const date = parseISO(rawDate.trim());
versions.set(ver, date);
}
}

if (versions.size === 0) {
return undefined;
}

const toReturn: VersionDetails[] = [];
for (const [version, date] of versions) {
toReturn.push({ version, date });
}

return toReturn;
}

/**
* A type of string used to denote a GitHub repo by owner and repo name.
*/
Expand Down
3 changes: 2 additions & 1 deletion build-tools/packages/build-cli/src/library/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ export {
export {
unscopedPackageNameString,
BaseCommand,
BaseCommandWithBuildProject,
GenerateEntrypointsCommand,
} from "./commands/index.js";
export { Context, VersionDetails, isMonoRepoKind, MonoRepoKind } from "./context.js";
export { Repository } from "./git.js";
export { Repository, getVersionsFromTags } from "./git.js";
export {
ensureDevDependencyExists,
filterVersionsOlderThan,
Expand Down
1 change: 0 additions & 1 deletion build-tools/packages/build-infrastructure/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"name": "@fluid-tools/build-infrastructure",
"version": "0.52.0",
"private": true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious about the motivation or use case for the whole change, if it requires us to make this public and get it published.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great question. The ultimate goal is to replace all of the release group/package handling code in build-cli commands with the new code from build-infra. But rolling out changes to all the commands at once is just too big a change/task, so the plan is to convert commands one-by-one by creating new versions of the commands that use this new base class. That way we can roll out the changes piecemeal.

Long-term, we may not need a separate package for the infra stuff, but at the moment it's much easier for it to be separate right now, because build-cli and fluid-build need the underlying code. Unfortunately, fluid-build cannot depend on build-cli today because there's already a dep going the other way (that is, build-cli depends on fluid-build).

"description": "Fluid build infrastructure",
"homepage": "https://fluidframework.com",
"repository": {
Expand Down
3 changes: 3 additions & 0 deletions build-tools/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading