70bf21e064
Deploy (to testing) and Test Playground Preview Worker / Deploy Playground Preview Worker (testing) (push) Has been skipped
Deploy Workers Shared Staging / Deploy Workers Shared Staging (push) Failing after 0s
Prerelease / build (push) Has been skipped
Handle Changesets / Handle Changesets (push) Has been cancelled
Semgrep OSS scan / semgrep-oss (push) Has been cancelled
215 lines
6.0 KiB
TypeScript
215 lines
6.0 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import { writeFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
import { argv } from "node:process";
|
|
import dedent from "ts-dedent";
|
|
|
|
if (require.main === module) {
|
|
try {
|
|
console.log(dedent`
|
|
Generate Dependabot Changeset
|
|
=============================
|
|
`);
|
|
|
|
main(processArgs());
|
|
} catch (e) {
|
|
if (e instanceof Error) {
|
|
console.error(e.message);
|
|
} else {
|
|
console.error(`Error: ${e}`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
type Args = {
|
|
// PR number
|
|
prNumber: string;
|
|
// Comma-separated package names
|
|
packageNames: string;
|
|
// Path to package.json
|
|
packageJSONPath: string;
|
|
// Changeset file prefix
|
|
changesetPrefix: string;
|
|
};
|
|
|
|
function processArgs(): Args {
|
|
const args = [...argv];
|
|
if (args[0] === process.execPath) {
|
|
args.shift();
|
|
}
|
|
if (args[0] === __filename) {
|
|
args.shift();
|
|
}
|
|
if (args.length !== 4) {
|
|
throw new Error(dedent`
|
|
Incorrect arguments, please provide:
|
|
- PR: The number of the current Dependabot PR
|
|
- Packages: Coma-separated names of the workers-sdk packages whose dependencies are being updated
|
|
- PackageJSON: The path to the package JSON being updated by Dependabot
|
|
- Changeset prefix: The prefix to go on the front of the filename of the generated changeset.
|
|
`);
|
|
}
|
|
return {
|
|
prNumber: args[0],
|
|
packageNames: args[1],
|
|
packageJSONPath: args[2],
|
|
changesetPrefix: args[3],
|
|
};
|
|
}
|
|
|
|
function main({
|
|
prNumber,
|
|
packageNames,
|
|
packageJSONPath,
|
|
changesetPrefix,
|
|
}: Args): void {
|
|
// Also diff the pnpm catalog. Catalog-pinned deps (e.g.
|
|
// "@cloudflare/workers-types", referenced as "catalog:default" in
|
|
// `package.json`) have their real versions in `pnpm-workspace.yaml`, so
|
|
// Dependabot bumps them there rather than in `package.json`.
|
|
const changes = getDependencyChanges([
|
|
packageJSONPath,
|
|
"pnpm-workspace.yaml",
|
|
]);
|
|
if (changes.size === 0) {
|
|
console.warn(dedent`
|
|
WARN: No dependency changes detected for "${packageNames}".
|
|
`);
|
|
return;
|
|
}
|
|
const packages = packageNames.split(",").map((name: string) => name.trim());
|
|
const changesetHeader = generateChangesetHeader(packages);
|
|
const commitMessage = generateCommitMessage(packages, changes);
|
|
console.log(dedent`
|
|
INFO: Writing changeset with the following commit message
|
|
${commitMessage}`);
|
|
writeChangeSet(changesetPrefix, prNumber, changesetHeader, commitMessage);
|
|
commitAndPush(commitMessage);
|
|
}
|
|
|
|
export function getPackageJsonDiff(packageJSONPath: string): string[] {
|
|
return executeCommand("git", ["diff", "HEAD~1", packageJSONPath]);
|
|
}
|
|
|
|
export type Change = {
|
|
from: string;
|
|
to: string;
|
|
};
|
|
|
|
// Collects dependency changes across every provided file, merging them into a
|
|
// single map. We diff both the target package.json and the pnpm catalog because
|
|
// catalog-pinned deps are bumped in the catalog, not in the package.json.
|
|
export function getDependencyChanges(diffPaths: string[]): Map<string, Change> {
|
|
const diffLines = diffPaths.flatMap((path) =>
|
|
getPackageJsonDiff(resolve(path))
|
|
);
|
|
return parseDiffForChanges(diffLines);
|
|
}
|
|
|
|
export function parseDiffForChanges(
|
|
diffLines: (string | undefined)[]
|
|
): Map<string, Change> {
|
|
// Matches a dependency line in either a package.json diff (JSON, so the key
|
|
// is always quoted, e.g. `"workerd": "1.2.3"`) or a pnpm-workspace.yaml
|
|
// catalog diff (YAML, where the key may be unquoted, e.g. `workerd: "1.2.3"`).
|
|
// The quotes around the key are therefore optional.
|
|
const diffLineRegex = new RegExp(`^[+-]\\s*"?([^":]+?)"?:\\s"(.*)",?`);
|
|
const changes = new Map<string, Change>();
|
|
for (const line of diffLines) {
|
|
const match = line?.match(diffLineRegex);
|
|
if (match) {
|
|
const [matchedLine, name, version] = match;
|
|
const fromToProp = matchedLine.startsWith("+") ? "to" : "from";
|
|
const change = changes.get(name) ?? { from: "", to: "" };
|
|
change[fromToProp] = version;
|
|
changes.set(name, change);
|
|
}
|
|
}
|
|
return changes;
|
|
}
|
|
|
|
export function generateChangesetHeader(packages: string[]): string {
|
|
const lines = ["---"];
|
|
for (const packageName of packages) {
|
|
lines.push(`"${packageName}": patch`);
|
|
}
|
|
lines.push("---");
|
|
return lines.join("\n");
|
|
}
|
|
|
|
export function generateCommitMessage(
|
|
packages: string[],
|
|
changes: Map<string, Change>
|
|
): string {
|
|
const widths = [10, 4, 2];
|
|
for (const [name, { from, to }] of changes.entries()) {
|
|
if (from && to) {
|
|
widths[0] = Math.max(widths[0], name.length);
|
|
widths[1] = Math.max(widths[1], from.length);
|
|
widths[2] = Math.max(widths[2], to.length);
|
|
}
|
|
}
|
|
|
|
function padded(text: string, column: number): string {
|
|
return text + " ".repeat(widths[column] - text.length);
|
|
}
|
|
|
|
let table =
|
|
`\n| ${padded("Dependency", 0)} | ${padded("From", 1)} | ${padded(
|
|
"To",
|
|
2
|
|
)} |` +
|
|
`\n| ${"-".repeat(widths[0])} | ${"-".repeat(widths[1])} | ${"-".repeat(
|
|
widths[2]
|
|
)} |`;
|
|
for (const [name, { from, to }] of changes.entries()) {
|
|
if (!from || !to) {
|
|
console.warn(dedent`
|
|
WARN: Unexpected changes for package "${name}", from: "${from}", to: "${to}".
|
|
Could not determine upgrade versions.`);
|
|
} else {
|
|
table += `\n| ${padded(name, 0)} | ${padded(from, 1)} | ${padded(
|
|
to,
|
|
2
|
|
)} |`;
|
|
}
|
|
}
|
|
return dedent`
|
|
Update dependencies of ${packages.map((p: string) => `"${p}"`).join(", ")}
|
|
|
|
The following dependency versions have been updated:
|
|
${table}
|
|
`;
|
|
}
|
|
|
|
export function writeChangeSet(
|
|
changesetPrefix: string,
|
|
prNumber: string,
|
|
changesetHeader: string,
|
|
commitMessage: string
|
|
): void {
|
|
writeFileSync(
|
|
`.changeset/${changesetPrefix}-${prNumber}.md`,
|
|
changesetHeader + "\n\n" + commitMessage + "\n"
|
|
);
|
|
}
|
|
|
|
export function commitAndPush(commitMessage: string): void {
|
|
executeCommand("git", ["add", ".changeset"]);
|
|
executeCommand("git", ["commit", "-m", commitMessage]);
|
|
executeCommand("git", ["push"]);
|
|
}
|
|
|
|
function executeCommand(command: string, args: string[]): string[] {
|
|
const { output, error, status, stderr } = spawnSync(command, args, {
|
|
encoding: "utf-8",
|
|
});
|
|
if (status || error) {
|
|
throw error ?? new Error(stderr);
|
|
}
|
|
return output
|
|
.flatMap((chunk) => chunk?.split("\n"))
|
|
.filter((line) => line !== undefined);
|
|
}
|