diff --git a/codex-rs/scripts/create_github_release b/codex-rs/scripts/create_github_release index 08f0b11f..120e0635 100755 --- a/codex-rs/scripts/create_github_release +++ b/codex-rs/scripts/create_github_release @@ -14,10 +14,24 @@ CARGO_TOML_PATH = "codex-rs/Cargo.toml" 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( - "version", - help="Version string used for Cargo.toml and the Git tag (e.g. 0.1.0-alpha.4).", + "-n", + "--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:]) @@ -25,6 +39,11 @@ def parse_args(argv: list[str]) -> argparse.Namespace: def main(argv: list[str]) -> int: args = parse_args(argv) try: + version = determine_version(args) + print(f"Publishing version {version}") + if args.dry_run: + return 0 + print("Fetching branch head...") base_commit = get_branch_head() print(f"Base commit: {base_commit}") @@ -34,7 +53,7 @@ def main(argv: list[str]) -> int: print("Fetching Cargo.toml...") current_contents = fetch_file_contents(base_commit) print("Updating version...") - updated_contents = replace_version(current_contents, args.version) + updated_contents = replace_version(current_contents, version) print("Creating blob...") blob_sha = create_blob(updated_contents) print(f"Blob SHA: {blob_sha}") @@ -42,13 +61,13 @@ def main(argv: list[str]) -> int: tree_sha = create_tree(base_tree, blob_sha) print(f"Tree SHA: {tree_sha}") 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("Creating tag...") - tag_sha = create_tag(args.version, commit_sha) + tag_sha = create_tag(version, commit_sha) print(f"Tag SHA: {tag_sha}") print("Creating tag ref...") - create_tag_ref(args.version, tag_sha) + create_tag_ref(version, tag_sha) print("Done.") except ReleaseError as error: print(f"ERROR: {error}", file=sys.stderr) @@ -59,6 +78,7 @@ def main(argv: list[str]) -> int: class ReleaseError(RuntimeError): pass + def run_gh_api(endpoint: str, *, method: str = "GET", payload: dict | None = None) -> dict: print(f"Running gh api {method} {endpoint}") 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__": sys.exit(main(sys.argv)) diff --git a/docs/release_management.md b/docs/release_management.md index 1b81bc3e..ed12de6e 100644 --- a/docs/release_management.md +++ b/docs/release_management.md @@ -8,16 +8,23 @@ Currently, we made Codex binaries available in three places: # 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.sh "$VERSION" +``` +./codex-rs/scripts/create_github_release --publish-alpha ``` -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.