Skip to content

Channel fallback can publish a prerelease tag to the latest dist-tag #1233

Description

@sahrizvi

Problem

If OPENCODE_CHANNEL is ever empty when publish.ts runs, a prerelease tag publishes to the latest dist-tag and auto-upgrades every existing stable user onto a beta.

The channel fallback in packages/script/src/index.ts:25-30 is not prerelease-aware:

const CHANNEL = await (async () => {
  if (env.OPENCODE_CHANNEL) return env.OPENCODE_CHANNEL
  if (env.OPENCODE_BUMP) return "latest"
  if (env.OPENCODE_VERSION && !env.OPENCODE_VERSION.replace(/^v/, "").startsWith("0.0.0-")) return "latest"
  return await $`git branch --show-current`.text().then((x) => x.trim())
})()

Failure case

.github/workflows/release.yml:338 publishes with OPENCODE_VERSION: ${{ github.ref_name }}. For a beta tag that is v0.10.0-beta.1. If OPENCODE_CHANNEL did not reach the step:

  1. env.OPENCODE_CHANNEL is empty → skipped
  2. OPENCODE_BUMP unset → skipped
  3. OPENCODE_VERSION = v0.10.0-beta.1.replace(/^v/, "")0.10.0-beta.1 → does not start with 0.0.0-returns latest

All three npm publish calls then run --tag latest, and the latest dist-tag moves to a prerelease. Every existing user auto-upgrades to the beta on next launch — the exact outcome the beta channel exists to prevent.

Why it has not happened

release.yml:98 and :344 set OPENCODE_CHANNEL to ${{ contains(github.ref_name, '-') && 'beta' || 'latest' }}, which always evaluates to one of those two strings and never to empty. Verified: there is exactly one publish step, and it carries the variable in its own env: block. All three npm publish calls in publish.ts pass --tag ${Script.channel}; none publishes bare.

So today the hazard is masked entirely by one workflow expression. Nothing in the fallback itself prevents it.

Why it is worth fixing anyway

The blast radius is the whole user base, and the documented recovery (npm dist-tag add @altimateai/altimate-code@<good> latest) requires npm publish credentials that most of the team does not hold — so detection without the ability to remediate is thin protection. The release process makes "confirm latest did not move" a mandatory post-publish assertion precisely because this class of mistake is unrecoverable for most people who would hit it.

Defence in depth: make the fallback refuse to route a prerelease version to latest, so the safety does not rest solely on an env var reaching a step.

Fix

In the OPENCODE_VERSION branch, treat a version carrying a semver prerelease component as beta rather than latest. The 0.0.0- preview path must keep falling through to the branch-name channel, so the guard is scoped to the branch that already excludes it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions