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

Compel users to release packages with breaking changes alongside their dependents #101

Merged
merged 11 commits into from
Oct 17, 2023
281 changes: 281 additions & 0 deletions src/release-specification.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -683,5 +683,286 @@ ${releaseSpecificationPath}
);
});
});

it("throws if there are any packages in the release with a major version bump, but their dependents via 'dependencies' are not listed in the release", 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 depend on released package a, are missing.

- b

Consider including them in the release spec so that they are compatible with the new 'a' version.

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 in the release with a major version bump, but their dependents via 'peerDependencies' are not listed in the release", 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 depend on released package a, are missing.

- b

Consider including them in the release spec so that they are compatible with the new 'a' version.

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 in the release with a major version bump, but their dependents via 'dependencies' have their version specified as null in the release spec", 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',
b: null,
},
}),
);

await expect(
validateReleaseSpecification(project, releaseSpecificationPath),
).rejects.toThrow(
`
Your release spec could not be processed due to the following issues:

* The following packages, which depend on released package a, are missing.

- b

Consider including them in the release spec so that they are compatible with the new 'a' version.

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("does not throw an error if packages in the release with a major version bump have their dependents via 'dependencies' with their version specified as 'intentionally-skip' in the release spec", 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',
b: 'intentionally-skip',
},
}),
);

const releaseSpecification = await validateReleaseSpecification(
project,
releaseSpecificationPath,
);

expect(releaseSpecification).toStrictEqual({
packages: {
a: 'major',
},
path: releaseSpecificationPath,
});
});
});

it('does not throw an error for packages in the release with a minor or patch version bump, regardless of their dependents', 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 releaseSpecificationPath1 = path.join(
sandbox.directoryPath,
'release-spec',
);
await fs.promises.writeFile(
releaseSpecificationPath1,
YAML.stringify({
packages: {
a: 'minor',
},
}),
);

const releaseSpecification1 = await validateReleaseSpecification(
project,
releaseSpecificationPath1,
);

expect(releaseSpecification1).toStrictEqual({
packages: {
a: 'minor',
},
path: releaseSpecificationPath1,
});

const releaseSpecificationPath2 = path.join(
sandbox.directoryPath,
'release-spec',
);
await fs.promises.writeFile(
releaseSpecificationPath2,
YAML.stringify({
packages: {
a: 'patch',
},
}),
);

const releaseSpecification2 = await validateReleaseSpecification(
project,
releaseSpecificationPath2,
);

expect(releaseSpecification2).toStrictEqual({
packages: {
a: 'patch',
},
path: releaseSpecificationPath2,
});
});
});
});
});
73 changes: 69 additions & 4 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 @@ -241,10 +242,8 @@ export async function validateReleaseSpecification(
});
}

Object.keys(unvalidatedReleaseSpecification.packages).forEach(
(packageName, index) => {
const versionSpecifierOrDirective =
unvalidatedReleaseSpecification.packages[packageName];
Object.entries(unvalidatedReleaseSpecification.packages).forEach(
([packageName, versionSpecifierOrDirective], index) => {
const lineNumber = indexOfFirstUsableLine + index + 2;
const pkg = project.workspacePackages[packageName];

Expand Down Expand Up @@ -301,6 +300,72 @@ export async function validateReleaseSpecification(
});
}
}

// Check to compel users to release packages with breaking changes alongside their dependents
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 depend on released package ${packageName}, are missing.`,
missingDependents
.map((dependent) => ` - ${dependent.validatedManifest.name}`)
.join('\n'),
Copy link
Contributor

Choose a reason for hiding this comment

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

Following the changes above, you should have names of packages again instead of whole package objects, so you should be able to simplify this to:

Suggested change
missingDependents
.map((dependent) => ` - ${dependent.validatedManifest.name}`)
.join('\n'),
missingDependentNames.join('\n'),

` Consider including them in the release spec so that they are compatible with the new '${packageName}' version.`,
` 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'),
});
}
}
},
);

Expand Down
Loading