Skip to content
Merged
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
136 changes: 47 additions & 89 deletions scripts/release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@
* Automated release script for prettier-vscode
*
* Usage:
* pnpm release major - Bump major version (11.0.0 -> 12.0.0)
* pnpm release minor - Bump minor version (11.0.0 -> 11.1.0)
* pnpm release patch - Bump patch version (11.0.0 -> 11.0.1)
* pnpm release preview - Create preview release (11.0.0 -> 11.0.0-preview.1)
* pnpm release preview 12.0.0-preview.1 - Create preview with specific version
* pnpm release major - Bump major version (11.0.0 -> 12.0.0)
* pnpm release minor - Bump minor version (11.0.0 -> 11.1.0)
* pnpm release patch - Bump patch version (11.0.0 -> 11.0.1)
* pnpm release patch --pre - Patch as prerelease (tag: v11.0.1-pre)
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

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

The documentation states "Patch as prerelease" but the behavior actually bumps the patch version before applying the prerelease tag. If the current version is 11.0.0, running pnpm release patch --pre will result in version 11.0.1 with tag v11.0.1-pre, not 11.0.0-pre. Consider clarifying this as "Bump patch and mark as prerelease" or "Patch bump with prerelease tag" to avoid confusion.

Suggested change
* pnpm release patch --pre - Patch as prerelease (tag: v11.0.1-pre)
* pnpm release patch --pre - Bump patch and mark as prerelease (tag: v11.0.1-pre)

Copilot uses AI. Check for mistakes.
* pnpm release minor 12.1.0 - Use specific version
* pnpm release patch 12.0.1 --pre - Specific version as prerelease
*/
import fs from "fs/promises";
import { execSync } from "child_process";

const RELEASE_TYPES = ["major", "minor", "patch", "preview"];
const RELEASE_TYPES = ["major", "minor", "patch"];

function exec(cmd, options = {}) {
console.log(`$ ${cmd}`);
Expand All @@ -24,34 +25,26 @@ function execQuiet(cmd) {
}

function parseVersion(version) {
// Handle prerelease versions like "11.1.0-preview.1"
const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-([a-z]+)\.(\d+))?$/i);
const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
if (!match) {
throw new Error(`Invalid version format: ${version}`);
}
return {
major: parseInt(match[1], 10),
minor: parseInt(match[2], 10),
patch: parseInt(match[3], 10),
prerelease: match[4] || null,
prereleaseNum: match[5] ? parseInt(match[5], 10) : null,
};
}
Comment on lines 27 to 37
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

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

The updated parseVersion function only accepts semver versions in X.Y.Z format and will fail if the current package.json version contains a prerelease identifier from the old system (e.g., 11.0.1-preview.1). This could break the script during the transition period. Consider adding backward compatibility to handle the old prerelease format, or document that package.json must be manually updated to a clean semver version before using this new script version.

Copilot uses AI. Check for mistakes.

function formatVersion({ major, minor, patch, prerelease, prereleaseNum }) {
let version = `${major}.${minor}.${patch}`;
if (prerelease) {
version += `-${prerelease}.${prereleaseNum}`;
}
return version;
function formatVersion({ major, minor, patch }) {
return `${major}.${minor}.${patch}`;
}

async function updateChangelog(version, releaseType) {
async function updateChangelog(version, isPrerelease) {
const CHANGELOG = "CHANGELOG.md";
const isPrerelease = releaseType === "preview" || version.includes("-");

if (isPrerelease) {
console.log(`Skipping changelog update for prerelease version ${version}`);
console.log(`Skipping changelog update for prerelease`);
return;
}

Expand All @@ -71,110 +64,73 @@ async function updateChangelog(version, releaseType) {

function calculateNewVersion(currentVersion, releaseType) {
const v = parseVersion(currentVersion);
const isCurrentPrerelease = v.prerelease !== null;

switch (releaseType) {
case "major":
// If current is prerelease of this major, just drop prerelease suffix
// e.g., 12.0.0-preview.1 -> 12.0.0
if (isCurrentPrerelease && v.minor === 0 && v.patch === 0) {
return formatVersion({ ...v, prerelease: null, prereleaseNum: null });
}
// Otherwise bump major
return formatVersion({
major: v.major + 1,
minor: 0,
patch: 0,
prerelease: null,
prereleaseNum: null,
});

case "minor":
// If current is prerelease of this minor, just drop prerelease suffix
// e.g., 11.1.0-preview.1 -> 11.1.0
if (isCurrentPrerelease && v.patch === 0) {
return formatVersion({ ...v, prerelease: null, prereleaseNum: null });
}
// Otherwise bump minor
return formatVersion({
major: v.major,
minor: v.minor + 1,
patch: 0,
prerelease: null,
prereleaseNum: null,
});

case "patch":
// If current is prerelease of this patch, just drop prerelease suffix
// e.g., 11.0.1-preview.1 -> 11.0.1
if (isCurrentPrerelease) {
return formatVersion({ ...v, prerelease: null, prereleaseNum: null });
}
// Otherwise bump patch
return formatVersion({
major: v.major,
minor: v.minor,
patch: v.patch + 1,
prerelease: null,
prereleaseNum: null,
});

case "preview":
// If already a preview, increment the preview number
if (isCurrentPrerelease && v.prerelease === "preview") {
return formatVersion({
...v,
prereleaseNum: v.prereleaseNum + 1,
});
}
// Otherwise, keep the same version and add preview.1
return formatVersion({
major: v.major,
minor: v.minor,
patch: v.patch,
prerelease: "preview",
prereleaseNum: 1,
});

default:
throw new Error(`Unknown release type: ${releaseType}`);
}
}

function parseArgs(argv) {
const args = argv.slice(2);
const isPrerelease = args.includes("--pre");
const positional = args.filter((arg) => !arg.startsWith("--"));

return {
releaseType: positional[0],
manualVersion: positional[1],
isPrerelease,
};
}

async function main() {
const releaseType = process.argv[2];
const manualVersion = process.argv[3];
const { releaseType, manualVersion, isPrerelease } = parseArgs(process.argv);

if (!releaseType || !RELEASE_TYPES.includes(releaseType)) {
console.error("Usage: pnpm release <major|minor|patch|preview> [version]");
console.error("Usage: pnpm release <major|minor|patch> [version] [--pre]");
console.error("");
console.error("Examples:");
console.error(" pnpm release major - 11.0.0 -> 12.0.0");
console.error(" pnpm release minor - 11.0.0 -> 11.1.0");
console.error(" pnpm release patch - 11.0.0 -> 11.0.1");
console.error(" pnpm release preview - 11.0.0 -> 11.0.0-preview.1");
console.error(" pnpm release major - 11.0.0 -> 12.0.0");
console.error(" pnpm release minor - 11.0.0 -> 11.1.0");
console.error(" pnpm release patch - 11.0.0 -> 11.0.1");
console.error(
" pnpm release patch --pre - 11.0.0 -> 11.0.1 (tag: v11.0.1-pre)",
);
console.error(" pnpm release minor 12.1.0 - Use specific version");
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

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

The example suggests that any release type can be used with a manual version (pnpm release minor 12.1.0), but the validation at line 128-136 doesn't enforce that the manual version is consistent with the release type. For example, pnpm release patch 13.0.0 would be accepted even though 13.0.0 represents a major version bump. Consider adding validation to ensure the manual version is semantically consistent with the specified release type, or clarify in the documentation that the release type is ignored when a manual version is provided.

Copilot uses AI. Check for mistakes.
console.error(
" pnpm release preview 12.0.0-preview.1 - Use specific version",
" pnpm release patch 12.0.1 --pre - Specific version as prerelease",
);
process.exit(1);
}

// Validate manual version if provided
if (manualVersion) {
if (releaseType !== "preview") {
console.error(
"Error: Manual version can only be specified with 'preview' release type.",
);
process.exit(1);
}
// Validate format
try {
parseVersion(manualVersion);
} catch {
console.error(`Error: Invalid version format: ${manualVersion}`);
console.error(
"Expected format: X.Y.Z or X.Y.Z-prerelease.N (e.g., 12.0.0-preview.1)",
);
console.error("Expected format: X.Y.Z (e.g., 12.0.1)");
process.exit(1);
}
}
Expand All @@ -189,13 +145,11 @@ async function main() {

// Stable releases must be on main branch
const currentBranch = execQuiet("git rev-parse --abbrev-ref HEAD");
if (releaseType !== "preview" && currentBranch !== "main") {
console.error(
`Error: ${releaseType} releases must be run from the main branch.`,
);
if (!isPrerelease && currentBranch !== "main") {
console.error(`Error: Stable releases must be run from the main branch.`);
console.error(`Current branch: ${currentBranch}`);
console.error("\nTo release a preview from this branch, use:");
console.error(" pnpm release preview");
console.error("\nTo release a prerelease from this branch, use:");
console.error(" pnpm release <major|minor|patch> --pre");
process.exit(1);
}

Expand All @@ -205,9 +159,14 @@ async function main() {
const newVersion =
manualVersion || calculateNewVersion(currentVersion, releaseType);

console.log(`\nRelease: ${releaseType}`);
const tagName = isPrerelease ? `v${newVersion}-pre` : `v${newVersion}`;
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

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

The tag suffix -pre is not recognized by the GitHub workflow's prerelease detection logic. The workflow at .github/workflows/main.yaml:90 checks for -preview|-beta|-alpha|-rc patterns but not -pre. This means prereleases created with this script will be published as stable releases. Either update the workflow to include -pre in the regex pattern, or change this script to use -preview instead of -pre to maintain compatibility with the existing workflow.

Copilot uses AI. Check for mistakes.

console.log(
`\nRelease: ${releaseType}${isPrerelease ? " (prerelease)" : ""}`,
);
console.log(`Current version: ${currentVersion}`);
console.log(`New version: ${newVersion}\n`);
console.log(`New version: ${newVersion}`);
console.log(`Tag: ${tagName}\n`);

// Update package.json
packageJson.version = newVersion;
Expand All @@ -218,13 +177,12 @@ async function main() {
console.log("Updated package.json");

// Update changelog (skip for prereleases)
await updateChangelog(newVersion, releaseType);
await updateChangelog(newVersion, isPrerelease);

// Stage changes
exec("git add package.json CHANGELOG.md");

// Create commit and tag
const tagName = `v${newVersion}`;
exec(`git commit -m "${tagName}"`);
exec(`git tag ${tagName}`);

Expand Down
Loading