Release 0.6.0 #82
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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::" |