diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e2e0c00 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,108 @@ +name: Release + +on: + push: + tags: + - "v*" + +jobs: + test: + name: Test + runs-on: macos-15 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Swift 6.2 + uses: swift-actions/setup-swift@v2 + with: + swift-version: "6.2" + + - name: Run tests + run: swift test -Xswiftc -strict-concurrency=minimal + + build-arm64: + name: Build (arm64) + needs: test + runs-on: macos-15 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Swift 6.2 + uses: swift-actions/setup-swift@v2 + with: + swift-version: "6.2" + + - name: Build release binaries + run: scripts/build-release.sh --arch arm64 --version "${GITHUB_REF_NAME}" --dist-root dist + + - name: Upload arm64 artifacts + uses: actions/upload-artifact@v4 + with: + name: dist-arm64 + path: dist/arm64 + if-no-files-found: error + + build-x86_64: + name: Build (x86_64) + needs: test + runs-on: macos-15 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Swift 6.2 + uses: swift-actions/setup-swift@v2 + with: + swift-version: "6.2" + + - name: Build release binaries + run: scripts/build-release.sh --arch x86_64 --version "${GITHUB_REF_NAME}" --dist-root dist + + - name: Upload x86_64 artifacts + uses: actions/upload-artifact@v4 + with: + name: dist-x86_64 + path: dist/x86_64 + if-no-files-found: error + + release: + name: Package and Publish + needs: + - build-arm64 + - build-x86_64 + runs-on: macos-15 + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Swift 6.2 + uses: swift-actions/setup-swift@v2 + with: + swift-version: "6.2" + + - name: Download arm64 artifacts + uses: actions/download-artifact@v4 + with: + name: dist-arm64 + path: dist/arm64 + + - name: Download x86_64 artifacts + uses: actions/download-artifact@v4 + with: + name: dist-x86_64 + path: dist/x86_64 + + - name: Package release archives + run: scripts/package-universal.sh --dist-root dist --output-dir release + + - name: Publish GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: | + release/*.tar.gz + release/SHA256SUMS.txt + generate_release_notes: true diff --git a/.gitignore b/.gitignore index 2330a87..bf10595 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ DerivedData/ .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .netrc .codex/tmp/ +/dist +/release diff --git a/README.md b/README.md index 5ea24c5..0010128 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,34 @@ See [Architecture](Docs/architecture.md) for the process overview. swift run -c release xcode-mcp-proxy-install ``` +### Install from GitHub Releases + +Each release tag (`v*`) publishes: + +- `xcode-mcp-proxy.tar.gz` (universal binary) +- `xcode-mcp-proxy-darwin-arm64.tar.gz` +- `xcode-mcp-proxy-darwin-x86_64.tar.gz` +- `SHA256SUMS.txt` + +Example: + +```bash +VERSION=v0.1.0 +BASE_URL="https://github.com//XcodeMCPKit/releases/download/${VERSION}" + +ARCHIVE="xcode-mcp-proxy.tar.gz" # or: xcode-mcp-proxy-darwin-arm64.tar.gz / xcode-mcp-proxy-darwin-x86_64.tar.gz +curl -fL -O "${BASE_URL}/${ARCHIVE}" +curl -fL -O "${BASE_URL}/SHA256SUMS.txt" +shasum -a 256 -c SHA256SUMS.txt + +tar -xzf "${ARCHIVE}" +mkdir -p "${HOME}/.local/bin" +cp bin/* "${HOME}/.local/bin/" +chmod +x "${HOME}/.local/bin/xcode-mcp-proxy" \ + "${HOME}/.local/bin/xcode-mcp-proxy-server" \ + "${HOME}/.local/bin/xcode-mcp-proxy-install" +``` + Replace `xcrun mcpbridge` with one of the following: ### Codex diff --git a/scripts/build-release.sh b/scripts/build-release.sh new file mode 100755 index 0000000..63084d7 --- /dev/null +++ b/scripts/build-release.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: scripts/build-release.sh --arch --version [--dist-root ] + +Builds release binaries and stages them under: + //bin/ +EOF +} + +arch="" +version="" +dist_root="dist" + +while [[ $# -gt 0 ]]; do + case "$1" in + --arch) + arch="${2:-}" + shift 2 + ;; + --version) + version="${2:-}" + shift 2 + ;; + --dist-root) + dist_root="${2:-}" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage + exit 1 + ;; + esac +done + +if [[ -z "$arch" ]]; then + echo "--arch is required." >&2 + usage + exit 1 +fi + +if [[ -z "$version" ]]; then + echo "--version is required." >&2 + usage + exit 1 +fi + +case "$arch" in + arm64|x86_64) ;; + *) + echo "Unsupported arch: $arch (expected arm64 or x86_64)" >&2 + exit 1 + ;; +esac + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +if [[ "$dist_root" = /* ]]; then + dist_base="$dist_root" +else + dist_base="$repo_root/$dist_root" +fi + +out_dir="$dist_base/$arch" +bin_out="$out_dir/bin" +products=( + "xcode-mcp-proxy" + "xcode-mcp-proxy-server" + "xcode-mcp-proxy-install" +) + +pushd "$repo_root" >/dev/null + +for product in "${products[@]}"; do + swift build -c release \ + -Xswiftc -strict-concurrency=minimal \ + --arch "$arch" \ + --product "$product" +done + +bin_path="$(swift build -c release --arch "$arch" --show-bin-path)" +rm -rf "$out_dir" +mkdir -p "$bin_out" + +for product in "${products[@]}"; do + source_path="$bin_path/$product" + if [[ ! -f "$source_path" ]]; then + source_path="$(find "$repo_root/.build" -type f -path "*/release/$product" | head -n 1 || true)" + fi + if [[ -z "$source_path" || ! -f "$source_path" ]]; then + echo "Failed to locate built binary: $product" >&2 + exit 1 + fi + + target_path="$bin_out/$product" + cp "$source_path" "$target_path" + chmod +x "$target_path" + if command -v codesign >/dev/null 2>&1; then + codesign --remove-signature "$target_path" >/dev/null 2>&1 || true + fi +done + +cat > "$out_dir/manifest.txt" </dev/null + +echo "Staged release binaries at: $out_dir" diff --git a/scripts/package-universal.sh b/scripts/package-universal.sh new file mode 100755 index 0000000..f629fc5 --- /dev/null +++ b/scripts/package-universal.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: scripts/package-universal.sh [--dist-root ] [--output-dir ] + +Requires staged binaries from: + /arm64/bin/ + /x86_64/bin/ + +Outputs: + /xcode-mcp-proxy.tar.gz + /xcode-mcp-proxy-darwin-arm64.tar.gz + /xcode-mcp-proxy-darwin-x86_64.tar.gz + /SHA256SUMS.txt +EOF +} + +dist_root="dist" +output_dir="release" + +while [[ $# -gt 0 ]]; do + case "$1" in + --dist-root) + dist_root="${2:-}" + shift 2 + ;; + --output-dir) + output_dir="${2:-}" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage + exit 1 + ;; + esac +done + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +if [[ "$dist_root" = /* ]]; then + dist_base="$dist_root" +else + dist_base="$repo_root/$dist_root" +fi +if [[ "$output_dir" = /* ]]; then + output_base="$output_dir" +else + output_base="$repo_root/$output_dir" +fi + +arm_bin="$dist_base/arm64/bin" +x86_bin="$dist_base/x86_64/bin" +for path in "$arm_bin" "$x86_bin"; do + if [[ ! -d "$path" ]]; then + echo "Missing staged directory: $path" >&2 + exit 1 + fi +done + +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT + +mkdir -p "$output_base" + +universal_archive="$output_base/xcode-mcp-proxy.tar.gz" +arm_archive="$output_base/xcode-mcp-proxy-darwin-arm64.tar.gz" +x86_archive="$output_base/xcode-mcp-proxy-darwin-x86_64.tar.gz" +find "$output_base" -maxdepth 1 -type f -name 'xcode-mcp-proxy*.tar.gz' -delete +rm -f "$output_base/SHA256SUMS.txt" + +products=( + "xcode-mcp-proxy" + "xcode-mcp-proxy-server" + "xcode-mcp-proxy-install" +) + +for product in "${products[@]}"; do + arm_product="$arm_bin/$product" + x86_product="$x86_bin/$product" + if [[ ! -f "$arm_product" ]]; then + echo "Missing staged binary: $arm_product" >&2 + exit 1 + fi + if [[ ! -f "$x86_product" ]]; then + echo "Missing staged binary: $x86_product" >&2 + exit 1 + fi +done + +mkdir -p "$tmp_dir/universal/bin" +for product in "${products[@]}"; do + target="$tmp_dir/universal/bin/$product" + lipo -create -output "$target" "$arm_bin/$product" "$x86_bin/$product" + chmod +x "$target" + if command -v codesign >/dev/null 2>&1; then + codesign --remove-signature "$target" >/dev/null 2>&1 || true + fi +done + +cp -R "$tmp_dir/universal/bin" "$tmp_dir/bin" +tar -C "$tmp_dir" -czf "$universal_archive" bin +rm -rf "$tmp_dir/bin" + +cp -R "$arm_bin" "$tmp_dir/bin" +tar -C "$tmp_dir" -czf "$arm_archive" bin +rm -rf "$tmp_dir/bin" + +cp -R "$x86_bin" "$tmp_dir/bin" +tar -C "$tmp_dir" -czf "$x86_archive" bin +rm -rf "$tmp_dir/bin" + +( + cd "$output_base" + shasum -a 256 \ + xcode-mcp-proxy.tar.gz \ + xcode-mcp-proxy-darwin-arm64.tar.gz \ + xcode-mcp-proxy-darwin-x86_64.tar.gz > SHA256SUMS.txt +) + +echo "Created release package: $universal_archive" +echo "Created release package: $arm_archive" +echo "Created release package: $x86_archive" +echo "Created checksum file: $output_base/SHA256SUMS.txt"