codexctl 0.10.0

Codex Controller - Full control plane for Codex CLI
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 }}"