From 172bf1b0fcc234a3e7799c0c8ca4ccd5495ea1ab Mon Sep 17 00:00:00 2001 From: minhn4 Date: Sun, 2 Aug 2026 23:17:31 +0700 Subject: [PATCH] fix(opencode): retry npm publish and skip the main dist manifest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A release publishes 13 packages concurrently, each a 40-60MB tarball, and the registry regularly times one of them out mid-upload with FETCH_ERROR — aborting the whole release. Retry each publish up to 5 times with linear backoff, and treat a version that appears on the registry after a failed attempt as published, since a timed-out request can still land. Resuming a release (`release --skip-build`) also scanned the main package directory that a previous run generated under dist/, which put codev-code into its own optionalDependencies. Skip that directory when collecting the platform manifests. Co-Authored-By: Claude Opus 5 (1M context) --- DIVERGENCES.md | 2 +- packages/opencode/script/publish.ts | 28 ++++++++++++++++++++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/DIVERGENCES.md b/DIVERGENCES.md index 1a7caed18e..fe34d51362 100644 --- a/DIVERGENCES.md +++ b/DIVERGENCES.md @@ -83,7 +83,7 @@ from the squash message first, or use a plain merge. | `packages/tui/src/ui/dialog-prompt.tsx` | Added `persistent?` (hides the `esc` hint + `DialogPrompt.show` re-arms the store flag), `onInput?` (fires on textarea content change), and `ref?` props | The configure flow's prompts (SSO login, manual credentials) are non-dismissable, and the SSO view watches `onInput` for the lone-`c` copy shortcut on the paste-back link (needs the `ref` to clear the field). | Keep our three props, the `Show` around the esc label, the textarea `onContentChange`, and the `.show` re-arm; take upstream's surrounding changes. | | `packages/tui/src/ui/dialog-prompt.tsx`, `packages/tui/src/config/keybind.ts`, `packages/tui/src/app.tsx` | Ctrl+V paste in dialog prompts. keybind.ts: added `"dialog.prompt.paste"` definition (`ctrl+v`, `preventDefault: false`, no CommandMap entry — dotted keys map to themselves). dialog-prompt.tsx: a `dialog.prompt.paste` command in the existing priority-1 binding layer reads `useClipboard().read?.()` and inserts `text/plain` content (newlines stripped, matching the single-line `InputRenderable` paste — dialog prompts are single-line fields and some upstream consumers store the value untrimmed); the command list is gathered alongside `dialog.prompt.submit`. app.tsx: `TuiInput.clipboard?: ClipboardService` passed to `` so tests inject a deterministic clipboard. Covered by the ctrl+v test in the fork-only `packages/tui/test/custom-provider.view.test.tsx`. | Many Windows terminals deliver ctrl+v as a key event instead of a bracketed paste, and upstream's clipboard-reading `prompt.paste` command targets only the main session prompt — so dialog fields (API keys in /connect, the custom-provider flow) could not paste at all (upstream issue anomalyco/opencode#39837). Bracketed paste still goes through the textarea's built-in default handler; the binding layer's `!props.busy` guard disables paste while busy. | Keep the keybind definition, the paste command + gathered binding list, and the `clipboard` input plumbing; take upstream's surrounding changes. Sent upstream as anomalyco/opencode#39844 (identical code) — once it lands in a synced release this row collapses to nothing; if upstream ships a different dialog paste fix, prefer theirs and drop this row. | | `packages/opencode/script/build.ts` | Platform packages' `package.json` name `codev-code-` (dist dir names unchanged — the bun compile target derives from them); compiled binary emitted as `bin/codev` (was `bin/opencode`; smoke-test path follows) | npm publishing under the fork's names; the binary inside platform packages carries the fork's name (postinstall.mjs copies it into the `codev` bin shim). The `Script.release` upload block stays upstream-shaped and unused — the fork's `.github/workflows/release.yml` packages and uploads GitHub-release artifacts itself. | Keep our `name:` line and `bin/codev` paths; take upstream's surrounding changes. | -| `packages/opencode/script/publish.ts` | `PUBLISH_NAME = "codev-code"` const; published-name→dir map; main pkg name + bin `codev` → `./bin/codev.exe` (postinstall stub file renamed from `${pkg.name}.exe`, matching the renamed platform binary); embedded README; no `repository` field in the generated package.json (npm renders it as the Repository link and derives a `#readme` Homepage from it — both kept off the npm page); env-guarded exit before the Docker/AUR/Homebrew section | npm-only publishing under the fork's names (unscoped, public on npmjs); the installed command is `codev`; upstream's other channels target anomalyco infra; the npm page should not link the fork's GitHub repo. | Keep our names, description, bin mapping, omitted `repository` + early exit; take upstream's npm-section changes above the exit. | +| `packages/opencode/script/publish.ts` | `PUBLISH_NAME = "codev-code"` const; published-name→dir map; main pkg name + bin `codev` → `./bin/codev.exe` (postinstall stub file renamed from `${pkg.name}.exe`, matching the renamed platform binary); embedded README; no `repository` field in the generated package.json (npm renders it as the Repository link and derives a `#readme` Homepage from it — both kept off the npm page); env-guarded exit before the Docker/AUR/Homebrew section; `npm publish` retried up to 5 times with linear backoff, treating a version that turns up on the registry after a failed attempt as published; the dist manifest scan skips the generated main package directory | npm-only publishing under the fork's names (unscoped, public on npmjs); the installed command is `codev`; upstream's other channels target anomalyco infra; the npm page should not link the fork's GitHub repo. Releases run from a laptop rather than CI, where the 13 concurrent 40-60MB uploads regularly time one package out (`npm error FETCH_ERROR`) and abort the release mid-way; resuming such a release must not make the main package depend on itself. | Keep our names, description, bin mapping, omitted `repository`, retry loop + early exit; take upstream's npm-section changes above the exit. If upstream adds its own publish retry, drop ours for theirs. | | `packages/opencode/bin/opencode` | `base` prefix `opencode-` → `codev-code-` | Runtime platform-binary lookup must match the published names. | Keep our prefix. | | `packages/opencode/script/postinstall.mjs` | `base` prefix `opencode-` → `codev-code-`; source/target binary names `opencode`/`opencode.exe` → `codev`/`codev.exe` (matches build.ts's `bin/codev` and publish.ts's `bin/codev.exe` stub); temp-dir prefix and failure message rebranded | Install-time platform-binary fetch must match the published package and binary names. | Keep our prefix and binary names. | | `packages/tui/src/logo.ts` | Pixel glyphs "open"+"code" → "co"+"dev"; added a `brand` export (`navy` `#19224c` / `red` `#ee0033`, the landing page's `--color-brand-navy`/`--color-brand-red`), a `wordmarkHalves` export (each row's "co" and "dev" halves with the shadow marks substituted to glyphs, so each half can take its own brand color), the composed `wordmark` (plain lines, built from `wordmarkHalves`) and `ansiWordmark` (bold, no shadow: "co" in the terminal's default foreground, "dev" in brand red truecolor) exports for CLI banners; added a `badge` export (the "c" of the wordmark — `logo.left` rows 1-3 sliced to the first glyph, with the `_` shadow-fill marks stripped to spaces so it renders as a flat outline) for the `--mini` splashes | Branded wordmark art; `ansiWordmark` feeds the CLI help banner and session epilogue. | Keep our glyphs + exports; take upstream's surrounding changes. | diff --git a/packages/opencode/script/publish.ts b/packages/opencode/script/publish.ts index 821f3ca3ed..15f262bc1d 100755 --- a/packages/opencode/script/publish.ts +++ b/packages/opencode/script/publish.ts @@ -13,6 +13,11 @@ process.chdir(dir) // compile target from them); only package.json `name` fields differ. const PUBLISH_NAME = "codev-code" +// All 13 packages publish concurrently and every tarball is 40-60MB, so the +// registry routinely times out one of them mid-upload (`npm error FETCH_ERROR`) +// and takes the whole release down with it. +const ATTEMPTS = 5 + async function published(name: string, version: string) { return (await $`npm view ${name}@${version} version`.nothrow()).exitCode === 0 } @@ -26,16 +31,31 @@ async function publish(dir: string, name: string, version: string) { return } await $`bun pm pack`.cwd(dir) - await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(dir) + for (let attempt = 1; attempt <= ATTEMPTS; attempt++) { + if ((await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(dir).nothrow()).exitCode === 0) return + // A timed-out request can still have landed the version, in which case a + // retry would only fail with "cannot publish over existing version". + if (await published(name, version)) { + console.log(`published ${name}@${version} despite the error above`) + return + } + if (attempt === ATTEMPTS) throw new Error(`failed to publish ${name}@${version} after ${ATTEMPTS} attempts`) + console.log(`retrying ${name}@${version} (attempt ${attempt + 1}/${ATTEMPTS})`) + await Bun.sleep(attempt * 10_000) + } } const binaries: Record = {} // Scoped published name -> unscoped dist directory (they differ in this fork). const dirs: Record = {} for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: "./dist" })) { - const pkg = await Bun.file(`./dist/${filepath}`).json() - binaries[pkg.name] = pkg.version - dirs[pkg.name] = filepath.replace("/package.json", "") + // A re-run (`release --skip-build` after a failed publish) scans a dist/ + // that already holds the main package directory generated below — leave it + // out or it lands in its own optionalDependencies. + if (filepath === `${pkg.name}/package.json`) continue + const manifest = await Bun.file(`./dist/${filepath}`).json() + binaries[manifest.name] = manifest.version + dirs[manifest.name] = filepath.replace("/package.json", "") } console.log("binaries", binaries) const version = Object.values(binaries)[0]