Skip to content

Commit e87974a

Browse files
authored
fix: improve npm release process (#2055)
This improves the release process by introducing `scripts/publish_to_npm.py` to automate publishing to npm (modulo the human 2fac step). As part of this, it updates `.github/workflows/rust-release.yml` to create the artifact for npm using `npm pack`. And finally, while it is long overdue, this memorializes the release process in `docs/release_management.md`.
1 parent 329f01b commit e87974a

File tree

3 files changed

+170
-1
lines changed

3 files changed

+170
-1
lines changed

.github/workflows/rust-release.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,11 @@ jobs:
182182
--release-version "${{ steps.release_name.outputs.name }}" \
183183
--tmp "${TMP_DIR}"
184184
mkdir -p dist/npm
185-
(cd "$TMP_DIR" && zip -r "${GITHUB_WORKSPACE}/dist/npm/codex-npm-${{ steps.release_name.outputs.name }}.zip" .)
185+
# Produce an npm-ready tarball using `npm pack` and store it in dist/npm.
186+
# We then rename it to a stable name used by our publishing script.
187+
(cd "$TMP_DIR" && npm pack --pack-destination "${GITHUB_WORKSPACE}/dist/npm")
188+
mv "${GITHUB_WORKSPACE}"/dist/npm/*.tgz \
189+
"${GITHUB_WORKSPACE}/dist/npm/codex-npm-${{ steps.release_name.outputs.name }}.tgz"
186190
187191
- name: Create GitHub Release
188192
uses: softprops/action-gh-release@v2

docs/release_management.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Release Management
2+
3+
Currently, we made Codex binaries available in three places:
4+
5+
- GitHub Releases https://github.com/openai/codex/releases/
6+
- `@openai/codex` on npm: https://www.npmjs.com/package/@openai/codex
7+
- `codex` on Homebrew: https://formulae.brew.sh/formula/codex
8+
9+
# Cutting a Release
10+
11+
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`.
12+
13+
Assuming you are trying to publish `0.21.0`, first you would run:
14+
15+
```shell
16+
VERSION=0.21.0
17+
./codex-rs/scripts/create_github_release.sh "$VERSION"
18+
```
19+
20+
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`.)
21+
22+
When the workflow finishes, the GitHub Release is "done," but you still have to consider npm and Homebrew.
23+
24+
## Publishing to npm
25+
26+
After the GitHub Release is done, you can publish to npm. Note the GitHub Release includes the appropriate artifact for npm (which is the output of `npm pack`), which should be named `codex-npm-VERSION.tgz`. To publish to npm, run:
27+
28+
```
29+
VERSION=0.21.0
30+
./scripts/publish_to_npm.py "$VERSION"
31+
```
32+
33+
Note that you must have permissions to publish to https://www.npmjs.com/package/@openai/codex for this to succeed.
34+
35+
## Publishing to Homebrew
36+
37+
For Homebrew, we are properly set up with their automation system, so every few hours or so it will check our GitHub repo to see if there is a new release. When it finds one, it will put up a PR to create the equivalent Homebrew release, which entails building Codex CLI from source on various versions of macOS.
38+
39+
Inevitably, you just have to refresh this page periodically to see if the release has been picked up by their automation system:
40+
41+
https://github.com/Homebrew/homebrew-core/pulls?q=%3Apr+codex
42+
43+
Once everything builds, a Homebrew admin has to approve the PR. Again, the whole process takes several hours and we don't have total control over it, but it seems to work pretty well.
44+
45+
For reference, our Homebrew formula lives at:
46+
47+
https://github.com/Homebrew/homebrew-core/blob/main/Formula/c/codex.rb

scripts/publish_to_npm.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#!/usr/bin/env python3
2+
3+
"""
4+
Download a release artifact for the npm package and publish it.
5+
6+
Given a release version like `0.20.0`, this script:
7+
- Downloads the `codex-npm-<version>.tgz` asset from the GitHub release
8+
tagged `rust-v<version>` in the `openai/codex` repository using `gh`.
9+
- Runs `npm publish` on the downloaded tarball to publish `@openai/codex`.
10+
11+
Flags:
12+
- `--dry-run` delegates to `npm publish --dry-run`. The artifact is still
13+
downloaded so npm can inspect the archive contents without publishing.
14+
15+
Requirements:
16+
- GitHub CLI (`gh`) must be installed and authenticated to access the repo.
17+
- npm must be logged in with an account authorized to publish
18+
`@openai/codex`. This may trigger a browser for 2FA.
19+
"""
20+
21+
import argparse
22+
import os
23+
import subprocess
24+
import sys
25+
import tempfile
26+
from pathlib import Path
27+
28+
29+
def run_checked(cmd: list[str], cwd: Path | None = None) -> None:
30+
"""Run a subprocess command and raise if it fails."""
31+
proc = subprocess.run(cmd, cwd=str(cwd) if cwd else None)
32+
proc.check_returncode()
33+
34+
35+
def main() -> int:
36+
parser = argparse.ArgumentParser(
37+
description=(
38+
"Download the npm release artifact for a given version and publish it."
39+
)
40+
)
41+
parser.add_argument(
42+
"version",
43+
help="Release version to publish, e.g. 0.20.0 (without the 'v' prefix)",
44+
)
45+
parser.add_argument(
46+
"--dir",
47+
type=Path,
48+
help=(
49+
"Optional directory to download the artifact into. Defaults to a temporary directory."
50+
),
51+
)
52+
parser.add_argument(
53+
"-n",
54+
"--dry-run",
55+
action="store_true",
56+
help="Delegate to `npm publish --dry-run` (still downloads the artifact).",
57+
)
58+
args = parser.parse_args()
59+
60+
version: str = args.version.lstrip("v")
61+
tag = f"rust-v{version}"
62+
asset_name = f"codex-npm-{version}.tgz"
63+
64+
download_dir_context_manager = (
65+
tempfile.TemporaryDirectory() if args.dir is None else None
66+
)
67+
# Use provided dir if set, else the temporary one created above
68+
download_dir: Path = args.dir if args.dir else Path(download_dir_context_manager.name) # type: ignore[arg-type]
69+
download_dir.mkdir(parents=True, exist_ok=True)
70+
71+
# 1) Download the artifact using gh
72+
repo = "openai/codex"
73+
gh_cmd = [
74+
"gh",
75+
"release",
76+
"download",
77+
tag,
78+
"--repo",
79+
repo,
80+
"--pattern",
81+
asset_name,
82+
"--dir",
83+
str(download_dir),
84+
]
85+
print(f"Downloading {asset_name} from {repo}@{tag} into {download_dir}...")
86+
# Even in --dry-run we download so npm can inspect the tarball.
87+
run_checked(gh_cmd)
88+
89+
artifact_path = download_dir / asset_name
90+
if not args.dry_run and not artifact_path.is_file():
91+
print(
92+
f"Error: expected artifact not found after download: {artifact_path}",
93+
file=sys.stderr,
94+
)
95+
return 1
96+
97+
# 2) Publish to npm
98+
npm_cmd = ["npm", "publish"]
99+
if args.dry_run:
100+
npm_cmd.append("--dry-run")
101+
npm_cmd.append(str(artifact_path))
102+
103+
# Ensure CI is unset so npm can open a browser for 2FA if needed.
104+
env = os.environ.copy()
105+
if env.get("CI"):
106+
env.pop("CI")
107+
108+
print("Running:", " ".join(npm_cmd))
109+
proc = subprocess.run(npm_cmd, env=env)
110+
proc.check_returncode()
111+
112+
print("Publish complete.")
113+
# Keep the temporary directory alive until here; it is cleaned up on exit
114+
return 0
115+
116+
117+
if __name__ == "__main__":
118+
sys.exit(main())

0 commit comments

Comments
 (0)