mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
4e30281a13
## Why For npm/Bun-managed installs, the update prompt was treating the latest GitHub release as ready to install. During the `0.124.0` release, GitHub and npm visibility were not atomic: the root npm wrapper could become visible before the npm registry marked that version as the package `latest`. That left a window where users could be prompted to upgrade before npm was ready for the release. ## What changed - Keep GitHub Releases as the candidate latest-version source for npm/Bun installs, but only write the existing `version.json` cache after npm registry metadata proves that same root version is ready. - Add `codex-rs/tui/src/npm_registry.rs` to validate npm readiness by checking `dist-tags.latest` and root package `dist` metadata for the GitHub candidate version. - Move version parsing helpers into `codex-rs/tui/src/update_versions.rs` so that logic can be tested without compiling the release-only `updates.rs` module under tests. - Update `.github/workflows/rust-release.yml` so the six known platform tarballs publish before the root `@openai/codex` wrapper. Other npm tarballs publish before the root wrapper, and the SDK publishes after the root package it depends on.
131 lines
4.0 KiB
Rust
131 lines
4.0 KiB
Rust
use serde::Deserialize;
|
|
use std::collections::HashMap;
|
|
|
|
#[cfg(not(debug_assertions))]
|
|
pub(crate) const PACKAGE_URL: &str = "https://registry.npmjs.org/@openai%2fcodex";
|
|
|
|
#[derive(Deserialize, Debug, Clone)]
|
|
pub(crate) struct NpmPackageInfo {
|
|
#[serde(rename = "dist-tags")]
|
|
dist_tags: HashMap<String, String>,
|
|
versions: HashMap<String, NpmPackageVersionInfo>,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug, Clone)]
|
|
struct NpmPackageVersionInfo {
|
|
dist: Option<NpmPackageDist>,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug, Clone)]
|
|
struct NpmPackageDist {
|
|
tarball: Option<String>,
|
|
integrity: Option<String>,
|
|
}
|
|
|
|
pub(crate) fn ensure_version_ready(
|
|
package_info: &NpmPackageInfo,
|
|
version: &str,
|
|
) -> anyhow::Result<()> {
|
|
let version = version.trim();
|
|
|
|
match package_info.dist_tags.get("latest").map(String::as_str) {
|
|
Some(latest) if latest == version => {}
|
|
Some(latest) => anyhow::bail!(
|
|
"npm latest dist-tag points to {latest}, expected GitHub release {version}"
|
|
),
|
|
None => anyhow::bail!("npm package is missing latest dist-tag"),
|
|
}
|
|
|
|
version_info_with_dist(package_info, version)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn version_info_with_dist<'a>(
|
|
package_info: &'a NpmPackageInfo,
|
|
version: &str,
|
|
) -> anyhow::Result<&'a NpmPackageVersionInfo> {
|
|
let info = package_info
|
|
.versions
|
|
.get(version)
|
|
.ok_or_else(|| anyhow::anyhow!("npm package version {version} is missing"))?;
|
|
let Some(dist) = info.dist.as_ref() else {
|
|
anyhow::bail!("npm package version {version} is missing dist metadata");
|
|
};
|
|
let has_tarball = dist
|
|
.tarball
|
|
.as_deref()
|
|
.is_some_and(|tarball| !tarball.is_empty());
|
|
if !has_tarball {
|
|
anyhow::bail!("npm package version {version} is missing dist.tarball");
|
|
}
|
|
let has_integrity = dist
|
|
.integrity
|
|
.as_ref()
|
|
.is_some_and(|integrity| !integrity.is_empty());
|
|
if !has_integrity {
|
|
anyhow::bail!("npm package version {version} is missing dist.integrity");
|
|
}
|
|
Ok(info)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn version_json(version: &str) -> serde_json::Value {
|
|
serde_json::json!({
|
|
"dist": {
|
|
"integrity": format!("sha512-{version}"),
|
|
"tarball": format!("https://registry.npmjs.org/@openai/codex/-/codex-{version}.tgz"),
|
|
}
|
|
})
|
|
}
|
|
|
|
fn package_info(github_latest: &str, npm_latest: &str) -> NpmPackageInfo {
|
|
let mut versions = serde_json::Map::new();
|
|
versions.insert(github_latest.to_string(), version_json(github_latest));
|
|
|
|
serde_json::from_value(serde_json::json!({
|
|
"dist-tags": { "latest": npm_latest },
|
|
"versions": serde_json::Value::Object(versions),
|
|
}))
|
|
.expect("valid npm package metadata")
|
|
}
|
|
|
|
#[test]
|
|
fn ready_version_requires_latest_dist_tag_and_root_dist() {
|
|
let latest = "1.2.3";
|
|
let package_info = package_info(latest, latest);
|
|
|
|
ensure_version_ready(&package_info, latest).expect("npm package is ready");
|
|
}
|
|
|
|
#[test]
|
|
fn ready_version_rejects_stale_latest_dist_tag() {
|
|
let package_info = package_info("1.2.3", "1.2.2");
|
|
|
|
let err = ensure_version_ready(&package_info, "1.2.3")
|
|
.expect_err("npm latest dist-tag must match GitHub latest");
|
|
assert!(
|
|
err.to_string().contains("latest dist-tag"),
|
|
"error should name stale latest dist-tag: {err}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ready_version_rejects_missing_root_dist() {
|
|
let package_info: NpmPackageInfo = serde_json::from_value(serde_json::json!({
|
|
"dist-tags": { "latest": "1.2.3" },
|
|
"versions": { "1.2.3": {} },
|
|
}))
|
|
.expect("valid npm package metadata");
|
|
|
|
let err = ensure_version_ready(&package_info, "1.2.3")
|
|
.expect_err("root package must have dist metadata");
|
|
assert!(
|
|
err.to_string().contains("missing dist metadata"),
|
|
"error should name missing dist metadata: {err}"
|
|
);
|
|
}
|
|
}
|