diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7ab2b7d..1ca878f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,245 +1,25 @@ -name: Release +name: macOS Release on: - # Run on version tags (releases) push: tags: - - "v*" - # Allow manual trigger + - "macos/devtoolbox/v*" workflow_dispatch: permissions: contents: write - pull-requests: write - -env: - # Necessary for most environments - CGO_ENABLED: 1 jobs: - release-please: - if: ${{ !startsWith(github.ref, 'refs/tags/v') }} - runs-on: ubuntu-latest - outputs: - release_created: ${{ steps.release.outputs.release_created }} - tag_name: ${{ steps.release.outputs.tag_name }} - steps: - - uses: googleapis/release-please-action@v4 - id: release - with: - token: ${{ secrets.GITHUB_TOKEN }} - - release-build: - needs: release-please - if: ${{ always() && (needs.release-please.outputs.release_created == 'true' || github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v')) }} - name: Release Build - strategy: - fail-fast: false - matrix: - build: [linux, windows, macos] - include: - - build: linux - os: ubuntu-latest - platform: linux/amd64 - - build: windows - os: windows-latest - platform: windows/amd64 - - build: macos - os: macos-latest - platform: darwin/amd64 - - runs-on: ${{ matrix.os }} - permissions: - contents: write - steps: - - name: Checkout - uses: actions/checkout@v6.0.3 - with: - submodules: recursive - ref: ${{ needs.release-please.outputs.tag_name || github.ref }} - - - name: Setup Go - uses: actions/setup-go@v6.4.0 - with: - go-version: "1.25.0" - check-latest: true - - - name: Setup Bun - uses: oven-sh/setup-bun@v2.2.0 - with: - bun-version: latest - - - name: Install Linux dependencies - if: matrix.os == 'ubuntu-latest' - run: | - sudo apt-get update - sudo apt-get install -y libgtk-4-dev libwebkitgtk-6.0-dev libvulkan-dev libgraphene-1.0-dev - - - name: Install Frontend Dependencies - run: | - cd frontend && bun install - shell: bash - - - name: Check macOS signing inputs - id: macos_signing - if: matrix.os == 'macos-latest' - env: - MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} - MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} - MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }} - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - run: | - missing=0 - for name in MACOS_CERTIFICATE MACOS_CERTIFICATE_PASSWORD MACOS_SIGN_IDENTITY APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID; do - if [ -z "${!name}" ]; then - echo "::error::$name is required for signed and notarized macOS releases" - missing=1 - fi - done - - if [ "$missing" -ne 0 ]; then - exit 1 - fi - - echo "available=true" >> "$GITHUB_OUTPUT" - shell: bash - - - name: Import macOS Developer ID certificate - if: matrix.os == 'macos-latest' && steps.macos_signing.outputs.available == 'true' - uses: apple-actions/import-codesign-certs@v2 - with: - p12-file-base64: ${{ secrets.MACOS_CERTIFICATE }} - p12-password: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} - keychain: build - keychain-password: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} - - - name: Build Application - run: | - cd frontend - bun run build - cd .. - - mkdir -p bin - - if [ "${{ matrix.os }}" = "macos-latest" ]; then - export GOOS=darwin - export CGO_ENABLED=1 - export CGO_CFLAGS="-mmacosx-version-min=10.15" - export CGO_LDFLAGS="-mmacosx-version-min=10.15" - export MACOSX_DEPLOYMENT_TARGET="10.15" - - GOARCH=amd64 go build -tags production -trimpath -buildvcs=false -ldflags="-w -s" -o bin/DevToolbox-amd64 . - GOARCH=arm64 go build -tags production -trimpath -buildvcs=false -ldflags="-w -s" -o bin/DevToolbox-arm64 . - lipo -create -output bin/DevToolbox bin/DevToolbox-amd64 bin/DevToolbox-arm64 - rm bin/DevToolbox-amd64 bin/DevToolbox-arm64 - - mkdir -p "bin/DevToolbox.app/Contents/MacOS" - mkdir -p "bin/DevToolbox.app/Contents/Resources" - cp "bin/DevToolbox" "bin/DevToolbox.app/Contents/MacOS/" - cp "build/darwin/Info.plist" "bin/DevToolbox.app/Contents/" - cp "build/darwin/icons.icns" "bin/DevToolbox.app/Contents/Resources/" - if [ -f "build/darwin/Assets.car" ]; then - cp "build/darwin/Assets.car" "bin/DevToolbox.app/Contents/Resources/" - fi - else - if [ "${{ matrix.os }}" = "windows-latest" ]; then - go build -tags production -trimpath -buildvcs=false -ldflags="-w -s" -o bin/DevToolbox.exe . - else - go build -tags production -trimpath -buildvcs=false -ldflags="-w -s" -o bin/DevToolbox . - fi - fi - shell: bash - - - name: Sign macOS app - if: matrix.os == 'macos-latest' && steps.macos_signing.outputs.available == 'true' - env: - MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }} - run: | - APP_BUNDLE="bin/DevToolbox.app" - test -d "$APP_BUNDLE" - - codesign \ - --force \ - --deep \ - --options runtime \ - --timestamp \ - --sign "$MACOS_SIGN_IDENTITY" \ - "$APP_BUNDLE" - - codesign --verify --deep --strict --verbose=2 "$APP_BUNDLE" - shell: bash - - - name: Notarize macOS App - if: matrix.os == 'macos-latest' && steps.macos_signing.outputs.available == 'true' - run: | - APP_BUNDLE="bin/DevToolbox.app" - NOTARY_ZIP="bin/DevToolbox-notary.zip" - - ditto -c -k --keepParent "$APP_BUNDLE" "$NOTARY_ZIP" - - xcrun notarytool submit "$NOTARY_ZIP" \ - --apple-id "$APPLE_ID" \ - --password "$APPLE_APP_SPECIFIC_PASSWORD" \ - --team-id "$APPLE_TEAM_ID" \ - --wait - - xcrun stapler staple "$APP_BUNDLE" - xcrun stapler validate "$APP_BUNDLE" - spctl --assess --type execute --verbose=4 "$APP_BUNDLE" - env: - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - shell: bash - - # Package and upload build artifacts - - name: Package Artifacts - run: | - mkdir -p release - - if [ "${{ matrix.os }}" = "macos-latest" ]; then - APP_BUNDLE="bin/DevToolbox.app" - test -d "$APP_BUNDLE" - brew install create-dmg - create-dmg \ - --volname "DevToolbox" \ - --window-pos 200 120 \ - --window-size 800 400 \ - --icon-size 100 \ - --app-drop-link 600 185 \ - "release/DevToolbox-${{ matrix.build }}.dmg" \ - "$APP_BUNDLE" - hdiutil verify "release/DevToolbox-${{ matrix.build }}.dmg" - elif [ "${{ matrix.os }}" = "windows-latest" ]; then - BINARY_NAME=$(ls bin/ | grep -i "devtoolbox.*\.exe$" | head -1) - test -n "$BINARY_NAME" - cp "bin/$BINARY_NAME" "release/DevToolbox-${{ matrix.build }}.exe" - else - BINARY_NAME=$(ls bin/ | grep -i "devtoolbox" | head -1) - test -n "$BINARY_NAME" - tar -czf "release/DevToolbox-${{ matrix.build }}.tar.gz" -C bin "$BINARY_NAME" - fi - - echo "=== Release contents ===" - ls -la release/ - shell: bash - - - name: Upload Artifacts - uses: actions/upload-artifact@v7.0.1 - with: - name: devtoolbox-${{ matrix.build }} - path: release/* - - # Create Release and upload assets (only on tags) - - name: Create Release - if: startsWith(github.ref, 'refs/tags/v') - uses: softprops/action-gh-release@v3.0.0 - with: - files: release/* - draft: false - prerelease: false - generate_release_notes: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + release: + uses: vuon9/gh-workflows/.github/workflows/macos-desktop-release.yml@khoa/macos-desktop-release-workflow + with: + app-name: DevToolbox + bundle-id: com.vuon9.devtoolbox + team-id: 256XRVYZ9V + package-command: scripts/package-macos-universal.sh + app-path: bin/DevToolbox.app + dmg-name: DevToolbox-macos-universal.dmg + artifact-name: devtoolbox-macos-release-${{ github.run_id }} + go-version-file: go.mod + runner-label: macos-26 + secrets: inherit diff --git a/docs/MACOS_RELEASE.md b/docs/MACOS_RELEASE.md index 29fd68e..de699d1 100644 --- a/docs/MACOS_RELEASE.md +++ b/docs/MACOS_RELEASE.md @@ -1,20 +1,26 @@ # macOS Signed Release This project ships macOS releases as a signed, notarized, and stapled -`DevToolbox-macos.dmg` from `.github/workflows/release.yml`. +`DevToolbox-macos-universal.dmg` from `.github/workflows/release.yml`. + +The first signed release is intentionally macOS-only. Linux and Windows release +artifacts are skipped until a later release pass. ## Required GitHub Secrets Configure these repository secrets before running a release: -- `MACOS_CERTIFICATE`: base64 encoded Developer ID Application `.p12` -- `MACOS_CERTIFICATE_PASSWORD`: password for the `.p12` -- `MACOS_SIGN_IDENTITY`: full Developer ID Application identity name -- `APPLE_ID`: Apple ID used for notarization -- `APPLE_APP_SPECIFIC_PASSWORD`: app-specific password for the Apple ID -- `APPLE_TEAM_ID`: Apple Developer Team ID +- `APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_BASE64`: base64 encoded Developer ID Application `.p12` +- `APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_PASSWORD`: password for the `.p12` +- `APP_STORE_CONNECT_API_KEY_P8`: App Store Connect API private key content +- `APP_STORE_CONNECT_API_KEY_ID`: App Store Connect API key ID +- `APP_STORE_CONNECT_API_ISSUER_ID`: App Store Connect issuer ID + +Optional: + +- `MACOS_CODESIGN_IDENTITY`: full `Developer ID Application: ... (TEAMID)` identity name. If omitted, the reusable workflow finds the imported Developer ID Application identity for team `256XRVYZ9V`. -The release workflow fails early on the macOS job if any of these secrets are +The release workflow fails early if required signing or notarization secrets are missing. Unsigned macOS release artifacts are not uploaded by the release job. ## What the Workflow Does @@ -25,22 +31,22 @@ On macOS runners, the release job: 2. Imports the Developer ID Application certificate into a temporary keychain. 3. Signs the app with hardened runtime and timestamping. 4. Verifies the signature with `codesign --verify`. -5. Submits the app to Apple notarization and waits for completion. -6. Staples and validates the notarization ticket. +5. Submits the app to Apple notarization through App Store Connect API keys. +6. Staples and validates the app notarization ticket. 7. Runs `spctl --assess --type execute`. -8. Packages the stapled app into `DevToolbox-macos.dmg`. -9. Verifies the DMG with `hdiutil verify`. +8. Packages the stapled app into `DevToolbox-macos-universal.dmg`. +9. Signs, notarizes, staples, and verifies the DMG. -Mini owns certificate setup, notarization credentials, and final local Gatekeeper -verification for the released artifact. +Mini owns Apple Developer certificate export, repository secret setup, and final +local Gatekeeper verification for the released artifact. ## Local macOS Verification After downloading the release DMG on macOS: ```bash -hdiutil verify DevToolbox-macos.dmg -hdiutil attach DevToolbox-macos.dmg +hdiutil verify DevToolbox-macos-universal.dmg +hdiutil attach DevToolbox-macos-universal.dmg spctl --assess --type execute --verbose=4 /Volumes/DevToolbox/DevToolbox.app codesign --verify --deep --strict --verbose=2 /Volumes/DevToolbox/DevToolbox.app open /Volumes/DevToolbox/DevToolbox.app diff --git a/scripts/package-macos-universal.sh b/scripts/package-macos-universal.sh new file mode 100755 index 0000000..c192345 --- /dev/null +++ b/scripts/package-macos-universal.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +app_name="${APP_NAME:-DevToolbox}" +bin_dir="${BIN_DIR:-bin}" + +export GOOS=darwin +export CGO_ENABLED=1 +export CGO_CFLAGS="-mmacosx-version-min=10.15" +export CGO_LDFLAGS="-mmacosx-version-min=10.15" +export MACOSX_DEPLOYMENT_TARGET="10.15" + +go mod download + +test -f build/darwin/Info.plist +test -f build/darwin/icons.icns + +( + cd frontend + bun install --frozen-lockfile + PRODUCTION=true bun run build +) + +mkdir -p "$bin_dir" + +GOARCH=amd64 go build -tags production -trimpath -buildvcs=false -ldflags="-w -s" -o "$bin_dir/$app_name-amd64" . +GOARCH=arm64 go build -tags production -trimpath -buildvcs=false -ldflags="-w -s" -o "$bin_dir/$app_name-arm64" . + +lipo -create -output "$bin_dir/$app_name" "$bin_dir/$app_name-amd64" "$bin_dir/$app_name-arm64" +rm "$bin_dir/$app_name-amd64" "$bin_dir/$app_name-arm64" + +app_bundle="$bin_dir/$app_name.app" +rm -rf "$app_bundle" +mkdir -p "$app_bundle/Contents/MacOS" +mkdir -p "$app_bundle/Contents/Resources" + +cp "$bin_dir/$app_name" "$app_bundle/Contents/MacOS/" +cp build/darwin/Info.plist "$app_bundle/Contents/" +cp build/darwin/icons.icns "$app_bundle/Contents/Resources/" +if [[ -f build/darwin/Assets.car ]]; then + cp build/darwin/Assets.car "$app_bundle/Contents/Resources/" +fi + +echo "Created $app_bundle"