-
Notifications
You must be signed in to change notification settings - Fork 21
1426 lines (1200 loc) · 56 KB
/
release.yml
File metadata and controls
1426 lines (1200 loc) · 56 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
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
name: Release
on:
push:
tags: ["v*"]
workflow_dispatch:
permissions:
contents: write
id-token: write
env:
GITHUB_ENVIRONMENT: production
jobs:
# Generate AI-powered release notes using Claude API
generate-notes:
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
outputs:
notes: ${{ steps.generate.outputs.notes }}
notes_file: ${{ steps.generate.outputs.notes_file }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for git log
- name: Get previous tag
id: prev_tag
run: |
# Get the tag before the current one
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
echo "tag=$PREV_TAG" >> $GITHUB_OUTPUT
if [ -n "$PREV_TAG" ]; then
echo "Previous tag: $PREV_TAG"
else
echo "No previous tag found (first release)"
fi
- name: Collect commits
id: commits
run: |
PREV_TAG="${{ steps.prev_tag.outputs.tag }}"
MAX_COMMITS=200
if [ -z "$PREV_TAG" ]; then
# First release - get all commits
echo "Collecting all commits (first release)"
COMMITS=$(git log --pretty=format:"- %s" --no-merges | head -n $MAX_COMMITS)
TOTAL=$(git rev-list --count --no-merges HEAD)
else
# Get commits since previous tag
echo "Collecting commits from $PREV_TAG to HEAD"
COMMITS=$(git log "${PREV_TAG}..HEAD" --pretty=format:"- %s" --no-merges | head -n $MAX_COMMITS)
TOTAL=$(git rev-list --count --no-merges "${PREV_TAG}..HEAD")
fi
# Filter out noise (merge commits, version bumps)
COMMITS=$(echo "$COMMITS" | grep -v -E "^- (Merge branch|Merge pull request|Bump version|chore\(deps\)|chore\(release\))" || echo "$COMMITS")
# Add truncation note if needed
if [ "$TOTAL" -gt "$MAX_COMMITS" ]; then
COMMITS="$COMMITS
(Showing $MAX_COMMITS most recent of $TOTAL commits)"
echo "::warning::Truncated to $MAX_COMMITS commits (total: $TOTAL)"
fi
# Output commits using heredoc for multiline
echo "commits<<EOF" >> $GITHUB_OUTPUT
echo "$COMMITS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
echo "Collected $(echo "$COMMITS" | wc -l) commits"
- name: Generate release notes with Claude
id: generate
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
VERSION="${{ github.ref_name }}"
COMMITS="${{ steps.commits.outputs.commits }}"
# Check if API key is configured
if [ -z "$ANTHROPIC_API_KEY" ]; then
echo "::warning::ANTHROPIC_API_KEY not configured, using fallback message"
NOTES="Release notes could not be generated automatically (API key not configured). See commit history for changes."
echo "notes<<EOF" >> $GITHUB_OUTPUT
echo "$NOTES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
exit 0
fi
# Build the prompt
PROMPT="Generate concise release notes for version ${VERSION} of MCPProxy (Smart MCP Proxy).
MCPProxy is a smart proxy for AI agents using the Model Context Protocol (MCP). It provides intelligent tool discovery, token savings, and security quarantine for MCP servers. MCPProxy has two editions: Personal (desktop app) and Teams (multi-user server). Note which changes affect which edition if applicable.
Commits since last release:
${COMMITS}
Requirements:
- Maximum 400 words
- Use markdown format
- DO NOT include a title or header like 'MCPProxy vX.X.X Release Notes' - GitHub already shows the version
- Start directly with a brief 1-2 sentence summary of this release
- Group changes into sections (use only sections that have content):
- **New Features** - New functionality (feat: commits)
- **Bug Fixes** - Fixed issues (fix: commits)
- **Breaking Changes** - Changes requiring user action
- **Improvements** - Enhancements to existing features
- Skip internal changes (chore:, docs:, test:, ci: commits) unless significant
- Use bullet points for each change
- Be specific but brief
- If there are no meaningful changes, say 'Minor internal improvements and maintenance updates.'"
# Call Claude API using jq for proper JSON escaping
PAYLOAD=$(jq -n \
--arg model "claude-sonnet-4-5-20250929" \
--argjson max_tokens 1024 \
--arg prompt "$PROMPT" \
'{
model: $model,
max_tokens: $max_tokens,
messages: [{
role: "user",
content: $prompt
}]
}')
echo "Calling Claude API..."
RESPONSE=$(curl -s --max-time 30 \
https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "content-type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d "$PAYLOAD" 2>&1) || {
echo "::warning::Claude API request failed (timeout or network error)"
NOTES="Release notes could not be generated automatically. See commit history for changes."
echo "notes<<EOF" >> $GITHUB_OUTPUT
echo "$NOTES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
exit 0
}
# Check for API errors
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // empty' 2>/dev/null || echo "")
if [ -n "$ERROR_MSG" ]; then
echo "::warning::Claude API error: $ERROR_MSG"
NOTES="Release notes could not be generated automatically. See commit history for changes."
echo "notes<<EOF" >> $GITHUB_OUTPUT
echo "$NOTES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
exit 0
fi
# Extract the generated notes
NOTES=$(echo "$RESPONSE" | jq -r '.content[0].text // empty' 2>/dev/null || echo "")
if [ -z "$NOTES" ]; then
echo "::warning::Failed to extract release notes from API response"
NOTES="Release notes could not be generated automatically. See commit history for changes."
else
echo "✅ Release notes generated successfully"
fi
# Output notes
echo "notes<<EOF" >> $GITHUB_OUTPUT
echo "$NOTES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# Save to file for artifact
NOTES_FILE="RELEASE_NOTES-${VERSION}.md"
echo "$NOTES" > "$NOTES_FILE"
echo "notes_file=$NOTES_FILE" >> $GITHUB_OUTPUT
- name: Upload release notes artifact
uses: actions/upload-artifact@v4
with:
name: release-notes
path: RELEASE_NOTES-${{ github.ref_name }}.md
if-no-files-found: ignore
build:
environment: production
# Only run on version tags
if: startsWith(github.ref, 'refs/tags/v')
strategy:
matrix:
include:
- os: ubuntu-latest
goos: linux
goarch: amd64
cgo: "0"
name: mcpproxy-linux-amd64
archive_format: tar.gz
- os: ubuntu-latest
goos: linux
goarch: arm64
cgo: "0"
name: mcpproxy-linux-arm64
archive_format: tar.gz
- os: windows-latest
goos: windows
goarch: amd64
cgo: "1"
name: mcpproxy-windows-amd64.exe
archive_format: zip
- os: windows-latest
goos: windows
goarch: arm64
cgo: "1"
name: mcpproxy-windows-arm64.exe
archive_format: zip
- os: macos-14
goos: darwin
goarch: amd64
cgo: "1"
name: mcpproxy-darwin-amd64
archive_format: tar.gz
- os: macos-14
goos: darwin
goarch: arm64
cgo: "1"
name: mcpproxy-darwin-arm64
archive_format: tar.gz
# Server edition (Linux only) — uncomment when server MVP is ready
# - os: ubuntu-latest
# goos: linux
# goarch: amd64
# cgo: "0"
# name: mcpproxy-server-linux-amd64
# archive_format: tar.gz
# edition: server
# - os: ubuntu-latest
# goos: linux
# goarch: arm64
# cgo: "0"
# name: mcpproxy-server-linux-arm64
# archive_format: tar.gz
# edition: server
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download release notes artifact
uses: actions/download-artifact@v4
with:
name: release-notes
path: .
continue-on-error: true # Don't fail if notes not yet available
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.25"
- name: Cache Go modules and build
uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
~/AppData/Local/go-build
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-1.25-
- name: Download dependencies
run: go mod download
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install frontend dependencies
run: cd frontend && npm ci
- name: Build frontend
run: cd frontend && npm run build
- name: Copy frontend dist to embed location
shell: bash
run: |
rm -rf web/frontend
mkdir -p web/frontend
cp -r frontend/dist web/frontend/
- name: Import Code-Signing Certificates (macOS)
if: matrix.goos == 'darwin'
run: |
set -euo pipefail
echo "📦 Preparing isolated keychain for code signing"
UNIQUE_ID="${{ matrix.goos }}-${{ matrix.goarch }}-$$-$(date +%s)"
TEMP_KEYCHAIN="mcpproxy-build-${UNIQUE_ID}.keychain"
security create-keychain -p "temp123" "$TEMP_KEYCHAIN"
security list-keychains -s "$TEMP_KEYCHAIN" ~/Library/Keychains/login.keychain-db /Library/Keychains/System.keychain
security unlock-keychain -p "temp123" "$TEMP_KEYCHAIN"
security set-keychain-settings -t 3600 -l "$TEMP_KEYCHAIN"
if [ -z "${{ secrets.APPLE_DEVELOPER_ID_CERT }}" ] || [ -z "${{ secrets.APPLE_DEVELOPER_ID_CERT_PASSWORD }}" ]; then
echo "❌ APPLE_DEVELOPER_ID_CERT and APPLE_DEVELOPER_ID_CERT_PASSWORD secrets are required"
exit 1
fi
echo "${{ secrets.APPLE_DEVELOPER_ID_CERT }}" | base64 -d > developer-id.p12
security import developer-id.p12 \
-k "$TEMP_KEYCHAIN" \
-P "${{ secrets.APPLE_DEVELOPER_ID_CERT_PASSWORD }}" \
-T /usr/bin/codesign \
-T /usr/bin/productbuild \
-T /usr/bin/productsign \
-T /usr/bin/security
rm -f developer-id.p12
echo "🔍 Checking for separate Developer ID Installer certificate"
INSTALLER_ID=$(security find-identity -v -p basic "$TEMP_KEYCHAIN" | grep "Developer ID Installer" || true)
if [ -z "$INSTALLER_ID" ]; then
if [ -z "${{ secrets.APPLE_DEVELOPER_ID_INSTALLER_CERT }}" ] || [ -z "${{ secrets.APPLE_DEVELOPER_ID_INSTALLER_CERT_PASSWORD }}" ]; then
echo "❌ Developer ID Installer identity not found in APPLE_DEVELOPER_ID_CERT"
echo " Provide APPLE_DEVELOPER_ID_INSTALLER_CERT and password secrets"
exit 1
fi
echo "Importing dedicated Developer ID Installer certificate"
echo "${{ secrets.APPLE_DEVELOPER_ID_INSTALLER_CERT }}" | base64 -d > developer-id-installer.p12
security import developer-id-installer.p12 \
-k "$TEMP_KEYCHAIN" \
-P "${{ secrets.APPLE_DEVELOPER_ID_INSTALLER_CERT_PASSWORD }}" \
-T /usr/bin/productsign \
-T /usr/bin/productbuild \
-T /usr/bin/codesign \
-T /usr/bin/security
rm -f developer-id-installer.p12
fi
security set-key-partition-list -S apple-tool:,apple: -s -k "temp123" "$TEMP_KEYCHAIN"
APP_CERT_IDENTITY=$(security find-identity -v -p codesigning "$TEMP_KEYCHAIN" | grep "Developer ID Application" | head -1 | grep -o '"[^"]*"' | tr -d '"')
PKG_CERT_IDENTITY=$(security find-identity -v -p basic "$TEMP_KEYCHAIN" | grep "Developer ID Installer" | head -1 | grep -o '"[^"]*"' | tr -d '"')
if [ -z "$APP_CERT_IDENTITY" ]; then
echo "❌ Developer ID Application identity not found after import"
exit 1
fi
if [ -z "$PKG_CERT_IDENTITY" ]; then
echo "❌ Developer ID Installer identity not found after import"
exit 1
fi
echo "✅ Using Developer ID Application: $APP_CERT_IDENTITY"
echo "✅ Using Developer ID Installer: $PKG_CERT_IDENTITY"
echo "APP_CERT_IDENTITY=$APP_CERT_IDENTITY" >> "$GITHUB_ENV"
echo "PKG_CERT_IDENTITY=$PKG_CERT_IDENTITY" >> "$GITHUB_ENV"
echo "$TEMP_KEYCHAIN" > .keychain_name
echo "=== Available signing identities in temporary keychain ==="
security find-identity -v "$TEMP_KEYCHAIN"
echo "✅ Certificate import completed"
- name: Build binary and create archives
shell: bash
env:
CGO_ENABLED: ${{ matrix.cgo }}
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
# ✅ Force minimum supported macOS version for compatibility
MACOSX_DEPLOYMENT_TARGET: "12.0"
# Defensive CGO flags to ensure proper deployment target
CGO_CFLAGS: "-mmacosx-version-min=12.0"
CGO_LDFLAGS: "-mmacosx-version-min=12.0"
run: |
VERSION=${GITHUB_REF#refs/tags/}
LDFLAGS="-s -w -X main.version=${VERSION} -X github.com/smart-mcp-proxy/mcpproxy-go/internal/httpapi.buildVersion=${VERSION}"
# Determine clean binary name and build flags
EDITION="${{ matrix.edition }}"
BUILD_TAGS=""
if [ "$EDITION" = "server" ]; then
CLEAN_BINARY="mcpproxy-server"
BUILD_TAGS="-tags server"
elif [ "${{ matrix.goos }}" = "windows" ]; then
CLEAN_BINARY="mcpproxy.exe"
else
CLEAN_BINARY="mcpproxy"
fi
# Create clean core binary for archive
go build ${BUILD_TAGS} -ldflags "${LDFLAGS}" -o ${CLEAN_BINARY} ./cmd/mcpproxy
# Build tray binary for platforms with GUI support (macOS and Windows, personal only)
if [ "$EDITION" != "server" ] && { [ "${{ matrix.goos }}" = "darwin" ] || [ "${{ matrix.goos }}" = "windows" ]; }; then
echo "Building mcpproxy-tray for ${{ matrix.goos }}..."
# Determine tray binary name
if [ "${{ matrix.goos }}" = "windows" ]; then
TRAY_BINARY="mcpproxy-tray.exe"
else
TRAY_BINARY="mcpproxy-tray"
fi
go build -ldflags "${LDFLAGS}" -o ${TRAY_BINARY} ./cmd/mcpproxy-tray
fi
# Code sign macOS binaries
if [ "${{ matrix.goos }}" = "darwin" ]; then
echo "Code signing macOS binary..."
# Debug: List all available certificates
echo "Available certificates:"
security find-identity -v -p codesigning
# Find the Developer ID certificate identity
CERT_IDENTITY=$(security find-identity -v -p codesigning | grep "Developer ID Application" | head -1 | grep -o '"[^"]*"' | tr -d '"')
# Verify we found a valid certificate
if [ -n "${CERT_IDENTITY}" ]; then
echo "✅ Found Developer ID certificate: ${CERT_IDENTITY}"
else
echo "❌ No Developer ID certificate found, using team ID as fallback"
CERT_IDENTITY="${{ secrets.APPLE_TEAM_ID }}"
echo "⚠️ Using fallback identity: ${CERT_IDENTITY}"
fi
# Validate entitlements file formatting (Apple's recommendation)
echo "=== Validating entitlements file ==="
if [ -f "scripts/entitlements.plist" ]; then
echo "Validating entitlements formatting with plutil..."
if plutil -lint scripts/entitlements.plist; then
echo "✅ Entitlements file is properly formatted"
else
echo "❌ Entitlements file has formatting issues"
exit 1
fi
# Convert to XML format if needed (Apple's recommendation)
plutil -convert xml1 scripts/entitlements.plist
echo "✅ Entitlements converted to XML format"
else
echo "⚠️ No entitlements file found"
fi
# Sign both binaries with proper Developer ID certificate, hardened runtime, and timestamp
echo "=== Signing binaries with hardened runtime ==="
# Install GNU coreutils for timeout command (macOS compatibility)
if ! command -v timeout &> /dev/null; then
echo "Installing GNU coreutils for timeout command..."
brew install coreutils
# Use gtimeout from coreutils
TIMEOUT_CMD="gtimeout"
else
TIMEOUT_CMD="timeout"
fi
# Sign core binary
echo "Signing core binary: ${CLEAN_BINARY}"
SIGN_SUCCESS=false
for attempt in 1 2 3; do
echo "Core binary signing attempt $attempt/3..."
# Use timeout command to prevent hanging (max 5 minutes per attempt)
if $TIMEOUT_CMD 300 codesign --force \
--options runtime \
--entitlements scripts/entitlements.plist \
--sign "${CERT_IDENTITY}" \
--timestamp \
${CLEAN_BINARY}; then
SIGN_SUCCESS=true
echo "✅ Core binary signing succeeded on attempt $attempt"
break
else
echo "❌ Core binary signing attempt $attempt failed or timed out"
if [ $attempt -lt 3 ]; then
echo "Retrying in 10 seconds..."
sleep 10
fi
fi
done
if [ "$SIGN_SUCCESS" != "true" ]; then
echo "❌ All core binary signing attempts failed"
exit 1
fi
# Sign tray binary
echo "Signing tray binary: mcpproxy-tray"
TRAY_SIGN_SUCCESS=false
for attempt in 1 2 3; do
echo "Tray binary signing attempt $attempt/3..."
# Use timeout command to prevent hanging (max 5 minutes per attempt)
if $TIMEOUT_CMD 300 codesign --force \
--options runtime \
--entitlements scripts/entitlements.plist \
--sign "${CERT_IDENTITY}" \
--timestamp \
mcpproxy-tray; then
TRAY_SIGN_SUCCESS=true
echo "✅ Tray binary signing succeeded on attempt $attempt"
break
else
echo "❌ Tray binary signing attempt $attempt failed or timed out"
if [ $attempt -lt 3 ]; then
echo "Retrying in 10 seconds..."
sleep 10
fi
fi
done
if [ "$TRAY_SIGN_SUCCESS" != "true" ]; then
echo "❌ All tray binary signing attempts failed"
exit 1
fi
# Verify signing, hardened runtime, and timestamp using Apple's recommended methods
echo "=== Verifying binary signatures (Apple's recommended verification) ==="
# Verify core binary
echo "=== Core binary verification ==="
codesign --verify --verbose ${CLEAN_BINARY}
echo "Core binary basic verification: $?"
# Apple's recommended strict verification for notarization
echo "=== Core binary strict verification (matches notarization requirements) ==="
if codesign -vvv --deep --strict ${CLEAN_BINARY}; then
echo "✅ Core binary strict verification PASSED - ready for notarization"
else
echo "❌ Core binary strict verification FAILED - will not pass notarization"
exit 1
fi
# Verify tray binary
echo "=== Tray binary verification ==="
codesign --verify --verbose mcpproxy-tray
echo "Tray binary basic verification: $?"
# Apple's recommended strict verification for notarization
echo "=== Tray binary strict verification (matches notarization requirements) ==="
if codesign -vvv --deep --strict mcpproxy-tray; then
echo "✅ Tray binary strict verification PASSED - ready for notarization"
else
echo "❌ Strict verification FAILED - will not pass notarization"
exit 1
fi
# Check for secure timestamp (Apple's recommended check)
echo "=== Checking for secure timestamp ==="
TIMESTAMP_CHECK=$(codesign -dvv ${CLEAN_BINARY} 2>&1)
if echo "$TIMESTAMP_CHECK" | grep -q "Timestamp="; then
echo "✅ Secure timestamp present:"
echo "$TIMESTAMP_CHECK" | grep "Timestamp="
else
echo "❌ No secure timestamp found"
echo "Full output:"
echo "$TIMESTAMP_CHECK"
fi
# Display detailed signature info
codesign --display --verbose=4 ${CLEAN_BINARY}
# Check entitlements formatting (Apple's recommendation)
echo "=== Checking entitlements formatting ==="
codesign --display --entitlements - ${CLEAN_BINARY} | head -10
# Verify with spctl (Gatekeeper assessment) - expected to fail before notarization
echo "=== Gatekeeper assessment (expected to fail before notarization) ==="
if spctl --assess --verbose ${CLEAN_BINARY}; then
echo "✅ Gatekeeper assessment: PASSED (unexpected but good!)"
else
echo "⚠️ Gatekeeper assessment: REJECTED (expected - binary needs notarization)"
echo "This is normal - the binary will pass after Apple completes notarization"
fi
echo "✅ Binary signed successfully with hardened runtime and timestamp"
fi
# Create archive with version info
ARCHIVE_BASE="mcpproxy-${VERSION#v}-${{ matrix.goos }}-${{ matrix.goarch }}"
LATEST_ARCHIVE_BASE="mcpproxy-latest-${{ matrix.goos }}-${{ matrix.goarch }}"
# Determine files to include in archive
FILES_TO_ARCHIVE="${CLEAN_BINARY}"
# Add tray binary if it exists (Windows and macOS)
if [ "${{ matrix.goos }}" = "windows" ] && [ -f "mcpproxy-tray.exe" ]; then
FILES_TO_ARCHIVE="${FILES_TO_ARCHIVE} mcpproxy-tray.exe"
echo "Including mcpproxy-tray.exe in archive"
elif [ "${{ matrix.goos }}" = "darwin" ] && [ -f "mcpproxy-tray" ]; then
FILES_TO_ARCHIVE="${FILES_TO_ARCHIVE} mcpproxy-tray"
echo "Including mcpproxy-tray in archive"
fi
if [ "${{ matrix.archive_format }}" = "zip" ]; then
# Create ZIP archive (Windows)
# Use PowerShell Compress-Archive on Windows since zip command isn't available
if [ "${{ matrix.goos }}" = "windows" ]; then
# Convert space-separated list to comma-separated for PowerShell
PS_FILES=$(echo ${FILES_TO_ARCHIVE} | sed 's/ /,/g')
powershell -Command "Compress-Archive -Path ${PS_FILES} -DestinationPath '${ARCHIVE_BASE}.zip'"
powershell -Command "Compress-Archive -Path ${PS_FILES} -DestinationPath '${LATEST_ARCHIVE_BASE}.zip'"
else
# Create versioned archive
zip "${ARCHIVE_BASE}.zip" ${FILES_TO_ARCHIVE}
# Create latest archive
zip "${LATEST_ARCHIVE_BASE}.zip" ${FILES_TO_ARCHIVE}
fi
else
# Create versioned archive
tar -czf "${ARCHIVE_BASE}.tar.gz" ${FILES_TO_ARCHIVE}
# Create latest archive
tar -czf "${LATEST_ARCHIVE_BASE}.tar.gz" ${FILES_TO_ARCHIVE}
fi
- name: Install Inno Setup (Windows)
if: matrix.goos == 'windows'
shell: pwsh
run: |
choco install innosetup -y
# Verify installation
if (Test-Path "C:\Program Files (x86)\Inno Setup 6\ISCC.exe") {
Write-Host "✅ Inno Setup installed successfully"
} else {
Write-Host "❌ Inno Setup installation failed"
exit 1
}
- name: Create Windows installer (Windows)
if: matrix.goos == 'windows'
shell: pwsh
run: |
$VERSION = "${{ github.ref_name }}"
$ARCH = "${{ matrix.goarch }}"
Write-Host "Building Windows installer for version ${VERSION} (${ARCH})"
# Run the build script
.\scripts\build-windows-installer.ps1 -Version $VERSION -Arch $ARCH
# Verify installer was created
$INSTALLER_NAME = "mcpproxy-setup-${VERSION}-${ARCH}.exe"
$INSTALLER_PATH = "dist\${INSTALLER_NAME}"
if (Test-Path $INSTALLER_PATH) {
Write-Host "✅ Windows installer created: ${INSTALLER_NAME}"
$size = (Get-Item $INSTALLER_PATH).Length / 1MB
Write-Host " Size: $([math]::Round($size, 2)) MB"
} else {
Write-Host "❌ Windows installer not found at ${INSTALLER_PATH}"
exit 1
}
- name: Upload unsigned Windows installer for signing
if: matrix.goos == 'windows'
uses: actions/upload-artifact@v4
id: upload-unsigned-installer
with:
name: unsigned-installer-windows-${{ matrix.goarch }}
# Upload exe directly - GitHub Actions will ZIP it automatically
path: dist/mcpproxy-setup-${{ github.ref_name }}-${{ matrix.goarch }}.exe
- name: Create .icns icon (macOS)
if: matrix.goos == 'darwin'
run: |
chmod +x scripts/create-icns.sh
./scripts/create-icns.sh
- name: Create DMG installer (macOS)
if: matrix.goos == 'darwin'
env:
# Ensure DMG creation also uses correct deployment target
MACOSX_DEPLOYMENT_TARGET: "12.0"
CGO_CFLAGS: "-mmacosx-version-min=12.0"
CGO_LDFLAGS: "-mmacosx-version-min=12.0"
run: |
VERSION=${GITHUB_REF#refs/tags/}
chmod +x scripts/create-dmg.sh
# Determine binary names
TRAY_BINARY="mcpproxy-tray"
CORE_BINARY="mcpproxy"
# Create DMG with both tray and core binaries
./scripts/create-dmg.sh ${TRAY_BINARY} ${CORE_BINARY} ${VERSION} ${{ matrix.goarch }}
# Sign DMG
DMG_NAME="mcpproxy-${VERSION#v}-darwin-${{ matrix.goarch }}.dmg"
echo "Signing DMG: ${DMG_NAME}"
# Find the Developer ID certificate identity
CERT_IDENTITY=$(security find-identity -v -p codesigning | grep "Developer ID Application" | head -1 | grep -o '"[^"]*"' | tr -d '"')
# Verify we found a valid certificate
if [ -n "${CERT_IDENTITY}" ]; then
echo "✅ Found Developer ID certificate for DMG: ${CERT_IDENTITY}"
else
echo "❌ No Developer ID certificate found for DMG, using team ID as fallback"
CERT_IDENTITY="${{ secrets.APPLE_TEAM_ID }}"
echo "⚠️ Using fallback identity for DMG: ${CERT_IDENTITY}"
fi
# Sign DMG with proper certificate and timestamp
codesign --force \
--sign "${CERT_IDENTITY}" \
--timestamp \
"${DMG_NAME}"
# Verify DMG signing
echo "=== Verifying DMG signature ==="
codesign --verify --verbose "${DMG_NAME}"
echo "DMG verification: $?"
codesign --display --verbose=4 "${DMG_NAME}"
echo "✅ DMG created and signed successfully: ${DMG_NAME}"
- name: Create PKG installer (macOS)
if: matrix.goos == 'darwin'
env:
# Ensure PKG creation also uses correct deployment target
MACOSX_DEPLOYMENT_TARGET: "12.0"
CGO_CFLAGS: "-mmacosx-version-min=12.0"
CGO_LDFLAGS: "-mmacosx-version-min=12.0"
run: |
VERSION=${GITHUB_REF#refs/tags/}
chmod +x scripts/create-pkg.sh
chmod +x scripts/create-installer-dmg.sh
# Set up certificate environment for PKG creation (reuse from binary signing)
echo "=== Setting up certificate environment for PKG creation ==="
# Debug: List all available certificates for PKG creation
echo "=== Available certificates for PKG creation ==="
echo "Codesigning certificates:"
security find-identity -v -p codesigning || echo "No codesigning certificates found"
echo "Basic certificates:"
security find-identity -v -p basic || echo "No basic certificates found"
echo "All certificates:"
security find-identity -v || echo "No certificates found"
# Prefer identities exported during certificate import, fallback to keychain lookup
APP_CERT_IDENTITY="${APP_CERT_IDENTITY:-}"
PKG_CERT_IDENTITY="${PKG_CERT_IDENTITY:-}"
if [ -z "${APP_CERT_IDENTITY}" ]; then
APP_CERT_IDENTITY=$(security find-identity -v -p codesigning | grep "Developer ID Application" | head -1 | grep -o '"[^"]*"' | tr -d '"')
fi
if [ -z "${PKG_CERT_IDENTITY}" ]; then
PKG_CERT_IDENTITY=$(security find-identity -v -p basic | grep "Developer ID Installer" | head -1 | grep -o '"[^"]*"' | tr -d '"')
fi
if [ -z "${APP_CERT_IDENTITY}" ]; then
echo "❌ Developer ID Application certificate not available in the build keychain"
exit 1
fi
if [ -z "${PKG_CERT_IDENTITY}" ]; then
echo "❌ Developer ID Installer certificate not available in the isolated keychain"
echo " Embed the 'Developer ID Installer' identity in APPLE_DEVELOPER_ID_CERT"
exit 1
fi
echo "✅ Using Developer ID Application certificate: ${APP_CERT_IDENTITY}"
echo "✅ Using Developer ID Installer certificate: ${PKG_CERT_IDENTITY}"
export APP_CERT_IDENTITY
export PKG_CERT_IDENTITY
# Determine binary names
TRAY_BINARY="mcpproxy-tray"
CORE_BINARY="mcpproxy"
# Create PKG installer with both tray and core binaries
echo "Creating signed PKG installer with certificate: ${PKG_CERT_IDENTITY}"
./scripts/create-pkg.sh ${TRAY_BINARY} ${CORE_BINARY} ${VERSION} ${{ matrix.goarch }}
# Create installer DMG containing the PKG
PKG_NAME="mcpproxy-${VERSION#v}-darwin-${{ matrix.goarch }}.pkg"
./scripts/create-installer-dmg.sh ${PKG_NAME} ${VERSION} ${{ matrix.goarch }}
echo "✅ PKG installer and installer DMG created successfully"
- name: Submit for notarization (macOS)
if: matrix.goos == 'darwin'
run: |
set -euo pipefail
VERSION=${GITHUB_REF#refs/tags/}
PKG_NAME="mcpproxy-${VERSION#v}-darwin-${{ matrix.goarch }}.pkg"
INSTALLER_DMG_NAME="mcpproxy-${VERSION#v}-darwin-${{ matrix.goarch }}-installer.dmg"
notarize_and_staple() {
local FILE_NAME="$1"
local FILE_LABEL="$2"
if [ ! -f "${FILE_NAME}" ]; then
echo "❌ ${FILE_LABEL} (${FILE_NAME}) not found"
return 1
fi
echo "Submitting ${FILE_LABEL} for notarization: ${FILE_NAME}"
local SUBMISSION_OUTPUT
if ! SUBMISSION_OUTPUT=$(xcrun notarytool submit "${FILE_NAME}" \
--apple-id "${{ secrets.APPLE_ID_USERNAME }}" \
--password "${{ secrets.APPLE_ID_APP_PASSWORD }}" \
--team-id "${{ secrets.APPLE_TEAM_ID }}" \
--wait \
--output-format json 2>&1); then
echo "❌ ${FILE_LABEL} notarization failed"
echo "Output: ${SUBMISSION_OUTPUT}"
return 1
fi
local SUBMISSION_ID
SUBMISSION_ID=$(echo "${SUBMISSION_OUTPUT}" | jq -r '.id // empty')
local STATUS
STATUS=$(echo "${SUBMISSION_OUTPUT}" | jq -r '.status // empty')
if [ -z "${SUBMISSION_ID}" ] || [ "${SUBMISSION_ID}" = "null" ] || [ "${STATUS}" != "Accepted" ]; then
echo "❌ ${FILE_LABEL} notarization did not succeed"
echo "Response: ${SUBMISSION_OUTPUT}"
return 1
fi
echo "✅ ${FILE_LABEL} notarization accepted (ID: ${SUBMISSION_ID})"
echo "${SUBMISSION_ID}" > "${FILE_NAME}.submission_id"
echo "Stapling notarization ticket to ${FILE_LABEL}"
xcrun stapler staple "${FILE_NAME}"
xcrun stapler validate "${FILE_NAME}"
}
notarize_and_staple "${PKG_NAME}" "PKG installer"
notarize_and_staple "${INSTALLER_DMG_NAME}" "Installer DMG"
echo "✅ Notarization and stapling complete"
- name: Cleanup isolated keychain (macOS)
if: matrix.goos == 'darwin' && always()
run: |
# Clean up the isolated keychain we created for this worker
if [ -f .keychain_name ]; then
TEMP_KEYCHAIN=$(cat .keychain_name)
echo "Cleaning up keychain: ${TEMP_KEYCHAIN}"
# Remove from search list and delete
security delete-keychain "$TEMP_KEYCHAIN" 2>/dev/null || echo "Keychain already cleaned up"
rm -f .keychain_name
echo "✅ Keychain cleanup completed"
else
echo "No keychain to clean up"
fi
- name: Upload versioned archive artifact
uses: actions/upload-artifact@v4
with:
name: versioned-${{ matrix.edition || 'personal' }}-${{ matrix.goos }}-${{ matrix.goarch }}
path: mcpproxy-*-${{ matrix.goos }}-${{ matrix.goarch }}.${{ matrix.archive_format }}
- name: Upload latest archive artifact
uses: actions/upload-artifact@v4
with:
name: latest-${{ matrix.edition || 'personal' }}-${{ matrix.goos }}-${{ matrix.goarch }}
path: mcpproxy-latest-${{ matrix.goos }}-${{ matrix.goarch }}.${{ matrix.archive_format }}
- name: Upload macOS installer DMG
if: matrix.goos == 'darwin'
run: |
set -euo pipefail
VERSION=${GITHUB_REF#refs/tags/}
INSTALLER_DMG_NAME="mcpproxy-${VERSION#v}-darwin-${{ matrix.goarch }}-installer.dmg"
echo "Looking for files:"
echo " Installer DMG: ${INSTALLER_DMG_NAME}"
if [ ! -f "${INSTALLER_DMG_NAME}" ]; then
echo "❌ Installer DMG not found: ${INSTALLER_DMG_NAME}"
exit 1
fi
mkdir -p installers-artifact
cp "${INSTALLER_DMG_NAME}" installers-artifact/
SUBMISSION_ID_FILE="${INSTALLER_DMG_NAME}.submission_id"
if [ -f "${SUBMISSION_ID_FILE}" ]; then
echo "✅ Found submission ID file: ${SUBMISSION_ID_FILE}"
cp "${SUBMISSION_ID_FILE}" installers-artifact/
else
echo "⚠️ No submission ID file found: ${SUBMISSION_ID_FILE}"
fi
echo "Files to upload:"
ls -la installers-artifact/
- name: Upload macOS installers artifact
if: matrix.goos == 'darwin'
uses: actions/upload-artifact@v4
with:
name: installers-${{ matrix.goos }}-${{ matrix.goarch }}
path: installers-artifact/*
# Sign Windows installers with SignPath
sign-windows:
needs: build
runs-on: ubuntu-latest
environment: production
if: startsWith(github.ref, 'refs/tags/v')
strategy:
matrix:
arch: [amd64, arm64]
steps:
- name: Download unsigned installer
uses: actions/download-artifact@v4
with:
name: unsigned-installer-windows-${{ matrix.arch }}
path: unsigned
- name: Re-upload for SignPath
id: reupload
uses: actions/upload-artifact@v4
with:
name: signpath-input-windows-${{ matrix.arch }}
# Upload exe - GitHub Actions will ZIP it for SignPath
path: unsigned/*.exe
- name: Submit to SignPath for signing
uses: signpath/github-action-submit-signing-request@v1
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
organization-id: '84efd51b-c11c-4a85-82e6-7c3b1157d7ca'
project-slug: 'mcpproxy-go'
signing-policy-slug: 'release-signing'
artifact-configuration-slug: 'initial'
github-artifact-id: '${{ steps.reupload.outputs.artifact-id }}'
wait-for-completion: true
wait-for-completion-timeout-in-seconds: 3600
output-artifact-directory: signed
- name: Prepare signed installer
run: |
VERSION=${GITHUB_REF#refs/tags/}
ARCH="${{ matrix.arch }}"
TARGET="mcpproxy-setup-${VERSION}-${ARCH}.exe"
echo "Preparing signed installer..."
cd signed
ls -la
# SignPath returns signed exe - verify it exists
if [ -f "$TARGET" ]; then
echo "✅ Signed installer already named correctly: $TARGET"
else
# Find and rename if needed
SIGNED_EXE=$(find . -name "*.exe" -type f | head -1)
if [ -n "$SIGNED_EXE" ]; then
mv "$SIGNED_EXE" "$TARGET"
echo "✅ Renamed to: $TARGET"
else
echo "❌ No signed .exe found in SignPath output"
exit 1
fi
fi
- name: Upload signed Windows installer
uses: actions/upload-artifact@v4
with:
name: installer-windows-${{ matrix.arch }}
path: signed/mcpproxy-setup-${{ github.ref_name }}-${{ matrix.arch }}.exe
build-docker:
runs-on: ubuntu-latest
needs: [build]
# Disabled until server MVP is ready — uncomment to enable
if: false && startsWith(github.ref, 'refs/tags/v')
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3