From 3c77896971fc90a63e4d2ab7731a593cff41ac1b Mon Sep 17 00:00:00 2001 From: cadamsdev Date: Mon, 2 Feb 2026 22:21:06 -0500 Subject: [PATCH 1/3] skip creating github release if it already exists --- .changeset/swift-icons-begin.md | 5 +++ src/publish.test.ts | 61 +++++++++++++++++++++++++++++++++ src/publish.ts | 7 ++++ 3 files changed, 73 insertions(+) create mode 100644 .changeset/swift-icons-begin.md diff --git a/.changeset/swift-icons-begin.md b/.changeset/swift-icons-begin.md new file mode 100644 index 0000000..fc8d2a5 --- /dev/null +++ b/.changeset/swift-icons-begin.md @@ -0,0 +1,5 @@ +--- +"@lazy-release/changesets": fix +--- + +Skip creating GitHub release if release already exists diff --git a/src/publish.test.ts b/src/publish.test.ts index 83fc109..54716bc 100644 --- a/src/publish.test.ts +++ b/src/publish.test.ts @@ -812,6 +812,67 @@ describe("publish command", () => { expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("No changelog found")); }); + test("should skip GitHub release when release already exists", async () => { + spyOn(tinyglobby, "globSync").mockReturnValue(["package.json"]); + spyOn(fs, "readFileSync").mockImplementation((path: any) => { + const pathStr = typeof path === "string" ? path : path.toString(); + if (pathStr.includes("package.json")) { + return JSON.stringify( + { + name: "@test/package", + version: "1.0.0", + }, + null, + 2, + ); + } + if (pathStr.includes("CHANGELOG.md")) { + return `## 1.0.0\n\n### šŸš€ feat\n- Test changeset`; + } + return ""; + }); + spyOn(fs, "existsSync").mockImplementation((path: any) => { + const pathStr = typeof path === "string" ? path : path.toString(); + return pathStr.includes("CHANGELOG.md"); + }); + spyOn(childProcess, "execSync").mockImplementation((cmd: string) => { + if (cmd.includes("ls-remote")) { + throw new Error("Tag not found"); + } + if (cmd.includes("git config")) { + return "git@github.com:owner/repo.git"; + } + return ""; + }); + spyOn(packageManagerDetector, "detect").mockResolvedValue({ name: "npm", agent: "npm" }); + + // Mock fetch to return 422 error (release already exists) + const fetchMock = async (url: string, options: any) => { + if (url.includes("releases") && options?.method === "POST") { + return { + ok: false, + status: 422, + text: async () => "Validation Failed", + } as Response; + } + return { + ok: false, + text: async () => "", + } as Response; + }; + global.fetch = fetchMock as any; + + process.env.GITHUB_TOKEN = "test-token"; + + await publish({ dryRun: false }); + + const calls = consoleLogSpy.mock.calls.flat(); + const hasSkippedRelease = calls.some( + (arg: any) => typeof arg === "string" && arg.includes("already exists"), + ); + expect(hasSkippedRelease).toBe(true); + }); + test("should ignore packages in config ignore list", async () => { spyOn(tinyglobby, "globSync").mockReturnValue(["package.json"]); spyOn(fs, "readFileSync").mockImplementation((path: any) => { diff --git a/src/publish.ts b/src/publish.ts index 10893d4..ffa87cd 100644 --- a/src/publish.ts +++ b/src/publish.ts @@ -236,6 +236,13 @@ async function createGitHubRelease( if (!response.ok) { const error = await response.text(); + + // GitHub returns 422 when a release already exists for the tag + if (response.status === 422) { + console.log(pc.dim(`GitHub release for ${tag} already exists. Skipping.`)); + return; + } + throw new Error(`GitHub API error: ${response.status} ${error}`); } From 42468f181de982c1c233c332a7d9f11b338a6dd2 Mon Sep 17 00:00:00 2001 From: cadamsdev Date: Mon, 2 Feb 2026 22:29:45 -0500 Subject: [PATCH 2/3] fix error handling when publishing --- .changeset/better-laws-stop.md | 5 ++ src/publish.test.ts | 157 +++++++++++++++++++++++++++++++++ src/publish.ts | 38 ++++++-- 3 files changed, 194 insertions(+), 6 deletions(-) create mode 100644 .changeset/better-laws-stop.md diff --git a/.changeset/better-laws-stop.md b/.changeset/better-laws-stop.md new file mode 100644 index 0000000..c25c4b7 --- /dev/null +++ b/.changeset/better-laws-stop.md @@ -0,0 +1,5 @@ +--- +"@lazy-release/changesets": fix +--- + +Fix error handling when publishing packages diff --git a/src/publish.test.ts b/src/publish.test.ts index 54716bc..9701a2f 100644 --- a/src/publish.test.ts +++ b/src/publish.test.ts @@ -941,4 +941,161 @@ describe("publish command", () => { expect.stringContaining("2 package(s)"), ); }); + + test("should continue publishing other packages when one fails to create git tag", async () => { + spyOn(tinyglobby, "globSync").mockReturnValue([ + "packages/package1/package.json", + "packages/package2/package.json", + ]); + let tagCreateCount = 0; + spyOn(fs, "readFileSync").mockImplementation((path: any) => { + const pathStr = typeof path === "string" ? path : path.toString(); + if (pathStr.includes("package1/package.json")) { + return JSON.stringify( + { + name: "@test/package1", + version: "1.0.0", + }, + null, + 2, + ); + } + if (pathStr.includes("package2/package.json")) { + return JSON.stringify( + { + name: "@test/package2", + version: "2.0.0", + }, + null, + 2, + ); + } + if (pathStr.includes("package1") && pathStr.includes("CHANGELOG.md")) { + return `## 1.0.0\n\n### šŸš€ feat\n- Test changeset`; + } + if (pathStr.includes("package2") && pathStr.includes("CHANGELOG.md")) { + return `## 2.0.0\n\n### šŸš€ feat\n- Test changeset`; + } + return ""; + }); + spyOn(fs, "existsSync").mockImplementation((path: any) => { + const pathStr = typeof path === "string" ? path : path.toString(); + return pathStr.includes("CHANGELOG.md"); + }); + spyOn(childProcess, "execSync").mockImplementation((cmd: string) => { + if (cmd.includes("ls-remote")) { + throw new Error("Tag not found"); + } + if (cmd.includes("git tag -a")) { + // First package fails to create tag, second succeeds + tagCreateCount++; + if (tagCreateCount === 1) { + throw new Error("Failed to create git tag"); + } + } + if (cmd.includes("git config")) { + return "git@github.com:owner/repo.git"; + } + return ""; + }); + spyOn(packageManagerDetector, "detect").mockResolvedValue({ name: "npm", agent: "npm" }); + + const fetchMock = async (url: string, options: any) => { + return { + ok: true, + text: async () => "", + } as Response; + }; + global.fetch = fetchMock as any; + process.env.GITHUB_TOKEN = "test-token"; + + await publish({ dryRun: false }); + + // Should show both packages were processed + expect(consoleLogSpy).toHaveBeenCalledWith( + expect.stringContaining("Found"), + expect.stringContaining("2 package(s)"), + ); + // Should show completion message with 1 success and 1 failure + const calls = consoleLogSpy.mock.calls.flat(); + const hasSuccessMessage = calls.some( + (arg: any) => typeof arg === "string" && arg.includes("successful") && arg.includes("failed"), + ); + expect(hasSuccessMessage).toBe(true); + // Should show error for first package + const errorCalls = consoleErrorSpy.mock.calls.flat(); + const hasErrorMessage = errorCalls.some( + (arg: any) => typeof arg === "string" && arg.includes("Failed to publish @test/package1"), + ); + expect(hasErrorMessage).toBe(true); + // Should show continuation message + expect(consoleLogSpy).toHaveBeenCalledWith( + expect.stringContaining("Continuing with remaining packages"), + ); + }); + + test("should continue with GitHub release even when npm publish fails", async () => { + spyOn(tinyglobby, "globSync").mockReturnValue(["package.json"]); + spyOn(fs, "readFileSync").mockImplementation((path: any) => { + const pathStr = typeof path === "string" ? path : path.toString(); + if (pathStr.includes("package.json")) { + return JSON.stringify( + { + name: "@test/package", + version: "1.0.0", + }, + null, + 2, + ); + } + if (pathStr.includes("CHANGELOG.md")) { + return `## 1.0.0\n\n### šŸš€ feat\n- Test changeset`; + } + return ""; + }); + spyOn(fs, "existsSync").mockImplementation((path: any) => { + const pathStr = typeof path === "string" ? path : path.toString(); + return pathStr.includes("CHANGELOG.md"); + }); + spyOn(childProcess, "execSync").mockImplementation((cmd: string) => { + if (cmd.includes("ls-remote")) { + throw new Error("Tag not found"); + } + if (cmd.includes("npm publish")) { + throw new Error("npm publish failed"); + } + if (cmd.includes("git config")) { + return "git@github.com:owner/repo.git"; + } + return ""; + }); + spyOn(packageManagerDetector, "detect").mockResolvedValue({ name: "npm", agent: "npm" }); + + const fetchMock = async (url: string, options: any) => { + return { + ok: true, + text: async () => "", + } as Response; + }; + global.fetch = fetchMock as any; + process.env.GITHUB_TOKEN = "test-token"; + + await publish({ dryRun: false }); + + // Should show npm publish failed + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining("āœ—"), + expect.stringContaining("Failed to publish to npm"), + ); + // Should show continuation message + expect(consoleLogSpy).toHaveBeenCalledWith( + expect.stringContaining("Continuing with GitHub release creation"), + ); + // Should still create GitHub release + const calls = consoleLogSpy.mock.calls.flat(); + const hasCreatedRelease = calls.some( + (arg: any) => typeof arg === "string" && arg.includes("Created GitHub release"), + ); + expect(hasCreatedRelease).toBe(true); + }); }); diff --git a/src/publish.ts b/src/publish.ts index ffa87cd..95df571 100644 --- a/src/publish.ts +++ b/src/publish.ts @@ -34,14 +34,26 @@ export async function publish({ console.log(pc.dim("Found"), pc.cyan(`${packages.length} package(s)`)); + const results = { success: 0, failed: 0 }; + for (const pkg of packages) { - await publishPackage(pkg, dryRun, config, githubToken, draft); + try { + await publishPackage(pkg, dryRun, config, githubToken, draft); + results.success++; + } catch (error) { + results.failed++; + console.error(pc.red(`\nāœ— Failed to publish ${pkg.name}`)); + if (error instanceof Error) { + console.error(pc.red(error.message)); + } + console.log(pc.yellow("Continuing with remaining packages...\n")); + } } if (dryRun) { console.log(pc.yellow("\nDry run complete - no changes were made.")); } else { - console.log(pc.green("\nāœ” Publish complete!")); + console.log(pc.green(`\nāœ” Publish complete! ${results.success} successful, ${results.failed} failed`)); } } @@ -115,7 +127,15 @@ async function publishPackage( } else if (dryRun) { console.log(pc.yellow("[DRY RUN]"), pc.dim("Would publish to npm")); } else { - await publishToNpm(pkg, config); + try { + await publishToNpm(pkg, config); + } catch (error) { + console.error(pc.red("āœ—"), "Failed to publish to npm"); + if (error instanceof Error) { + console.error(pc.red(error.message)); + } + console.log(pc.yellow("Continuing with GitHub release creation...")); + } } if (dryRun) { @@ -135,7 +155,15 @@ async function publishPackage( console.log(pc.dim(" Body:"), pc.yellow("(No changelog found for this version)")); } } else { - await createGitHubRelease(pkg, tag, githubToken, draft); + try { + await createGitHubRelease(pkg, tag, githubToken, draft); + } catch (error) { + console.error(pc.red("āœ—"), "Failed to create GitHub release"); + if (error instanceof Error) { + console.error(pc.red(error.message)); + } + // Don't throw, just log and continue + } } } @@ -186,7 +214,6 @@ async function publishToNpm(pkg: PackageInfo, config: ChangesetConfig) { execSync(publishCmd, { cwd: pkg.dir, stdio: "inherit" }); console.log(pc.green("āœ”"), "Published to npm"); } catch (error) { - console.error(pc.red("āœ—"), "Failed to publish to npm"); throw error; } } @@ -248,7 +275,6 @@ async function createGitHubRelease( console.log(pc.green("āœ”"), "Created GitHub release"); } catch (error) { - console.error(pc.red("āœ—"), "Failed to create GitHub release"); throw error; } } From 19c3066804b37db3b3e3eac3c0f0a982bdb40dee Mon Sep 17 00:00:00 2001 From: cadamsdev Date: Mon, 2 Feb 2026 22:32:28 -0500 Subject: [PATCH 3/3] format code --- src/publish.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/publish.ts b/src/publish.ts index 95df571..e8d7fef 100644 --- a/src/publish.ts +++ b/src/publish.ts @@ -53,7 +53,9 @@ export async function publish({ if (dryRun) { console.log(pc.yellow("\nDry run complete - no changes were made.")); } else { - console.log(pc.green(`\nāœ” Publish complete! ${results.success} successful, ${results.failed} failed`)); + console.log( + pc.green(`\nāœ” Publish complete! ${results.success} successful, ${results.failed} failed`), + ); } } @@ -263,13 +265,13 @@ async function createGitHubRelease( if (!response.ok) { const error = await response.text(); - + // GitHub returns 422 when a release already exists for the tag if (response.status === 422) { console.log(pc.dim(`GitHub release for ${tag} already exists. Skipping.`)); return; } - + throw new Error(`GitHub API error: ${response.status} ${error}`); }