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). 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 env: RELEASE_TAG_INPUT: ${{ inputs.release-tag }} RELEASE_VERSION_INPUT: ${{ inputs.release-version }} run: | set -euo pipefail version="$RELEASE_VERSION_INPUT" release_tag="$RELEASE_TAG_INPUT" 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" 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 libncursesw5-dev elif command -v dnf >/dev/null 2>&1; then dnf install -y git gcc gcc-c++ make bison autoconf gettext ncurses-devel elif command -v yum >/dev/null 2>&1; then yum install -y git gcc gcc-c++ make bison autoconf gettext ncurses-devel 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 https://git.savannah.gnu.org/git/bash /tmp/bash cd /tmp/bash 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@v7 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 https://git.savannah.gnu.org/git/bash /tmp/bash cd /tmp/bash 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@v7 with: name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }} path: artifacts/** if-no-files-found: error zsh-linux: name: Build zsh (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 libncursesw5-dev elif command -v dnf >/dev/null 2>&1; then dnf install -y git gcc gcc-c++ make bison autoconf gettext ncurses-devel elif command -v yum >/dev/null 2>&1; then yum install -y git gcc gcc-c++ make bison autoconf gettext ncurses-devel else echo "Unsupported package manager in container" exit 1 fi - name: Checkout repository uses: actions/checkout@v6 - name: Build patched zsh shell: bash run: | set -euo pipefail git clone https://git.code.sf.net/p/zsh/code /tmp/zsh cd /tmp/zsh git checkout 77045ef899e53b9598bebc5a41db93a548a40ca6 git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/zsh-exec-wrapper.patch" ./Util/preconfig ./configure cores="$(command -v nproc >/dev/null 2>&1 && nproc || getconf _NPROCESSORS_ONLN)" make -j"${cores}" dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/zsh/${{ matrix.variant }}" mkdir -p "$dest" cp Src/zsh "$dest/zsh" - name: Smoke test zsh exec wrapper shell: bash run: | set -euo pipefail tmpdir="$(mktemp -d)" cat > "$tmpdir/exec-wrapper" <<'EOF' #!/usr/bin/env bash set -euo pipefail : "${CODEX_WRAPPER_LOG:?missing CODEX_WRAPPER_LOG}" printf '%s\n' "$@" > "$CODEX_WRAPPER_LOG" file="$1" shift if [[ "$#" -eq 0 ]]; then exec "$file" fi arg0="$1" shift exec -a "$arg0" "$file" "$@" EOF chmod +x "$tmpdir/exec-wrapper" CODEX_WRAPPER_LOG="$tmpdir/wrapper.log" \ EXEC_WRAPPER="$tmpdir/exec-wrapper" \ /tmp/zsh/Src/zsh -fc '/bin/echo smoke-zsh' > "$tmpdir/stdout.txt" grep -Fx "smoke-zsh" "$tmpdir/stdout.txt" grep -Fx "/bin/echo" "$tmpdir/wrapper.log" - uses: actions/upload-artifact@v7 with: name: shell-tool-mcp-zsh-${{ matrix.target }}-${{ matrix.variant }} path: artifacts/** if-no-files-found: error zsh-darwin: name: Build zsh (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: Install build prerequisites shell: bash run: | set -euo pipefail if ! command -v autoconf >/dev/null 2>&1; then brew install autoconf fi - name: Checkout repository uses: actions/checkout@v6 - name: Build patched zsh shell: bash run: | set -euo pipefail git clone https://git.code.sf.net/p/zsh/code /tmp/zsh cd /tmp/zsh git checkout 77045ef899e53b9598bebc5a41db93a548a40ca6 git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/zsh-exec-wrapper.patch" ./Util/preconfig ./configure cores="$(getconf _NPROCESSORS_ONLN)" make -j"${cores}" dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/zsh/${{ matrix.variant }}" mkdir -p "$dest" cp Src/zsh "$dest/zsh" - name: Smoke test zsh exec wrapper shell: bash run: | set -euo pipefail tmpdir="$(mktemp -d)" cat > "$tmpdir/exec-wrapper" <<'EOF' #!/usr/bin/env bash set -euo pipefail : "${CODEX_WRAPPER_LOG:?missing CODEX_WRAPPER_LOG}" printf '%s\n' "$@" > "$CODEX_WRAPPER_LOG" file="$1" shift if [[ "$#" -eq 0 ]]; then exec "$file" fi arg0="$1" shift exec -a "$arg0" "$file" "$@" EOF chmod +x "$tmpdir/exec-wrapper" CODEX_WRAPPER_LOG="$tmpdir/wrapper.log" \ EXEC_WRAPPER="$tmpdir/exec-wrapper" \ /tmp/zsh/Src/zsh -fc '/bin/echo smoke-zsh' > "$tmpdir/stdout.txt" grep -Fx "smoke-zsh" "$tmpdir/stdout.txt" grep -Fx "/bin/echo" "$tmpdir/wrapper.log" - uses: actions/upload-artifact@v7 with: name: shell-tool-mcp-zsh-${{ matrix.target }}-${{ matrix.variant }} path: artifacts/** if-no-files-found: error package: name: Package npm module needs: - metadata - bash-linux - bash-darwin - zsh-linux - zsh-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@v8 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/" 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 env: STAGING_DIR: ${{ steps.staging.outputs.dir }} run: | set -euo pipefail chmod +x \ "$STAGING_DIR"/vendor/*/bash/*/bash \ "$STAGING_DIR"/vendor/*/zsh/*/zsh - name: Create npm tarball shell: bash env: STAGING_DIR: ${{ steps.staging.outputs.dir }} run: | set -euo pipefail mkdir -p dist/npm pack_info=$(cd "$STAGING_DIR" && 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@v7 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@v8 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[@]}"