mirror of
https://github.com/openai/codex.git
synced 2026-04-28 02:11:08 +03:00
665 lines
24 KiB
YAML
665 lines
24 KiB
YAML
# Release workflow for codex-rs.
|
||
# To release, follow a workflow like:
|
||
# ```
|
||
# git tag -a rust-v0.1.0 -m "Release 0.1.0"
|
||
# git push origin rust-v0.1.0
|
||
# ```
|
||
|
||
name: rust-release
|
||
on:
|
||
push:
|
||
tags:
|
||
- "rust-v*.*.*"
|
||
pull_request:
|
||
paths:
|
||
- ".github/workflows/rust-release.yml"
|
||
- "codex-rs/**"
|
||
|
||
concurrency:
|
||
group: ${{ github.workflow }}
|
||
cancel-in-progress: true
|
||
|
||
jobs:
|
||
tag-check:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v5
|
||
|
||
- name: Validate tag matches Cargo.toml version
|
||
shell: bash
|
||
run: |
|
||
set -euo pipefail
|
||
echo "::group::Tag validation"
|
||
|
||
if [[ "${GITHUB_EVENT_NAME}" != "push" || "${GITHUB_REF_TYPE}" != "tag" ]]; then
|
||
echo "ℹ️ Skipping tag validation for ${GITHUB_EVENT_NAME} event"
|
||
exit 0
|
||
fi
|
||
|
||
# 1. Must be a tag and match the regex
|
||
[[ "${GITHUB_REF_TYPE}" == "tag" ]] \
|
||
|| { echo "❌ Not a tag push"; exit 1; }
|
||
[[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?$ ]] \
|
||
|| { echo "❌ Tag '${GITHUB_REF_NAME}' doesn't match expected format"; exit 1; }
|
||
|
||
# 2. Extract versions
|
||
tag_ver="${GITHUB_REF_NAME#rust-v}"
|
||
cargo_ver="$(grep -m1 '^version' codex-rs/Cargo.toml \
|
||
| sed -E 's/version *= *"([^"]+)".*/\1/')"
|
||
|
||
# 3. Compare
|
||
[[ "${tag_ver}" == "${cargo_ver}" ]] \
|
||
|| { echo "❌ Tag ${tag_ver} ≠ Cargo.toml ${cargo_ver}"; exit 1; }
|
||
|
||
echo "✅ Tag and Cargo.toml agree (${tag_ver})"
|
||
echo "::endgroup::"
|
||
|
||
build:
|
||
needs: tag-check
|
||
name: Build - ${{ matrix.runner }} - ${{ matrix.target }}
|
||
runs-on: ${{ matrix.runner }}
|
||
permissions:
|
||
contents: read
|
||
id-token: write
|
||
timeout-minutes: 30
|
||
defaults:
|
||
run:
|
||
working-directory: codex-rs
|
||
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
include:
|
||
# - runner: macos-15-xlarge
|
||
# target: aarch64-apple-darwin
|
||
# - runner: macos-15-xlarge
|
||
# target: x86_64-apple-darwin
|
||
- runner: ubuntu-24.04
|
||
target: x86_64-unknown-linux-musl
|
||
- runner: ubuntu-24.04
|
||
target: x86_64-unknown-linux-gnu
|
||
- runner: ubuntu-24.04-arm
|
||
target: aarch64-unknown-linux-musl
|
||
- runner: ubuntu-24.04-arm
|
||
target: aarch64-unknown-linux-gnu
|
||
# - runner: windows-latest
|
||
# target: x86_64-pc-windows-msvc
|
||
# - runner: windows-11-arm
|
||
# target: aarch64-pc-windows-msvc
|
||
|
||
steps:
|
||
- uses: actions/checkout@v5
|
||
- uses: dtolnay/rust-toolchain@1.90
|
||
with:
|
||
targets: ${{ matrix.target }}
|
||
|
||
- uses: actions/cache@v4
|
||
with:
|
||
path: |
|
||
~/.cargo/bin/
|
||
~/.cargo/registry/index/
|
||
~/.cargo/registry/cache/
|
||
~/.cargo/git/db/
|
||
${{ github.workspace }}/codex-rs/target/
|
||
key: cargo-${{ matrix.runner }}-${{ matrix.target }}-release-${{ hashFiles('**/Cargo.lock') }}
|
||
|
||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
|
||
name: Install musl build tools
|
||
run: |
|
||
sudo apt-get update
|
||
sudo apt-get install -y musl-tools pkg-config
|
||
|
||
- name: Cargo build
|
||
run: cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-responses-api-proxy
|
||
|
||
- if: ${{ matrix.runner == 'macos-15-xlarge' && github.event_name == 'push' }}
|
||
name: Configure Apple code signing
|
||
shell: bash
|
||
env:
|
||
KEYCHAIN_PASSWORD: actions
|
||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE_P12 }}
|
||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||
run: |
|
||
set -euo pipefail
|
||
|
||
if [[ -z "${APPLE_CERTIFICATE:-}" ]]; then
|
||
echo "APPLE_CERTIFICATE is required for macOS signing"
|
||
exit 1
|
||
fi
|
||
|
||
if [[ -z "${APPLE_CERTIFICATE_PASSWORD:-}" ]]; then
|
||
echo "APPLE_CERTIFICATE_PASSWORD is required for macOS signing"
|
||
exit 1
|
||
fi
|
||
|
||
cert_path="${RUNNER_TEMP}/apple_signing_certificate.p12"
|
||
echo "$APPLE_CERTIFICATE" | base64 -d > "$cert_path"
|
||
|
||
keychain_path="${RUNNER_TEMP}/codex-signing.keychain-db"
|
||
security create-keychain -p "$KEYCHAIN_PASSWORD" "$keychain_path"
|
||
security set-keychain-settings -lut 21600 "$keychain_path"
|
||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$keychain_path"
|
||
|
||
keychain_args=()
|
||
cleanup_keychain() {
|
||
if ((${#keychain_args[@]} > 0)); then
|
||
security list-keychains -s "${keychain_args[@]}" || true
|
||
security default-keychain -s "${keychain_args[0]}" || true
|
||
else
|
||
security list-keychains -s || true
|
||
fi
|
||
if [[ -f "$keychain_path" ]]; then
|
||
security delete-keychain "$keychain_path" || true
|
||
fi
|
||
}
|
||
|
||
while IFS= read -r keychain; do
|
||
[[ -n "$keychain" ]] && keychain_args+=("$keychain")
|
||
done < <(security list-keychains | sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/"//g')
|
||
|
||
if ((${#keychain_args[@]} > 0)); then
|
||
security list-keychains -s "$keychain_path" "${keychain_args[@]}"
|
||
else
|
||
security list-keychains -s "$keychain_path"
|
||
fi
|
||
|
||
security default-keychain -s "$keychain_path"
|
||
security import "$cert_path" -k "$keychain_path" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
|
||
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$keychain_path" > /dev/null
|
||
|
||
codesign_hashes=()
|
||
while IFS= read -r hash; do
|
||
[[ -n "$hash" ]] && codesign_hashes+=("$hash")
|
||
done < <(security find-identity -v -p codesigning "$keychain_path" \
|
||
| sed -n 's/.*\([0-9A-F]\{40\}\).*/\1/p' \
|
||
| sort -u)
|
||
|
||
if ((${#codesign_hashes[@]} == 0)); then
|
||
echo "No signing identities found in $keychain_path"
|
||
cleanup_keychain
|
||
rm -f "$cert_path"
|
||
exit 1
|
||
fi
|
||
|
||
if ((${#codesign_hashes[@]} > 1)); then
|
||
echo "Multiple signing identities found in $keychain_path:"
|
||
printf ' %s\n' "${codesign_hashes[@]}"
|
||
cleanup_keychain
|
||
rm -f "$cert_path"
|
||
exit 1
|
||
fi
|
||
|
||
APPLE_CODESIGN_IDENTITY="${codesign_hashes[0]}"
|
||
|
||
rm -f "$cert_path"
|
||
|
||
echo "APPLE_CODESIGN_IDENTITY=$APPLE_CODESIGN_IDENTITY" >> "$GITHUB_ENV"
|
||
echo "APPLE_CODESIGN_KEYCHAIN=$keychain_path" >> "$GITHUB_ENV"
|
||
echo "::add-mask::$APPLE_CODESIGN_IDENTITY"
|
||
|
||
- if: ${{ matrix.runner == 'macos-15-xlarge' && github.event_name == 'push' }}
|
||
name: Sign macOS binaries
|
||
shell: bash
|
||
run: |
|
||
set -euo pipefail
|
||
|
||
if [[ -z "${APPLE_CODESIGN_IDENTITY:-}" ]]; then
|
||
echo "APPLE_CODESIGN_IDENTITY is required for macOS signing"
|
||
exit 1
|
||
fi
|
||
|
||
keychain_args=()
|
||
if [[ -n "${APPLE_CODESIGN_KEYCHAIN:-}" && -f "${APPLE_CODESIGN_KEYCHAIN}" ]]; then
|
||
keychain_args+=(--keychain "${APPLE_CODESIGN_KEYCHAIN}")
|
||
fi
|
||
|
||
for binary in codex codex-responses-api-proxy; do
|
||
path="target/${{ matrix.target }}/release/${binary}"
|
||
codesign --force --options runtime --timestamp --sign "$APPLE_CODESIGN_IDENTITY" "${keychain_args[@]}" "$path"
|
||
done
|
||
|
||
- if: ${{ matrix.runner == 'macos-15-xlarge' && github.event_name == 'push' }}
|
||
name: Notarize macOS binaries
|
||
shell: bash
|
||
env:
|
||
APPLE_NOTARIZATION_KEY_P8: ${{ secrets.APPLE_NOTARIZATION_KEY_P8 }}
|
||
APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
|
||
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
|
||
run: |
|
||
set -euo pipefail
|
||
|
||
for var in APPLE_NOTARIZATION_KEY_P8 APPLE_NOTARIZATION_KEY_ID APPLE_NOTARIZATION_ISSUER_ID; do
|
||
if [[ -z "${!var:-}" ]]; then
|
||
echo "$var is required for notarization"
|
||
exit 1
|
||
fi
|
||
done
|
||
|
||
notary_key_path="${RUNNER_TEMP}/notarytool.key.p8"
|
||
echo "$APPLE_NOTARIZATION_KEY_P8" | base64 -d > "$notary_key_path"
|
||
cleanup_notary() {
|
||
rm -f "$notary_key_path"
|
||
}
|
||
trap cleanup_notary EXIT
|
||
|
||
notarize_binary() {
|
||
local binary="$1"
|
||
local source_path="target/${{ matrix.target }}/release/${binary}"
|
||
local archive_path="${RUNNER_TEMP}/${binary}.zip"
|
||
|
||
if [[ ! -f "$source_path" ]]; then
|
||
echo "Binary $source_path not found"
|
||
exit 1
|
||
fi
|
||
|
||
rm -f "$archive_path"
|
||
ditto -c -k --keepParent "$source_path" "$archive_path"
|
||
|
||
submission_json=$(xcrun notarytool submit "$archive_path" \
|
||
--key "$notary_key_path" \
|
||
--key-id "$APPLE_NOTARIZATION_KEY_ID" \
|
||
--issuer "$APPLE_NOTARIZATION_ISSUER_ID" \
|
||
--output-format json \
|
||
--wait)
|
||
|
||
status=$(printf '%s\n' "$submission_json" | jq -r '.status // "Unknown"')
|
||
submission_id=$(printf '%s\n' "$submission_json" | jq -r '.id // ""')
|
||
|
||
if [[ -z "$submission_id" ]]; then
|
||
echo "Failed to retrieve submission ID for $binary"
|
||
exit 1
|
||
fi
|
||
|
||
echo "::notice title=Notarization::$binary submission ${submission_id} completed with status ${status}"
|
||
|
||
if [[ "$status" != "Accepted" ]]; then
|
||
echo "Notarization failed for ${binary} (submission ${submission_id}, status ${status})"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
notarize_binary "codex"
|
||
notarize_binary "codex-responses-api-proxy"
|
||
|
||
- if: ${{ startsWith(matrix.runner, 'windows') && github.event_name == 'push' }}
|
||
name: Configure Windows code signing
|
||
shell: pwsh
|
||
env:
|
||
WINDOWS_CODESIGN_CERTIFICATE_PFX: ${{ secrets.WINDOWS_CODESIGN_CERTIFICATE_PFX }}
|
||
WINDOWS_CODESIGN_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_CERTIFICATE_PASSWORD }}
|
||
run: |
|
||
Set-StrictMode -Version Latest
|
||
|
||
if (-not $env:WINDOWS_CODESIGN_CERTIFICATE_PFX) {
|
||
Write-Error "WINDOWS_CODESIGN_CERTIFICATE_PFX is required for Windows signing"
|
||
}
|
||
|
||
if (-not $env:WINDOWS_CODESIGN_CERTIFICATE_PASSWORD) {
|
||
Write-Error "WINDOWS_CODESIGN_CERTIFICATE_PASSWORD is required for Windows signing"
|
||
}
|
||
|
||
$certPath = Join-Path $env:RUNNER_TEMP 'windows_signing_certificate.pfx'
|
||
[System.IO.File]::WriteAllBytes($certPath, [System.Convert]::FromBase64String($env:WINDOWS_CODESIGN_CERTIFICATE_PFX))
|
||
|
||
"WINDOWS_CODESIGN_CERTIFICATE_PATH=$certPath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||
|
||
- if: ${{ startsWith(matrix.runner, 'windows') && github.event_name == 'push' }}
|
||
name: Sign Windows binaries
|
||
shell: pwsh
|
||
env:
|
||
WINDOWS_CODESIGN_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_CERTIFICATE_PASSWORD }}
|
||
MATRIX_TARGET: ${{ matrix.target }}
|
||
run: |
|
||
Set-StrictMode -Version Latest
|
||
|
||
$certPath = $env:WINDOWS_CODESIGN_CERTIFICATE_PATH
|
||
if (-not $certPath) {
|
||
Write-Error "WINDOWS_CODESIGN_CERTIFICATE_PATH is required for Windows signing"
|
||
}
|
||
|
||
if (-not (Test-Path $certPath)) {
|
||
Write-Error "Certificate file not found at $certPath"
|
||
}
|
||
|
||
$arch = if ($env:MATRIX_TARGET -eq 'aarch64-pc-windows-msvc') { 'arm64' } else { 'x64' }
|
||
$signtoolSearchRoot = "${env:ProgramFiles(x86)}\Windows Kits\10\bin"
|
||
$signtool = Get-ChildItem -Path $signtoolSearchRoot -Recurse -Filter signtool.exe | Where-Object { $_.FullName -match "\\$arch\\" } | Sort-Object FullName -Descending | Select-Object -First 1
|
||
if (-not $signtool) {
|
||
$signtool = Get-ChildItem -Path $signtoolSearchRoot -Recurse -Filter signtool.exe | Sort-Object FullName -Descending | Select-Object -First 1
|
||
}
|
||
|
||
if (-not $signtool) {
|
||
Write-Error "signtool.exe not found"
|
||
}
|
||
|
||
$binaries = @(
|
||
"target/${{ matrix.target }}/release/codex.exe",
|
||
"target/${{ matrix.target }}/release/codex-responses-api-proxy.exe"
|
||
)
|
||
|
||
foreach ($binary in $binaries) {
|
||
if (-not (Test-Path $binary)) {
|
||
Write-Error "Binary $binary not found"
|
||
}
|
||
}
|
||
|
||
foreach ($binary in $binaries) {
|
||
& $signtool.FullName sign `
|
||
/fd SHA256 `
|
||
/td SHA256 `
|
||
/tr http://timestamp.digicert.com `
|
||
/f $certPath `
|
||
/p $env:WINDOWS_CODESIGN_CERTIFICATE_PASSWORD `
|
||
$binary
|
||
}
|
||
|
||
- name: Stage artifacts
|
||
shell: bash
|
||
run: |
|
||
dest="dist/${{ matrix.target }}"
|
||
mkdir -p "$dest"
|
||
|
||
if [[ "${{ matrix.runner }}" == windows* ]]; then
|
||
cp target/${{ matrix.target }}/release/codex.exe "$dest/codex-${{ matrix.target }}.exe"
|
||
cp target/${{ matrix.target }}/release/codex-responses-api-proxy.exe "$dest/codex-responses-api-proxy-${{ matrix.target }}.exe"
|
||
else
|
||
cp target/${{ matrix.target }}/release/codex "$dest/codex-${{ matrix.target }}"
|
||
cp target/${{ matrix.target }}/release/codex-responses-api-proxy "$dest/codex-responses-api-proxy-${{ matrix.target }}"
|
||
fi
|
||
|
||
- if: ${{ matrix.runner == 'windows-11-arm' }}
|
||
name: Install zstd
|
||
shell: powershell
|
||
run: choco install -y zstandard
|
||
|
||
- name: Compress artifacts
|
||
shell: bash
|
||
run: |
|
||
# Path that contains the uncompressed binaries for the current
|
||
# ${{ matrix.target }}
|
||
dest="dist/${{ matrix.target }}"
|
||
|
||
# For compatibility with environments that lack the `zstd` tool we
|
||
# additionally create a `.tar.gz` for all platforms and `.zip` for
|
||
# Windows alongside every single binary that we publish. The end result is:
|
||
# codex-<target>.zst (existing)
|
||
# codex-<target>.tar.gz (new)
|
||
# codex-<target>.zip (only for Windows)
|
||
|
||
# 1. Produce a .tar.gz for every file in the directory *before* we
|
||
# run `zstd --rm`, because that flag deletes the original files.
|
||
for f in "$dest"/*; do
|
||
base="$(basename "$f")"
|
||
# Skip files that are already archives (shouldn't happen, but be
|
||
# safe).
|
||
if [[ "$base" == *.tar.gz || "$base" == *.zip ]]; then
|
||
continue
|
||
fi
|
||
|
||
# Create per-binary tar.gz
|
||
tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base"
|
||
|
||
# Create zip archive for Windows binaries
|
||
# Must run from inside the dest dir so 7z won't
|
||
# embed the directory path inside the zip.
|
||
if [[ "${{ matrix.runner }}" == windows* ]]; then
|
||
(cd "$dest" && 7z a "${base}.zip" "$base")
|
||
fi
|
||
|
||
# Also create .zst (existing behaviour) *and* remove the original
|
||
# uncompressed binary to keep the directory small.
|
||
zstd -T0 -19 --rm "$dest/$base"
|
||
done
|
||
|
||
- if: ${{ contains(matrix.target, 'unknown-linux')}}
|
||
name: Install cosign
|
||
uses: sigstore/cosign-installer@v3.7.0
|
||
|
||
- if: ${{ contains(matrix.target, 'unknown-linux')}}
|
||
name: Cosign Linux artifacts
|
||
shell: bash
|
||
env:
|
||
COSIGN_EXPERIMENTAL: "1"
|
||
COSIGN_YES: "true"
|
||
COSIGN_OIDC_CLIENT_ID: "sigstore"
|
||
COSIGN_OIDC_ISSUER: "https://oauth2.sigstore.dev/auth"
|
||
run: |
|
||
set -euo pipefail
|
||
|
||
dest="dist/${{ matrix.target }}"
|
||
if [[ ! -d "$dest" ]]; then
|
||
echo "Destination $dest does not exist"
|
||
exit 1
|
||
fi
|
||
|
||
shopt -s nullglob
|
||
for artifact in "$dest"/*; do
|
||
if [[ -f "$artifact" ]]; then
|
||
cosign sign-blob \
|
||
--yes \
|
||
--output-signature "${artifact}.sig" \
|
||
--output-certificate "${artifact}.pem" \
|
||
"$artifact"
|
||
fi
|
||
done
|
||
|
||
- name: Remove signing keychain
|
||
if: ${{ always() && matrix.runner == 'macos-15-xlarge' && github.event_name == 'push' }}
|
||
shell: bash
|
||
env:
|
||
APPLE_CODESIGN_KEYCHAIN: ${{ env.APPLE_CODESIGN_KEYCHAIN }}
|
||
run: |
|
||
set -euo pipefail
|
||
if [[ -n "${APPLE_CODESIGN_KEYCHAIN:-}" ]]; then
|
||
keychain_args=()
|
||
while IFS= read -r keychain; do
|
||
[[ "$keychain" == "$APPLE_CODESIGN_KEYCHAIN" ]] && continue
|
||
[[ -n "$keychain" ]] && keychain_args+=("$keychain")
|
||
done < <(security list-keychains | sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/"//g')
|
||
if ((${#keychain_args[@]} > 0)); then
|
||
security list-keychains -s "${keychain_args[@]}"
|
||
security default-keychain -s "${keychain_args[0]}"
|
||
fi
|
||
|
||
if [[ -f "$APPLE_CODESIGN_KEYCHAIN" ]]; then
|
||
security delete-keychain "$APPLE_CODESIGN_KEYCHAIN"
|
||
fi
|
||
fi
|
||
|
||
- name: Remove Windows signing certificate
|
||
if: ${{ always() && startsWith(matrix.runner, 'windows') && github.event_name == 'push' }}
|
||
shell: pwsh
|
||
run: |
|
||
if ($env:WINDOWS_CODESIGN_CERTIFICATE_PATH -and (Test-Path $env:WINDOWS_CODESIGN_CERTIFICATE_PATH)) {
|
||
Remove-Item -Force $env:WINDOWS_CODESIGN_CERTIFICATE_PATH
|
||
}
|
||
- uses: actions/upload-artifact@v4
|
||
with:
|
||
name: ${{ matrix.target }}
|
||
# Upload the per-binary .zst files as well as the new .tar.gz
|
||
# equivalents we generated in the previous step.
|
||
path: |
|
||
codex-rs/dist/${{ matrix.target }}/*
|
||
|
||
release:
|
||
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/rust-v') }}
|
||
needs: build
|
||
name: release
|
||
runs-on: ubuntu-latest
|
||
permissions:
|
||
contents: write
|
||
actions: read
|
||
outputs:
|
||
version: ${{ steps.release_name.outputs.name }}
|
||
tag: ${{ github.ref_name }}
|
||
should_publish_npm: ${{ steps.npm_publish_settings.outputs.should_publish }}
|
||
npm_tag: ${{ steps.npm_publish_settings.outputs.npm_tag }}
|
||
|
||
steps:
|
||
- name: Checkout repository
|
||
uses: actions/checkout@v5
|
||
|
||
- uses: actions/download-artifact@v4
|
||
with:
|
||
path: dist
|
||
|
||
- name: List
|
||
run: ls -R dist/
|
||
|
||
- name: Define release name
|
||
id: release_name
|
||
run: |
|
||
# Extract the version from the tag name, which is in the format
|
||
# "rust-v0.1.0".
|
||
version="${GITHUB_REF_NAME#rust-v}"
|
||
echo "name=${version}" >> $GITHUB_OUTPUT
|
||
|
||
- name: Determine npm publish settings
|
||
id: npm_publish_settings
|
||
env:
|
||
VERSION: ${{ steps.release_name.outputs.name }}
|
||
run: |
|
||
set -euo pipefail
|
||
version="${VERSION}"
|
||
|
||
if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||
echo "should_publish=true" >> "$GITHUB_OUTPUT"
|
||
echo "npm_tag=" >> "$GITHUB_OUTPUT"
|
||
elif [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then
|
||
echo "should_publish=true" >> "$GITHUB_OUTPUT"
|
||
echo "npm_tag=alpha" >> "$GITHUB_OUTPUT"
|
||
else
|
||
echo "should_publish=false" >> "$GITHUB_OUTPUT"
|
||
echo "npm_tag=" >> "$GITHUB_OUTPUT"
|
||
fi
|
||
|
||
- name: Setup pnpm
|
||
uses: pnpm/action-setup@v4
|
||
with:
|
||
run_install: false
|
||
|
||
- name: Setup Node.js for npm packaging
|
||
uses: actions/setup-node@v5
|
||
with:
|
||
node-version: 22
|
||
|
||
- name: Install dependencies
|
||
run: pnpm install --frozen-lockfile
|
||
|
||
# stage_npm_packages.py requires DotSlash when staging releases.
|
||
- uses: facebook/install-dotslash@v2
|
||
- name: Stage npm packages
|
||
env:
|
||
GH_TOKEN: ${{ github.token }}
|
||
run: |
|
||
./scripts/stage_npm_packages.py \
|
||
--release-version "${{ steps.release_name.outputs.name }}" \
|
||
--package codex \
|
||
--package codex-responses-api-proxy \
|
||
--package codex-sdk
|
||
|
||
- name: Create GitHub Release
|
||
uses: softprops/action-gh-release@v2
|
||
with:
|
||
name: ${{ steps.release_name.outputs.name }}
|
||
tag_name: ${{ github.ref_name }}
|
||
files: dist/**
|
||
# Mark as prerelease only when the version has a suffix after x.y.z
|
||
# (e.g. -alpha, -beta). Otherwise publish a normal release.
|
||
prerelease: ${{ contains(steps.release_name.outputs.name, '-') }}
|
||
|
||
- uses: facebook/dotslash-publish-release@v2
|
||
env:
|
||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
with:
|
||
tag: ${{ github.ref_name }}
|
||
config: .github/dotslash-config.json
|
||
|
||
# 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
|
||
publish-npm:
|
||
if: ${{ needs.release.outputs.should_publish_npm == 'true' }}
|
||
# Publish to npm for stable releases and alpha pre-releases with numeric suffixes.
|
||
name: publish-npm
|
||
needs: release
|
||
runs-on: ubuntu-latest
|
||
permissions:
|
||
id-token: write # Required for OIDC
|
||
contents: read
|
||
|
||
steps:
|
||
- 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: Download npm tarballs from release
|
||
env:
|
||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
run: |
|
||
set -euo pipefail
|
||
version="${{ needs.release.outputs.version }}"
|
||
tag="${{ needs.release.outputs.tag }}"
|
||
mkdir -p dist/npm
|
||
gh release download "$tag" \
|
||
--repo "${GITHUB_REPOSITORY}" \
|
||
--pattern "codex-npm-${version}.tgz" \
|
||
--dir dist/npm
|
||
gh release download "$tag" \
|
||
--repo "${GITHUB_REPOSITORY}" \
|
||
--pattern "codex-responses-api-proxy-npm-${version}.tgz" \
|
||
--dir dist/npm
|
||
gh release download "$tag" \
|
||
--repo "${GITHUB_REPOSITORY}" \
|
||
--pattern "codex-sdk-npm-${version}.tgz" \
|
||
--dir dist/npm
|
||
|
||
# No NODE_AUTH_TOKEN needed because we use OIDC.
|
||
- name: Publish to npm
|
||
env:
|
||
VERSION: ${{ needs.release.outputs.version }}
|
||
NPM_TAG: ${{ needs.release.outputs.npm_tag }}
|
||
run: |
|
||
set -euo pipefail
|
||
tag_args=()
|
||
if [[ -n "${NPM_TAG}" ]]; then
|
||
tag_args+=(--tag "${NPM_TAG}")
|
||
fi
|
||
|
||
tarballs=(
|
||
"codex-npm-${VERSION}.tgz"
|
||
"codex-responses-api-proxy-npm-${VERSION}.tgz"
|
||
"codex-sdk-npm-${VERSION}.tgz"
|
||
)
|
||
|
||
for tarball in "${tarballs[@]}"; do
|
||
npm publish "${GITHUB_WORKSPACE}/dist/npm/${tarball}" "${tag_args[@]}"
|
||
done
|
||
|
||
update-branch:
|
||
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/rust-v') }}
|
||
name: Update latest-alpha-cli branch
|
||
permissions:
|
||
contents: write
|
||
needs: release
|
||
runs-on: ubuntu-latest
|
||
|
||
steps:
|
||
- name: Update latest-alpha-cli branch
|
||
env:
|
||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
run: |
|
||
set -euo pipefail
|
||
gh api \
|
||
repos/${GITHUB_REPOSITORY}/git/refs/heads/latest-alpha-cli \
|
||
-X PATCH \
|
||
-f sha="${GITHUB_SHA}" \
|
||
-F force=true
|