chore: Release hyper-mcp-remote version 0.3.2 #10
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - "v*" # Match tags like v1.0.0, v2.1.3 etc. | |
| permissions: | |
| contents: read # checkout repository | |
| concurrency: | |
| group: ${{ github.workflow }} | |
| cancel-in-progress: false | |
| env: | |
| CARGO_TERM_COLOR: always | |
| jobs: | |
| # 1) Build & push per-arch images, sign each by digest | |
| build-oci-images: | |
| name: Build OCI image (${{ matrix.arch }}) | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-24.04 | |
| arch: amd64 | |
| platform: linux/amd64 | |
| - os: ubuntu-24.04-arm | |
| arch: arm64 | |
| platform: linux/arm64 | |
| runs-on: ${{ matrix.os }} | |
| permissions: | |
| contents: read # checkout repository | |
| packages: write # push container images to GHCR | |
| id-token: write # keyless cosign signing | |
| outputs: | |
| digest_amd64: ${{ steps.meta.outputs.digest_amd64 }} | |
| digest_arm64: ${{ steps.meta.outputs.digest_arm64 }} | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| fetch-depth: 0 | |
| submodules: true | |
| persist-credentials: false | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 | |
| - name: Install cosign | |
| uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build & push (by platform) with BuildKit cache | |
| id: build | |
| uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 | |
| with: | |
| context: . | |
| push: true | |
| platforms: ${{ matrix.platform }} | |
| tags: | | |
| ghcr.io/${{ github.repository_owner }}/hyper-mcp-remote:${{ github.ref_name }}-${{ matrix.arch }} | |
| cache-from: type=gha,scope=hyper-mcp-remote-release-${{ matrix.arch }} | |
| cache-to: type=gha,mode=max,scope=hyper-mcp-remote-release-${{ matrix.arch }} | |
| - name: Resolve and sign arch image by digest | |
| id: meta | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| IMAGE="ghcr.io/${REPOSITORY_OWNER}/hyper-mcp-remote" | |
| DIGEST="${STEPS_BUILD_OUTPUTS_DIGEST}" | |
| REF="${IMAGE}@${DIGEST}" | |
| echo "Signing arch image by digest: ${REF}" | |
| cosign sign --yes "${REF}" | |
| if [[ "${ARCH}" == "amd64" ]]; then | |
| echo "digest_amd64=${DIGEST}" >> "$GITHUB_OUTPUT" | |
| elif [[ "${ARCH}" == "arm64" ]]; then | |
| echo "digest_arm64=${DIGEST}" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "Unknown arch: ${ARCH}" >&2 | |
| exit 1 | |
| fi | |
| echo "Built ${IMAGE}:${REF_NAME}-${ARCH} -> ${DIGEST}" | |
| env: | |
| REPOSITORY_OWNER: ${{ github.repository_owner }} | |
| ARCH: ${{ matrix.arch }} | |
| STEPS_BUILD_OUTPUTS_DIGEST: ${{ steps.build.outputs.digest }} | |
| REF_NAME: ${{ github.ref_name }} | |
| # 2) Create multi-arch manifests, sign manifests by digest, export digests for the GH release body | |
| create-multiarch-manifests: | |
| name: Create & sign multi-arch manifests | |
| needs: build-oci-images | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read # checkout repository | |
| packages: write # push manifest to GHCR | |
| id-token: write # keyless cosign signing | |
| outputs: | |
| tag_digest: ${{ steps.digest.outputs.tag_digest }} | |
| latest_digest: ${{ steps.digest.outputs.latest_digest }} | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 | |
| - name: Install cosign | |
| uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Create, push, and sign manifests by digest | |
| id: digest | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| TAG="${GITHUB_REF#refs/tags/}" | |
| IMAGE="ghcr.io/${REPOSITORY_OWNER}/hyper-mcp-remote" | |
| AMD64_DIGEST="${NEEDS_BUILD_OCI_IMAGES_OUTPUTS_DIGEST_AMD64}" | |
| ARM64_DIGEST="${NEEDS_BUILD_OCI_IMAGES_OUTPUTS_DIGEST_ARM64}" | |
| if [[ -z "${AMD64_DIGEST}" || -z "${ARM64_DIGEST}" ]]; then | |
| echo "Missing per-arch digests from build job." >&2 | |
| exit 1 | |
| fi | |
| # ---------------- Tag manifest ---------------- | |
| echo "Creating multi-arch manifest: ${IMAGE}:${TAG}" | |
| docker buildx imagetools create \ | |
| -t "${IMAGE}:${TAG}" \ | |
| "${IMAGE}@${AMD64_DIGEST}" \ | |
| "${IMAGE}@${ARM64_DIGEST}" | |
| TAG_DIGEST="$(docker buildx imagetools inspect "${IMAGE}:${TAG}" | grep -E '^Digest:' | awk '{print $2}')" | |
| echo "tag_digest=${TAG_DIGEST}" >> "$GITHUB_OUTPUT" | |
| echo "Signing tag manifest by digest: ${IMAGE}@${TAG_DIGEST}" | |
| cosign sign --yes "${IMAGE}@${TAG_DIGEST}" | |
| # ---------------- Latest manifest ---------------- | |
| echo "Creating multi-arch manifest: ${IMAGE}:latest" | |
| docker buildx imagetools create \ | |
| -t "${IMAGE}:latest" \ | |
| "${IMAGE}@${AMD64_DIGEST}" \ | |
| "${IMAGE}@${ARM64_DIGEST}" | |
| LATEST_DIGEST="$( | |
| docker buildx imagetools inspect "${IMAGE}:latest" --format '{{json .Manifest}}' \ | |
| | python3 -c 'import json,sys; print(json.load(sys.stdin)["digest"])' | |
| )" | |
| if [[ ! "$LATEST_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then | |
| echo "ERROR: Invalid digest: '$LATEST_DIGEST'" >&2 | |
| exit 1 | |
| fi | |
| echo "latest_digest=${LATEST_DIGEST}" >> "$GITHUB_OUTPUT" | |
| echo "Signing latest manifest by digest: ${IMAGE}@${LATEST_DIGEST}" | |
| cosign sign --yes "${IMAGE}@${LATEST_DIGEST}" | |
| echo "Resolved ${IMAGE}:${TAG} -> ${TAG_DIGEST}" | |
| echo "Resolved ${IMAGE}:latest -> ${LATEST_DIGEST}" | |
| env: | |
| REPOSITORY_OWNER: ${{ github.repository_owner }} | |
| NEEDS_BUILD_OCI_IMAGES_OUTPUTS_DIGEST_AMD64: ${{ needs.build-oci-images.outputs.digest_amd64 }} | |
| NEEDS_BUILD_OCI_IMAGES_OUTPUTS_DIGEST_ARM64: ${{ needs.build-oci-images.outputs.digest_arm64 }} | |
| # 3) Build release binaries (matrix), upload as artifacts (no GH release here) | |
| build-release-binaries: | |
| name: Build release binaries (${{ matrix.target }}) | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-latest | |
| target: x86_64-unknown-linux-gnu | |
| ext: "" | |
| - os: ubuntu-24.04-arm | |
| target: aarch64-unknown-linux-gnu | |
| ext: "" | |
| - os: macos-latest | |
| target: aarch64-apple-darwin | |
| ext: "" | |
| - os: windows-latest | |
| target: x86_64-pc-windows-msvc | |
| ext: ".exe" | |
| runs-on: ${{ matrix.os }} | |
| permissions: | |
| contents: read # checkout repository | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Install Rust toolchain | |
| run: rustup show | |
| - name: Install cargo-auditable | |
| run: cargo install cargo-auditable | |
| - name: Install zipsign | |
| run: cargo install zipsign --locked | |
| - name: Install target | |
| shell: bash | |
| run: rustup target add "${TARGET}" | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| - name: Build | |
| shell: bash | |
| run: cargo auditable build --target "${TARGET}" --release --locked | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| - name: Package + checksums | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p dist | |
| bin="hyper-mcp-remote${EXT}" | |
| src="target/${TARGET}/release/${bin}" | |
| if [[ "${RUNNER_OS}" == "Windows" ]]; then | |
| # Use PowerShell's Compress-Archive? We'll keep it simple with tar if available; | |
| # but Windows runners have bsdtar via Git. Use zip for compatibility. | |
| 7z a "dist/hyper-mcp-remote-${TARGET}.zip" "${src}" | |
| pkg="dist/hyper-mcp-remote-${TARGET}.zip" | |
| else | |
| tar -czf "dist/hyper-mcp-remote-${TARGET}.tar.gz" -C "target/${TARGET}/release" "${bin}" | |
| pkg="dist/hyper-mcp-remote-${TARGET}.tar.gz" | |
| fi | |
| # Sign the archive in place with zipsign (ed25519). The private key is | |
| # provided base64-encoded via the ED25519_PRIVATE_KEY secret; decode it | |
| # to the binary key file zipsign expects. Signing happens before the | |
| # checksum is computed so the checksum covers the signed artifact. | |
| if [[ -z "${ED25519_PRIVATE_KEY_B64:-}" ]]; then | |
| echo "ED25519_PRIVATE_KEY secret is not set" >&2 | |
| exit 1 | |
| fi | |
| trap 'rm -f zipsign-priv.key' EXIT | |
| printf '%s' "${ED25519_PRIVATE_KEY_B64}" | openssl base64 -d -A > zipsign-priv.key | |
| if [[ "${RUNNER_OS}" == "Windows" ]]; then | |
| zipsign sign zip "${pkg}" zipsign-priv.key | |
| else | |
| zipsign sign tar "${pkg}" zipsign-priv.key | |
| fi | |
| # checksums | |
| if command -v sha256sum >/dev/null 2>&1; then | |
| sha256sum "${pkg}" > "dist/checksums-${TARGET}.txt" | |
| elif command -v shasum >/dev/null 2>&1; then | |
| shasum -a 256 "${pkg}" > "dist/checksums-${TARGET}.txt" | |
| elif [[ "$RUNNER_OS" == "Windows" ]]; then | |
| powershell.exe -NoProfile -Command \ | |
| "\$h=(Get-FileHash -Algorithm SHA256 '${pkg}').Hash.ToLower(); \"\$h ${pkg}\" | Out-File -Encoding ascii 'dist/checksums-${TARGET}.txt'" | |
| else | |
| echo "No SHA256 tool found" >&2 | |
| exit 1 | |
| fi | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| EXT: ${{ matrix.ext }} | |
| RUNNER_OS: ${{ runner.os }} | |
| ED25519_PRIVATE_KEY_B64: ${{ secrets.ED25519_PRIVATE_KEY }} | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: release-${{ matrix.target }} | |
| path: | | |
| dist/hyper-mcp-remote-${{ matrix.target }}.tar.gz | |
| dist/hyper-mcp-remote-${{ matrix.target }}.zip | |
| dist/checksums-${{ matrix.target }}.txt | |
| if-no-files-found: error | |
| sbom: | |
| name: SBOM | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read # checkout repository | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| fetch-depth: 0 | |
| submodules: true | |
| persist-credentials: false | |
| - name: Install cargo-cyclonedx | |
| run: cargo install cargo-cyclonedx --locked | |
| - name: Generate CycloneDX SBOM | |
| run: cargo cyclonedx -f json --all-features --override-filename temp | |
| - name: Clean SBOM | |
| run: jq ' | |
| del(.metadata.component."bom-ref") | |
| | del(.metadata.component.purl) | |
| | del(.metadata.component.components[0]."bom-ref") | |
| | del(.metadata.component.components[0].purl) | |
| ' temp.json > sbom.cdx.json | |
| - name: Upload sbom artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: sbom-cdx-json | |
| path: sbom.cdx.json | |
| if-no-files-found: error | |
| # 4) Publish GitHub Release once (download artifacts, write body incl digest, upload assets) | |
| publish-github-release: | |
| name: Publish GitHub Release | |
| needs: | |
| - build-release-binaries | |
| - create-multiarch-manifests | |
| - sbom | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write # create GitHub release and upload assets | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: release-* | |
| path: dist | |
| - name: Flatten artifacts | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p out | |
| find dist -type f -maxdepth 3 -print -exec cp {} out/ \; | |
| - name: Download sbom artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: sbom-cdx-json | |
| path: . | |
| - name: Write release body (include immutable digest) | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| TAG="${GITHUB_REF#refs/tags/}" | |
| IMAGE="ghcr.io/${REPOSITORY_OWNER}/hyper-mcp-remote" | |
| TAG_DIGEST="${NEEDS_CREATE_MULTIARCH_MANIFESTS_OUTPUTS_TAG_DIGEST}" | |
| SHA="${GITHUB_SHA}" | |
| cat > release-body.md <<EOF | |
| Final release for \`${TAG}\`. | |
| Commit: | |
| - \`${SHA}\` | |
| Included: | |
| - hyper-mcp-remote binaries for Linux & macOS | |
| - hyper-mcp-remote container image: \`${IMAGE}:${TAG}\` | |
| ✅ Multi-arch digest (immutable, recommended for pinning): | |
| - \`${IMAGE}@${TAG_DIGEST}\` | |
| All container images are signed with Cosign. Verify with: | |
| \`\`\`bash | |
| cosign verify \ | |
| --certificate-identity "https://github.com/hyper-mcp-rs/hyper-mcp-remote/.github/workflows/release.yml@refs/tags/${TAG}" \ | |
| --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ | |
| ${IMAGE}@${TAG_DIGEST} | |
| \`\`\` | |
| EOF | |
| env: | |
| REPOSITORY_OWNER: ${{ github.repository_owner }} | |
| NEEDS_CREATE_MULTIARCH_MANIFESTS_OUTPUTS_TAG_DIGEST: ${{ needs.create-multiarch-manifests.outputs.tag_digest }} | |
| - name: Create GitHub Release | |
| shell: bash | |
| run: | | |
| TAG="${GITHUB_REF#refs/tags/}" | |
| gh release create "${TAG}" \ | |
| --title "Release ${TAG}" \ | |
| --generate-notes \ | |
| --notes-file release-body.md \ | |
| out/* \ | |
| sbom.cdx.json | |
| # 5) Publish/update the Homebrew formula in hyper-mcp-rs/homebrew-tap. | |
| # Only the aarch64-apple-darwin, aarch64-unknown-linux-gnu, and | |
| # x86_64-unknown-linux-gnu binaries are wired into the formula. | |
| # | |
| # Requires a repo or fine-grained PAT with `contents: write` on | |
| # hyper-mcp-rs/homebrew-tap, exposed to this workflow as the secret | |
| # `HOMEBREW_TAP_TOKEN`. | |
| update-homebrew-tap: | |
| name: Update Homebrew tap | |
| needs: | |
| - publish-github-release | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read # this repo; tap write is granted via HOMEBREW_TAP_TOKEN | |
| steps: | |
| - name: Download release binaries | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: release-* | |
| path: dist | |
| - name: Render Homebrew formula | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| TAG="${GITHUB_REF#refs/tags/}" | |
| VERSION="${TAG#v}" | |
| darwin_arm64="dist/release-aarch64-apple-darwin/hyper-mcp-remote-aarch64-apple-darwin.tar.gz" | |
| linux_arm64="dist/release-aarch64-unknown-linux-gnu/hyper-mcp-remote-aarch64-unknown-linux-gnu.tar.gz" | |
| linux_amd64="dist/release-x86_64-unknown-linux-gnu/hyper-mcp-remote-x86_64-unknown-linux-gnu.tar.gz" | |
| for f in "${darwin_arm64}" "${linux_arm64}" "${linux_amd64}"; do | |
| if [[ ! -f "${f}" ]]; then | |
| echo "Missing expected artifact: ${f}" >&2 | |
| exit 1 | |
| fi | |
| done | |
| sha_darwin_arm64=$(sha256sum "${darwin_arm64}" | awk '{print $1}') | |
| sha_linux_arm64=$(sha256sum "${linux_arm64}" | awk '{print $1}') | |
| sha_linux_amd64=$(sha256sum "${linux_amd64}" | awk '{print $1}') | |
| base_url="https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}" | |
| mkdir -p rendered | |
| cat > rendered/hyper-mcp-remote.rb <<EOF | |
| # This file is generated by .github/workflows/release.yml. | |
| # Do not edit by hand; changes will be overwritten on the next release. | |
| class HyperMcpRemote < Formula | |
| desc "Stdio to Streamable-HTTP MCP proxy with OAuth support" | |
| homepage "https://github.com/hyper-mcp-rs/hyper-mcp-remote" | |
| version "${VERSION}" | |
| license "Apache-2.0" | |
| on_macos do | |
| on_arm do | |
| url "${base_url}/hyper-mcp-remote-aarch64-apple-darwin.tar.gz" | |
| sha256 "${sha_darwin_arm64}" | |
| end | |
| end | |
| on_linux do | |
| on_arm do | |
| url "${base_url}/hyper-mcp-remote-aarch64-unknown-linux-gnu.tar.gz" | |
| sha256 "${sha_linux_arm64}" | |
| end | |
| on_intel do | |
| url "${base_url}/hyper-mcp-remote-x86_64-unknown-linux-gnu.tar.gz" | |
| sha256 "${sha_linux_amd64}" | |
| end | |
| end | |
| def install | |
| bin.install "hyper-mcp-remote" | |
| end | |
| test do | |
| assert_match version.to_s, shell_output("#{bin}/hyper-mcp-remote --version") | |
| end | |
| end | |
| EOF | |
| echo "---- rendered formula ----" | |
| cat rendered/hyper-mcp-remote.rb | |
| - name: Checkout homebrew tap | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| repository: hyper-mcp-rs/homebrew-tap | |
| token: ${{ secrets.HOMEBREW_TAP_TOKEN }} | |
| path: homebrew-tap | |
| persist-credentials: true | |
| - name: Commit and push formula | |
| shell: bash | |
| working-directory: homebrew-tap | |
| run: | | |
| set -euo pipefail | |
| mkdir -p Formula | |
| cp ../rendered/hyper-mcp-remote.rb Formula/hyper-mcp-remote.rb | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add Formula/hyper-mcp-remote.rb | |
| if git diff --cached --quiet; then | |
| echo "Formula already up to date for ${TAG}; nothing to commit." | |
| exit 0 | |
| fi | |
| git commit -m "hyper-mcp-remote ${TAG}" | |
| git push | |
| env: | |
| TAG: ${{ github.ref_name }} |