Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 95 additions & 7 deletions codex-rs/scripts/create_github_release
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,36 @@ 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.",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a confirmation for this option?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We didn't have one before, so I'm inclined to say no? There's no short-form version of it, so it's hard to fat-finger it?

)
return parser.parse_args(argv[1:])


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}")
Expand All @@ -34,21 +53,21 @@ 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}")
print("Creating tree...")
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)
Expand All @@ -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 = [
Expand Down Expand Up @@ -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))
19 changes: 13 additions & 6 deletions docs/release_management.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

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.

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`.)
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.

Expand Down
Loading