name: Release
on:
push:
tags:
- "v*"
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
env:
CARGO_TERM_COLOR: always
jobs:
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 packages: write id-token: write
outputs:
digest_amd64: ${{ steps.meta.outputs.digest_amd64 }}
digest_arm64: ${{ steps.meta.outputs.digest_arm64 }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with:
fetch-depth: 0
submodules: true
persist-credentials: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6
- name: Log in to GitHub Container Registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee 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@f9f3042f7e2789586610d6e8b85c8f03e5195baf 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 }}
create-multiarch-manifests:
name: Create & sign multi-arch manifests
needs: build-oci-images
runs-on: ubuntu-latest
permissions:
contents: read packages: write id-token: write
outputs:
tag_digest: ${{ steps.digest.outputs.tag_digest }}
latest_digest: ${{ steps.digest.outputs.latest_digest }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with:
fetch-depth: 0
persist-credentials: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6
- name: Log in to GitHub Container Registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee 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 }}
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
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd 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 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
# 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 }}
- name: Upload artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a 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
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd 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.components[0]."bom-ref")
| del(.metadata.component.components[0].purl)
' temp.json > sbom.cdx.json
- name: Upload sbom artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with:
name: sbom-cdx-json
path: sbom.cdx.json
if-no-files-found: error
publish-github-release:
name: Publish GitHub Release
needs:
- build-release-binaries
- create-multiarch-manifests
- sbom
runs-on: ubuntu-latest
permissions:
contents: write env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with:
fetch-depth: 0
persist-credentials: false
- name: Download build artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c 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 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
update-homebrew-tap:
name: Update Homebrew tap
needs:
- publish-github-release
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Download release binaries
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd 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 }}