This commit is contained in:
Elizabeth Craig 2023-05-17 15:55:39 -07:00 коммит произвёл GitHub
Родитель bd352a4732
Коммит 8eb91797c1
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
19 изменённых файлов: 194 добавлений и 146 удалений

Просмотреть файл

@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Improve validation logging and performance",
"packageName": "beachball",
"email": "elcraig@microsoft.com",
"dependentChangeType": "patch"
}

Просмотреть файл

@ -4,24 +4,28 @@ export function bumpMinSemverRange(minVersion: string, semverRange: string) {
if (semverRange === '*') {
return semverRange;
}
if (new Set(['workspace:*', 'workspace:~', 'workspace:^']).has(semverRange)) {
if (['workspace:*', 'workspace:~', 'workspace:^'].includes(semverRange)) {
// For basic workspace ranges we can just preserve current value and replace during publish
// https://pnpm.io/workspaces#workspace-protocol-workspace
return semverRange;
} else if (semverRange.startsWith('~') || semverRange.startsWith('^')) {
}
if (semverRange[0] === '~' || semverRange[0] === '^') {
// ~1.0.0
// ^1.0.0
return semverRange[0] + minVersion;
} else if (semverRange.startsWith('workspace:~') || semverRange.startsWith('workspace:^')) {
}
if (semverRange.startsWith('workspace:~') || semverRange.startsWith('workspace:^')) {
// workspace:~1.0.0
// workspace:^1.0.0
return `workspace:${semverRange[10]}${minVersion}`;
} else if (semverRange.includes('>')) {
// Less frequently used, but we treat any of these kinds of ranges to be within a minor band for now: more complex understand of the semver range utility is needed to do more
}
if (semverRange.includes('>')) {
// Less frequently used, but we treat any of these kinds of ranges to be within a minor band for now:
// more complex understanding of the semver range utility is needed to do more
// >=1.0.0 <2.0.0
return `>=${minVersion} <${semver.inc(minVersion, 'major')}`;
} else if (semverRange.includes(' - ')) {
}
if (semverRange.includes(' - ')) {
// 1.0.0 - 2.0.0
return `${minVersion} - ${semver.inc(minVersion, 'major')}`;
}

Просмотреть файл

@ -9,19 +9,15 @@ export function bumpPackageInfoVersion(pkgName: string, bumpInfo: BumpInfo, opti
const { calculatedChangeTypes, packageInfos, modifiedPackages } = bumpInfo;
const info = packageInfos[pkgName];
const changeType = calculatedChangeTypes[pkgName];
if (!info) {
console.log(`Unknown package named "${pkgName}" detected from change files, skipping!`);
return;
}
if (changeType === 'none') {
} else if (changeType === 'none') {
console.log(`"${pkgName}" has a "none" change type, no version bump is required.`);
return;
}
if (info.private) {
} else if (info.private) {
console.log(`Skipping bumping private package "${pkgName}"`);
return;
}
if (!info.private) {
} else {
// Version should be updated
info.version = semver.inc(
info.version,
options.prereleasePrefix ? 'prerelease' : changeType,

Просмотреть файл

@ -3,9 +3,9 @@ import { BumpInfo } from '../types/BumpInfo';
import { getPackageGroups } from '../monorepo/getPackageGroups';
export function setGroupsInBumpInfo(bumpInfo: BumpInfo, options: BeachballOptions) {
bumpInfo.packageGroups = getPackageGroups(bumpInfo.packageInfos, options.path, options.groups);
if (options.groups) {
bumpInfo.packageGroups = getPackageGroups(bumpInfo.packageInfos, options.path, options.groups);
for (const grpName of Object.keys(bumpInfo.packageGroups)) {
const grpOptions = options.groups.find(groupItem => groupItem.name === grpName)!;
bumpInfo.groupOptions[grpName] = grpOptions;

Просмотреть файл

@ -21,11 +21,6 @@ import { ChangeType } from '../types/ChangeInfo';
* - bumpInfo.calculatedChangeTypes: will not mutate the entryPoint `pkgName` change type
*
* Inputs from bumpInfo are listed in the [^1] below in the function body
*
* @param entryPointPackageName
* @param bumpInfo
* @param bumpDeps
* @returns
*/
export function updateRelatedChangeType(changeFile: string, bumpInfo: BumpInfo, bumpDeps: boolean) {
/** [^1]: all the information needed from `bumpInfo` */

Просмотреть файл

@ -129,13 +129,13 @@ async function writeChangelogFiles(
try {
previousJson = fs.existsSync(changelogJsonFile) ? fs.readJSONSync(changelogJsonFile) : undefined;
} catch (e) {
console.warn('CHANGELOG.json is invalid:', e);
console.warn(`${changelogJsonFile} is invalid: ${e}`);
}
try {
const nextJson = renderJsonChangelog(newVersionChangelog, previousJson);
fs.writeJSONSync(changelogJsonFile, nextJson, { spaces: 2 });
} catch (e) {
console.warn('Problem writing to CHANGELOG.json:', e);
console.warn(`Problem writing to ${changelogJsonFile}: ${e}`);
}
// Update CHANGELOG.md

Просмотреть файл

@ -1,3 +1,12 @@
/** Format strings as a bulleted list with line breaks */
export function formatList(items: string[]) {
return items.map(item => `- ${item}`).join('\n');
}
/**
* Format an object on a single line with spaces between the properties and brackets
* (similar to `JSON.stringify(obj, null, 2)` but without the line breaks).
*/
export function singleLineStringify(obj: any) {
return JSON.stringify(obj, null, 2).replace(/\n\s*/g, ' ');
}

Просмотреть файл

@ -12,7 +12,7 @@ export function getPackageGroups(packageInfos: PackageInfos, root: string, group
const packageNameToGroup: { [packageName: string]: string } = {};
let hasError = false;
const errorPackages: Record<string, VersionGroupOptions[]> = {};
// Check every package to see which group it belongs to
for (const [pkgName, info] of Object.entries(packageInfos)) {
@ -22,10 +22,7 @@ export function getPackageGroups(packageInfos: PackageInfos, root: string, group
const groupsForPkg = groups.filter(group => isPathIncluded(relativePath, group.include, group.exclude));
if (groupsForPkg.length > 1) {
// Keep going after this error to ensure we report all errors
console.error(
`ERROR: "${pkgName}" cannot belong to multiple groups: [${groupsForPkg.map(g => g.name).join(', ')}]`
);
hasError = true;
errorPackages[pkgName] = groupsForPkg;
} else if (groupsForPkg.length === 1) {
const group = groupsForPkg[0];
packageNameToGroup[pkgName] = group.name;
@ -38,7 +35,14 @@ export function getPackageGroups(packageInfos: PackageInfos, root: string, group
}
}
if (hasError) {
if (errorPackages.length) {
console.error(
`ERROR: Found package(s) belonging to multiple groups:\n` +
Object.entries(errorPackages)
.map(([pkgName, groups]) => `- ${pkgName}: [${groups.map(g => g.name).join(', ')}]`)
.sort()
.join('\n')
);
// TODO: probably more appropriate to throw here and let the caller handle it?
process.exit(1);
}

Просмотреть файл

@ -11,6 +11,9 @@ const BUMP_PUSH_RETRIES = 5;
/** Use verbose logging for these steps to make it easier to debug if something goes wrong */
const verbose = true;
/**
* Bump versions locally, commit, optionally tag, and push to git.
*/
export async function bumpAndPush(bumpInfo: BumpInfo, publishBranch: string, options: BeachballOptions) {
const { path: cwd, branch, depth, gitTimeout } = options;
const { remote, remoteBranch } = parseRemoteBranch(branch);
@ -25,7 +28,7 @@ export async function bumpAndPush(bumpInfo: BumpInfo, publishBranch: string, opt
while (tryNumber < BUMP_PUSH_RETRIES && !completed) {
tryNumber++;
console.log('-'.repeat(80));
console.log(`Trying to push to git. Attempt ${tryNumber}/${BUMP_PUSH_RETRIES}`);
console.log(`Bumping versions and pushing to git (attempt ${tryNumber}/${BUMP_PUSH_RETRIES})`);
console.log('Reverting');
revertLocalChanges(cwd);
@ -47,7 +50,7 @@ export async function bumpAndPush(bumpInfo: BumpInfo, publishBranch: string, opt
}
// bump the version
console.log('\nBumping the versions for git push');
console.log('\nBumping versions locally (and writing changelogs if requested)');
await performBump(bumpInfo, options);
// checkin

Просмотреть файл

@ -1,7 +1,7 @@
import { BumpInfo } from '../types/BumpInfo';
export function displayManualRecovery(bumpInfo: BumpInfo, succeededPackages: Set<string> = new Set<string>()) {
const errorLines = ['Something went wrong with publishing! Manually update these package and versions:'];
const errorLines: string[] = [];
const succeededLines: string[] = [];
bumpInfo.modifiedPackages.forEach(pkg => {
@ -14,13 +14,15 @@ export function displayManualRecovery(bumpInfo: BumpInfo, succeededPackages: Set
}
});
console.error(errorLines.join('\n') + '\n');
console.error(
'Something went wrong with publishing! Manually update these package and versions:\n' + errorLines.sort().join('\n')
);
if (succeededLines.length) {
console.warn(
'These packages and versions were successfully published, but may be invalid if they depend on ' +
'package versions for which publishing failed:\n' +
succeededLines.join('\n') +
succeededLines.sort().join('\n') +
'\n\nTo recover from this, run "beachball sync" to synchronize local package.json files with the registry. ' +
'If necessary, unpublish or deprecate any invalid packages from the above list after "beachball sync".'
);

Просмотреть файл

@ -43,7 +43,7 @@ export function getPackagesToPublish(bumpInfo: BumpInfo, validationMode?: boolea
// this log is not helpful when called from `validate`
if (skippedPackageReasons.length && !validationMode) {
console.log(`\nSkipping publishing the following packages:\n${formatList(skippedPackageReasons)}`);
console.log(`\nSkipping publishing the following packages:\n${formatList(skippedPackageReasons.sort())}`);
}
return packagesToPublish;

Просмотреть файл

@ -18,7 +18,10 @@ export function validatePackageDependencies(packagesToValidate: string[], packag
if (errorDeps.length) {
console.error(
`ERROR: Found private packages among published package dependencies:\n` +
errorDeps.map(dep => `- ${dep}: used by ${allDeps[dep].join(', ')}`).join('\n')
errorDeps
.map(dep => `- ${dep}: used by ${allDeps[dep].join(', ')}`)
.sort()
.join('\n')
);
return false;
}

Просмотреть файл

@ -29,12 +29,13 @@ export async function validatePackageVersions(
}
if (okVersions.length) {
// keep the original order here to show what order they'll be published in
console.log(`\nPackage versions are OK to publish:\n${formatList(okVersions)}`);
}
if (errorVersions.length) {
console.error(
`\nERROR: Attempting to publish package versions that already exist in the registry:\n` +
formatList(errorVersions)
formatList(errorVersions.sort())
);
return false;
}

Просмотреть файл

@ -6,11 +6,6 @@ export function areChangeFilesDeleted(options: BeachballOptions): boolean {
const { branch, path: cwd } = options;
const root = findProjectRoot(cwd);
if (!root) {
console.error('Failed to find the project root');
process.exit(1);
}
const changePath = getChangePath(cwd);
console.log(`Checking for deleted change files against "${branch}"`);
@ -24,19 +19,11 @@ export function areChangeFilesDeleted(options: BeachballOptions): boolean {
root
);
// if this value is undefined, git has failed to execute the command above.
if (!changeFilesDeletedSinceRef) {
process.exit(1);
}
const changeFilesDeleted = changeFilesDeletedSinceRef.length > 0;
if (changeFilesDeleted) {
if (changeFilesDeletedSinceRef.length) {
const changeFiles = changeFilesDeletedSinceRef.map(file => `- ${file}`);
const errorMessage = 'The following change files were deleted:';
console.error(`${errorMessage}\n${changeFiles.join('\n')}\n`);
console.error(`ERROR: The following change files were deleted:\n${changeFiles.join('\n')}\n`);
return true;
}
return changeFilesDeleted;
return false;
}

Просмотреть файл

@ -14,6 +14,7 @@ export function isChangeFileNeeded(options: BeachballOptions, packageInfos: Pack
.map(pkg => `\n ${pkg}`)
.join('')}`
);
return true;
}
return changedPackages.length > 0;
return false;
}

Просмотреть файл

@ -1,3 +1,5 @@
import { SortedChangeTypes } from '../changefile/changeTypes';
export function isValidChangeType(changeType: string) {
return ['patch', 'major', 'minor', 'prerelease', 'none'].includes(changeType);
return SortedChangeTypes.includes(changeType as any);
}

Просмотреть файл

@ -1,30 +1,20 @@
import { ChangelogOptions, ChangelogGroupOptions } from '../types/ChangelogOptions';
import { singleLineStringify } from '../logging/format';
import { ChangelogOptions } from '../types/ChangelogOptions';
export function isValidChangelogOptions(options: ChangelogOptions): boolean {
if (options.groups) {
if (!isValidChangelogGroupOptions(options.groups)) {
return false;
}
if (!options.groups) {
return true;
}
return true;
}
function isValidChangelogGroupOptions(groupOptions: ChangelogGroupOptions[]): boolean {
for (const options of groupOptions) {
if (!options.changelogPath) {
console.log('changelog group options cannot contain empty changelogPath.');
return false;
}
const badGroups = options.groups.filter(group => !group.changelogPath || !group.masterPackageName || !group.include);
if (!options.masterPackageName) {
console.log('changelog group options cannot contain empty masterPackageName.');
return false;
}
if (!options.include) {
console.log('changelog group options cannot contain empty include.');
return false;
}
if (badGroups.length) {
console.error(
'ERROR: "changelog.groups" entries must define "changelogPath", "masterPackageName", and "include". ' +
'Found invalid groups:\n' +
badGroups.map(group => ' ' + singleLineStringify(group)).join('\n')
);
return false;
}
return true;

Просмотреть файл

@ -1,34 +1,47 @@
import { formatList, singleLineStringify } from '../logging/format';
import { VersionGroupOptions } from '../types/BeachballOptions';
import { getPackageGroups } from '../monorepo/getPackageGroups';
import { getPackageInfos } from '../monorepo/getPackageInfos';
import { PackageGroups, PackageInfos } from '../types/PackageInfo';
export function isValidGroupOptions(root: string, groups: VersionGroupOptions[]) {
export function isValidGroupOptions(groups: VersionGroupOptions[]) {
// Values that violate types could happen in a user-provided object
if (!Array.isArray(groups)) {
console.error(
'ERROR: Expected "groups" configuration setting to be an array. Received:\n' + JSON.stringify(groups)
);
return false;
}
for (const group of groups) {
if (!group.include || !group.name) {
return false;
}
const badGroups = groups.filter(group => !group.include || !group.name);
if (badGroups.length) {
console.error(
'ERROR: "groups" configuration entries must define "include" and "name". Found invalid groups:\n' +
badGroups.map(group => ' ' + singleLineStringify(group)).join('\n')
);
return false;
}
}
const packageInfos = getPackageInfos(root);
const packageGroups = getPackageGroups(packageInfos, root, groups);
// make sure no disallowed changetype options exist inside an individual package
/** Validate per-package beachball options are valid for packages in groups */
export function isValidGroupedPackageOptions(packageInfos: PackageInfos, packageGroups: PackageGroups) {
const errorPackages: string[] = [];
for (const grp of Object.keys(packageGroups)) {
const pkgs = packageGroups[grp].packageNames;
for (const pkgName of pkgs) {
// make sure no disallowed change type options exist inside an individual package
for (const [groupName, { packageNames }] of Object.entries(packageGroups)) {
for (const pkgName of packageNames) {
if (packageInfos[pkgName].packageOptions.disallowedChangeTypes) {
console.error(
`Cannot have a disallowedChangeType inside a package config (${pkgName}) when there is a group defined; use the groups.disallowedChangeTypes instead.`
);
return false;
errorPackages.push(`${pkgName} in group "${groupName}"`);
}
}
}
if (errorPackages.length) {
console.error(
'ERROR: Found package configs that define disallowedChangeTypes and are also part of a group. ' +
'Define disallowedChangeTypes in the group instead.\n' +
formatList(errorPackages.sort())
);
return false;
}
return true;
}

Просмотреть файл

@ -3,7 +3,7 @@ import { getUntrackedChanges } from 'workspace-tools';
import { isValidAuthType } from './isValidAuthType';
import { isValidChangeType } from './isValidChangeType';
import { isChangeFileNeeded } from './isChangeFileNeeded';
import { isValidGroupOptions } from './isValidGroupOptions';
import { isValidGroupedPackageOptions, isValidGroupOptions } from './isValidGroupOptions';
import { BeachballOptions } from '../types/BeachballOptions';
import { isValidChangelogOptions } from './isValidChangelogOptions';
import { readChangeFiles } from '../changefile/readChangeFiles';
@ -15,114 +15,143 @@ import { validatePackageDependencies } from '../publish/validatePackageDependenc
import { gatherBumpInfo } from '../bump/gatherBumpInfo';
import { isValidDependentChangeType } from './isValidDependentChangeType';
import { getPackagesToPublish } from '../publish/getPackagesToPublish';
import { env } from '../env';
type ValidationOptions = {
allowMissingChangeFiles?: boolean;
/**
* Defaults to true. If false, skip fetching the latest from the remote and don't check whether
* changes files are needed (or whether change files are deleted).
*/
allowFetching?: boolean;
/**
* If true, skip checking whether change files are needed. Ignored if `allowFetching` is false.
*/
allowMissingChangeFiles?: boolean;
};
export function validate(options: BeachballOptions, validateOptions?: Partial<ValidationOptions>) {
const { allowMissingChangeFiles = false, allowFetching = true } = validateOptions || {};
console.log('\nValidating options and change files...');
// Run the validation checks in stages and wait to exit until the end of the stage.
// This provides more potentially useful info the user rather than hiding errors.
let hasError = false;
const logValidationError = (message: string) => {
console.error(`ERROR: ${message}`);
hasError = true;
};
if (!isGitAvailable(options.path)) {
console.error('ERROR: Please make sure git is installed and initialize the repository with "git init".');
logValidationError('Please make sure git is installed and initialize the repository with "git init"');
process.exit(1);
}
const untracked = getUntrackedChanges(options.path);
if (untracked && untracked.length > 0) {
console.warn('WARN: There are untracked changes in your repository:');
console.warn('- ' + untracked.join('\n- '));
console.warn('Changes in these files will not trigger a prompt for change descriptions');
if (untracked.length) {
console.warn('WARN: There are untracked changes in your repository:\n' + untracked.join('\n- '));
!env.isCI && console.warn('Changes in these files will not trigger a prompt for change descriptions');
}
const packageInfos = getPackageInfos(options.path);
if (typeof options.package === 'string' && !packageInfos[options.package]) {
console.error(`ERROR: package "${options.package}" was not found`);
process.exit(1);
}
const invalidPackages = Array.isArray(options.package)
? options.package.filter(pkg => !packageInfos[pkg])
: undefined;
if (invalidPackages?.length) {
console.error(`ERROR: package(s) ${invalidPackages.map(pkg => `"${pkg}"`).join(', ')} were not found`);
process.exit(1);
logValidationError(`package "${options.package}" was not found`);
} else {
const invalidPackages = Array.isArray(options.package)
? options.package.filter(pkg => !packageInfos[pkg])
: undefined;
if (invalidPackages?.length) {
logValidationError(`package(s) ${invalidPackages.map(pkg => `"${pkg}"`).join(', ')} were not found`);
}
}
if (options.authType && !isValidAuthType(options.authType)) {
console.error(`ERROR: auth type "${options.authType}" is not valid`);
process.exit(1);
logValidationError(`authType "${options.authType}" is not valid`);
}
if (options.dependentChangeType && !isValidChangeType(options.dependentChangeType)) {
console.error(`ERROR: dependent change type "${options.dependentChangeType}" is not valid`);
process.exit(1);
logValidationError(`dependentChangeType "${options.dependentChangeType}" is not valid`);
}
if (options.type && !isValidChangeType(options.type)) {
console.error(`ERROR: change type "${options.type}" is not valid`);
logValidationError(`Change type "${options.type}" is not valid`);
}
if (options.changelog && !isValidChangelogOptions(options.changelog)) {
hasError = true; // the helper logs this
}
if (options.groups && !isValidGroupOptions(options.groups)) {
hasError = true; // the helper logs this
}
// this exits the process if any package belongs to multiple groups
const packageGroups = getPackageGroups(packageInfos, options.path, options.groups);
if (options.groups && !isValidGroupedPackageOptions(packageInfos, packageGroups)) {
hasError = true; // the helper logs this
}
if (hasError) {
// If any of the above basic checks failed, it doesn't make sense to check if change files are needed
process.exit(1);
}
let isChangeNeeded = false;
if (allowFetching) {
// This has the side effect of fetching, so call it even if !allowMissingChangeFiles for now
isChangeNeeded = isChangeFileNeeded(options, packageInfos);
if (isChangeNeeded && !allowMissingChangeFiles) {
console.error('ERROR: Change files are needed!');
logValidationError('Change files are needed!');
console.log(options.changehint);
process.exit(1);
process.exit(1); // exit here (this is the main poin)
}
if (options.disallowDeletedChangeFiles && areChangeFilesDeleted(options)) {
console.error('ERROR: Change files must not be deleted!');
logValidationError('Change files must not be deleted!');
process.exit(1);
}
}
if (options.groups && !isValidGroupOptions(options.path, options.groups)) {
console.error('ERROR: Groups defined inside the configuration is invalid');
console.log(options.groups);
process.exit(1);
}
if (options.changelog && !isValidChangelogOptions(options.changelog)) {
console.error('ERROR: Changelog defined inside the configuration is invalid');
console.log(options.changelog);
process.exit(1);
}
const changeSet = readChangeFiles(options, packageInfos);
const packageGroups = getPackageGroups(packageInfos, options.path, options.groups);
for (const { changeFile, change } of changeSet) {
const disallowedChangeTypes = getDisallowedChangeTypes(change.packageName, packageInfos, packageGroups);
if (!change.type || !isValidChangeType(change.type) || disallowedChangeTypes?.includes(change.type)) {
console.error(
`ERROR: there is an invalid change type detected ${changeFile}: "${change.type}" is not a valid change type`
);
process.exit(1);
if (!change.type) {
logValidationError(`Change type is missing in ${changeFile}`);
hasError = true;
} else if (!isValidChangeType(change.type)) {
logValidationError(`Invalid change type detected in ${changeFile}: "${change.type}"`);
hasError = true;
} else if (disallowedChangeTypes?.includes(change.type)) {
logValidationError(`Disallowed change type detected in ${changeFile}: "${change.type}"`);
hasError = true;
}
if (!change.dependentChangeType || !isValidDependentChangeType(change.dependentChangeType, disallowedChangeTypes)) {
console.error(
`ERROR: there is an invalid dependentChangeType detected ${changeFile}: "${change.dependentChangeType}" is not a valid dependentChangeType`
);
process.exit(1);
if (!change.dependentChangeType) {
logValidationError(`dependentChangeType is missing in ${changeFile}`);
hasError = true;
} else if (!isValidDependentChangeType(change.dependentChangeType, disallowedChangeTypes)) {
logValidationError(`Invalid dependentChangeType detected in ${changeFile}: "${change.dependentChangeType}"`);
hasError = true;
}
}
if (!isChangeNeeded) {
console.log('\nValidating package dependencies...');
// TODO: It would be preferable if this could be done without getting the full bump info,
// or at least if the bump info could be passed back out to other methods which currently
// duplicate the calculation (it can be expensive, especially in large repos).
const bumpInfo = gatherBumpInfo(options, packageInfos);
const packagesToPublish = getPackagesToPublish(bumpInfo, true /*validationMode*/);
if (!validatePackageDependencies(packagesToPublish, bumpInfo.packageInfos)) {
console.error(`ERROR: one or more published packages depend on an unpublished package!
logValidationError(`One or more published packages depend on an unpublished package!
Consider one of the following solutions:
- If the unpublished package should be published, remove \`"private": true\` from its package.json.
@ -132,6 +161,8 @@ Consider one of the following solutions:
}
}
console.log();
return {
isChangeNeeded,
};