Skip to content

Release 0.6.0

Release 0.6.0 #82

Workflow file for this run

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::"