Skip to content

chore: consolidate changelog for v0.0.13 initial public release #122

chore: consolidate changelog for v0.0.13 initial public release

chore: consolidate changelog for v0.0.13 initial public release #122

Workflow file for this run

name: Release
on:
push:
branches: [main]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
env:
CARGO_TERM_COLOR: always
jobs:
# ─────────────────────────────────────────────────────────────
# Release Please: Create/update release PR with version bump
#
# When a release PR is merged, release-please creates a published
# release (required for git tag creation — draft releases don't
# create tags, which breaks version tracking). We immediately
# convert it to draft so users never see an empty release.
#
# This is safe because:
# - /releases/latest API returns the PREVIOUS release while draft
# - Tauri updater checks /releases/latest/download/latest.json
# → still resolves to old release → no broken updates
# - discover.ui fetches /releases/latest → still gets old release
# - Once publish-release un-drafts, /latest atomically switches
# to the new release with all artifacts already attached
# ─────────────────────────────────────────────────────────────
release-please:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
outputs:
release_created: ${{ steps.release.outputs.release_created }}
release_id: ${{ steps.release.outputs.id }}
tag_name: ${{ steps.release.outputs.tag_name }}
version: ${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }}
steps:
- uses: googleapis/release-please-action@v4
id: release
with:
manifest-file: .release-please-manifest.json
config-file: release-please-config.json
# release-please creates a published release (so the git tag is created
# and version tracking works). Immediately convert to draft so users
# don't see an empty release while artifacts are being built.
# Uses github-script (not gh CLI) for minimal latency — the octokit
# client is pre-authenticated, no process spawn needed.
- name: Convert release to draft
if: steps.release.outputs.release_created == 'true'
uses: actions/github-script@v7
with:
script: |
await github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: ${{ steps.release.outputs.id }},
draft: true,
});
# ─────────────────────────────────────────────────────────────
# Build Release: Build Tauri app when release is created
# ─────────────────────────────────────────────────────────────
build-release:
needs: release-please
if: needs.release-please.outputs.release_created == 'true'
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
artifact: linux
- os: windows-latest
target: x86_64-pc-windows-msvc
artifact: windows
- os: macos-latest
target: aarch64-apple-darwin
artifact: macos-arm
- os: macos-latest
target: x86_64-apple-darwin
artifact: macos-intel
runs-on: ${{ matrix.os }}
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Install Linux deps
if: matrix.os == 'ubuntu-latest'
uses: ./.github/actions/install-linux-deps
with:
verify_glib: 'false'
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
env:
PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}-release
env:
PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
# Import Apple certificate for macOS signing (only when Developer ID is available)
- name: Import Apple certificate
if: matrix.os == 'macos-latest' || matrix.os == 'macos-15'
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
if [ -z "$APPLE_CERTIFICATE" ] || [ -z "$KEYCHAIN_PASSWORD" ]; then
echo "No Apple certificate configured — skipping import"
exit 0
fi
echo "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12
# Validate the decoded file is a real PKCS#12 before attempting import
if [ ! -s certificate.p12 ]; then
echo "Certificate decode produced empty file — skipping import"
rm -f certificate.p12
exit 0
fi
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
if ! security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign; then
echo "⚠ Certificate import failed — falling back to ad-hoc signing"
rm -f certificate.p12
exit 0
fi
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
rm certificate.p12
# Determine macOS signing identity: Developer ID if available, ad-hoc (-) otherwise
- name: Resolve macOS signing identity
if: matrix.os == 'macos-latest' || matrix.os == 'macos-15'
id: macos-signing
env:
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
run: |
if [ -n "$APPLE_SIGNING_IDENTITY" ]; then
echo "identity=$APPLE_SIGNING_IDENTITY" >> "$GITHUB_OUTPUT"
echo "Using Developer ID signing"
else
echo "identity=-" >> "$GITHUB_OUTPUT"
echo "Using ad-hoc signing (no Apple Developer ID)"
fi
- name: Build Tauri app
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig
# macOS signing — falls back to ad-hoc (-) when no Apple Developer cert
APPLE_SIGNING_IDENTITY: ${{ steps.macos-signing.outputs.identity }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
# macOS notarization (only works with Developer ID)
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
projectPath: apps/desktop
# Upload to the existing draft release
releaseId: ${{ needs.release-please.outputs.release_id }}
updaterJsonKeepUniversal: true
# ─────────────────────────────────────────────────────────────
# Publish Release: Flip draft → published after all artifacts
# are attached, so /releases/latest always has all assets
# ─────────────────────────────────────────────────────────────
publish-release:
needs: [release-please, build-release]
if: needs.release-please.outputs.release_created == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Publish release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release edit "${{ needs.release-please.outputs.tag_name }}" \
--draft=false \
--repo "${{ github.repository }}"
# ─────────────────────────────────────────────────────────────
# Update Homebrew Tap: Push new version to homebrew-mcpmux
# ─────────────────────────────────────────────────────────────
update-homebrew:
needs: [release-please, publish-release]
if: needs.release-please.outputs.release_created == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Compute SHA256 and update cask
env:
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
run: |
VERSION="${{ needs.release-please.outputs.version }}"
BASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}"
# Download DMGs and compute SHA256
echo "Downloading macOS DMGs..."
curl -fSL "${BASE_URL}/McpMux_${VERSION}_aarch64.dmg" -o arm64.dmg || { echo "ARM64 DMG not found — skipping Homebrew update"; exit 0; }
curl -fSL "${BASE_URL}/McpMux_${VERSION}_x64.dmg" -o x64.dmg || { echo "x64 DMG not found — skipping Homebrew update"; exit 0; }
SHA_ARM64=$(shasum -a 256 arm64.dmg | cut -d' ' -f1)
SHA_X64=$(shasum -a 256 x64.dmg | cut -d' ' -f1)
echo "ARM64 SHA256: $SHA_ARM64"
echo "x64 SHA256: $SHA_X64"
# Clone the tap repo and update the cask
git clone https://x-access-token:${GH_TOKEN}@github.com/mcpmux/homebrew-mcpmux.git tap
# Generate cask file (Ruby heredoc content with shell variable expansion)
CASK_FILE="tap/Casks/mcpmux.rb"
{
echo 'cask "mcpmux" do'
echo ' arch arm: "aarch64", intel: "x64"'
echo ''
echo " version \"${VERSION}\""
echo " sha256 arm: \"${SHA_ARM64}\","
echo " intel: \"${SHA_X64}\""
echo ''
echo ' url "https://github.com/mcpmux/mcp-mux/releases/download/v#{version}/McpMux_#{version}_#{arch}.dmg",'
echo ' verified: "github.com/mcpmux/mcp-mux/"'
echo ''
echo ' name "McpMux"'
echo ' desc "Unified MCP gateway and manager for AI clients"'
echo ' homepage "https://mcpmux.com"'
echo ''
echo ' depends_on macos: ">= :high_sierra"'
echo ''
echo ' livecheck do'
echo ' url "https://github.com/mcpmux/mcp-mux/releases/latest"'
echo ' strategy :github_latest'
echo ' end'
echo ''
echo ' app "McpMux.app"'
echo ''
echo ' # Remove quarantine for ad-hoc signed app (no Apple Developer ID)'
echo ' postflight do'
echo ' system_command "/usr/bin/xattr",'
echo ' args: ["-cr", "#{appdir}/McpMux.app"]'
echo ' end'
echo ''
echo ' zap trash: ['
echo ' "~/Library/Application Support/com.mcpmux.desktop",'
echo ' "~/Library/Preferences/com.mcpmux.desktop.plist",'
echo ' "~/Library/Caches/com.mcpmux.desktop",'
echo ' "~/Library/Saved Application State/com.mcpmux.desktop.savedState",'
echo ' ]'
echo 'end'
} > "$CASK_FILE"
cd tap
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Casks/mcpmux.rb
git commit -m "Update mcpmux to ${VERSION}"
git push
# ─────────────────────────────────────────────────────────────
# Update APT Repository: Add .deb to self-hosted APT repo on R2
# ─────────────────────────────────────────────────────────────
update-apt-repo:
needs: [release-please, publish-release]
if: needs.release-please.outputs.release_created == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Install tools
run: sudo apt-get update && sudo apt-get install -y reprepro
- name: Configure AWS CLI for R2
run: |
aws configure set aws_access_key_id "${{ secrets.R2_ACCESS_KEY_ID }}"
aws configure set aws_secret_access_key "${{ secrets.R2_SECRET_ACCESS_KEY }}"
aws configure set default.region auto
env:
AWS_DEFAULT_OUTPUT: json
- name: Import GPG signing key
run: echo "${{ secrets.APT_GPG_PRIVATE_KEY }}" | gpg --batch --import
- name: Download .deb from GitHub Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ needs.release-please.outputs.version }}"
mkdir -p artifacts
# Download all .deb files from the release
gh release download "v${VERSION}" --pattern "*.deb" --dir artifacts
- name: Sync existing APT repo from R2
run: |
mkdir -p repo
aws s3 sync "s3://mcpmux-apt/" repo/ \
--endpoint-url "${{ secrets.R2_ENDPOINT }}" \
|| echo "No existing repo (first run)"
- name: Update APT repo with new packages
run: |
# Copy reprepro config
cp -r scripts/apt-repo/conf repo/
# Add each .deb package
for deb in artifacts/*.deb; do
echo "Adding: $deb"
reprepro -b repo includedeb stable "$deb"
done
# Export public key
gpg --armor --export hello@mcpmux.com > repo/key.gpg
- name: Sync APT repo back to R2
run: |
aws s3 sync repo/ "s3://mcpmux-apt/" \
--endpoint-url "${{ secrets.R2_ENDPOINT }}" \
--delete