diff --git a/.github/workflows/release-mcp-npm.yml b/.github/workflows/release-mcp-npm.yml new file mode 100644 index 0000000..ca002e4 --- /dev/null +++ b/.github/workflows/release-mcp-npm.yml @@ -0,0 +1,184 @@ +name: Release native MCP npm packages + +on: + push: + tags: + - "mcp-npm-v*" + +permissions: + contents: read + +concurrency: + group: release-mcp-npm-${{ github.ref }} + cancel-in-progress: false + +jobs: + metadata: + name: validate metadata + runs-on: ubuntu-24.04 + outputs: + matrix: ${{ steps.metadata.outputs.matrix }} + version: ${{ steps.metadata.outputs.version }} + + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Read release metadata + id: metadata + shell: bash + env: + RELEASE_TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + node <<'NODE' + const fs = require('node:fs'); + const manifest = fs.readFileSync('mcp/Cargo.toml', 'utf8'); + const packageSection = manifest.match(/\[package\]([\s\S]*?)(?=\n\[|$)/); + const version = packageSection?.[1].match(/^version\s*=\s*"([^"]+)"\s*$/m)?.[1]; + if (!version) throw new Error('could not read rustwright-mcp crate version'); + const expectedTag = `mcp-npm-v${version}`; + if (process.env.RELEASE_TAG !== expectedTag) { + throw new Error(`tag ${process.env.RELEASE_TAG} does not match ${expectedTag}`); + } + const targets = JSON.parse(fs.readFileSync('mcp/npm/targets.json', 'utf8')); + if (targets.length !== 5) throw new Error(`expected 5 targets, found ${targets.length}`); + fs.appendFileSync(process.env.GITHUB_OUTPUT, `version=${version}\n`); + fs.appendFileSync( + process.env.GITHUB_OUTPUT, + `matrix=${JSON.stringify({ include: targets })}\n`, + ); + NODE + + - name: Validate main package generation + run: node mcp/npm/scripts/build-npm.mjs --main-only + + build: + name: build ${{ matrix.id }} + needs: metadata + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.metadata.outputs.matrix) }} + + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "24" + + - name: Set up Rust target + shell: bash + run: rustup toolchain install 1.95.0 --profile minimal --target "${{ matrix.rustTarget }}" + + - name: Set up Zig + if: ${{ matrix.zig }} + uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 + with: + version: 0.14.1 + + - name: Install cargo-zigbuild + if: ${{ matrix.zig }} + uses: taiki-e/install-action@2ca9b94c269419b7b0c711c09d0b21c4e1d51145 # v2.83.1 + with: + tool: cargo-zigbuild@0.23.0 + + - name: Build native MCP server + if: ${{ !matrix.zig }} + shell: bash + run: >- + cargo +1.95.0 build + --release + --target "${{ matrix.rustTarget }}" + --manifest-path mcp/Cargo.toml + + - name: Build portable GNU/Linux MCP server + if: ${{ matrix.zig }} + shell: bash + run: >- + cargo +1.95.0 zigbuild + --release + --target "${{ matrix.zigTarget }}" + --manifest-path mcp/Cargo.toml + + - name: Assemble platform package + shell: bash + run: | + set -euo pipefail + node mcp/npm/scripts/build-npm.mjs \ + --target "${{ matrix.id }}" \ + --binary "$GITHUB_WORKSPACE/mcp/target/${{ matrix.rustTarget }}/release/${{ matrix.binary }}" + mkdir -p "$RUNNER_TEMP/npm-dist" + npm pack "$GITHUB_WORKSPACE/mcp/npm/packages/${{ matrix.id }}" \ + --pack-destination "$RUNNER_TEMP/npm-dist" + + - name: Upload platform package + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: rustwright-mcp-${{ matrix.id }}-${{ needs.metadata.outputs.version }} + path: ${{ runner.temp }}/npm-dist/*.tgz + if-no-files-found: error + + publish: + name: publish npm packages + needs: [metadata, build] + runs-on: ubuntu-24.04 + environment: + name: npm + url: https://www.npmjs.com/package/rustwright-mcp + permissions: + contents: read + id-token: write + + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + + - name: Download platform packages + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: rustwright-mcp-*-${{ needs.metadata.outputs.version }} + path: mcp/npm/release-packages + merge-multiple: true + + - name: Validate release payload + shell: bash + run: | + set -euo pipefail + package_count="$(find mcp/npm/release-packages -type f -name '*.tgz' | wc -l | tr -d ' ')" + test "$package_count" = "5" + node mcp/npm/scripts/build-npm.mjs --main-only + test "$(node -p "require('./mcp/npm/main/package.json').version")" = \ + "${{ needs.metadata.outputs.version }}" + + - name: Select dist-tag from version + id: dist_tag + shell: bash + run: | + set -euo pipefail + version="${GITHUB_REF_NAME#mcp-npm-v}" + case "$version" in + *-*) tag=next ;; + *) tag=latest ;; + esac + echo "tag=$tag" >> "$GITHUB_OUTPUT" + + - name: Publish platform packages, then main package + shell: bash + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + set -euo pipefail + while IFS= read -r package_archive; do + npm publish "$package_archive" --access public --provenance --tag "${{ steps.dist_tag.outputs.tag }}" + done < <(find mcp/npm/release-packages -type f -name '*.tgz' | sort) + npm publish mcp/npm/main --access public --provenance --tag "${{ steps.dist_tag.outputs.tag }}"