From 71cd72bfb4650ed6938dc2163c247f0e77ce9550 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 8 Aug 2025 17:54:59 -0700 Subject: [PATCH] fix: improve npm release process --- .github/workflows/rust-release.yml | 6 +- scripts/publish_to_npm.py | 118 +++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) create mode 100755 scripts/publish_to_npm.py diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 8e5aef2757..8bf7361da9 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -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 diff --git a/scripts/publish_to_npm.py b/scripts/publish_to_npm.py new file mode 100755 index 0000000000..f79843ffc8 --- /dev/null +++ b/scripts/publish_to_npm.py @@ -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-.tgz` asset from the GitHub release + tagged `rust-v` 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())