Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
805129e
Initial plan
Copilot Nov 24, 2025
e4e64e5
Add CheckCompatLayerGeneration to release prepare command
Copilot Nov 24, 2025
05fec9f
Fix CheckCompatLayerGeneration function signature and return statement
Copilot Nov 24, 2025
3b4a3db
Remove unnecessary eslint-disable comment
Copilot Nov 24, 2025
7571073
Mark unused releaseGroupOrPackage parameter with underscore prefix
Copilot Nov 24, 2025
2197af7
Use direct function calls instead of running pnpm command for layer g…
Copilot Dec 2, 2025
3b76f2c
Remove trailing whitespace and clarify comment
Copilot Dec 2, 2025
5d87e28
Extract minimumCompatWindowMonths constant
Copilot Dec 2, 2025
804c2aa
Add missing log method to noop logger and clarify comment
Copilot Dec 2, 2025
10cf5c4
Add onlyConfiguredPackages parameter to filter packages by metadata c…
Copilot Dec 2, 2025
a1a4052
Document how to set up unconfigured packages for layer compatibility
Copilot Dec 2, 2025
7bc2409
Remove unused onlyConfiguredPackages parameter to simplify implementa…
Copilot Dec 2, 2025
4541d93
Move layer compatibility functions to library and update imports
Copilot Dec 2, 2025
157bbd0
Update build-tools/packages/build-cli/src/handlers/checkFunctions.ts
tylerbutler Dec 3, 2025
ef2a5a9
Refactor layer compatibility functions based on code review feedback
Copilot Dec 3, 2025
afa24cf
Fix layer generation check to scope to release group/package and upda…
Copilot Dec 3, 2025
ce6cf23
Merge branch 'main' of https://github.com/microsoft/FluidFramework in…
Copilot Dec 3, 2025
03714cf
format
tylerbutler Dec 3, 2025
45ab3c3
build
tylerbutler Dec 3, 2025
43b8af4
Update comments to reference compatLayerGeneration command name
Copilot Dec 3, 2025
276a999
Merge branch 'main' into copilot/update-release-prepare-command
tylerbutler Dec 4, 2025
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/packages/build-cli/docs/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ DESCRIPTION
- Has no pre-release Fluid dependencies
- No repo policy violations
- No untagged asserts
- Compatibility layer generation is up to date

ALIASES
$ flub release prep
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,17 @@ import path from "node:path";
import { updatePackageJsonFile } from "@fluid-tools/build-infrastructure";
import type {
IFluidCompatibilityMetadata,
Logger,
Package,
PackageJson,
} from "@fluidframework/build-tools";
import { Flags } from "@oclif/core";
import { formatISO, isDate, isValid, parseISO } from "date-fns";
import { diff, parse } from "semver";
import { formatISO } from "date-fns";
import { PackageCommand } from "../../BasePackageCommand.js";
import type { PackageSelectionDefault } from "../../flags.js";

// Approximate month as 33 days to add some buffer and avoid over-counting months in longer spans.
export const daysInMonthApproximation = 33;
import {
isCurrentPackageVersionPatch,
maybeGetNewGeneration,
} from "../../library/layerCompatibility.js";

/**
* Command to update the generation value of Fluid's compatibility layers.
Expand Down Expand Up @@ -97,29 +96,6 @@ export default class UpdateGenerationLayerCommand extends PackageCommand<
}
}

/**
* Determines if the current package version represents a patch release.
*
* @param pkgVersion - The semantic version of the package (e.g., "2.0.1")
* @returns True if the version is a patch release, false otherwise
*
* @throws Error When the provided version string is not a valid semantic version
*
* @example
* ```typescript
* isCurrentPackageVersionPatch("2.0.1"); // returns true
* isCurrentPackageVersionPatch("2.1.0"); // returns false
* isCurrentPackageVersionPatch("3.0.0"); // returns false
* ```
*/
export function isCurrentPackageVersionPatch(pkgVersion: string): boolean {
const parsed = parse(pkgVersion);
if (parsed === null) {
throw new Error(`Package version ${pkgVersion} is not a valid semver`);
}
return parsed.patch > 0;
}

/**
* Generates the complete content for a layer generation TypeScript file.
*
Expand Down Expand Up @@ -147,71 +123,3 @@ export function generateLayerFileContent(generation: number): string {
export const generation = ${generation};
`;
}

/**
* Determines if a new generation should be generated based on package version changes and time since
* the last release.
*
* This function parses an existing layer generation file and decides whether to increment the generation
* number based on:
* 1. Whether the package version has changed since the last update
* 2. How much time has elapsed since the previous release date
* 3. The minimum compatibility window constraints
*
* The generation increment is calculated as the number of months since the previous release,
* but capped at (minimumCompatWindowMonths - 1) to maintain compatibility requirements.
*
* @param currentPkgVersion - The current package version to compare against the stored version
* @param fluidCompatMetadata - The existing Fluid compatibility metadata from the previous generation
* @param minimumCompatWindowMonths - The maximum number of months of compatibility to maintain across layers
* @param log - Logger instance for verbose output about the calculation process
* @returns The new generation number if an update is needed, or undefined if no update is required
*
* @throws Error When the generation file content doesn't match the expected format
* @throws Error When the current date is older than the previous release date
*/
export function maybeGetNewGeneration(
currentPkgVersion: string,
fluidCompatMetadata: IFluidCompatibilityMetadata,
minimumCompatWindowMonths: number,
log: Logger,
): number | undefined {
// Only "minor" or "major" version changes trigger generation updates.
const result = diff(currentPkgVersion, fluidCompatMetadata.releasePkgVersion);
if (result === null || (result !== "minor" && result !== "major")) {
log.verbose(`No minor or major release since last update; skipping generation update.`);
return undefined;
}

log.verbose(
`Previous package version: ${fluidCompatMetadata.releasePkgVersion}, Current package version: ${currentPkgVersion}`,
);

const previousReleaseDate = parseISO(fluidCompatMetadata.releaseDate);
if (!isValid(previousReleaseDate) || !isDate(previousReleaseDate)) {
throw new Error(
`Previous release date "${fluidCompatMetadata.releaseDate}" is not a valid date.`,
);
}

const today = new Date();
const timeDiff = today.getTime() - previousReleaseDate.getTime();
if (timeDiff < 0) {
throw new Error("Current date is older that previous release date");
}
const daysBetweenReleases = Math.round(timeDiff / (1000 * 60 * 60 * 24));
const monthsBetweenReleases = Math.floor(daysBetweenReleases / daysInMonthApproximation);
log.verbose(`Previous release date: ${previousReleaseDate}, Today: ${today}`);
log.verbose(
`Time between releases: ${daysBetweenReleases} day(s) or ~${monthsBetweenReleases} month(s)`,
);

const newGeneration =
fluidCompatMetadata.generation +
Math.min(monthsBetweenReleases, minimumCompatWindowMonths - 1);
if (newGeneration === fluidCompatMetadata.generation) {
log.verbose(`Generation remains the same (${newGeneration}); skipping generation update.`);
return undefined;
}
return newGeneration;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import chalk from "picocolors";
import { findPackageOrReleaseGroup, packageOrReleaseGroupArg } from "../../args.js";
import { BaseCommand } from "../../library/index.js";
import {
CheckCompatLayerGeneration,
CheckDependenciesInstalled,
type CheckFunction,
CheckHasNoPrereleaseDependencies,
Expand All @@ -32,6 +33,7 @@ const allChecks: ReadonlyMap<string, CheckFunction> = new Map([
["Has no pre-release Fluid dependencies", CheckHasNoPrereleaseDependencies],
["No repo policy violations", CheckNoPolicyViolations],
["No untagged asserts", CheckNoUntaggedAsserts],
["Compatibility layer generation is up to date", CheckCompatLayerGeneration],
]);

/**
Expand Down
27 changes: 13 additions & 14 deletions build-tools/packages/build-cli/src/handlers/checkFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import execa from "execa";
import type { Machine } from "jssm";

import { bumpVersionScheme } from "@fluid-tools/version-tools";
import { FluidRepo } from "@fluidframework/build-tools";
import { FluidRepo, type Package } from "@fluidframework/build-tools";

import {
generateBumpDepsBranchName,
Expand All @@ -23,6 +23,7 @@ import {
getReleaseSourceForReleaseGroup,
isReleased,
} from "../library/index.js";
import { runCompatLayerGenerationCheck } from "../library/releasePrepChecks.js";
import type { CommandLogger } from "../logging.js";
import type { MachineState } from "../machines/index.js";
import { type ReleaseSource, isReleaseGroup } from "../releaseGroups.js";
Expand Down Expand Up @@ -923,29 +924,27 @@ export const checkCompatLayerGeneration: StateHandlerFunction = async (
): Promise<boolean> => {
if (testMode) return true;

const { context, bumpType } = data;
const { context, bumpType, releaseGroup } = data;

if (bumpType === "patch") {
log.verbose(`Skipping layer compat generation check for patch release.`);
BaseStateHandler.signalSuccess(machine, state);
return true;
}

// layerGeneration:gen should be run from the root. It will only update packages that have the layerGeneration:gen
// script defined in their package.json.
const result = await execa.command(`pnpm run -r layerGeneration:gen`, {
cwd: context.root,
});
log.verbose(result.stdout);
// Get packages for the release group or individual package being released
const packagesToCheck = isReleaseGroup(releaseGroup)
? context.packagesInReleaseGroup(releaseGroup)
: [context.fullPackageMap.get(releaseGroup)].filter(
(pkg): pkg is Package => pkg !== undefined,
);

// check for policy check violation
const gitRepo = await context.getGitRepository();
const afterPolicyCheckStatus = await gitRepo.gitClient.status();
const isClean = afterPolicyCheckStatus.isClean();
if (!isClean) {
const isUpToDate = await runCompatLayerGenerationCheck(packagesToCheck);

if (!isUpToDate) {
log.logHr();
log.errorLog(
`Layer generation needs to be updated. Please create a PR for the changes and merge before retrying.\n${afterPolicyCheckStatus.files.map((fileStatus) => `${fileStatus.index} ${fileStatus.path}`).join("\n")}`,
`Compat layer generation needs to be updated. Run "pnpm run layerGeneration:gen" from the repo root, then create a PR for the changes and merge before retrying.`,
);
BaseStateHandler.signalFailure(machine, state);
return false;
Expand Down
113 changes: 113 additions & 0 deletions build-tools/packages/build-cli/src/library/layerCompatibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

import type { IFluidCompatibilityMetadata, Logger } from "@fluidframework/build-tools";
import { formatISO, isDate, isValid, parseISO } from "date-fns";
import { diff, parse } from "semver";

/**
* Approximate month as 33 days to add some buffer and avoid over-counting months in longer spans.
*/
export const DAYS_IN_MONTH_APPROXIMATION = 33;

/**
* The default minimum compatibility window in months for layer generation.
* This matches the default value used in the compatLayerGeneration command.
*/
export const DEFAULT_MINIMUM_COMPAT_WINDOW_MONTHS = 3;

/**
* Determines if the current package version represents a patch release.
*
* @param pkgVersion - The semantic version of the package (e.g., "2.0.1")
* @returns True if the version is a patch release, false otherwise
*
* @throws Error When the provided version string is not a valid semantic version
*
* @example
* ```typescript
* isCurrentPackageVersionPatch("2.0.1"); // returns true
* isCurrentPackageVersionPatch("2.1.0"); // returns false
* isCurrentPackageVersionPatch("3.0.0"); // returns false
* ```
*/
export function isCurrentPackageVersionPatch(pkgVersion: string): boolean {
const parsed = parse(pkgVersion);
if (parsed === null) {
throw new Error(`Package version ${pkgVersion} is not a valid semver`);
}
return parsed.patch > 0;
}

/**
* Determines if a new generation should be generated based on package version changes and time since
* the last release.
*
* This function parses an existing layer generation file and decides whether to increment the generation
* number based on:
* 1. Whether the package version has changed since the last update
* 2. How much time has elapsed since the previous release date
* 3. The minimum compatibility window constraints
*
* The generation increment is calculated as the number of months since the previous release,
* but capped at (minimumCompatWindowMonths - 1) to maintain compatibility requirements.
*
* @param currentPkgVersion - The current package version to compare against the stored version
* @param fluidCompatMetadata - The existing Fluid compatibility metadata from the previous generation
* @param minimumCompatWindowMonths - The maximum number of months of compatibility to maintain across layers
* @param log - Optional logger instance for verbose output about the calculation process
* @param currentDate - Optional current date for testing purposes. Defaults to new Date()
* @returns The new generation number if an update is needed, or undefined if no update is required
*
* @throws Error When the generation file content doesn't match the expected format
* @throws Error When the current date is older than the previous release date
*/
export function maybeGetNewGeneration(
currentPkgVersion: string,
fluidCompatMetadata: IFluidCompatibilityMetadata,
minimumCompatWindowMonths: number,
log?: Logger,
currentDate: Date = new Date(),
): number | undefined {
// Only "minor" or "major" version changes trigger generation updates.
const result = diff(currentPkgVersion, fluidCompatMetadata.releasePkgVersion);
if (result === null || (result !== "minor" && result !== "major")) {
log?.verbose(`No minor or major release since last update; skipping generation update.`);
return undefined;
}

log?.verbose(
`Previous package version: ${fluidCompatMetadata.releasePkgVersion}, Current package version: ${currentPkgVersion}`,
);

const previousReleaseDate = parseISO(fluidCompatMetadata.releaseDate);
if (!isValid(previousReleaseDate) || !isDate(previousReleaseDate)) {
throw new Error(
`Previous release date "${fluidCompatMetadata.releaseDate}" is not a valid date.`,
);
}

const timeDiff = currentDate.getTime() - previousReleaseDate.getTime();
if (timeDiff < 0) {
throw new Error("Current date is older that previous release date");
}
const daysBetweenReleases = Math.round(timeDiff / (1000 * 60 * 60 * 24));
const monthsBetweenReleases = Math.floor(daysBetweenReleases / DAYS_IN_MONTH_APPROXIMATION);
log?.verbose(`Previous release date: ${previousReleaseDate}, Today: ${currentDate}`);
log?.verbose(
`Time between releases: ${daysBetweenReleases} day(s) or ~${monthsBetweenReleases} month(s)`,
);

const newGeneration =
fluidCompatMetadata.generation +
Math.min(monthsBetweenReleases, minimumCompatWindowMonths - 1);
if (newGeneration === fluidCompatMetadata.generation) {
log?.verbose(
`Generation remains the same (${newGeneration}); skipping generation update.`,
);
return undefined;
}
return newGeneration;
}
Loading
Loading