Skip to content

Commit

Permalink
Compel users to release packages with breaking changes alongside thei…
Browse files Browse the repository at this point in the history
…r dependents
  • Loading branch information
cryptodev-2s committed Sep 28, 2023
1 parent 73f60d0 commit eff572d
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 3 deletions.
118 changes: 117 additions & 1 deletion src/release-specification.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,13 @@ packages:
await withSandbox(async (sandbox) => {
const project = buildMockProject({
workspacePackages: {
a: buildMockPackage('a'),
a: buildMockPackage('a', {
unvalidatedManifest: {
dependencies: {
b: '1.0.0',
},
},
}),
b: buildMockPackage('b'),
c: buildMockPackage('c'),
d: buildMockPackage('d'),
Expand Down Expand Up @@ -678,6 +684,116 @@ Your release spec could not be processed due to the following issues:
The release spec file has been retained for you to edit again and make the necessary fixes. Once you've done this, re-run this tool.
${releaseSpecificationPath}
`.trim(),
);
});
});

it('throws if there are any packages listed in the release but their dependent via "dependencies" is not listed', async () => {
await withSandbox(async (sandbox) => {
const project = buildMockProject({
workspacePackages: {
a: buildMockPackage('a', {
hasChangesSinceLatestRelease: true,
}),
b: buildMockPackage('b', {
hasChangesSinceLatestRelease: false,
unvalidatedManifest: {
dependencies: {
a: '1.0.0',
},
},
}),
},
});
const releaseSpecificationPath = path.join(
sandbox.directoryPath,
'release-spec',
);
await fs.promises.writeFile(
releaseSpecificationPath,
YAML.stringify({
packages: {
a: 'major',
},
}),
);

await expect(
validateReleaseSpecification(project, releaseSpecificationPath),
).rejects.toThrow(
`
Your release spec could not be processed due to the following issues:
* The following packages, which depends on released package a, are missing.
- b
Consider including them in the release spec so that they won't break in production.
If you are ABSOLUTELY SURE that this won't occur, however, and want to postpone the release of a package, then list it with a directive of "intentionally-skip". For example:
packages:
b: intentionally-skip
The release spec file has been retained for you to edit again and make the necessary fixes. Once you've done this, re-run this tool.
${releaseSpecificationPath}
`.trim(),
);
});
});

it('throws if there are any packages listed in the release but their dependent via "peerDependencies" is not listed', async () => {
await withSandbox(async (sandbox) => {
const project = buildMockProject({
workspacePackages: {
a: buildMockPackage('a', {
hasChangesSinceLatestRelease: true,
}),
b: buildMockPackage('b', {
hasChangesSinceLatestRelease: false,
unvalidatedManifest: {
peerDependencies: {
a: '1.0.0',
},
},
}),
},
});
const releaseSpecificationPath = path.join(
sandbox.directoryPath,
'release-spec',
);
await fs.promises.writeFile(
releaseSpecificationPath,
YAML.stringify({
packages: {
a: 'major',
},
}),
);

await expect(
validateReleaseSpecification(project, releaseSpecificationPath),
).rejects.toThrow(
`
Your release spec could not be processed due to the following issues:
* The following packages, which depends on released package a, are missing.
- b
Consider including them in the release spec so that they won't break in production.
If you are ABSOLUTELY SURE that this won't occur, however, and want to postpone the release of a package, then list it with a directive of "intentionally-skip". For example:
packages:
b: intentionally-skip
The release spec file has been retained for you to edit again and make the necessary fixes. Once you've done this, re-run this tool.
${releaseSpecificationPath}
`.trim(),
);
Expand Down
74 changes: 74 additions & 0 deletions src/release-specification.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fs, { WriteStream } from 'fs';
import YAML from 'yaml';
import { diff } from 'semver';
import { Editor } from './editor';
import { readFile } from './fs';
import {
Expand Down Expand Up @@ -304,6 +305,79 @@ export async function validateReleaseSpecification(
},
);

Object.keys(unvalidatedReleaseSpecification.packages).forEach(
(packageName) => {
const versionSpecifierOrDirective =
unvalidatedReleaseSpecification.packages[packageName];
const pkg = project.workspacePackages[packageName];

if (
versionSpecifierOrDirective === 'major' ||
(isValidSemver(versionSpecifierOrDirective) &&
diff(
pkg.validatedManifest.version,
versionSpecifierOrDirective as string,
) === 'major')
) {
const missingDependents = Object.values(
project.workspacePackages,
).filter((dependent) => {
const { dependencies, peerDependencies } =
dependent.unvalidatedManifest;
const isDependent =
(dependencies && hasProperty(dependencies, packageName)) ||
(peerDependencies && hasProperty(peerDependencies, packageName));

if (!isDependent) {
return false;
}

const dependentVersionSpecifierOrDirective =
unvalidatedReleaseSpecification.packages[
dependent.validatedManifest.name
];

return (
dependentVersionSpecifierOrDirective !== SKIP_PACKAGE_DIRECTIVE &&
dependentVersionSpecifierOrDirective !==
INTENTIONALLY_SKIP_PACKAGE_DIRECTIVE &&
!hasProperty(
IncrementableVersionParts,
dependentVersionSpecifierOrDirective,
) &&
!isValidSemver(dependentVersionSpecifierOrDirective)
);
});

if (missingDependents.length > 0) {
errors.push({
message: [
`The following packages, which depends on released package ${packageName}, are missing.`,
missingDependents
.map((dependent) => ` - ${dependent.validatedManifest.name}`)
.join('\n'),
" Consider including them in the release spec so that they won't break in production.",
` If you are ABSOLUTELY SURE that this won't occur, however, and want to postpone the release of a package, then list it with a directive of "intentionally-skip". For example:`,
YAML.stringify({
packages: missingDependents.reduce((object, dependent) => {
return {
...object,
[dependent.validatedManifest.name]:
INTENTIONALLY_SKIP_PACKAGE_DIRECTIVE,
};
}, {}),
})
.trim()
.split('\n')
.map((line) => ` ${line}`)
.join('\n'),
].join('\n\n'),
});
}
}
},
);

if (errors.length > 0) {
const message = [
'Your release spec could not be processed due to the following issues:',
Expand Down
9 changes: 7 additions & 2 deletions tests/unit/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { SemVer } from 'semver';
import { isPlainObject } from '@metamask/utils';
import type { Package } from '../../src/package';
import { PackageManifestFieldNames } from '../../src/package-manifest';
import type { ValidatedPackageManifest } from '../../src/package-manifest';
import type {
UnvalidatedPackageManifest,
ValidatedPackageManifest,
} from '../../src/package-manifest';
import type { Project } from '../../src/project';

/**
Expand Down Expand Up @@ -35,6 +38,7 @@ type MockPackageOverrides = Omit<
Partial<ValidatedPackageManifest>,
PackageManifestFieldNames.Name | PackageManifestFieldNames.Version
>;
unvalidatedManifest?: UnvalidatedPackageManifest;
};

/**
Expand Down Expand Up @@ -102,6 +106,7 @@ export function buildMockPackage(

const {
validatedManifest = {},
unvalidatedManifest = {},
directoryPath = `/path/to/packages/${name}`,
manifestPath = path.join(directoryPath, 'package.json'),
changelogPath = path.join(directoryPath, 'CHANGELOG.md'),
Expand All @@ -110,7 +115,7 @@ export function buildMockPackage(

return {
directoryPath,
unvalidatedManifest: {},
unvalidatedManifest,
validatedManifest: buildMockManifest({
...validatedManifest,
[PackageManifestFieldNames.Name]: name,
Expand Down

0 comments on commit eff572d

Please sign in to comment.