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`.
This commit is contained in:
6
.github/workflows/rust-release.yml
vendored
6
.github/workflows/rust-release.yml
vendored
@@ -182,7 +182,11 @@ jobs:
|
||||
--release-version "${{ steps.release_name.outputs.name }}" \
|
||||
--tmp "${TMP_DIR}"
|
||||
mkdir -p dist/npm
|
||||
(cd "$TMP_DIR" && zip -r "${GITHUB_WORKSPACE}/dist/npm/codex-npm-${{ steps.release_name.outputs.name }}.zip" .)
|
||||
# Produce an npm-ready tarball using `npm pack` and store it in dist/npm.
|
||||
# We then rename it to a stable name used by our publishing script.
|
||||
(cd "$TMP_DIR" && npm pack --pack-destination "${GITHUB_WORKSPACE}/dist/npm")
|
||||
mv "${GITHUB_WORKSPACE}"/dist/npm/*.tgz \
|
||||
"${GITHUB_WORKSPACE}/dist/npm/codex-npm-${{ steps.release_name.outputs.name }}.tgz"
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
|
||||
47
docs/release_management.md
Normal file
47
docs/release_management.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Release Management
|
||||
|
||||
Currently, we made Codex binaries available in three places:
|
||||
|
||||
- GitHub Releases https://github.com/openai/codex/releases/
|
||||
- `@openai/codex` on npm: https://www.npmjs.com/package/@openai/codex
|
||||
- `codex` on Homebrew: https://formulae.brew.sh/formula/codex
|
||||
|
||||
# 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`.
|
||||
|
||||
Assuming you are trying to publish `0.21.0`, first you would run:
|
||||
|
||||
```shell
|
||||
VERSION=0.21.0
|
||||
./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`.)
|
||||
|
||||
When the workflow finishes, the GitHub Release is "done," but you still have to consider npm and Homebrew.
|
||||
|
||||
## Publishing to npm
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
VERSION=0.21.0
|
||||
./scripts/publish_to_npm.py "$VERSION"
|
||||
```
|
||||
|
||||
Note that you must have permissions to publish to https://www.npmjs.com/package/@openai/codex for this to succeed.
|
||||
|
||||
## Publishing to Homebrew
|
||||
|
||||
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.
|
||||
|
||||
Inevitably, you just have to refresh this page periodically to see if the release has been picked up by their automation system:
|
||||
|
||||
https://github.com/Homebrew/homebrew-core/pulls?q=%3Apr+codex
|
||||
|
||||
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.
|
||||
|
||||
For reference, our Homebrew formula lives at:
|
||||
|
||||
https://github.com/Homebrew/homebrew-core/blob/main/Formula/c/codex.rb
|
||||
118
scripts/publish_to_npm.py
Executable file
118
scripts/publish_to_npm.py
Executable file
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Download a release artifact for the npm package and publish it.
|
||||
|
||||
Given a release version like `0.20.0`, this script:
|
||||
- Downloads the `codex-npm-<version>.tgz` asset from the GitHub release
|
||||
tagged `rust-v<version>` in the `openai/codex` repository using `gh`.
|
||||
- Runs `npm publish` on the downloaded tarball to publish `@openai/codex`.
|
||||
|
||||
Flags:
|
||||
- `--dry-run` delegates to `npm publish --dry-run`. The artifact is still
|
||||
downloaded so npm can inspect the archive contents without publishing.
|
||||
|
||||
Requirements:
|
||||
- GitHub CLI (`gh`) must be installed and authenticated to access the repo.
|
||||
- npm must be logged in with an account authorized to publish
|
||||
`@openai/codex`. This may trigger a browser for 2FA.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def run_checked(cmd: list[str], cwd: Path | None = None) -> None:
|
||||
"""Run a subprocess command and raise if it fails."""
|
||||
proc = subprocess.run(cmd, cwd=str(cwd) if cwd else None)
|
||||
proc.check_returncode()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Download the npm release artifact for a given version and publish it."
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
"version",
|
||||
help="Release version to publish, e.g. 0.20.0 (without the 'v' prefix)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dir",
|
||||
type=Path,
|
||||
help=(
|
||||
"Optional directory to download the artifact into. Defaults to a temporary directory."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Delegate to `npm publish --dry-run` (still downloads the artifact).",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
version: str = args.version.lstrip("v")
|
||||
tag = f"rust-v{version}"
|
||||
asset_name = f"codex-npm-{version}.tgz"
|
||||
|
||||
download_dir_context_manager = (
|
||||
tempfile.TemporaryDirectory() if args.dir is None else None
|
||||
)
|
||||
# Use provided dir if set, else the temporary one created above
|
||||
download_dir: Path = args.dir if args.dir else Path(download_dir_context_manager.name) # type: ignore[arg-type]
|
||||
download_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 1) Download the artifact using gh
|
||||
repo = "openai/codex"
|
||||
gh_cmd = [
|
||||
"gh",
|
||||
"release",
|
||||
"download",
|
||||
tag,
|
||||
"--repo",
|
||||
repo,
|
||||
"--pattern",
|
||||
asset_name,
|
||||
"--dir",
|
||||
str(download_dir),
|
||||
]
|
||||
print(f"Downloading {asset_name} from {repo}@{tag} into {download_dir}...")
|
||||
# Even in --dry-run we download so npm can inspect the tarball.
|
||||
run_checked(gh_cmd)
|
||||
|
||||
artifact_path = download_dir / asset_name
|
||||
if not args.dry_run and not artifact_path.is_file():
|
||||
print(
|
||||
f"Error: expected artifact not found after download: {artifact_path}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
# 2) Publish to npm
|
||||
npm_cmd = ["npm", "publish"]
|
||||
if args.dry_run:
|
||||
npm_cmd.append("--dry-run")
|
||||
npm_cmd.append(str(artifact_path))
|
||||
|
||||
# Ensure CI is unset so npm can open a browser for 2FA if needed.
|
||||
env = os.environ.copy()
|
||||
if env.get("CI"):
|
||||
env.pop("CI")
|
||||
|
||||
print("Running:", " ".join(npm_cmd))
|
||||
proc = subprocess.run(npm_cmd, env=env)
|
||||
proc.check_returncode()
|
||||
|
||||
print("Publish complete.")
|
||||
# Keep the temporary directory alive until here; it is cleaned up on exit
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user