Allow overriding Codex package versions (#39298)

## What changed

- Add `--package-version` to set the version written to `codex-package.json`, while retaining the workspace package version as the default.
- Reject values that are not runtime-compatible semantic versions, including overflowing numeric components and numeric prerelease identifiers with leading zeroes.
- Document the new option.

## Testing

- Add unit coverage for valid release, prerelease, and build versions, plus malformed and out-of-range values.

GitOrigin-RevId: cee996a6721e3fac9bd88f0bc0302a7ec7d2b2fb
This commit is contained in:
Adam Perry @ OpenAI
2026-08-18 21:32:44 +00:00
committed by copyberry
parent 87070a7792
commit 45528c5132
3 changed files with 83 additions and 4 deletions

View File

@@ -31,8 +31,9 @@ artifacts; pass a GNU Linux target explicitly for native glibc local builds. If
prints its path after the package is built.
The `--variant` flag selects the package entrypoint. Supported variants are
`codex` and `codex-app-server`. The `version` field in `codex-package.json` is
read from `[workspace.package].version` in `codex-rs/Cargo.toml`.
`codex` and `codex-app-server`. The `--package-version` flag sets the version in
`codex-package.json`; it defaults to `[workspace.package].version` in
`codex-rs/Cargo.toml`.
## Source-built artifacts

View File

@@ -1,6 +1,7 @@
"""Command-line interface for building Codex package directories."""
import argparse
import re
import tempfile
from pathlib import Path
@@ -19,6 +20,30 @@ from .zsh import resolve_zsh_bin
from .version import read_workspace_version
# Release pipelines run this builder with system Python, so avoid new dependencies.
SEMVER_PATTERN = re.compile(
r"(?P<major>0|[1-9][0-9]*)\."
r"(?P<minor>0|[1-9][0-9]*)\."
r"(?P<patch>0|[1-9][0-9]*)"
r"(?:-(?P<prerelease>[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?"
r"(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?"
)
def parse_package_version(value: str) -> str:
match = SEMVER_PATTERN.fullmatch(value)
if match is not None:
components = (match.group(name) for name in ("major", "minor", "patch"))
prerelease = match.group("prerelease") or ""
if all(int(component) <= 2**64 - 1 for component in components) and all(
not (part.isdigit() and len(part) > 1 and part.startswith("0"))
for part in prerelease.split(".")
):
return value
raise argparse.ArgumentTypeError(f"invalid semantic version: {value!r}")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Build a canonical Codex package directory and optional archive.",
@@ -39,6 +64,12 @@ def parse_args() -> argparse.Namespace:
default="codex",
help="Package variant to build.",
)
parser.add_argument(
"--package-version",
type=parse_package_version,
default=read_workspace_version(),
help="Semantic version to record in codex-package.json.",
)
parser.add_argument(
"--package-dir",
type=Path,
@@ -183,7 +214,6 @@ def main() -> int:
"--codex-windows-sandbox-setup-bin",
),
)
version = read_workspace_version()
inputs = PackageInputs(
entrypoint_bin=source_outputs.entrypoint_bin,
code_mode_host_bin=source_outputs.code_mode_host_bin,
@@ -194,7 +224,7 @@ def main() -> int:
codex_windows_sandbox_setup_bin=source_outputs.codex_windows_sandbox_setup_bin,
)
prepare_package_dir(package_dir, force=args.force)
build_package_dir(package_dir, version, variant, spec, inputs)
build_package_dir(package_dir, args.package_version, variant, spec, inputs)
validate_package_dir(
package_dir, variant, spec, include_zsh=inputs.zsh_bin is not None
)

View File

@@ -0,0 +1,48 @@
#!/usr/bin/env python3
import argparse
from pathlib import Path
import sys
import unittest
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from codex_package.cli import parse_package_version
class PackageVersionTest(unittest.TestCase):
def test_accepts_release_prerelease_and_build_versions(self) -> None:
for version in (
"0.0.0",
"1.2.3",
"0.0.0-internal.deadbeef",
"1.2.3-alpha.1+build.01",
"18446744073709551615.0.0",
):
with self.subTest(version=version):
self.assertEqual(parse_package_version(version), version)
def test_rejects_versions_the_runtime_cannot_parse(self) -> None:
for version in (
"",
"1",
"1.2",
"1.2.3.4",
"v1.2.3",
"01.2.3",
"1.02.3",
"1.2.03",
"1.2.3-",
"1.2.3-alpha..1",
"1.2.3-01",
"1.2.3+",
"1.2.3+build..1",
"18446744073709551616.0.0",
):
with self.subTest(version=version):
with self.assertRaises(argparse.ArgumentTypeError):
parse_package_version(version)
if __name__ == "__main__":
unittest.main()