Skip to content

Commit b2cdb27

Browse files
committed
fix: harden validation, version prompt, and registry fetch
- Add AbortSignal.timeout(30_000) to the npm registry fetch in getPackageMetadata so hung requests fail after 30 s instead of hanging forever - Validate that a custom version entered in selectVersionPrompt is greater than the current version, preventing accidental downgrades - Throw ReleaseError (instead of plain Error) for missing token, invalid repo format, and package discovery failures so withErrorBoundary formats them consistently rather than letting them surface as unhandled exceptions
1 parent d2bc5a1 commit b2cdb27

File tree

4 files changed

+14
-10
lines changed

4 files changed

+14
-10
lines changed

src/core/npm.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ async function getPackageMetadata(
8383
headers: {
8484
Accept: "application/json",
8585
},
86+
signal: AbortSignal.timeout(30_000),
8687
});
8788

8889
if (!response.ok) {

src/core/prompts.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import type { BumpKind } from "#shared/types";
33
import {
44
getNextPrereleaseVersion,
55
getNextStableVersion,
6-
getNextVersion,
76
getPrereleaseIdentifier,
87
isValidSemver,
98
} from "#operations/semver";
109
import farver from "farver";
1110
import prompts from "prompts";
11+
import semver from "semver";
1212

1313
export async function selectPackagePrompt(
1414
packages: WorkspacePackage[],
@@ -121,11 +121,13 @@ export async function selectVersionPrompt(
121121
message: "Enter the new version number:",
122122
initial: suggestedVersion,
123123
validate: (custom: string) => {
124-
if (isValidSemver(custom)) {
125-
return true;
124+
if (!isValidSemver(custom)) {
125+
return "That's not a valid version number";
126126
}
127-
128-
return "That's not a valid version number";
127+
if (!semver.gt(custom, currentVersion)) {
128+
return `Version must be greater than the current version (${currentVersion})`;
129+
}
130+
return true;
129131
},
130132
});
131133

src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,15 @@ export async function createReleaseScripts(options: ReleaseScriptsOptionsInput):
6868
async list(): Promise<WorkspacePackage[]> {
6969
return withErrorBoundary(async () => {
7070
const result = await discoverWorkspacePackages(normalizedOptions.workspaceRoot, normalizedOptions);
71-
if (!result.ok) throw new Error(result.error.message);
71+
if (!result.ok) throw new ReleaseError(result.error.message);
7272
return result.value;
7373
});
7474
},
7575

7676
async get(packageName: string): Promise<WorkspacePackage | undefined> {
7777
return withErrorBoundary(async () => {
7878
const result = await discoverWorkspacePackages(normalizedOptions.workspaceRoot, normalizedOptions);
79-
if (!result.ok) throw new Error(result.error.message);
79+
if (!result.ok) throw new ReleaseError(result.error.message);
8080
return result.value.find((p) => p.name === packageName);
8181
});
8282
},

src/options.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { GitHubClient } from "#core/github";
22
import type { CommitTypeRule } from "#shared/types";
33
import process from "node:process";
44
import { createGitHubClient } from "#core/github";
5+
import { ReleaseError } from "#shared/errors";
56
import { dedent } from "@luxass/utils";
67

78
type DeepRequired<T> = Required<{
@@ -139,16 +140,16 @@ export function normalizeReleaseScriptsOptions(options: ReleaseScriptsOptionsInp
139140

140141
const token = githubToken.trim();
141142
if (!token) {
142-
throw new Error("GitHub token is required. Pass it in via options.");
143+
throw new ReleaseError("GitHub token is required. Pass it in via options.");
143144
}
144145

145146
if (!fullRepo || !fullRepo.trim() || !fullRepo.includes("/")) {
146-
throw new Error("Repository (repo) is required. Specify in 'owner/repo' format (e.g., 'octocat/hello-world').");
147+
throw new ReleaseError("Repository (repo) is required. Specify in 'owner/repo' format (e.g., 'octocat/hello-world').");
147148
}
148149

149150
const [owner, repo] = fullRepo.split("/");
150151
if (!owner || !repo) {
151-
throw new Error(`Invalid repo format: "${fullRepo}". Expected format: "owner/repo" (e.g., "octocat/hello-world").`);
152+
throw new ReleaseError(`Invalid repo format: "${fullRepo}". Expected format: "owner/repo" (e.g., "octocat/hello-world").`);
152153
}
153154

154155
const normalizedPackages = typeof packages === "object" && !Array.isArray(packages)

0 commit comments

Comments
 (0)