name: shell-tool-mcp

on:
  workflow_call:
    inputs:
      release-version:
        description: Version to publish (x.y.z or x.y.z-alpha.N). Defaults to GITHUB_REF_NAME when it starts with rust-v.
        required: false
        type: string
      release-tag:
        description: Tag name to use when downloading release artifacts (defaults to rust-v<version>).
        required: false
        type: string
      publish:
        description: Whether to publish to npm when the version is releasable.
        required: false
        default: true
        type: boolean

env:
  NODE_VERSION: 22

jobs:
  metadata:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.compute.outputs.version }}
      release_tag: ${{ steps.compute.outputs.release_tag }}
      should_publish: ${{ steps.compute.outputs.should_publish }}
      npm_tag: ${{ steps.compute.outputs.npm_tag }}
    steps:
      - name: Compute version and tags
        id: compute
        run: |
          set -euo pipefail

          version="${{ inputs.release-version }}"
          release_tag="${{ inputs.release-tag }}"

          if [[ -z "$version" ]]; then
            if [[ -n "$release_tag" && "$release_tag" =~ ^rust-v.+ ]]; then
              version="${release_tag#rust-v}"
            elif [[ "${GITHUB_REF_NAME:-}" =~ ^rust-v.+ ]]; then
              version="${GITHUB_REF_NAME#rust-v}"
              release_tag="${GITHUB_REF_NAME}"
            else
              echo "release-version is required when GITHUB_REF_NAME is not a rust-v tag."
              exit 1
            fi
          fi

          if [[ -z "$release_tag" ]]; then
            release_tag="rust-v${version}"
          fi

          npm_tag=""
          should_publish="false"
          if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
            should_publish="true"
          elif [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then
            should_publish="true"
            npm_tag="alpha"
          fi

          echo "version=${version}" >> "$GITHUB_OUTPUT"
          echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT"
          echo "npm_tag=${npm_tag}" >> "$GITHUB_OUTPUT"
          echo "should_publish=${should_publish}" >> "$GITHUB_OUTPUT"

  rust-binaries:
    name: Build Rust - ${{ matrix.target }}
    needs: metadata
    runs-on: ${{ matrix.runner }}
    timeout-minutes: 30
    env:
      CARGO_PROFILE_RELEASE_LTO: ${{ contains(needs.metadata.outputs.version, '-alpha') && 'thin' || 'fat' }}
    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
            install_musl: true
          - runner: ubuntu-24.04-arm
            target: aarch64-unknown-linux-musl
            install_musl: true
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Install UBSan runtime (musl)
        if: ${{ matrix.install_musl }}
        shell: bash
        run: |
          set -euo pipefail
          if command -v apt-get >/dev/null 2>&1; then
            sudo apt-get update -y
            sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1
          fi

      - uses: dtolnay/rust-toolchain@1.93
        with:
          targets: ${{ matrix.target }}

      - if: ${{ matrix.install_musl }}
        name: Install Zig
        uses: mlugg/setup-zig@v2
        with:
          version: 0.14.0

      - if: ${{ matrix.install_musl }}
        name: Install musl build dependencies
        env:
          TARGET: ${{ matrix.target }}
        run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh"

      - if: ${{ matrix.install_musl }}
        name: Configure rustc UBSan wrapper (musl host)
        shell: bash
        run: |
          set -euo pipefail
          ubsan=""
          if command -v ldconfig >/dev/null 2>&1; then
            ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')"
          fi
          wrapper_root="${RUNNER_TEMP:-/tmp}"
          wrapper="${wrapper_root}/rustc-ubsan-wrapper"
          cat > "${wrapper}" <<EOF
          #!/usr/bin/env bash
          set -euo pipefail
          if [[ -n "${ubsan}" ]]; then
            export LD_PRELOAD="${ubsan}\${LD_PRELOAD:+:\${LD_PRELOAD}}"
          fi
          exec "\$1" "\${@:2}"
          EOF
          chmod +x "${wrapper}"
          echo "RUSTC_WRAPPER=${wrapper}" >> "$GITHUB_ENV"
          echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV"

      - if: ${{ matrix.install_musl }}
        name: Clear sanitizer flags (musl)
        shell: bash
        run: |
          set -euo pipefail
          # Clear global Rust flags so host/proc-macro builds don't pull in UBSan.
          echo "RUSTFLAGS=" >> "$GITHUB_ENV"
          echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV"
          echo "RUSTDOCFLAGS=" >> "$GITHUB_ENV"
          # Override any runner-level Cargo config rustflags as well.
          echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV"
          echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV"
          echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV"
          echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV"
          echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV"

          sanitize_flags() {
            local input="$1"
            input="${input//-fsanitize=undefined/}"
            input="${input//-fno-sanitize-recover=undefined/}"
            input="${input//-fno-sanitize-trap=undefined/}"
            echo "$input"
          }

          cflags="$(sanitize_flags "${CFLAGS-}")"
          cxxflags="$(sanitize_flags "${CXXFLAGS-}")"
          echo "CFLAGS=${cflags}" >> "$GITHUB_ENV"
          echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV"

      - name: Build exec server binaries
        run: cargo build --release --target ${{ matrix.target }} --bin codex-exec-mcp-server --bin codex-execve-wrapper

      - name: Stage exec server binaries
        run: |
          dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}"
          mkdir -p "$dest"
          cp "target/${{ matrix.target }}/release/codex-exec-mcp-server" "$dest/"
          cp "target/${{ matrix.target }}/release/codex-execve-wrapper" "$dest/"

      - uses: actions/upload-artifact@v6
        with:
          name: shell-tool-mcp-rust-${{ matrix.target }}
          path: artifacts/**
          if-no-files-found: error

  bash-linux:
    name: Build Bash (Linux) - ${{ matrix.variant }} - ${{ matrix.target }}
    needs: metadata
    runs-on: ${{ matrix.runner }}
    timeout-minutes: 30
    container:
      image: ${{ matrix.image }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - runner: ubuntu-24.04
            target: x86_64-unknown-linux-musl
            variant: ubuntu-24.04
            image: ubuntu:24.04
          - runner: ubuntu-24.04
            target: x86_64-unknown-linux-musl
            variant: ubuntu-22.04
            image: ubuntu:22.04
          - runner: ubuntu-24.04
            target: x86_64-unknown-linux-musl
            variant: debian-12
            image: debian:12
          - runner: ubuntu-24.04
            target: x86_64-unknown-linux-musl
            variant: debian-11
            image: debian:11
          - runner: ubuntu-24.04
            target: x86_64-unknown-linux-musl
            variant: centos-9
            image: quay.io/centos/centos:stream9
          - runner: ubuntu-24.04-arm
            target: aarch64-unknown-linux-musl
            variant: ubuntu-24.04
            image: arm64v8/ubuntu:24.04
          - runner: ubuntu-24.04-arm
            target: aarch64-unknown-linux-musl
            variant: ubuntu-22.04
            image: arm64v8/ubuntu:22.04
          - runner: ubuntu-24.04-arm
            target: aarch64-unknown-linux-musl
            variant: ubuntu-20.04
            image: arm64v8/ubuntu:20.04
          - runner: ubuntu-24.04-arm
            target: aarch64-unknown-linux-musl
            variant: debian-12
            image: arm64v8/debian:12
          - runner: ubuntu-24.04-arm
            target: aarch64-unknown-linux-musl
            variant: debian-11
            image: arm64v8/debian:11
          - runner: ubuntu-24.04-arm
            target: aarch64-unknown-linux-musl
            variant: centos-9
            image: quay.io/centos/centos:stream9
    steps:
      - name: Install build prerequisites
        shell: bash
        run: |
          set -euo pipefail
          if command -v apt-get >/dev/null 2>&1; then
            apt-get update
            DEBIAN_FRONTEND=noninteractive apt-get install -y git build-essential bison autoconf gettext
          elif command -v dnf >/dev/null 2>&1; then
            dnf install -y git gcc gcc-c++ make bison autoconf gettext
          elif command -v yum >/dev/null 2>&1; then
            yum install -y git gcc gcc-c++ make bison autoconf gettext
          else
            echo "Unsupported package manager in container"
            exit 1
          fi

      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Build patched Bash
        shell: bash
        run: |
          set -euo pipefail
          git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash
          cd /tmp/bash
          git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
          git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
          git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch"
          ./configure --without-bash-malloc
          cores="$(command -v nproc >/dev/null 2>&1 && nproc || getconf _NPROCESSORS_ONLN)"
          make -j"${cores}"

          dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}"
          mkdir -p "$dest"
          cp bash "$dest/bash"

      - uses: actions/upload-artifact@v6
        with:
          name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }}
          path: artifacts/**
          if-no-files-found: error

  bash-darwin:
    name: Build Bash (macOS) - ${{ matrix.variant }} - ${{ matrix.target }}
    needs: metadata
    runs-on: ${{ matrix.runner }}
    timeout-minutes: 30
    strategy:
      fail-fast: false
      matrix:
        include:
          - runner: macos-15-xlarge
            target: aarch64-apple-darwin
            variant: macos-15
          - runner: macos-14
            target: aarch64-apple-darwin
            variant: macos-14
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Build patched Bash
        shell: bash
        run: |
          set -euo pipefail
          git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash
          cd /tmp/bash
          git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
          git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
          git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch"
          ./configure --without-bash-malloc
          cores="$(getconf _NPROCESSORS_ONLN)"
          make -j"${cores}"

          dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}"
          mkdir -p "$dest"
          cp bash "$dest/bash"

      - uses: actions/upload-artifact@v6
        with:
          name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }}
          path: artifacts/**
          if-no-files-found: error

  package:
    name: Package npm module
    needs:
      - metadata
      - rust-binaries
      - bash-linux
      - bash-darwin
    runs-on: ubuntu-latest
    env:
      PACKAGE_VERSION: ${{ needs.metadata.outputs.version }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Setup pnpm
        uses: pnpm/action-setup@v4
        with:
          run_install: false

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: ${{ env.NODE_VERSION }}

      - name: Install JavaScript dependencies
        run: pnpm install --frozen-lockfile

      - name: Build (shell-tool-mcp)
        run: pnpm --filter @openai/codex-shell-tool-mcp run build

      - name: Download build artifacts
        uses: actions/download-artifact@v7
        with:
          path: artifacts

      - name: Assemble staging directory
        id: staging
        shell: bash
        run: |
          set -euo pipefail
          staging="${STAGING_DIR}"
          mkdir -p "$staging" "$staging/vendor"
          cp shell-tool-mcp/README.md "$staging/"
          cp shell-tool-mcp/package.json "$staging/"
          cp -R shell-tool-mcp/bin "$staging/"

          found_vendor="false"
          shopt -s nullglob
          for vendor_dir in artifacts/*/vendor; do
            rsync -av "$vendor_dir/" "$staging/vendor/"
            found_vendor="true"
          done
          if [[ "$found_vendor" == "false" ]]; then
            echo "No vendor payloads were downloaded."
            exit 1
          fi

          node - <<'NODE'
            import fs from "node:fs";
            import path from "node:path";

            const stagingDir = process.env.STAGING_DIR;
            const version = process.env.PACKAGE_VERSION;
            const pkgPath = path.join(stagingDir, "package.json");
            const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
            pkg.version = version;
            fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
          NODE

          echo "dir=$staging" >> "$GITHUB_OUTPUT"
        env:
          STAGING_DIR: ${{ runner.temp }}/shell-tool-mcp

      - name: Ensure binaries are executable
        run: |
          set -euo pipefail
          staging="${{ steps.staging.outputs.dir }}"
          chmod +x \
            "$staging"/vendor/*/codex-exec-mcp-server \
            "$staging"/vendor/*/codex-execve-wrapper \
            "$staging"/vendor/*/bash/*/bash

      - name: Create npm tarball
        shell: bash
        run: |
          set -euo pipefail
          mkdir -p dist/npm
          staging="${{ steps.staging.outputs.dir }}"
          pack_info=$(cd "$staging" && npm pack --ignore-scripts --json --pack-destination "${GITHUB_WORKSPACE}/dist/npm")
          filename=$(PACK_INFO="$pack_info" node -e 'const data = JSON.parse(process.env.PACK_INFO); console.log(data[0].filename);')
          mv "dist/npm/${filename}" "dist/npm/codex-shell-tool-mcp-npm-${PACKAGE_VERSION}.tgz"

      - uses: actions/upload-artifact@v6
        with:
          name: codex-shell-tool-mcp-npm
          path: dist/npm/codex-shell-tool-mcp-npm-${{ env.PACKAGE_VERSION }}.tgz
          if-no-files-found: error

  publish:
    name: Publish npm package
    needs:
      - metadata
      - package
    if: ${{ inputs.publish && needs.metadata.outputs.should_publish == 'true' }}
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: ${{ env.NODE_VERSION }}
          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 tarball
        uses: actions/download-artifact@v7
        with:
          name: codex-shell-tool-mcp-npm
          path: dist/npm

      - name: Publish to npm
        env:
          NPM_TAG: ${{ needs.metadata.outputs.npm_tag }}
          VERSION: ${{ needs.metadata.outputs.version }}
        shell: bash
        run: |
          set -euo pipefail
          tag_args=()
          if [[ -n "${NPM_TAG}" ]]; then
            tag_args+=(--tag "${NPM_TAG}")
          fi
          npm publish "dist/npm/codex-shell-tool-mcp-npm-${VERSION}.tgz" "${tag_args[@]}"
