-
Notifications
You must be signed in to change notification settings - Fork 3
883 lines (756 loc) Β· 40.9 KB
/
Copy pathrelease.yml
File metadata and controls
883 lines (756 loc) Β· 40.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
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
name: Production Release (Streamlined)
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
version:
description: 'Release version (e.g., v1.2.3)'
required: true
type: string
prerelease:
description: 'Mark as pre-release'
required: false
default: false
type: boolean
skip_tests:
description: 'Skip test validation (emergency use only)'
required: false
default: false
type: boolean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DEVELOPER_ID_CERTIFICATE: ${{ secrets.DEVELOPER_ID_CERTIFICATE }}
DEVELOPER_ID_CERTIFICATE_PASSWORD: ${{ secrets.DEVELOPER_ID_CERTIFICATE_PASSWORD }}
NOTARIZATION_USERNAME: ${{ secrets.NOTARIZATION_USERNAME }}
NOTARIZATION_PASSWORD: ${{ secrets.NOTARIZATION_PASSWORD }}
# Base64-encoded .provisionprofile carrying the DriverKit managed capabilities.
# Without it the DriverKit entitlements are signed in but inert β see
# Documentation/development/entitlement-validation.md
DRIVERKIT_PROVISIONING_PROFILE: ${{ secrets.DRIVERKIT_PROVISIONING_PROFILE }}
jobs:
release-validation:
name: Release Validation
runs-on: macos-latest
outputs:
version: ${{ steps.extract-version.outputs.version }}
is-prerelease: ${{ steps.extract-version.outputs.prerelease }}
release-type: ${{ steps.extract-version.outputs.release-type }}
steps:
- name: π·οΈ Starting Release Validation
run: |
echo "::notice title=Release Validation::Starting streamlined release validation and version extraction"
echo "π·οΈ This stage validates release triggers and extracts necessary metadata"
echo "π Status: STARTING"
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: π Extract Version Information
id: extract-version
run: |
echo "::group::Version Extraction"
echo "::notice title=Version Processing::Extracting and validating version information"
if [ "${{ github.event_name }}" = "push" ]; then
VERSION="${{ github.ref_name }}"
echo "π Tag-triggered release: $VERSION"
PRERELEASE=$(echo "$VERSION" | grep -E '-(alpha|beta|rc)' >/dev/null && echo "true" || echo "false")
RELEASE_TYPE="automated"
else
VERSION="${{ github.event.inputs.version }}"
PRERELEASE="${{ github.event.inputs.prerelease }}"
RELEASE_TYPE="manual"
echo "π§ Manual release dispatch: $VERSION"
fi
# Validate semantic versioning
if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then
echo "::error title=Invalid Version::Version must follow semantic versioning (vX.Y.Z or vX.Y.Z-suffix)"
exit 1
fi
echo "β
Version validated: $VERSION"
echo "π·οΈ Pre-release: $PRERELEASE"
echo "π§ Release type: $RELEASE_TYPE"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "prerelease=$PRERELEASE" >> $GITHUB_OUTPUT
echo "release-type=$RELEASE_TYPE" >> $GITHUB_OUTPUT
echo "::endgroup::"
- name: π Release Validation Summary
run: |
echo "::group::Release Validation Summary"
echo "::notice title=Release Validated::Release version and trigger validated successfully"
echo "β
Release trigger: ${{ github.event_name }}"
echo "β
Version: ${{ steps.extract-version.outputs.version }}"
echo "β
Pre-release: ${{ steps.extract-version.outputs.prerelease }}"
echo "β
Release type: ${{ steps.extract-version.outputs.release-type }}"
echo "π Status: VALIDATED"
echo "::endgroup::"
ci-validation:
name: CI Validation
needs: release-validation
if: ${{ !github.event.inputs.skip_tests || github.event.inputs.skip_tests == 'false' }}
uses: ./.github/workflows/ci.yml
with:
release_validation: true
test_environment: 'ci'
enable_qemu_tests: false
secrets: inherit
build-artifacts:
name: Build Release Artifacts
runs-on: macos-latest
needs: [release-validation, ci-validation]
if: always() && needs.release-validation.result == 'success' && (needs.ci-validation.result == 'success' || github.event.inputs.skip_tests == 'true')
outputs:
artifact-paths: ${{ steps.build-artifacts.outputs.paths }}
checksums: ${{ steps.generate-checksums.outputs.checksums }}
steps:
- name: π¦ Starting Artifact Building
run: |
echo "::notice title=Artifact Building::Starting streamlined release artifact building and packaging"
echo "π¦ This stage builds and packages release artifacts with code signing"
echo "π Status: STARTING"
- name: Checkout code
uses: actions/checkout@v4
- name: π§ Setup Swift Environment
uses: ./.github/actions/setup-swift-environment
with:
cache-key-suffix: '-release'
install-swiftlint: 'false'
setup-test-scripts: 'false'
validate-environment: 'false'
- name: π Setup Code Signing
if: env.DEVELOPER_ID_CERTIFICATE != ''
run: |
echo "::group::Code Signing Setup"
echo "::notice title=Code Signing::Setting up Apple Developer certificates for release signing"
# Import Developer ID certificate
echo "$DEVELOPER_ID_CERTIFICATE" | base64 --decode > certificate.p12
security create-keychain -p "" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "" build.keychain
security import certificate.p12 -k build.keychain -P "$DEVELOPER_ID_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k "" build.keychain
rm certificate.p12
echo "::notice title=Signing Ready::Code signing environment configured"
echo "β
Code signing ready"
echo "::endgroup::"
- name: ποΈ Build Release Artifacts
id: build-artifacts
run: |
echo "::group::Release Artifact Building"
echo "::notice title=Artifact Building::Building optimized release artifacts"
VERSION="${{ needs.release-validation.outputs.version }}"
ARTIFACTS_DIR="release-artifacts"
mkdir -p "$ARTIFACTS_DIR"
echo "ποΈ Building release artifacts for $VERSION..."
# Build optimized release binaries.
#
# usbipd is built for both architectures and then split, rather than shipped
# as one universal binary. Releases through v0.5.1 were arm64 only, because
# this ran a plain `swift build` on an Apple silicon runner, so Intel Macs got
# "Bad CPU type in executable" (#32). A universal binary fixes that but doubles
# the download for everyone; splitting gives each user one ~5.4 MB artifact,
# slightly smaller than the arm64-only build was.
swift build --configuration release --arch arm64 --arch x86_64 --product usbipd
UNIVERSAL="$(swift build --configuration release --arch arm64 --arch x86_64 --product usbipd --show-bin-path 2>/dev/null)/usbipd"
if [ ! -f "$UNIVERSAL" ]; then
UNIVERSAL=".build/apple/Products/Release/usbipd"
fi
if [ ! -f "$UNIVERSAL" ]; then
echo "::error title=Build Missing::No universal usbipd binary was produced"
exit 1
fi
echo "π Universal binary: $(lipo -archs "$UNIVERSAL")"
# The rest of the build stays host-architecture: only the CLI is distributed
# to end users, and the system extension is inert (see the formula).
swift build --configuration release
echo "ποΈ Building system extension bundle..."
swift build --configuration release --product USBIPDSystemExtension
# Split into per-architecture artifacts. lipo -thin drops the code signature,
# so each slice is signed separately further down β signing before this point
# would be wasted.
USBIPD_ARM64="$ARTIFACTS_DIR/usbipd-$VERSION-macos-arm64"
USBIPD_X86_64="$ARTIFACTS_DIR/usbipd-$VERSION-macos-x86_64"
lipo "$UNIVERSAL" -thin arm64 -output "$USBIPD_ARM64"
lipo "$UNIVERSAL" -thin x86_64 -output "$USBIPD_X86_64"
for slice in "$USBIPD_ARM64:arm64" "$USBIPD_X86_64:x86_64"; do
path="${slice%:*}"; want="${slice##*:}"
got=$(lipo -archs "$path")
if [ "$got" != "$want" ]; then
echo "::error title=Wrong Architecture::$path is $got, expected $want"
exit 1
fi
echo " $(basename "$path"): $got, $(stat -f%z "$path") bytes"
done
# Kept so existing references and the checksums glob still see a usbipd binary.
USBIPD_PATH="$ARTIFACTS_DIR/usbipd-$VERSION-macos"
cp "$USBIPD_ARM64" "$USBIPD_PATH"
# Package system extension bundle
echo "π¦ Packaging system extension bundle..."
BUNDLE_PATH=$(find .build/release -name "*.systemextension" -type d | head -1)
if [ -n "$BUNDLE_PATH" ]; then
echo "Found system extension bundle at: $BUNDLE_PATH"
cp -R "$BUNDLE_PATH" "$ARTIFACTS_DIR/USBIPDSystemExtension.systemextension"
else
echo "Creating system extension bundle structure..."
mkdir -p "$ARTIFACTS_DIR/USBIPDSystemExtension.systemextension/Contents/MacOS"
cp .build/release/USBIPDSystemExtension "$ARTIFACTS_DIR/USBIPDSystemExtension.systemextension/Contents/MacOS/"
# Create Info.plist for system extension
cat > "$ARTIFACTS_DIR/USBIPDSystemExtension.systemextension/Contents/Info.plist" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>USBIPDSystemExtension</string>
<key>CFBundleIdentifier</key>
<string>com.github.usbipd-mac.systemextension</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>USBIPD System Extension</string>
<key>CFBundlePackageType</key>
<string>SYSX</string>
<key>CFBundleShortVersionString</key>
<string>$VERSION</string>
<key>CFBundleVersion</key>
<string>$VERSION</string>
<key>NSSystemExtensionUsageDescription</key>
<string>Enables USB device sharing over IP networks</string>
</dict>
</plist>
EOF
fi
# Copy entitlements files for code signing
echo "π Copying entitlements..."
cp Sources/SystemExtension/SystemExtension.entitlements "$ARTIFACTS_DIR/"
cp usbipd.entitlements "$ARTIFACTS_DIR/"
# Embed the provisioning profile into the system extension bundle.
#
# The DriverKit entitlements in SystemExtension.entitlements are managed
# capabilities: codesign will happily embed the keys, but the kernel only
# honours them when the same keys appear in an Apple-issued provisioning
# profile shipped inside the bundle. Without this the entitlements are inert,
# which is indistinguishable at signing time from them working.
if [ -n "$DRIVERKIT_PROVISIONING_PROFILE" ]; then
echo "π Embedding provisioning profile..."
if [ -d "$ARTIFACTS_DIR/USBIPDSystemExtension.systemextension" ]; then
echo "$DRIVERKIT_PROVISIONING_PROFILE" | base64 --decode \
> "$ARTIFACTS_DIR/USBIPDSystemExtension.systemextension/Contents/embedded.provisionprofile"
echo "β
Provisioning profile embedded"
else
echo "::warning::DRIVERKIT_PROVISIONING_PROFILE set but no system extension bundle to embed it in"
fi
else
echo "::warning::No DRIVERKIT_PROVISIONING_PROFILE secret configured. The DriverKit entitlements will be signed into the bundle but will not take effect, so device claiming will fail at runtime. See Documentation/development/entitlement-validation.md"
fi
# Package QEMU test server if available
if [ -f .build/release/QEMUTestServer ]; then
QEMU_SERVER_PATH="$ARTIFACTS_DIR/QEMUTestServer-$VERSION-macos"
cp .build/release/QEMUTestServer "$QEMU_SERVER_PATH"
fi
# Code sign binaries if certificates available
if [ -n "$DEVELOPER_ID_CERTIFICATE" ]; then
echo "π Code signing release artifacts..."
# usbipd carries com.apple.developer.system-extension.install, which is a
# RESTRICTED entitlement: AMFI kills a binary that claims it without a
# provisioning profile authorising it. Measured, not assumed β a binary
# signed with just that key and no profile exits 137 on launch.
#
# So the entitlements are only applied when a profile is present. Without
# one, sign exactly as before: hardened runtime for notarization, no
# entitlements, and a daemon that actually starts.
# Every usbipd artifact is signed, including both architecture slices:
# lipo -thin strips the signature, so an unsigned slice would ship.
for target in "$USBIPD_PATH" "$USBIPD_ARM64" "$USBIPD_X86_64"; do
[ -f "$target" ] || continue
if [ -n "$DRIVERKIT_PROVISIONING_PROFILE" ]; then
echo " signing $(basename "$target") with entitlements (provisioning profile present)"
codesign --sign "Developer ID Application" --entitlements "$ARTIFACTS_DIR/usbipd.entitlements" --options runtime --timestamp "$target" || echo "::warning::Code signing failed for $(basename "$target")"
else
echo " signing $(basename "$target") without entitlements (no provisioning profile)"
codesign --sign "Developer ID Application" --options runtime --timestamp "$target" || echo "::warning::Code signing failed for $(basename "$target")"
fi
done
if [ -z "$DRIVERKIT_PROVISIONING_PROFILE" ]; then
echo "::warning::usbipd signed without entitlements. It cannot install the System Extension, but it will launch. Set DRIVERKIT_PROVISIONING_PROFILE to enable them."
fi
# Sign system extension bundle
if [ -d "$ARTIFACTS_DIR/USBIPDSystemExtension.systemextension" ]; then
echo "π Code signing system extension bundle..."
codesign --sign "Developer ID Application" --entitlements "$ARTIFACTS_DIR/SystemExtension.entitlements" --options runtime --timestamp "$ARTIFACTS_DIR/USBIPDSystemExtension.systemextension" || echo "::warning::Code signing failed for system extension"
fi
if [ -f "$QEMU_SERVER_PATH" ]; then
codesign --sign "Developer ID Application" --timestamp "$QEMU_SERVER_PATH" || echo "::warning::Code signing failed for QEMUTestServer"
fi
echo "β
Code signing completed"
# Read the entitlements back out of the signatures rather than trusting
# what we passed in. A managed capability that was silently dropped looks
# identical to a successful signing run without this check.
echo "π Verifying embedded entitlements..."
codesign -d --entitlements - --xml "$USBIPD_PATH" 2>/dev/null || echo "::warning::Could not read back usbipd entitlements"
if [ -d "$ARTIFACTS_DIR/USBIPDSystemExtension.systemextension" ]; then
SYSEXT_ENTITLEMENTS=$(codesign -d --entitlements - --xml "$ARTIFACTS_DIR/USBIPDSystemExtension.systemextension" 2>/dev/null || echo "")
echo "$SYSEXT_ENTITLEMENTS"
if ! echo "$SYSEXT_ENTITLEMENTS" | grep -q "com.apple.developer.driverkit.transport.usb"; then
echo "::warning::com.apple.developer.driverkit.transport.usb is not present in the signed system extension. Device claiming will not work."
fi
fi
# Notarize system extension bundle if notarization credentials available
if [ -n "$NOTARIZATION_USERNAME" ] && [ -n "$NOTARIZATION_PASSWORD" ] && [ -d "$ARTIFACTS_DIR/USBIPDSystemExtension.systemextension" ]; then
echo "π Notarizing system extension bundle..."
# Create zip for notarization (required format)
ditto -c -k --keepParent "$ARTIFACTS_DIR/USBIPDSystemExtension.systemextension" "$ARTIFACTS_DIR/USBIPDSystemExtension_notarization.zip"
# Submit for notarization with timeout
echo "π€ Submitting system extension for notarization..."
xcrun notarytool submit "$ARTIFACTS_DIR/USBIPDSystemExtension_notarization.zip" \
--apple-id "$NOTARIZATION_USERNAME" \
--password "$NOTARIZATION_PASSWORD" \
--team-id "592A3U6J26" \
--wait --timeout 10m || {
echo "::warning::System extension notarization failed or timed out"
echo "::warning::This may prevent proper System Extension installation"
}
# Staple the notarization if successful
if xcrun stapler staple "$ARTIFACTS_DIR/USBIPDSystemExtension.systemextension" 2>/dev/null; then
echo "β
System extension notarization completed and stapled"
else
echo "::warning::Failed to staple notarization to system extension"
fi
# Clean up notarization zip
rm -f "$ARTIFACTS_DIR/USBIPDSystemExtension_notarization.zip"
else
echo "::warning::Notarization credentials not available - System Extension will not be notarized"
echo "::warning::This will prevent proper System Extension installation on user machines"
fi
else
echo "::warning::No code signing certificates available - binaries will be unsigned"
fi
# Create compressed system extension bundle for release
if [ -d "$ARTIFACTS_DIR/USBIPDSystemExtension.systemextension" ]; then
echo "π¦ Creating compressed system extension bundle..."
tar -czf "$ARTIFACTS_DIR/USBIPDSystemExtension.systemextension.tar.gz" -C "$ARTIFACTS_DIR" USBIPDSystemExtension.systemextension
# Remove the directory after creating the tar.gz to avoid upload conflicts
rm -rf "$ARTIFACTS_DIR/USBIPDSystemExtension.systemextension"
fi
# Create archive with all non-tar.gz files
ARCHIVE_PATH="$ARTIFACTS_DIR/usbipd-mac-$VERSION.tar.gz"
# Include all files except other .tar.gz files
find "$ARTIFACTS_DIR" -maxdepth 1 -type f ! -name "*.tar.gz" -exec basename {} \; | \
xargs -I {} tar -czf "$ARCHIVE_PATH" -C "$ARTIFACTS_DIR" {}
# Output artifact paths
PATHS="$USBIPD_PATH"
if [ -f "$QEMU_SERVER_PATH" ]; then
PATHS="$PATHS,$QEMU_SERVER_PATH"
fi
if [ -f "$ARTIFACTS_DIR/USBIPDSystemExtension.systemextension.tar.gz" ]; then
PATHS="$PATHS,$ARTIFACTS_DIR/USBIPDSystemExtension.systemextension.tar.gz"
fi
PATHS="$PATHS,$ARCHIVE_PATH"
echo "paths=$PATHS" >> $GITHUB_OUTPUT
echo "::notice title=Artifacts Built::Release artifacts built successfully"
echo "β
Artifacts: $PATHS"
echo "::endgroup::"
- name: π Generate Checksums
id: generate-checksums
run: |
echo "::group::Checksum Generation"
echo "::notice title=Checksums::Generating SHA256 checksums for release artifacts"
cd release-artifacts
CHECKSUMS_FILE="checksums-${{ needs.release-validation.outputs.version }}.sha256"
echo "π Generating checksums..."
# Only checksum files, not directories
# The binaries are named usbipd-vX.Y.Z-macos, so they end in "-macos" and the
# old "*.macos" glob never matched one. checksums-v0.5.0.sha256 shipped
# covering only the tarballs, with no hash for the binary anyone downloads.
find . -maxdepth 1 -type f \( -name "*-macos" -o -name "*.tar.gz" \) -exec shasum -a 256 {} \; > "$CHECKSUMS_FILE"
if ! grep -q -- "-macos" "$CHECKSUMS_FILE"; then
echo "::error title=Checksums Incomplete::No binary checksum was recorded"
exit 1
fi
# Extract specific checksums for metadata generation
CLI_SHA256=$(shasum -a 256 usbipd-${{ needs.release-validation.outputs.version }}-macos | cut -d' ' -f1)
echo "CLI_SHA256=$CLI_SHA256" >> $GITHUB_ENV
if [ -f "USBIPDSystemExtension.systemextension.tar.gz" ]; then
SYSEXT_SHA256=$(shasum -a 256 USBIPDSystemExtension.systemextension.tar.gz | cut -d' ' -f1)
echo "SYSEXT_SHA256=$SYSEXT_SHA256" >> $GITHUB_ENV
echo "β
System extension checksum: ${SYSEXT_SHA256:0:16}..."
else
echo "SYSEXT_SHA256=" >> $GITHUB_ENV
echo "β οΈ System extension bundle not found"
fi
echo "::notice title=Checksums Generated::SHA256 checksums created for all artifacts"
echo "β
Checksums saved to $CHECKSUMS_FILE"
echo "β
CLI binary checksum: ${CLI_SHA256:0:16}..."
echo "checksums=$CHECKSUMS_FILE" >> $GITHUB_OUTPUT
echo "::endgroup::"
- name: π€ Upload Release Artifacts
uses: actions/upload-artifact@v4
with:
name: release-artifacts-${{ needs.release-validation.outputs.version }}
path: release-artifacts/
retention-days: 30
generate-homebrew-metadata:
name: Generate Homebrew Metadata
runs-on: macos-latest
needs: [release-validation, build-artifacts]
outputs:
metadata-file: ${{ steps.generate-metadata.outputs.metadata-file }}
steps:
- name: πΊ Starting Homebrew Metadata Generation
run: |
echo "::notice title=Homebrew Metadata::Starting automated homebrew metadata generation"
echo "πΊ This stage generates structured metadata for tap repository consumption"
echo "π Status: STARTING"
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: π€ Download Release Artifacts
uses: actions/download-artifact@v4
with:
name: release-artifacts-${{ needs.release-validation.outputs.version }}
path: release-artifacts/
- name: π§ Setup Metadata Generation Environment
run: |
echo "::group::Environment Setup"
echo "::notice title=Environment::Setting up metadata generation environment"
# Install jq if not available
if ! command -v jq >/dev/null 2>&1; then
echo "π¦ Installing jq for JSON processing..."
brew install jq
fi
echo "β
Environment ready for metadata generation"
echo "::endgroup::"
- name: π’ Calculate Binary SHA256
id: calculate-sha256
run: |
echo "::group::SHA256 Calculation"
echo "::notice title=SHA256::Calculating release binary checksum for metadata"
VERSION="${{ needs.release-validation.outputs.version }}"
BINARY_URL="https://github.com/beriberikix/usbipd-mac/releases/download/$VERSION/usbipd-$VERSION-macos"
echo "π Downloading and calculating SHA256 for release binary..."
echo " URL: $BINARY_URL"
# Download and calculate SHA256 of the release binary from artifacts
# Use local binary from artifacts instead of downloading from GitHub
BINARY_FILE="release-artifacts/usbipd-$VERSION-macos"
if [ ! -f "$BINARY_FILE" ]; then
echo "::error title=Binary Not Found::Release binary not found in artifacts: $BINARY_FILE"
exit 1
fi
SHA256_CHECKSUM=$(shasum -a 256 "$BINARY_FILE" | cut -d' ' -f1)
if [[ ! "$SHA256_CHECKSUM" =~ ^[a-fA-F0-9]{64}$ ]]; then
echo "::error title=Invalid Checksum::Generated checksum format is invalid"
exit 1
fi
echo "β
SHA256 calculated: $SHA256_CHECKSUM"
echo " This checksum will be used for binary validation in tap repository"
echo "sha256=$SHA256_CHECKSUM" >> $GITHUB_OUTPUT
echo "::endgroup::"
- name: πΊ Generate Homebrew Metadata
id: generate-metadata
run: |
echo "::group::Metadata Generation"
echo "::notice title=Metadata Generation::Generating structured homebrew metadata JSON"
VERSION="${{ needs.release-validation.outputs.version }}"
SHA256_CHECKSUM="${{ steps.calculate-sha256.outputs.sha256 }}"
ARCHIVE_URL="https://github.com/beriberikix/usbipd-mac/archive/$VERSION.tar.gz"
echo "πΊ Generating metadata..."
echo " Version: $VERSION"
echo " SHA256: $SHA256_CHECKSUM"
echo " Archive URL: $ARCHIVE_URL"
# Create enhanced metadata with system extension support
mkdir -p .build/homebrew-metadata
METADATA_FILE=".build/homebrew-metadata/homebrew-metadata.json"
# Get release notes from latest commit
RELEASE_NOTES=$(git log -1 --pretty=format:"%s")
cat > "$METADATA_FILE" << EOF
{
"schema_version": "1.0",
"metadata": {
"version": "$VERSION",
"archive_url": "$ARCHIVE_URL",
"binary_url": "https://github.com/beriberikix/usbipd-mac/releases/download/$VERSION/usbipd-$VERSION-macos",
"sha256": "$SHA256_CHECKSUM",
"release_notes": "$RELEASE_NOTES",
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"generator": "usbipd-mac homebrew metadata generator v2.0"
EOF
# Add system extension metadata if available
if [ -n "${SYSEXT_SHA256:-}" ] && [ "$SYSEXT_SHA256" != "" ]; then
cat >> "$METADATA_FILE" << EOF
,
"systemextension_url": "https://github.com/beriberikix/usbipd-mac/releases/download/$VERSION/USBIPDSystemExtension.systemextension.tar.gz",
"systemextension_sha256": "$SYSEXT_SHA256"
EOF
echo "β
System extension metadata included"
else
echo "β οΈ System extension metadata not available"
fi
cat >> "$METADATA_FILE" << EOF
},
"formula_updates": {
"version_placeholder": "{{VERSION}}",
"sha256_placeholder": "{{SHA256}}",
"url_pattern": "releases/download/{{VERSION}}/usbipd-{{VERSION}}-macos"
EOF
# Add system extension formula updates if available
if [ -n "${SYSEXT_SHA256:-}" ] && [ "$SYSEXT_SHA256" != "" ]; then
cat >> "$METADATA_FILE" << EOF
,
"sysext_sha256_placeholder": "{{SYSEXT_SHA256}}",
"sysext_url_pattern": "releases/download/{{VERSION}}/USBIPDSystemExtension.systemextension.tar.gz"
EOF
fi
cat >> "$METADATA_FILE" << EOF
}
}
EOF
# Verify metadata was generated
if [ ! -f "$METADATA_FILE" ]; then
echo "::error title=Metadata Missing::Generated metadata file not found"
exit 1
fi
echo "::notice title=Metadata Generated::Homebrew metadata generated successfully"
echo "β
Metadata generated: $METADATA_FILE"
echo "metadata-file=$METADATA_FILE" >> $GITHUB_OUTPUT
echo "::endgroup::"
- name: π Validate Generated Metadata
run: |
echo "::group::Metadata Validation"
echo "::notice title=Metadata Validation::Validating generated metadata against schema"
METADATA_FILE="${{ steps.generate-metadata.outputs.metadata-file }}"
echo "π Validating metadata..."
echo " File: $METADATA_FILE"
# Validate metadata using the validation script
./Scripts/validate-homebrew-metadata.sh \
--file "$METADATA_FILE" \
--skip-network \
--skip-checksum \
--verbose
echo "::notice title=Metadata Validated::Metadata validation completed successfully"
echo "β
Metadata validation passed"
echo "::endgroup::"
- name: π Metadata Summary
run: |
echo "::group::Metadata Summary"
echo "::notice title=Metadata Summary::Displaying generated metadata summary"
METADATA_FILE="${{ steps.generate-metadata.outputs.metadata-file }}"
echo "π Generated Metadata Summary:"
echo " β’ Schema Version: $(jq -r '.schema_version' "$METADATA_FILE")"
echo " β’ Version: $(jq -r '.metadata.version' "$METADATA_FILE")"
echo " β’ Archive URL: $(jq -r '.metadata.archive_url' "$METADATA_FILE")"
echo " β’ SHA256: $(jq -r '.metadata.sha256' "$METADATA_FILE" | head -c 16)..."
echo " β’ Timestamp: $(jq -r '.metadata.timestamp' "$METADATA_FILE")"
echo " β’ File Size: $(wc -c < "$METADATA_FILE") bytes"
echo "β
Metadata ready for tap repository consumption"
echo "::endgroup::"
- name: π€ Upload Metadata as Artifact
uses: actions/upload-artifact@v4
with:
name: homebrew-metadata-${{ needs.release-validation.outputs.version }}
path: ${{ steps.generate-metadata.outputs.metadata-file }}
retention-days: 90
create-release:
name: Create GitHub Release
runs-on: macos-latest
needs: [release-validation, build-artifacts, generate-homebrew-metadata]
steps:
- name: π Starting Release Creation
run: |
echo "::notice title=Release Creation::Creating GitHub release with artifacts"
echo "π This stage creates the GitHub release and uploads artifacts"
echo "π Status: STARTING"
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: π€ Download Release Artifacts
uses: actions/download-artifact@v4
with:
name: release-artifacts-${{ needs.release-validation.outputs.version }}
path: release-artifacts/
- name: π₯ Download Homebrew Metadata
uses: actions/download-artifact@v4
with:
name: homebrew-metadata-${{ needs.release-validation.outputs.version }}
path: homebrew-metadata/
- name: π Generate Release Notes
id: release-notes
run: |
echo "::group::Release Notes Generation"
echo "::notice title=Release Notes::Generating release notes from commit history"
VERSION="${{ needs.release-validation.outputs.version }}"
PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
echo "π Generating release notes for $VERSION..."
if [ -n "$PREVIOUS_TAG" ]; then
echo "π Changes since $PREVIOUS_TAG:"
CHANGELOG=$(git log --pretty=format:"- %s" $PREVIOUS_TAG..HEAD | head -20)
else
echo "π Initial release changes:"
CHANGELOG=$(git log --pretty=format:"- %s" HEAD | head -20)
fi
# Create release notes
cat > release-notes.md << EOF
# Release $VERSION
## What's Changed
$CHANGELOG
## Artifacts
This release includes the following artifacts:
- **usbipd**: Main USB/IP daemon executable for macOS
- **USBIPDSystemExtension.systemextension.tar.gz**: System Extension bundle for advanced USB device access
- **QEMUTestServer**: QEMU integration test server (if available)
- **Archive**: Complete packaged release (tar.gz)
- **Checksums**: SHA256 verification checksums
## Installation
**Homebrew (Recommended):**
\`\`\`bash
brew tap beriberikix/usbipd-mac
brew install usbip
sudo usbipd install-system-extension
sudo brew services start usbip
\`\`\`
**Manual Installation:**
1. Download \`usbipd-$VERSION-macos\` and \`USBIPDSystemExtension.systemextension.tar.gz\`
2. Make the binary executable: \`chmod +x usbipd-$VERSION-macos\`
3. Extract and install the system extension bundle
4. Install the system extension: \`sudo ./usbipd-$VERSION-macos install-system-extension\`
## Requirements
- macOS 11.0 or later
- Administrator privileges for USB device access
- System extension approval in System Preferences
## Notes
- Binaries are code-signed with Apple Developer ID when available
- System extension requires user approval on first installation
- Full functionality requires both the CLI binary and system extension
EOF
echo "::notice title=Release Notes Ready::Release notes generated successfully"
echo "β
Release notes prepared"
echo "::endgroup::"
- name: π Create GitHub Release
run: |
echo "::group::GitHub Release Creation"
echo "::notice title=GitHub Release::Creating GitHub release with artifacts"
VERSION="${{ needs.release-validation.outputs.version }}"
PRERELEASE="${{ needs.release-validation.outputs.is-prerelease }}"
echo "π Creating GitHub release $VERSION..."
# Create release with artifacts and metadata
if [ "$PRERELEASE" = "true" ]; then
echo "π·οΈ Creating pre-release"
gh release create "$VERSION" release-artifacts/* homebrew-metadata/* \
--title "usbipd-mac $VERSION" \
--notes-file release-notes.md \
--prerelease
else
echo "π·οΈ Creating stable release"
gh release create "$VERSION" release-artifacts/* homebrew-metadata/* \
--title "usbipd-mac $VERSION" \
--notes-file release-notes.md
fi
echo "::notice title=Release Created::GitHub release created successfully"
echo "β
Release $VERSION published"
echo "π Release URL: ${{ github.server_url }}/${{ github.repository }}/releases/tag/$VERSION"
echo "::endgroup::"
update-homebrew-formula:
name: Update Homebrew Formula
runs-on: ubuntu-latest
needs: [release-validation, create-release, generate-homebrew-metadata]
if: always() && needs.create-release.result == 'success'
steps:
# The tap pulls; this repository no longer pushes to it.
#
# This job used to send a repository_dispatch to homebrew-usbipd-mac, which needed
# a personal access token with write access there. The token expired after a year,
# the dispatch began returning 401, and the step reported success regardless β so
# v0.5.0 shipped with the formula still pointing at a deleted release.
#
# homebrew-usbipd-mac now runs formula-sync.yml on a schedule: it reads the latest
# release here, which is public and needs no credential, and commits with its own
# GITHUB_TOKEN. Nothing expires, and a missed run is corrected by the next one.
- name: πΊ Homebrew formula updates by pull
run: |
echo "::notice title=Homebrew Update::The tap syncs itself; no dispatch is sent"
echo "The formula is updated by formula-sync.yml in homebrew-usbipd-mac,"
echo "which polls this repository's latest release every six hours."
echo ""
echo "To update immediately rather than waiting for the schedule:"
echo " gh workflow run formula-sync.yml --repo beriberikix/homebrew-usbipd-mac"
echo ""
echo "Tap runs: https://github.com/beriberikix/homebrew-usbipd-mac/actions"
post-release:
name: Post-Release Validation
runs-on: macos-latest
needs: [release-validation, create-release, generate-homebrew-metadata, update-homebrew-formula]
if: always() && needs.create-release.result == 'success'
steps:
- name: π Starting Post-Release Validation
run: |
echo "::notice title=Post-Release::Starting post-release validation and summary"
echo "π This stage validates the published release and provides completion summary"
echo "π Status: STARTING"
- name: Checkout code
uses: actions/checkout@v4
- name: β
Validate Release
run: |
echo "::group::Release Validation"
echo "::notice title=Release Validation::Validating published release"
VERSION="${{ needs.release-validation.outputs.version }}"
echo "β
Validating release $VERSION..."
# Verify release exists and is accessible
if gh release view "$VERSION" > /dev/null 2>&1; then
echo "::notice title=Release Verified::Release is publicly accessible"
echo "β
Release $VERSION verified"
# Display release summary
echo "π Release Summary:"
echo " β’ Version: $VERSION"
echo " β’ Pre-release: ${{ needs.release-validation.outputs.is-prerelease }}"
echo " β’ Release type: ${{ needs.release-validation.outputs.release-type }}"
echo " β’ Artifacts: Available"
echo " β’ Status: Published"
else
echo "::error title=Release Verification Failed::Cannot verify release accessibility"
echo "β Release verification failed"
exit 1
fi
echo "::endgroup::"
- name: π Release Success Summary
run: |
echo "::group::Release Success"
echo "::notice title=Release Complete::Streamlined production release completed successfully"
echo "π Release ${{ needs.release-validation.outputs.version }} has been successfully published!"
echo ""
echo "π¦ **Release Details:**"
echo " β’ Version: ${{ needs.release-validation.outputs.version }}"
echo " β’ Pre-release: ${{ needs.release-validation.outputs.is-prerelease }}"
echo " β’ Release type: ${{ needs.release-validation.outputs.release-type }}"
echo " β’ Release URL: ${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ needs.release-validation.outputs.version }}"
echo ""
echo "β
**Completed Steps:**"
echo " β’ Release version validation"
echo " β’ Comprehensive CI validation (via workflow_call)"
echo " β’ Optimized artifact building and signing"
echo " β’ Automated Homebrew metadata generation"
echo " β’ GitHub release creation with artifacts and metadata"
echo " β’ Homebrew formula update via homebrew-releaser"
echo " β’ Post-release verification"
echo ""
echo "π **Workflow Benefits:**"
echo " β’ Reused CI validation logic from consolidated workflow"
echo " β’ Streamlined artifact building process"
echo " β’ Consistent validation across CI and release pipelines"
echo " β’ Automated external tap repository integration"
echo " β’ Reduced workflow duplication and maintenance"
echo ""
echo "πΊ **Homebrew Integration:**"
if [[ "${{ needs.update-homebrew-formula.result }}" == "success" ]]; then
echo " β’ β
Homebrew formula update completed successfully"
echo " β’ β
Formula committed to tap repository via homebrew-releaser"
echo " β’ β
Users can upgrade via: brew upgrade usbip"
else
echo " β’ β οΈ Homebrew formula update failed"
echo " β’ π§ Review action logs for configuration issues"
echo " β’ π§ Manual formula update may be needed"
fi
echo ""
echo "π Status: **SUCCESS**"
echo "::endgroup::"