fix: change create_github_release to take either --publish-alpha or --publish-release (#3231)

No more picking out version numbers by hand! Now we let the script do
it:

```
$ ./codex-rs/scripts/create_github_release --dry-run --publish-alpha
Running gh api GET /repos/openai/codex/releases/latest
Running gh api GET /repos/openai/codex/releases?per_page=100
Publishing version 0.31.0-alpha.3
$ ./codex-rs/scripts/create_github_release --dry-run --publish-release
Running gh api GET /repos/openai/codex/releases/latest
Publishing version 0.31.0
```

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/3230).
* __->__ #3231
* #3230
* #3228
* #3226
This commit is contained in:
Michael Bolin
2025-09-05 22:08:34 -07:00
committed by GitHub
parent b1d5f7c0bd
commit 70a6d4b1b4
2 changed files with 108 additions and 13 deletions

View File

@@ -14,10 +14,24 @@ CARGO_TOML_PATH = "codex-rs/Cargo.toml"
def parse_args(argv: list[str]) -> argparse.Namespace: def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Create a tagged Codex release.") parser = argparse.ArgumentParser(description="Publish a tagged Codex release.")
parser.add_argument( parser.add_argument(
"version", "-n",
help="Version string used for Cargo.toml and the Git tag (e.g. 0.1.0-alpha.4).", "--dry-run",
action="store_true",
help="Print the version that would be used and exit before making changes.",
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument(
"--publish-alpha",
action="store_true",
help="Publish the next alpha release for the upcoming minor version.",
)
group.add_argument(
"--publish-release",
action="store_true",
help="Publish the next stable release by bumping the minor version.",
) )
return parser.parse_args(argv[1:]) return parser.parse_args(argv[1:])
@@ -25,6 +39,11 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
def main(argv: list[str]) -> int: def main(argv: list[str]) -> int:
args = parse_args(argv) args = parse_args(argv)
try: try:
version = determine_version(args)
print(f"Publishing version {version}")
if args.dry_run:
return 0
print("Fetching branch head...") print("Fetching branch head...")
base_commit = get_branch_head() base_commit = get_branch_head()
print(f"Base commit: {base_commit}") print(f"Base commit: {base_commit}")
@@ -34,7 +53,7 @@ def main(argv: list[str]) -> int:
print("Fetching Cargo.toml...") print("Fetching Cargo.toml...")
current_contents = fetch_file_contents(base_commit) current_contents = fetch_file_contents(base_commit)
print("Updating version...") print("Updating version...")
updated_contents = replace_version(current_contents, args.version) updated_contents = replace_version(current_contents, version)
print("Creating blob...") print("Creating blob...")
blob_sha = create_blob(updated_contents) blob_sha = create_blob(updated_contents)
print(f"Blob SHA: {blob_sha}") print(f"Blob SHA: {blob_sha}")
@@ -42,13 +61,13 @@ def main(argv: list[str]) -> int:
tree_sha = create_tree(base_tree, blob_sha) tree_sha = create_tree(base_tree, blob_sha)
print(f"Tree SHA: {tree_sha}") print(f"Tree SHA: {tree_sha}")
print("Creating commit...") print("Creating commit...")
commit_sha = create_commit(args.version, tree_sha, base_commit) commit_sha = create_commit(version, tree_sha, base_commit)
print(f"Commit SHA: {commit_sha}") print(f"Commit SHA: {commit_sha}")
print("Creating tag...") print("Creating tag...")
tag_sha = create_tag(args.version, commit_sha) tag_sha = create_tag(version, commit_sha)
print(f"Tag SHA: {tag_sha}") print(f"Tag SHA: {tag_sha}")
print("Creating tag ref...") print("Creating tag ref...")
create_tag_ref(args.version, tag_sha) create_tag_ref(version, tag_sha)
print("Done.") print("Done.")
except ReleaseError as error: except ReleaseError as error:
print(f"ERROR: {error}", file=sys.stderr) print(f"ERROR: {error}", file=sys.stderr)
@@ -59,6 +78,7 @@ def main(argv: list[str]) -> int:
class ReleaseError(RuntimeError): class ReleaseError(RuntimeError):
pass pass
def run_gh_api(endpoint: str, *, method: str = "GET", payload: dict | None = None) -> dict: def run_gh_api(endpoint: str, *, method: str = "GET", payload: dict | None = None) -> dict:
print(f"Running gh api {method} {endpoint}") print(f"Running gh api {method} {endpoint}")
command = [ command = [
@@ -204,5 +224,73 @@ def create_tag_ref(version: str, tag_sha: str) -> None:
) )
def determine_version(args: argparse.Namespace) -> str:
latest_version = get_latest_release_version()
major, minor, patch = parse_semver(latest_version)
next_minor_version = format_version(major, minor + 1, patch)
if args.publish_release:
return next_minor_version
alpha_prefix = f"{next_minor_version}-alpha."
releases = list_releases()
highest_alpha = 0
found_alpha = False
for release in releases:
tag = release.get("tag_name", "")
candidate = strip_tag_prefix(tag)
if candidate and candidate.startswith(alpha_prefix):
suffix = candidate[len(alpha_prefix) :]
try:
alpha_number = int(suffix)
except ValueError:
continue
highest_alpha = max(highest_alpha, alpha_number)
found_alpha = True
if found_alpha:
return f"{alpha_prefix}{highest_alpha + 1}"
return f"{alpha_prefix}1"
def get_latest_release_version() -> str:
response = run_gh_api(f"/repos/{REPO}/releases/latest")
tag = response.get("tag_name")
version = strip_tag_prefix(tag)
if not version:
raise ReleaseError("Latest release tag has unexpected format.")
return version
def list_releases() -> list[dict]:
response = run_gh_api(f"/repos/{REPO}/releases?per_page=100")
if not isinstance(response, list):
raise ReleaseError("Unexpected response when listing releases.")
return response
def strip_tag_prefix(tag: str | None) -> str | None:
if not tag:
return None
prefix = "rust-v"
if not tag.startswith(prefix):
return None
return tag[len(prefix) :]
def parse_semver(version: str) -> tuple[int, int, int]:
parts = version.split(".")
if len(parts) != 3:
raise ReleaseError(f"Unexpected version format: {version}")
try:
return int(parts[0]), int(parts[1]), int(parts[2])
except ValueError as error:
raise ReleaseError(f"Version components must be integers: {version}") from error
def format_version(major: int, minor: int, patch: int) -> str:
return f"{major}.{minor}.{patch}"
if __name__ == "__main__": if __name__ == "__main__":
sys.exit(main(sys.argv)) sys.exit(main(sys.argv))

View File

@@ -8,16 +8,23 @@ Currently, we made Codex binaries available in three places:
# Cutting a Release # Cutting a Release
Currently, choosing the version number for the next release is a manual process. In general, just go to https://github.com/openai/codex/releases/latest and see what the latest release is and increase the minor version by `1`, so if the current release is `0.20.0`, then the next release should be `0.21.0`. Run the `codex-rs/scripts/create_github_release` script in the repository to publish a new release. The script will choose the appropriate version number depending on the type of release you are creating.
Assuming you are trying to publish `0.21.0`, first you would run: To cut a new alpha release from `main` (feel free to cut alphas liberally):
```shell ```
VERSION=0.21.0 ./codex-rs/scripts/create_github_release --publish-alpha
./codex-rs/scripts/create_github_release.sh "$VERSION"
``` ```
This will kick off a GitHub Action to build the release, so go to https://github.com/openai/codex/actions/workflows/rust-release.yml to find the corresponding workflow. (Note: we should automate finding the workflow URL with `gh`.) To cut a new _public_ release from `main` (which requires more caution), run:
```
./codex-rs/scripts/create_github_release --publish-release
```
TIP: Add the `--dry-run` flag to report the next version number for the respective release and exit.
Running the publishing script will kick off a GitHub Action to build the release, so go to https://github.com/openai/codex/actions/workflows/rust-release.yml to find the corresponding workflow. (Note: we should automate finding the workflow URL with `gh`.)
When the workflow finishes, the GitHub Release is "done," but you still have to consider npm and Homebrew. When the workflow finishes, the GitHub Release is "done," but you still have to consider npm and Homebrew.