name: Publish to npm
on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: "Version to publish (without leading v). If omitted, uses Cargo.toml version."
required: false
type: string
permissions:
contents: read
id-token: write
concurrency:
group: npm-publish-${{ github.event_name }}-${{ github.event.release.tag_name || github.ref_name }}
cancel-in-progress: false
jobs:
get-version:
name: Resolve version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v6
- name: Resolve version from release tag, workflow input, or Cargo.toml
id: version
run: |
if [ "${{ github.event_name }}" = "release" ]; then
VERSION="${{ github.event.release.tag_name }}"
VERSION="${VERSION#v}"
elif [ -n "${{ github.event.inputs.version }}" ]; then
VERSION="${{ github.event.inputs.version }}"
else
VERSION=$(grep '^version = ' Cargo.toml | head -n 1 | sed 's/version = "//;s/"//' | tr -d ' ')
fi
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][A-Za-z0-9]+)*$ ]]; then
echo "Invalid version resolved: $VERSION"
exit 1
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
publish-platforms:
name: Publish platform packages
runs-on: ubuntu-latest
needs: get-version
strategy:
fail-fast: false
matrix:
platform:
- linux-x64
- linux-arm64
- darwin-x64
- darwin-arm64
- win32-x64
steps:
- uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
registry-url: 'https://registry.npmjs.org'
- name: Download release assets for version
run: |
TAG="v${{ needs.get-version.outputs.version }}"
gh release view "$TAG" --repo "${{ github.repository }}" >/dev/null
for i in {1..6}; do
if gh release download "$TAG" --repo "${{ github.repository }}" --dir artifacts; then
break
fi
if [ "$i" -eq 6 ]; then
echo "Failed to download release assets for $TAG after retries"
exit 1
fi
sleep 10
done
ls -la artifacts/
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Extract binary
run: |
mkdir -p npm-platforms/${{ matrix.platform }}/bin
case "${{ matrix.platform }}" in
linux-x64)
tar -xzf artifacts/codexctl-x86_64-unknown-linux-gnu.tar.gz -C npm-platforms/${{ matrix.platform }}/bin
;;
linux-arm64)
tar -xzf artifacts/codexctl-aarch64-unknown-linux-gnu.tar.gz -C npm-platforms/${{ matrix.platform }}/bin
;;
darwin-x64)
tar -xzf artifacts/codexctl-x86_64-apple-darwin.tar.gz -C npm-platforms/${{ matrix.platform }}/bin
;;
darwin-arm64)
tar -xzf artifacts/codexctl-aarch64-apple-darwin.tar.gz -C npm-platforms/${{ matrix.platform }}/bin
;;
win32-x64)
unzip -o artifacts/codexctl-x86_64-pc-windows-msvc.zip -d npm-platforms/${{ matrix.platform }}/bin
;;
esac
ls -la npm-platforms/${{ matrix.platform }}/bin/
- name: Update version
run: |
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"${{ needs.get-version.outputs.version }}\"/" npm-platforms/${{ matrix.platform }}/package.json
cat npm-platforms/${{ matrix.platform }}/package.json
- name: Validate package repository metadata
run: |
node -e '
const pkg = require("./npm-platforms/${{ matrix.platform }}/package.json");
const expected = `github.com/${process.env.GITHUB_REPOSITORY}.git`;
const actual = pkg?.repository?.url || "";
if (!actual.includes(expected)) {
console.error(`repository.url mismatch for ${pkg.name}: expected to include ${expected}, got ${actual}`);
process.exit(1);
}
console.log(`repository.url OK for ${pkg.name}`);
'
- name: Check if version already exists on npm
id: publish_check
run: |
PKG_NAME=$(node -p "require('./npm-platforms/${{ matrix.platform }}/package.json').name")
PKG_VERSION="${{ needs.get-version.outputs.version }}"
if npm view "${PKG_NAME}@${PKG_VERSION}" version >/dev/null 2>&1; then
echo "publish=false" >> "$GITHUB_OUTPUT"
echo "${PKG_NAME}@${PKG_VERSION} is already published. Skipping."
else
echo "publish=true" >> "$GITHUB_OUTPUT"
fi
- name: Publish to npm
if: steps.publish_check.outputs.publish == 'true'
run: |
cd npm-platforms/${{ matrix.platform }}
npm publish --access public
publish-main:
name: Publish main package
runs-on: ubuntu-latest
needs: [get-version]
steps:
- uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
registry-url: 'https://registry.npmjs.org'
- name: Update version
working-directory: ./npm
run: |
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
pkg.version = '${{ needs.get-version.outputs.version }}';
for (const key of Object.keys(pkg.optionalDependencies)) {
pkg.optionalDependencies[key] = '^${{ needs.get-version.outputs.version }}';
}
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
"
cat package.json
- name: Validate package repository metadata
working-directory: ./npm
run: |
node -e '
const pkg = require("./package.json");
const expected = `github.com/${process.env.GITHUB_REPOSITORY}.git`;
const actual = pkg?.repository?.url || "";
if (!actual.includes(expected)) {
console.error(`repository.url mismatch for ${pkg.name}: expected to include ${expected}, got ${actual}`);
process.exit(1);
}
console.log(`repository.url OK for ${pkg.name}`);
'
- name: Check if main package version already exists on npm
id: publish_check
working-directory: ./npm
run: |
PKG_NAME=$(node -p "require('./package.json').name")
PKG_VERSION="${{ needs.get-version.outputs.version }}"
if npm view "${PKG_NAME}@${PKG_VERSION}" version >/dev/null 2>&1; then
echo "publish=false" >> "$GITHUB_OUTPUT"
echo "${PKG_NAME}@${PKG_VERSION} is already published. Skipping."
else
echo "publish=true" >> "$GITHUB_OUTPUT"
fi
- name: Publish to npm
if: steps.publish_check.outputs.publish == 'true'
working-directory: ./npm
run: npm publish --access public
publish-success:
name: Notify completion
runs-on: ubuntu-latest
needs: [publish-platforms, publish-main]
if: always()
steps:
- run: |
echo "Publishing completed"
echo "Platform packages: ${{ needs.publish-platforms.result }}"
echo "Main package: ${{ needs.publish-main.result }}"