From 791d7b125f4166ef576531075688aac339350011 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 16 Sep 2025 20:33:59 -0700 Subject: [PATCH] fix: make GitHub Action publish to npm using trusted publishing (#3431) --- .github/workflows/rust-release.yml | 23 ++++++ codex-cli/package.json | 3 +- docs/release_management.md | 9 +-- scripts/publish_to_npm.py | 118 ----------------------------- 4 files changed, 26 insertions(+), 127 deletions(-) delete mode 100755 scripts/publish_to_npm.py diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 07af62a1..804ac83d 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -11,6 +11,9 @@ on: tags: - "rust-v*.*.*" +permissions: + id-token: write # Required for OIDC + concurrency: group: ${{ github.workflow }} cancel-in-progress: true @@ -187,6 +190,20 @@ jobs: version="${GITHUB_REF_NAME#rust-v}" echo "name=${version}" >> $GITHUB_OUTPUT + # Publish to npm using OIDC authentication. + # July 31, 2025: https://github.blog/changelog/2025-07-31-npm-trusted-publishing-with-oidc-is-generally-available/ + # npm docs: https://docs.npmjs.com/trusted-publishers + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: 22 + registry-url: "https://registry.npmjs.org" + scope: "@openai" + + # Trusted publishing requires npm CLI version 11.5.1 or later. + - name: Update npm + run: npm install -g npm@latest + - name: Stage npm package env: GH_TOKEN: ${{ github.token }} @@ -220,6 +237,12 @@ jobs: tag: ${{ github.ref_name }} config: .github/dotslash-config.json + # No NODE_AUTH_TOKEN needed because we use OIDC. + - name: Publish to npm + # Do not publish alphas to npm. + if: ${{ !contains(steps.release_name.outputs.name, '-') }} + run: npm publish "${GITHUB_WORKSPACE}/dist/npm/codex-npm-${{ steps.release_name.outputs.name }}.tgz" + update-branch: name: Update latest-alpha-cli branch permissions: diff --git a/codex-cli/package.json b/codex-cli/package.json index 614ca1a8..02124f32 100644 --- a/codex-cli/package.json +++ b/codex-cli/package.json @@ -15,7 +15,8 @@ ], "repository": { "type": "git", - "url": "git+https://github.com/openai/codex.git" + "url": "git+https://github.com/openai/codex.git", + "directory": "codex-cli" }, "dependencies": { "@vscode/ripgrep": "^1.15.14" diff --git a/docs/release_management.md b/docs/release_management.md index ed12de6e..d2b32645 100644 --- a/docs/release_management.md +++ b/docs/release_management.md @@ -30,14 +30,7 @@ When the workflow finishes, the GitHub Release is "done," but you still have to ## 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. +The GitHub Action is responsible for publishing to npm. ## Publishing to Homebrew diff --git a/scripts/publish_to_npm.py b/scripts/publish_to_npm.py deleted file mode 100755 index f79843ff..00000000 --- a/scripts/publish_to_npm.py +++ /dev/null @@ -1,118 +0,0 @@ -#!/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())