@akphi/dev-utils
Version:
180 lines (169 loc) • 6.54 kB
JavaScript
import { resolve } from 'path';
import chalk from 'chalk';
import { getChangedPackagesSinceRef } from '@changesets/git';
import readChangesets from '@changesets/read';
import getReleasePlan from '@changesets/get-release-plan';
import { error, warn, info, log } from '@changesets/logger';
import { getPackages } from '@manypkg/get-packages';
import { read } from '@changesets/config';
import writeChangeset from '@changesets/write';
import assembleReleasePlan from '@changesets/assemble-release-plan';
/**
* Ref `master` does not seem to be available in github-actions pipeline when using with action/checkout
* So we have to use `origin/master`
*
* See https://github.com/atlassian/changesets/issues/517
*/
const DEFAULT_SINCE_REF = 'origin/master';
/**
* Make sure the changeset covers all the packages whose files are changed since the specified reference.
* This is good to check changesets in a PR to ensure that the contributors have at least did a `patch`
* bump for all the changes they made.
*
* This is different from the philosophy of the creator of `changesets` where the reviewers and the contributors
* make `human` decision about the changes and that we can `trust` this decision. The problem with this
* approach is that sometimes, both the reviewers and the contributors can miss out on the changeset.
* And a package which is supposed to be released was not released! - This is really bad.
*
* By using this check, for whatever changes made, at least a `patch` bump is required. This covers the base
* for any PR.
*/
export async function validateChangesets(cwd, sinceRef) {
const packages = await getPackages(cwd);
const config = await read(cwd, packages);
const sinceBranch = sinceRef ?? DEFAULT_SINCE_REF;
const changesetPackageNames = (
await getReleasePlan.default(cwd, sinceBranch, config)
).releases
// packages whose versions are bumped because they depend on a package
// whose version is explicitly bumped is not of our concern
.filter((pkg) => pkg.changesets.length)
.map((pkg) => pkg.name);
const changedPackageNames = (
await getChangedPackagesSinceRef({
cwd,
ref: sinceBranch || config.baseBranch,
})
).map((pkg) => pkg.packageJson.name);
// Check for packages listeded in changeset(s) but no longer exists
// This is useful in case the current PR deletes some packages
// making some changesets invalid and can potentially break the release
const knownPackages = packages.packages.map((pkg) => pkg.packageJson.name);
const unknownPackagesMap = new Map();
const allExistingChangesets = await readChangesets.default(cwd);
allExistingChangesets.forEach((changeset) => {
const unknownPackages = new Set();
changeset.releases.forEach((release) => {
if (!knownPackages.includes(release.name)) {
unknownPackages.add(release.name);
}
});
if (unknownPackages.size) {
unknownPackagesMap.set(changeset.id, unknownPackages);
}
});
if (unknownPackagesMap.size) {
unknownPackagesMap.forEach((unknownPackages, changesetId) => {
error(
`Found ${
unknownPackages.size
} package(s) specified in changeset '${changesetId}' but do not exist in the project:\n${Array.from(
unknownPackages.values(),
)
.map((pkg) => `\u2A2F ${pkg}`)
.join('\n')}`,
);
});
error(
`Your changeset(s) are probably outdated: please update them and remove the missing packages. To generate changesets properly, please make sure to keep your fork up-to-date.`,
);
process.exit(1);
}
// Check for packages that have been modified but does not have a changeset entry
const packagesWithoutChangeset = new Set();
changedPackageNames.forEach((pkgName) => {
if (!changesetPackageNames.includes(pkgName)) {
packagesWithoutChangeset.add(pkgName);
}
});
if (packagesWithoutChangeset.size) {
error(
`Found ${
packagesWithoutChangeset.size
} package(s) that have been changed but no changesets for them were found:\n${Array.from(
packagesWithoutChangeset.values(),
)
.map((pkg) => `\u2A2F ${pkg}`)
.join('\n')}`,
);
error(
`Run \`yarn changeset -v <VERSION> -m "e.g. some message ..."\` to quickly add a changeset. ` +
`'<VERSION>' stands for the release branch you are working off or you can use 'latest' if you are working off the default branch`,
);
process.exit(1);
}
// Check for packages that have not been modified but still has a changeset entry
const extraPackages = new Set(); // packages that don't need changesets but specified
changesetPackageNames.forEach((pkg) => {
if (!changedPackageNames.includes(pkg)) {
extraPackages.add(pkg);
}
});
if (extraPackages.size) {
warn(
`Found ${
extraPackages.size
} package(s) with changesets but have not been modified:\n${Array.from(
extraPackages.values(),
)
.map((pkg) => `- ${pkg}`)
.join('\n')}`,
);
warn(`Please make sure this is what you wanted!`);
} else {
log(
chalk.green(`All changed packages have been listed in the changesets!`),
);
}
}
export async function generateChangeset(cwd, message, sinceRef) {
const packages = await getPackages(cwd);
const config = await read(cwd, packages);
const sinceBranch = sinceRef ?? DEFAULT_SINCE_REF;
const changedPackages = new Set(
(
await getChangedPackagesSinceRef({
cwd,
ref: sinceBranch || config.baseBranch,
})
).map((pkg) => pkg.packageJson.name),
);
if (!changedPackages.size) {
info(chalk.blue(`No changeset is needed as you haven't made any changes!`));
}
const newChangeset = {
releases: Array.from(changedPackages.values()).map((pkg) => ({
name: pkg,
type: 'patch',
})),
summary: message,
};
const changesetID = await writeChangeset.default(newChangeset, cwd);
log(
chalk.green(
'Successfully generated changeset! If you want to modify or expand on the changeset summary, you can find it here:',
),
);
info(chalk.blue(resolve(resolve(cwd, '.changeset'), `${changesetID}.md`)));
}
export async function getNextReleasePlan(cwd) {
const packages = await getPackages(cwd);
const config = await read(cwd, packages);
const releasePlan = assembleReleasePlan.default(
await readChangesets.default(cwd),
packages,
config,
undefined,
);
return releasePlan.releases;
}