-
-
Notifications
You must be signed in to change notification settings - Fork 9
428 lines (389 loc) · 17.9 KB
/
Copy pathrelease.yml
File metadata and controls
428 lines (389 loc) · 17.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
name: Release
on:
push:
branches: [main]
workflow_dispatch:
inputs:
tag:
description: 'Re-publish an existing draft release (e.g. v0.1.1). Leave empty for normal release-please flow.'
required: false
type: string
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-13
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 ourselves, then DON'T pass APPLE_CERTIFICATE
# to tauri-action. Tauri's bundler uses var_os() which treats empty
# strings as present (Some("")), so we must completely omit the env var.
# Instead we import the cert here and only pass APPLE_SIGNING_IDENTITY.
- name: Import Apple certificate
if: startsWith(matrix.os, 'macos-')
id: apple-cert
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 — using ad-hoc signing"
echo "identity=-" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12
if [ ! -s certificate.p12 ]; then
echo "Certificate decode produced empty file — using ad-hoc signing"
rm -f certificate.p12
echo "identity=-" >> "$GITHUB_OUTPUT"
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 — using ad-hoc signing"
rm -f certificate.p12
echo "identity=-" >> "$GITHUB_OUTPUT"
exit 0
fi
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
rm certificate.p12
echo "identity=${{ secrets.APPLE_SIGNING_IDENTITY }}" >> "$GITHUB_OUTPUT"
echo "cert_ok=true" >> "$GITHUB_OUTPUT"
# IMPORTANT: Do NOT pass APPLE_CERTIFICATE, APPLE_ID, APPLE_PASSWORD,
# or APPLE_TEAM_ID to tauri-action. Tauri's bundler uses var_os() which
# treats empty strings as "present" and attempts certificate import /
# notarization even when values are empty, causing build failures.
# We handle cert import ourselves above and only pass APPLE_SIGNING_IDENTITY.
# When a valid Apple Developer certificate is configured, add notarization
# env vars back here (APPLE_ID, APPLE_PASSWORD, APPLE_TEAM_ID).
- 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
APPLE_SIGNING_IDENTITY: ${{ steps.apple-cert.outputs.identity }}
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]
# Run after a successful build OR when manually re-publishing an existing release
if: >-
always() &&
(
needs.build-release.result == 'success' ||
(github.event_name == 'workflow_dispatch' && inputs.tag != '')
)
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
tag: ${{ steps.resolve.outputs.tag }}
steps:
# Normalize the tag: ensure it starts with "v" whether the user
# typed "0.1.1" or "v0.1.1" in the workflow_dispatch input.
- name: Resolve tag
id: resolve
run: |
RAW="${{ inputs.tag || needs.release-please.outputs.tag_name }}"
TAG="${RAW#v}" # strip leading v if present
TAG="v${TAG}" # re-add it consistently
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
- name: Validate existing release has assets
if: inputs.tag != ''
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="${{ steps.resolve.outputs.tag }}"
ASSET_COUNT=$(gh release view "$TAG" \
--repo "${{ github.repository }}" \
--json assets --jq '.assets | length')
echo "Release $TAG has $ASSET_COUNT asset(s)"
if [ "$ASSET_COUNT" -eq 0 ]; then
echo "::error::Release $TAG has no assets to sign"
exit 1
fi
- name: Import GPG signing key
run: echo "${{ secrets.APT_GPG_PRIVATE_KEY }}" | gpg --batch --import
- name: Sign release artifacts
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="${{ steps.resolve.outputs.tag }}"
REPO="${{ github.repository }}"
mkdir -p artifacts sigs
# Download all release assets (installers only, skip Tauri updater metadata)
gh release download "$TAG" --dir artifacts --repo "$REPO" \
--pattern "*.deb" --pattern "*.rpm" --pattern "*.AppImage" \
--pattern "*.dmg" --pattern "*.exe" --pattern "*.msi" \
--pattern "*.nsis.zip" || true
# Create detached signatures and upload
for file in artifacts/*; do
[ -f "$file" ] || continue
gpg --batch --yes --detach-sign --armor -o "sigs/$(basename "$file").sig" "$file"
done
# Upload all .sig files to the release
if ls sigs/*.sig &>/dev/null; then
gh release upload "$TAG" sigs/*.sig --repo "$REPO" --clobber
fi
- name: Publish release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release edit "${{ steps.resolve.outputs.tag }}" \
--draft=false \
--repo "${{ github.repository }}"
# ─────────────────────────────────────────────────────────────
# Update Homebrew Tap: Push new version to homebrew-tap
# ─────────────────────────────────────────────────────────────
update-homebrew:
needs: [release-please, publish-release]
if: >-
always() &&
needs.publish-release.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Compute SHA256 and update cask
env:
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
run: |
TAG="${{ needs.publish-release.outputs.tag || needs.release-please.outputs.tag_name }}"
VERSION="${TAG#v}"
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; }
SHA_ARM64=$(shasum -a 256 arm64.dmg | cut -d' ' -f1)
echo "ARM64 SHA256: $SHA_ARM64"
# x64 DMG is optional (macos-latest runs on ARM; cross-compiled targets don't produce DMGs)
HAS_X64=false
if curl -fSL "${BASE_URL}/McpMux_${VERSION}_x64.dmg" -o x64.dmg 2>/dev/null; then
SHA_X64=$(shasum -a 256 x64.dmg | cut -d' ' -f1)
echo "x64 SHA256: $SHA_X64"
HAS_X64=true
else
echo "x64 DMG not found — generating ARM-only cask"
fi
# Clone the tap repo and update the cask
git clone https://x-access-token:${GH_TOKEN}@github.com/mcpmux/homebrew-tap.git tap
# Generate cask file
CASK_FILE="tap/Casks/mcpmux.rb"
{
echo 'cask "mcpmux" do'
if [ "$HAS_X64" = true ]; then
echo ' arch arm: "aarch64", intel: "x64"'
fi
echo ''
echo " version \"${VERSION}\""
if [ "$HAS_X64" = true ]; then
echo " sha256 arm: \"${SHA_ARM64}\","
echo " intel: \"${SHA_X64}\""
else
echo " sha256 \"${SHA_ARM64}\""
fi
echo ''
if [ "$HAS_X64" = true ]; then
echo ' url "https://github.com/mcpmux/mcp-mux/releases/download/v#{version}/McpMux_#{version}_#{arch}.dmg",'
else
echo ' url "https://github.com/mcpmux/mcp-mux/releases/download/v#{version}/McpMux_#{version}_aarch64.dmg",'
fi
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 ''
if [ "$HAS_X64" != true ]; then
echo ' depends_on arch: :arm64'
fi
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: >-
always() &&
needs.publish-release.result == 'success'
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: |
TAG="${{ needs.publish-release.outputs.tag || needs.release-please.outputs.tag_name }}"
mkdir -p artifacts
# Download all .deb files from the release
gh release download "$TAG" --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 (--section/--priority override in case
# the .deb control file is missing these fields)
for deb in artifacts/*.deb; do
echo "Adding: $deb"
reprepro -b repo --section utils --priority optional 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