name: Build and Release on: push: branches: [ "main" ] tags: [ "v*" ] env: APP_NAME: StudioBridge MAIN_JAR: StudioBridge.jar MODULES: java.base,java.desktop,java.net.http,java.security.jgss,java.security.sasl,java.naming,jdk.crypto.ec JAVA_OPTS: "-Xmx512m" COPYRIGHT: Rdiger-36 VENDOR: Rdiger-36 DESCRIPTION: "With StudioBridge it is possible to make Bambu Lab 3D Printers visible in Bambu Studio or Orca Slicer, that can not be automatically added by them." ABOUT_URL: https://github.com/Rdiger-36/StudioBridge WIN_UUID: b062e5bc-0c30-48e7-8f48-9067789d244b jobs: # ───────────────────────────────────────────────────────────────────────── # JAR — cross-platform fat JAR # Runs on every push to main (CI check) and on tag pushes (release) # ───────────────────────────────────────────────────────────────────────── build-jar: runs-on: ubuntu-latest outputs: version: ${{ steps.version.outputs.value }} steps: - uses: actions/checkout@v5 - uses: actions/setup-java@v5 with: java-version: '21' distribution: 'temurin' cache: 'maven' - name: Get project version id: version run: echo "value=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" >> $GITHUB_OUTPUT - name: Build fat JAR run: mvn clean package -q - name: Prepare JAR artifact run: | mkdir input cp target/*-jar-with-dependencies.jar input/${{ env.MAIN_JAR }} cp target/*-jar-with-dependencies.jar "target/StudioBridge-${{ steps.version.outputs.value }}.jar" - uses: actions/upload-artifact@v7 with: name: jar-release path: target/StudioBridge-*.jar - uses: actions/upload-artifact@v7 with: name: jar-input path: input/StudioBridge.jar # ───────────────────────────────────────────────────────────────────────── # Windows — MSI installer + portable ZIP (x86 and arm64) # ───────────────────────────────────────────────────────────────────────── build-windows: needs: build-jar if: startsWith(github.ref, 'refs/tags/v') strategy: fail-fast: false matrix: include: - runner: windows-latest arch: x86 - runner: windows-11-arm arch: arm64 runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v5 - uses: actions/setup-java@v5 with: java-version: '21' distribution: 'temurin' # icon.ico is pre-converted and stored in .github/packaging/icons/ - name: Install WiX toolset (ARM64 only — not pre-installed on windows-11-arm) if: matrix.arch == 'arm64' shell: pwsh run: | choco install wixtoolset --no-progress -y $wixDir = Get-ChildItem "C:\Program Files (x86)\WiX Toolset*\bin" | Select-Object -First 1 -ExpandProperty FullName echo "$wixDir" >> $env:GITHUB_PATH - name: Download JAR uses: actions/download-artifact@v7 with: name: jar-input path: input - name: Build minimal runtime via jlink shell: pwsh run: | jlink ` --add-modules "${{ env.MODULES }}" ` --strip-debug ` --no-header-files ` --no-man-pages ` --compress=zip-6 ` --output runtime - name: Build MSI installer shell: pwsh run: | # Add icon to CLI properties Copy-Item .github/packaging/cli-win.properties cli-win.properties Add-Content cli-win.properties "icon=.github/packaging/icons/icon.ico" jpackage ` --type msi ` --name "${{ env.APP_NAME }}" ` --app-version "${{ needs.build-jar.outputs.version }}" ` --input input ` --main-jar "${{ env.MAIN_JAR }}" ` --icon .github/packaging/icons/icon.ico ` --runtime-image runtime ` --java-options "${{ env.JAVA_OPTS }}" ` --copyright "${{ env.COPYRIGHT }}" ` --vendor "${{ env.VENDOR }}" ` --description "${{ env.DESCRIPTION }}" ` --about-url "${{ env.ABOUT_URL }}" ` --win-menu --win-shortcut ` --win-dir-chooser --win-shortcut-prompt ` --win-upgrade-uuid "${{ env.WIN_UUID }}" ` --add-launcher "StudioBridgeCLI=cli-win.properties" ` --dest msi-output - name: Build portable app-image shell: pwsh run: | Copy-Item .github/packaging/cli-win.properties cli-win.properties Add-Content cli-win.properties "icon=.github/packaging/icons/icon.ico" jpackage ` --type app-image ` --name "${{ env.APP_NAME }}" ` --app-version "${{ needs.build-jar.outputs.version }}" ` --input input ` --main-jar "${{ env.MAIN_JAR }}" ` --icon .github/packaging/icons/icon.ico ` --runtime-image runtime ` --java-options "${{ env.JAVA_OPTS }}" ` --add-launcher "StudioBridgeCLI=cli-win.properties" ` --dest app-image - name: Create portable ZIP shell: pwsh run: | $v = "${{ needs.build-jar.outputs.version }}" Compress-Archive ` -Path "app-image\${{ env.APP_NAME }}\*" ` -DestinationPath "StudioBridge-${v}_win_${{ matrix.arch }}_NO-INSTALL.zip" - name: Rename and move MSI shell: pwsh run: | $v = "${{ needs.build-jar.outputs.version }}" Get-ChildItem msi-output\*.msi | Move-Item -Destination "StudioBridge-${v}_win_${{ matrix.arch }}.msi" - uses: actions/upload-artifact@v7 with: name: windows-${{ matrix.arch }} path: | *.msi *.zip # ───────────────────────────────────────────────────────────────────────── # Linux — AppImage (x86_64 and aarch64) # ───────────────────────────────────────────────────────────────────────── build-linux: needs: build-jar if: startsWith(github.ref, 'refs/tags/v') strategy: fail-fast: false matrix: include: - runner: ubuntu-latest arch: x86_64 - runner: ubuntu-24.04-arm arch: aarch64 runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v5 - uses: actions/setup-java@v5 with: java-version: '21' distribution: 'temurin' - name: Install dependencies run: sudo apt-get install -y libfuse2 - name: Download appimagetool run: | wget -q \ "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-${{ matrix.arch }}.AppImage" \ -O appimagetool chmod +x appimagetool - name: Download JAR uses: actions/download-artifact@v7 with: name: jar-input path: input - name: Build minimal runtime via jlink run: | jlink \ --add-modules "${{ env.MODULES }}" \ --strip-debug \ --no-header-files \ --no-man-pages \ --compress=zip-6 \ --output runtime - name: Build app-image via jpackage run: | jpackage \ --type app-image \ --name "${{ env.APP_NAME }}" \ --app-version "${{ needs.build-jar.outputs.version }}" \ --input input \ --main-jar "${{ env.MAIN_JAR }}" \ --icon src/main/resources/icon.png \ --runtime-image runtime \ --java-options "${{ env.JAVA_OPTS }}" - name: Package as AppImage run: | VERSION="${{ needs.build-jar.outputs.version }}" APPDIR="build/${{ env.APP_NAME }}.AppDir" # FHS-compliant AppDir structure mkdir -p \ "${APPDIR}/usr/bin" \ "${APPDIR}/usr/lib/${{ env.APP_NAME }}" \ "${APPDIR}/usr/share/applications" \ "${APPDIR}/usr/share/icons/hicolor/256x256/apps" # Copy jpackage app-image into usr/lib cp -a "${{ env.APP_NAME }}/." "${APPDIR}/usr/lib/${{ env.APP_NAME }}/" # Symlink launcher into PATH ln -sfn "../lib/${{ env.APP_NAME }}/bin/${{ env.APP_NAME }}" \ "${APPDIR}/usr/bin/${{ env.APP_NAME }}" # AppRun cat > "${APPDIR}/AppRun" << 'APPRUN' #!/bin/sh HERE="$(dirname "$(readlink -f "$0")")" exec "$HERE/usr/bin/StudioBridge" "$@" APPRUN chmod +x "${APPDIR}/AppRun" # .desktop entry cat > "${APPDIR}/${{ env.APP_NAME }}.desktop" << 'DESKTOP' [Desktop Entry] Type=Application Name=StudioBridge Exec=StudioBridge Icon=StudioBridge Categories=Utility; Terminal=false DESKTOP # Icons install -m 0644 src/main/resources/icon.png "${APPDIR}/${{ env.APP_NAME }}.png" install -m 0644 src/main/resources/icon.png \ "${APPDIR}/usr/share/icons/hicolor/256x256/apps/${{ env.APP_NAME }}.png" ARCH=${{ matrix.arch }} APPIMAGE_EXTRACT_AND_RUN=1 \ ./appimagetool "${APPDIR}" \ "${{ env.APP_NAME }}-${VERSION}-${{ matrix.arch }}.AppImage" - uses: actions/upload-artifact@v7 with: name: linux-${{ matrix.arch }} path: "*.AppImage" # ───────────────────────────────────────────────────────────────────────── # macOS — signed + notarized DMG (arm64 / Apple Silicon only) # # Intel (x86) support dropped: macOS 27 (September 2026) removes Rosetta 2, # making x86 app execution on Apple Silicon impossible. Intel Macs cannot # upgrade to macOS 27, so the x86 user base will effectively be zero. # # Required secrets: # MACOS_CERTIFICATE — base64-encoded Developer ID Application .p12 # MACOS_CERTIFICATE_PWD — export password for the .p12 # MACOS_KEYCHAIN_PWD — temporary keychain password (any value) # APPLE_TEAM_NAME — name part of "Developer ID Application: Name (ID)" # APPLE_TEAM_ID — 10-character Apple Team ID # APPLE_ID — Apple ID e-mail for notarytool # APPLE_APP_PASSWORD — app-specific password (appleid.apple.com) # ───────────────────────────────────────────────────────────────────────── build-macos: needs: build-jar if: startsWith(github.ref, 'refs/tags/v') runs-on: macos-latest # arm64 — Apple Silicon (M-Series) steps: - uses: actions/checkout@v5 - uses: actions/setup-java@v5 with: java-version: '21' distribution: 'temurin' - name: Import Developer ID certificate env: MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }} MACOS_KEYCHAIN_PWD: ${{ secrets.MACOS_KEYCHAIN_PWD }} run: | echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12 security create-keychain -p "$MACOS_KEYCHAIN_PWD" build.keychain security default-keychain -s build.keychain security unlock-keychain -p "$MACOS_KEYCHAIN_PWD" build.keychain security import certificate.p12 \ -k build.keychain \ -P "$MACOS_CERTIFICATE_PWD" \ -T /usr/bin/codesign \ -T /usr/bin/jpackage security set-key-partition-list \ -S apple-tool:,apple: \ -s -k "$MACOS_KEYCHAIN_PWD" build.keychain # icon.icns is pre-converted and stored in .github/packaging/icons/ - name: Download JAR uses: actions/download-artifact@v7 with: name: jar-input path: input - name: Build minimal runtime via jlink run: | jlink \ --add-modules "${{ env.MODULES }}" \ --strip-debug \ --no-header-files \ --no-man-pages \ --compress=zip-6 \ --output runtime - name: Sign all native libraries inside JAR # Apple notarization scans inside JARs for .dylib/.jnilib and rejects unsigned ones. # Multiple dependencies (JNA, FlatLaf, ...) bundle macOS native code — sign them all. env: APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_TEAM_NAME: ${{ secrets.APPLE_TEAM_NAME }} run: | mkdir -p jar-natives # Collect paths of all native libs inside the JAR NATIVE_LIBS=$(jar tf input/StudioBridge.jar | grep -E '\.(jnilib|dylib)$') if [ -z "$NATIVE_LIBS" ]; then echo "No native libs found in JAR — skipping." else # Extract only those files (cd jar-natives && echo "$NATIVE_LIBS" | xargs jar xf ../input/StudioBridge.jar) # Sign each one with hardened runtime + secure timestamp find jar-natives -name "*.jnilib" -o -name "*.dylib" | \ while read lib; do echo "Signing: $lib" codesign --force \ --sign "Developer ID Application: ${APPLE_TEAM_NAME} (${APPLE_TEAM_ID})" \ --options runtime \ --timestamp \ --entitlements .github/entitlements.plist \ "$lib" done # Repack signed natives back into the JAR (cd jar-natives && find . -name "*.jnilib" -o -name "*.dylib" | \ sed 's|^\./||' | xargs jar uf ../input/StudioBridge.jar) fi rm -rf jar-natives - name: Build signed DMG env: APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_TEAM_NAME: ${{ secrets.APPLE_TEAM_NAME }} run: | # Add icon to CLI properties cp .github/packaging/cli-mac.properties cli-mac.properties echo "icon=.github/packaging/icons/icon.icns" >> cli-mac.properties jpackage \ --type dmg \ --name "${{ env.APP_NAME }}" \ --app-version "${{ needs.build-jar.outputs.version }}" \ --input input \ --main-jar "${{ env.MAIN_JAR }}" \ --icon .github/packaging/icons/icon.icns \ --runtime-image runtime \ --java-options "${{ env.JAVA_OPTS }}" \ --copyright "${{ env.COPYRIGHT }}" \ --vendor "${{ env.VENDOR }}" \ --description "${{ env.DESCRIPTION }}" \ --mac-sign \ --mac-signing-key-user-name "Developer ID Application: ${APPLE_TEAM_NAME} (${APPLE_TEAM_ID})" \ --mac-entitlements .github/entitlements.plist \ --add-launcher "StudioBridgeCLI=cli-mac.properties" \ --dest dmg-output - name: Rename DMG run: | V="${{ needs.build-jar.outputs.version }}" mv dmg-output/*.dmg "StudioBridge-${V}_macOS_arm64.dmg" - name: Notarize and staple DMG env: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} run: | DMG=$(ls *.dmg | head -1) # Submit and capture result as JSON for error logging RESULT=$(xcrun notarytool submit "$DMG" \ --apple-id "$APPLE_ID" \ --team-id "$APPLE_TEAM_ID" \ --password "$APPLE_APP_PASSWORD" \ --wait --output-format json) echo "$RESULT" STATUS=$(echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin)['status'])") ID=$(echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])") # On failure print the detailed log from Apple before exiting if [ "$STATUS" != "Accepted" ]; then echo "Notarization failed (status: $STATUS) — fetching log..." xcrun notarytool log "$ID" \ --apple-id "$APPLE_ID" \ --team-id "$APPLE_TEAM_ID" \ --password "$APPLE_APP_PASSWORD" exit 1 fi xcrun stapler staple "$DMG" - uses: actions/upload-artifact@v7 with: name: macos-arm64 path: "*.dmg" # ───────────────────────────────────────────────────────────────────────── # Release — collects all artifacts, creates GitHub Release on tag push # ───────────────────────────────────────────────────────────────────────── release: needs: [ build-jar, build-windows, build-linux, build-macos ] if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest permissions: contents: write pull-requests: read steps: - name: Download all artifacts uses: actions/download-artifact@v7 with: path: artifacts - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: files: | artifacts/jar-release/* artifacts/windows-x86/* artifacts/windows-arm64/* artifacts/linux-x86_64/* artifacts/linux-aarch64/* artifacts/macos-arm64/* name: "Version ${{ needs.build-jar.outputs.version }}" generate_release_notes: true append_body: true body: | --- ## Downloads ### Windows | | Installer (MSI) | Portable (ZIP) | |---|---|---| | **x86 (Intel/AMD)** | [⬇ StudioBridge-${{ needs.build-jar.outputs.version }}_win_x86.msi](https://github.com/Rdiger-36/StudioBridge/releases/download/${{ github.ref_name }}/StudioBridge-${{ needs.build-jar.outputs.version }}_win_x86.msi) | [⬇ NO-INSTALL x86](https://github.com/Rdiger-36/StudioBridge/releases/download/${{ github.ref_name }}/StudioBridge-${{ needs.build-jar.outputs.version }}_win_x86_NO-INSTALL.zip) | | **ARM64** | [⬇ StudioBridge-${{ needs.build-jar.outputs.version }}_win_arm64.msi](https://github.com/Rdiger-36/StudioBridge/releases/download/${{ github.ref_name }}/StudioBridge-${{ needs.build-jar.outputs.version }}_win_arm64.msi) | [⬇ NO-INSTALL arm64](https://github.com/Rdiger-36/StudioBridge/releases/download/${{ github.ref_name }}/StudioBridge-${{ needs.build-jar.outputs.version }}_win_arm64_NO-INSTALL.zip) | ### Linux | | AppImage | |---|---| | **x86_64 (Intel/AMD)** | [⬇ StudioBridge-${{ needs.build-jar.outputs.version }}-x86_64.AppImage](https://github.com/Rdiger-36/StudioBridge/releases/download/${{ github.ref_name }}/StudioBridge-${{ needs.build-jar.outputs.version }}-x86_64.AppImage) | | **aarch64 (ARM)** | [⬇ StudioBridge-${{ needs.build-jar.outputs.version }}-aarch64.AppImage](https://github.com/Rdiger-36/StudioBridge/releases/download/${{ github.ref_name }}/StudioBridge-${{ needs.build-jar.outputs.version }}-aarch64.AppImage) | ### macOS | | DMG | |---|---| | **Apple Silicon (M-Series)** | [⬇ StudioBridge-${{ needs.build-jar.outputs.version }}_macOS_arm64.dmg](https://github.com/Rdiger-36/StudioBridge/releases/download/${{ github.ref_name }}/StudioBridge-${{ needs.build-jar.outputs.version }}_macOS_arm64.dmg) | ### JAR (alle Plattformen, erfordert Java 21+) | | | |---|---| | **Standalone JAR** | [⬇ StudioBridge-${{ needs.build-jar.outputs.version }}.jar](https://github.com/Rdiger-36/StudioBridge/releases/download/${{ github.ref_name }}/StudioBridge-${{ needs.build-jar.outputs.version }}.jar) | prerelease: ${{ contains(github.ref_name, '-') }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}