Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
148 changes: 124 additions & 24 deletions get-changed-packages.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import nodePath from "path";
import assembleReleasePlan from "@changesets/assemble-release-plan";
import { parse as parseConfig } from "@changesets/config";
import parseChangeset from "@changesets/parse";
import { assembleReleasePlan } from "@changesets/assemble-release-plan";
import { validateConfig } from "@changesets/config";
import { parseChangesetFile } from "@changesets/parse";
import type {
NewChangeset,
Package,
Packages,
PreState,
WrittenConfig,
PackageJSON as ChangesetPackageJSON,
} from "@changesets/types";
import type { Packages, Tool } from "@manypkg/get-packages";
import jsYaml from "js-yaml";
import micromatch from "micromatch";
import type { ProbotOctokit } from "probot";
import subset from "semver/ranges/subset.js";
import { isChangeset } from "./is-changeset.ts";

interface PackageJSON extends ChangesetPackageJSON {
Expand All @@ -23,6 +25,75 @@ interface PnpmWorkspace {
packages: ReadonlyArray<string>;
}

type ToolType = Packages["tool"]["type"];

/** Expected validation failures that should be surfaced in the PR comment. */
export class UserValidationError extends Error {}

const changesetsV2Range = ">=2.0.0 <3.0.0";

function isChangesetsV2Range(declaredVersion: string | undefined) {
if (declaredVersion === undefined) {
return false;
}

try {
return subset(declaredVersion, changesetsV2Range);
} catch {
return false;
}
}

function getReleasePlanConfig(
rawConfig: WrittenConfig & { prettier?: unknown },
rootPackageJsonContent: PackageJSON,
): WrittenConfig {
// The bot only calculates a release plan, so options used exclusively for formatting,
// writing files, Git comparisons, publishing, and snapshots are intentionally ignored.
const {
access: _access,
baseBranch: _baseBranch,
changedFilePatterns: _changedFilePatterns,
changelog: _changelog,
commit: _commit,
format: _format,
prettier: _prettier,
snapshot: _snapshot,
...releasePlanConfig
} = rawConfig;

const declaredChangesetsVersion =
rootPackageJsonContent.devDependencies?.["@changesets/cli"] ??
rootPackageJsonContent.dependencies?.["@changesets/cli"];
if (!isChangesetsV2Range(declaredChangesetsVersion)) {
return releasePlanConfig;
}

const privatePackages = rawConfig.privatePackages;
if (!("privatePackages" in rawConfig)) {
return { ...releasePlanConfig, privatePackages: { version: true } };
}
if (privatePackages === true) {
throw new UserValidationError(
"The `privatePackages` option can only be `false` or an object when using Changesets v2.",
);
}
// Changesets v2 defaulted an omitted `version` to `true` even inside the object form.
// Only adapt that valid shape; invalid values must pass through to `validateConfig`.
if (
typeof privatePackages === "object" &&
privatePackages !== null &&
!Array.isArray(privatePackages) &&
!("version" in privatePackages)
) {
return {
...releasePlanConfig,
privatePackages: { version: true, ...privatePackages },
};
}
return releasePlanConfig;
}

// TODO: it might be possible to remove this if improvements to `Array.isArray` ever land
// related thread: github.com/microsoft/TypeScript/issues/36554
function isArray<T>(
Expand Down Expand Up @@ -126,16 +197,24 @@ export const getChangedPackages = async ({
const id = res[1];

changesetPromises.push(
fetchTextFile(item.path).then((text) => ({
...parseChangeset(text),
id,
})),
fetchTextFile(item.path).then((text) => {
try {
return {
...parseChangesetFile(text),
id,
};
} catch (error) {
throw new UserValidationError(Error.isError(error) ? error.message : String(error), {
cause: error,
});
}
}),
);
}
}
let tool:
| {
tool: Tool;
type: ToolType;
globs: ReadonlyArray<string>;
}
| undefined;
Expand All @@ -146,7 +225,7 @@ export const getChangedPackages = async ({

if (pnpmWorkspace.packages) {
tool = {
tool: "pnpm",
type: "pnpm",
globs: pnpmWorkspace.packages,
};
}
Expand All @@ -156,31 +235,34 @@ export const getChangedPackages = async ({
if (rootPackageJsonContent.workspaces) {
if (isArray(rootPackageJsonContent.workspaces)) {
tool = {
tool: "yarn",
type: "yarn",
globs: rootPackageJsonContent.workspaces,
};
} else {
tool = {
tool: "yarn",
type: "yarn",
globs: rootPackageJsonContent.workspaces.packages,
};
}
} else if (rootPackageJsonContent.bolt && rootPackageJsonContent.bolt.workspaces) {
tool = {
tool: "bolt",
type: "bolt",
globs: rootPackageJsonContent.bolt.workspaces,
};
}
}

const rootPackageJsonContent = await rootPackageJsonContentsPromise;

const rootPackage: Package = {
dir: "/",
packageJson: rootPackageJsonContent,
};

const packages: Packages = {
root: {
dir: "/",
packageJson: rootPackageJsonContent,
},
tool: tool ? tool.tool : "root",
rootDir: "/",
rootPackage,
tool: { type: tool ? tool.type : "root" },
packages: [],
};

Expand All @@ -195,26 +277,44 @@ export const getChangedPackages = async ({

packages.packages = await Promise.all(matches.map((dir) => getPackage(dir)));
} else {
packages.packages.push(packages.root);
packages.packages.push(rootPackage);
}
if (hasErrored) {
throw new Error("an error occurred when fetching files");
}

const rawConfig = await rawConfigPromise;

const configResult = validateConfig(
getReleasePlanConfig(rawConfig, rootPackageJsonContent),
packages,
);

if (configResult.errors) {
throw new UserValidationError(
"Some errors occurred when validating the changesets config:\n" +
configResult.errors.join("\n"),
);
}

const releasePlan = assembleReleasePlan(
await Promise.all(changesetPromises),
packages,
parseConfig(await rawConfigPromise, packages),
configResult.config,
await preStatePromise,
);

return {
changedPackages: (packages.tool === "root"
// A root-only project has a single package covering the whole repository,
// so there is no directory to narrow the changed files down to.
const changedPackages =
packages.tool.type === "root"
? packages.packages
: packages.packages.filter((pkg) =>
changedFiles.some((changedFile) => changedFile.startsWith(`${pkg.dir}/`)),
)
).map((pkg) => pkg.packageJson.name),
);

return {
changedPackages: changedPackages.map((pkg) => pkg.packageJson.name),
releasePlan,
};
};
5 changes: 2 additions & 3 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { ValidationError } from "@changesets/errors";
import type { ReleasePlan, ComprehensiveRelease, VersionType } from "@changesets/types";
import type { EmitterWebhookEvent } from "@octokit/webhooks";
import { captureException } from "@sentry/node";
import { humanId } from "human-id";
import markdownTable from "markdown-table";
import type { Probot, Context } from "probot";
import { getChangedPackages } from "./get-changed-packages.ts";
import { getChangedPackages, UserValidationError } from "./get-changed-packages.ts";
import { isChangeset } from "./is-changeset.ts";

const getReleasePlanMessage = (releasePlan: ReleasePlan | null) => {
Expand Down Expand Up @@ -163,7 +162,7 @@ export default (app: Probot) => {
})
).data.token,
}).catch((err) => {
if (err instanceof ValidationError) {
if (err instanceof UserValidationError) {
errFromFetchingChangedFiles = `<details><summary>💥 An error occurred when fetching the changed packages and changesets in this PR</summary>\n\n\`\`\`\n${err.message}\n\`\`\`\n\n</details>\n`;
} else {
console.error(err);
Expand Down
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,10 @@
"test": "vitest"
},
"dependencies": {
"@changesets/assemble-release-plan": "^6.0.2",
"@changesets/config": "^3.0.1",
"@changesets/errors": "^0.2.0",
"@changesets/parse": "^0.4.0",
"@changesets/types": "^6.0.0",
"@manypkg/get-packages": "^1.1.3",
"@changesets/assemble-release-plan": "^7.0.0",
"@changesets/config": "^4.0.0",
"@changesets/parse": "^1.0.0",
"@changesets/types": "^7.0.0",
"@octokit/webhooks": "^9.8.4",
"@sentry/node": "^6.0.0",
"@types/js-yaml": "^3.12.2",
Expand All @@ -35,9 +33,11 @@
"markdown-table": "^2.0.0",
"micromatch": "^4.0.2",
"probot": "^12.2.4",
"semver": "^7.8.5",
"typescript": "^6.0.2"
},
"devDependencies": {
"@types/semver": "^7.8.0",
"knip": "^6.11.0",
"msw": "^2.12.14",
"oxfmt": "^0.42.0",
Expand Down
Loading