From 3bea6a043d49078bc657175457d0cd5e62975c6a Mon Sep 17 00:00:00 2001 From: justsisyphus Date: Thu, 22 Jan 2026 10:40:39 +0900 Subject: [PATCH] fix(publish): robust error handling, republish mode, separate tag/branch push - Fix 404 error handling: no longer incorrectly marks failed publishes as 'already published' - Add REPUBLISH mode: allows re-publishing missing platform packages without version check - Separate tag and branch push: tag push (critical) succeeds even if branch push fails - Fix changelog for beta releases: compares against previous beta tag instead of latest stable - Add checkPackageVersionExists for accurate E403 error handling --- .github/workflows/publish.yml | 6 +++ script/publish.ts | 98 ++++++++++++++++++++++++++++++----- 2 files changed, 90 insertions(+), 14 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3ea38dc2a..f8f3828c8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -22,6 +22,11 @@ on: required: false type: boolean default: false + republish: + description: "Re-publish mode: skip version check, only publish missing packages" + required: false + type: boolean + default: false concurrency: ${{ github.workflow }}-${{ github.ref }} @@ -144,6 +149,7 @@ jobs: BUMP: ${{ inputs.bump }} VERSION: ${{ inputs.version }} SKIP_PLATFORM_PACKAGES: ${{ inputs.skip_platform }} + REPUBLISH: ${{ inputs.republish }} CI: true GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_CONFIG_PROVENANCE: true diff --git a/script/publish.ts b/script/publish.ts index ded30b486..46300109a 100644 --- a/script/publish.ts +++ b/script/publish.ts @@ -7,6 +7,7 @@ import { join } from "node:path" const PACKAGE_NAME = "oh-my-opencode" const bump = process.env.BUMP as "major" | "minor" | "patch" | undefined const versionOverride = process.env.VERSION +const republishMode = process.env.REPUBLISH === "true" const PLATFORM_PACKAGES = [ "darwin-arm64", @@ -83,11 +84,36 @@ async function updateAllPackageVersions(newVersion: string): Promise { } } -async function generateChangelog(previous: string): Promise { +async function findPreviousTag(currentVersion: string): Promise { + // For beta versions, find the previous beta tag (e.g., 3.0.0-beta.11 for 3.0.0-beta.12) + const betaMatch = currentVersion.match(/^(\d+\.\d+\.\d+)-beta\.(\d+)$/) + if (betaMatch) { + const [, base, num] = betaMatch + const prevNum = parseInt(num) - 1 + if (prevNum >= 1) { + const prevTag = `${base}-beta.${prevNum}` + const exists = await $`git rev-parse v${prevTag}`.nothrow() + if (exists.exitCode === 0) return prevTag + } + } + return null +} + +async function generateChangelog(previous: string, currentVersion?: string): Promise { const notes: string[] = [] + // Try to find the most accurate previous tag for comparison + let compareTag = previous + if (currentVersion) { + const prevBetaTag = await findPreviousTag(currentVersion) + if (prevBetaTag) { + compareTag = prevBetaTag + console.log(`Using previous beta tag for comparison: v${compareTag}`) + } + } + try { - const log = await $`git log v${previous}..HEAD --oneline --format="%h %s"`.text() + const log = await $`git log v${compareTag}..HEAD --oneline --format="%h %s"`.text() const commits = log .split("\n") .filter((line) => line && !line.match(/^\w+ (ignore:|test:|chore:|ci:|release:)/i)) @@ -161,7 +187,25 @@ interface PublishResult { error?: string } -async function publishPackage(cwd: string, distTag: string | null, useProvenance = true): Promise { +async function checkPackageVersionExists(pkgName: string, version: string): Promise { + try { + const res = await fetch(`https://registry.npmjs.org/${pkgName}/${version}`) + return res.ok + } catch { + return false + } +} + +async function publishPackage(cwd: string, distTag: string | null, useProvenance = true, pkgName?: string, version?: string): Promise { + // In republish mode, skip if package already exists on npm + if (republishMode && pkgName && version) { + const exists = await checkPackageVersionExists(pkgName, version) + if (exists) { + return { success: true, alreadyPublished: true } + } + console.log(` ${pkgName}@${version} not found on npm, publishing...`) + } + const tagArgs = distTag ? ["--tag", distTag] : [] const provenanceArgs = process.env.CI && useProvenance ? ["--provenance"] : [] @@ -171,19 +215,31 @@ async function publishPackage(cwd: string, distTag: string | null, useProvenance } catch (error: any) { const stderr = error?.stderr?.toString() || error?.message || "" - // E409/E403 = version already exists (idempotent success) - // E404 + "Access token expired" = OIDC token expired while publishing already-published package + // Only treat as "already published" if we're certain the package exists + // E409/EPUBLISHCONFLICT = definitive "version already exists" if ( stderr.includes("EPUBLISHCONFLICT") || stderr.includes("E409") || - stderr.includes("E403") || stderr.includes("cannot publish over") || - stderr.includes("already exists") || - (stderr.includes("E404") && stderr.includes("Access token expired")) + stderr.includes("You cannot publish over the previously published versions") ) { return { success: true, alreadyPublished: true } } + // E403 can mean "already exists" OR "no permission" - verify by checking npm registry + if (stderr.includes("E403")) { + if (pkgName && version) { + const exists = await checkPackageVersionExists(pkgName, version) + if (exists) { + return { success: true, alreadyPublished: true } + } + } + // If we can't verify or it doesn't exist, it's a real error + return { success: false, error: stderr } + } + + // 404 errors are NEVER "already published" - they indicate the package doesn't exist + // or OIDC token issues. Always treat as failure. return { success: false, error: stderr } } } @@ -215,7 +271,7 @@ async function publishAllPackages(version: string): Promise { const pkgName = `oh-my-opencode-${platform}` console.log(` Starting ${pkgName}...`) - const result = await publishPackage(pkgDir, distTag, false) + const result = await publishPackage(pkgDir, distTag, false, pkgName, version) return { platform, pkgName, result } }) @@ -243,7 +299,7 @@ async function publishAllPackages(version: string): Promise { // Publish main package last console.log(`\n📦 Publishing main package...`) - const mainResult = await publishPackage(process.cwd(), distTag) + const mainResult = await publishPackage(process.cwd(), distTag, true, PACKAGE_NAME, version) if (mainResult.success) { if (mainResult.alreadyPublished) { @@ -298,7 +354,16 @@ async function gitTagAndRelease(newVersion: string, notes: string[]): Promise 0 ? notes.join("\n") : "No notable changes" @@ -325,12 +390,17 @@ async function main() { console.log(`New version: ${newVersion}\n`) if (await checkVersionExists(newVersion)) { - console.log(`Version ${newVersion} already exists on npm. Skipping publish.`) - process.exit(0) + if (republishMode) { + console.log(`Version ${newVersion} exists on npm. REPUBLISH mode: checking for missing platform packages...`) + } else { + console.log(`Version ${newVersion} already exists on npm. Skipping publish.`) + console.log(`(Use REPUBLISH=true to publish missing platform packages)`) + process.exit(0) + } } await updateAllPackageVersions(newVersion) - const changelog = await generateChangelog(previous) + const changelog = await generateChangelog(previous, newVersion) const contributors = await getContributors(previous) const notes = [...changelog, ...contributors]