name: Unified Build and Deploy on: push: branches: - '**' tags: - 'v*' pull_request: branches: - '**' workflow_dispatch: inputs: force_release: description: "Force release creation" required: false default: "false" type: choice options: - true - false components: description: "Components to build (comma separated)" required: false default: "webclient,desktop,android,backend" type: string env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: # Initialize and update submodules init-submodules: runs-on: ubuntu-latest name: Initialize Submodules outputs: webclient-changed: ${{ steps.changes.outputs.webclient }} desktop-changed: ${{ steps.changes.outputs.desktop }} android-changed: ${{ steps.changes.outputs.android }} backend-changed: ${{ steps.changes.outputs.backend }} should-build-webclient: ${{ steps.decision.outputs.webclient }} should-build-desktop: ${{ steps.decision.outputs.desktop }} should-build-android: ${{ steps.decision.outputs.android }} should-build-backend: ${{ steps.decision.outputs.backend }} steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 submodules: recursive - name: Detect changes id: changes run: | # Get changed files if [ ${{ github.event_name }} == 'push' ]; then if [ ${{ github.ref }} == 'refs/heads/main' ] || [ ${{ github.ref }} == 'refs/heads/master' ]; then BASE="HEAD~1" else BASE="origin/main" fi elif [ ${{ github.event_name }} == 'pull_request' ]; then BASE="${{ github.event.pull_request.base.sha }}" else BASE="HEAD~1" fi echo "Checking changes between $BASE and HEAD" # Check submodule changes git diff --name-only $BASE HEAD | grep -E "^swingmusic-webclient/" > /dev/null && echo "webclient=true" >> $GITHUB_OUTPUT || echo "webclient=false" >> $GITHUB_OUTPUT git diff --name-only $BASE HEAD | grep -E "^swingmusic-desktop/" > /dev/null && echo "desktop=true" >> $GITHUB_OUTPUT || echo "desktop=false" >> $GITHUB_OUTPUT git diff --name-only $BASE HEAD | grep -E "^swingmusic-android/" > /dev/null && echo "android=true" >> $GITHUB_OUTPUT || echo "android=false" >> $GITHUB_OUTPUT git diff --name-only $BASE HEAD | grep -E "^src/" > /dev/null && echo "backend=true" >> $GITHUB_OUTPUT || echo "backend=false" >> $GITHUB_OUTPUT # Also check if submodules themselves changed git submodule status --recursive - name: Decide what to build id: decision run: | # Parse user input or build all INPUT_COMPONENTS="${{ github.event.inputs.components || 'webclient,desktop,android,backend' }}" IFS=',' read -ra COMPONENTS <<< "$INPUT_COMPONENTS" # Default to building everything on tags/main branch if [[ $GITHUB_REF == refs/tags/* ]] || [[ $GITHUB_REF == refs/heads/main ]] || [[ $GITHUB_REF == refs/heads/master ]]; then echo "webclient=true" >> $GITHUB_OUTPUT echo "desktop=true" >> $GITHUB_OUTPUT echo "android=true" >> $GITHUB_OUTPUT echo "backend=true" >> $GITHUB_OUTPUT else # Build only what changed or was specified for component in "${COMPONENTS[@]}"; do component=$(echo "$component" | xargs) # trim whitespace if [[ "${{ steps.changes.outputs.webclient }}" == "true" ]] || [[ "$component" == "webclient" ]]; then echo "webclient=true" >> $GITHUB_OUTPUT else echo "webclient=false" >> $GITHUB_OUTPUT fi if [[ "${{ steps.changes.outputs.desktop }}" == "true" ]] || [[ "$component" == "desktop" ]]; then echo "desktop=true" >> $GITHUB_OUTPUT else echo "desktop=false" >> $GITHUB_OUTPUT fi if [[ "${{ steps.changes.outputs.android }}" == "true" ]] || [[ "$component" == "android" ]]; then echo "android=true" >> $GITHUB_OUTPUT else echo "android=false" >> $GITHUB_OUTPUT fi if [[ "${{ steps.changes.outputs.backend }}" == "true" ]] || [[ "$component" == "backend" ]]; then echo "backend=true" >> $GITHUB_OUTPUT else echo "backend=false" >> $GITHUB_OUTPUT fi done fi # Build Web Client build-webclient: runs-on: ubuntu-latest name: Build Web Client needs: init-submodules if: needs.init-submodules.outputs.should-build-webclient == 'true' outputs: client-sha: ${{ steps.sha.outputs.sha }} steps: - name: Checkout repository uses: actions/checkout@v4 with: submodules: recursive - name: Setup Node 24 uses: actions/setup-node@v4 with: node-version: 24.x - name: Build client run: | cd swingmusic-webclient npm install npm run build cd .. - name: Generate client SHA id: sha run: | cd swingmusic-webclient/dist sha256sum * | sort | sha256sum | cut -d' ' -f1 > client.sha echo "sha=$(cat client.sha)" >> $GITHUB_OUTPUT - name: Upload client artifact uses: actions/upload-artifact@v4 with: path: "swingmusic-webclient/dist/" compression-level: 0 name: "webclient" # Build Desktop App build-desktop: runs-on: ubuntu-latest name: Build Desktop App needs: init-submodules if: needs.init-submodules.outputs.should-build-desktop == 'true' strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] include: - os: ubuntu-latest platform: linux target: x86_64-unknown-linux-gnu - os: windows-latest platform: windows target: x86_64-pc-windows-msvc - os: macos-latest platform: macos target: x86_64-apple-darwin steps: - name: Checkout repository uses: actions/checkout@v4 with: submodules: recursive - name: Install dependencies (Ubuntu) if: matrix.os == 'ubuntu-latest' run: | sudo apt-get update sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf - name: Rust setup uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - name: Rust Cache uses: swatinem/rust-cache@v2 with: workspaces: "swingmusic-desktop -> target" key: ${{ matrix.platform }}-x64 - name: Install Tauri CLI run: npm install -g @tauri-apps/cli - name: Install desktop dependencies run: | cd swingmusic-desktop npm install - name: Build the app run: | cd swingmusic-desktop npm run tauri build -- --target ${{ matrix.target }} - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: swingmusic-desktop-${{ matrix.platform }}-x64 path: | swingmusic-desktop/target/${{ matrix.target }}/release/bundle/ !swingmusic-desktop/target/${{ matrix.target }}/release/bundle/.*/ retention-days: 30 # Build Android App build-android: runs-on: ubuntu-latest name: Build Android App needs: init-submodules if: needs.init-submodules.outputs.should-build-android == 'true' steps: - name: Checkout repository uses: actions/checkout@v4 with: submodules: recursive - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' - name: Setup Android SDK uses: android-actions/setup-android@v3 - name: Grant execute permission for gradlew run: | cd swingmusic-android chmod +x gradlew - name: Build Android App run: | cd swingmusic-android ./gradlew assembleRelease - name: Upload Android artifacts uses: actions/upload-artifact@v4 with: name: swingmusic-android path: swingmusic-android/app/build/outputs/apk/release/*.apk retention-days: 30 # Build Backend (Python) build-backend: runs-on: ubuntu-latest name: Build Backend needs: [init-submodules, build-webclient] if: needs.init-submodules.outputs.should-build-backend == 'true' outputs: version: ${{ steps.version.outputs.version }} steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 submodules: recursive - name: Download webclient artifact if: needs.init-submodules.outputs.should-build-webclient == 'true' uses: actions/download-artifact@v4 with: name: webclient path: swingmusic-webclient/dist - name: Compress client and copy to src/swingmusic/client.zip if: needs.init-submodules.outputs.should-build-webclient == 'true' run: | cd swingmusic-webclient/dist zip -r client.zip . cd ../.. cp swingmusic-webclient/dist/client.zip src/swingmusic/client.zip - uses: actions/setup-python@v5 with: python-version: "3.11" - name: Get version id: version run: | if [[ $GITHUB_REF == refs/tags/* ]]; then VERSION=${GITHUB_REF#refs/tags/v} else VERSION=$(python -c "import sys; sys.path.insert(0, 'src'); from swingmusic import __version__; print(__version__)") VERSION="$VERSION-${{ github.run_number }}" fi echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Build wheels run: pip wheel . -w wheelhouse --no-deps - name: Upload wheels uses: actions/upload-artifact@v4 with: path: ./wheelhouse/*.whl compression-level: 0 name: "backend-wheels" # Build Docker Image (includes backend + webclient) build-docker: runs-on: ubuntu-latest name: Build Docker Image needs: [init-submodules, build-webclient, build-backend] if: needs.init-submodules.outputs.should-build-backend == 'true' permissions: contents: read packages: write outputs: image: ${{ steps.meta.outputs.tags }} digest: ${{ steps.build.outputs.digest }} steps: - name: Checkout repository uses: actions/checkout@v4 with: submodules: recursive - name: Download webclient artifact if: needs.init-submodules.outputs.should-build-webclient == 'true' uses: actions/download-artifact@v4 with: name: webclient path: swingmusic-webclient/dist - name: Download backend wheels uses: actions/download-artifact@v4 with: name: backend-wheels path: wheels merge-multiple: true - name: Compress client and copy to src/swingmusic/client.zip if: needs.init-submodules.outputs.should-build-webclient == 'true' run: | cd swingmusic-webclient/dist zip -r client.zip . cd ../.. cp swingmusic-webclient/dist/client.zip src/swingmusic/client.zip - name: Create version.txt run: echo "${{ needs.build-backend.outputs.version }}" > version.txt - name: Log in to Container Registry if: github.event_name != 'pull_request' uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=ref,event=branch,suffix=-${{ github.run_number }} type=ref,event=pr,suffix=-pr-${{ github.event.number }} type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=raw,value=latest,enable={{is_default_branch}} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Build and push Docker image id: build uses: docker/build-push-action@v5 with: context: . platforms: linux/amd64,linux/arm64 push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: | app_version=${{ needs.build-backend.outputs.version }} client_sha=${{ needs.build-webclient.outputs.client-sha }} cache-from: type=gha cache-to: type=gha,mode=max # Create Release create-release: runs-on: ubuntu-latest name: Create Release needs: [init-submodules, build-webclient, build-desktop, build-android, build-backend, build-docker] if: | startsWith(github.ref, 'refs/tags/') || github.event.inputs.force_release == 'true' permissions: contents: write steps: - name: Checkout repository uses: actions/checkout@v4 - name: Download all artifacts uses: actions/download-artifact@v4 - name: Create release uses: softprops/action-gh-release@v1 with: name: Release ${{ needs.build-backend.outputs.version }} tag_name: v${{ needs.build-backend.outputs.version }} body: | ## Release ${{ needs.build-backend.outputs.version }} ### Docker Image - `${{ needs.build-docker.outputs.image }}` ### Components - **Web Client**: Built and included in Docker image - **Desktop Apps**: Available in artifacts - **Android App**: Available in artifacts - **Backend**: Python wheels available ### Changes - View full changelog in [CHANGELOG.md](CHANGELOG.md) ### Installation #### Docker (Recommended) ```bash docker pull ${{ needs.build-docker.outputs.image }} docker run -p 1979:1979 ${{ needs.build-docker.outputs.image }} ``` #### Desktop Download the appropriate artifact for your platform: - Linux: swingmusic-desktop-linux-x64 - Windows: swingmusic-desktop-windows-x64 - macOS: swingmusic-desktop-macos-x64 #### Android Download the APK from the swingmusic-android artifact. files: | backend-wheels/* webclient/* swingmusic-desktop-*/ swingmusic-android/* draft: false prerelease: ${{ contains(needs.build-backend.outputs.version, '-') }} generate_release_notes: true # Summary build-summary: runs-on: ubuntu-latest name: Build Summary needs: [init-submodules, build-webclient, build-desktop, build-android, build-backend, build-docker] if: always() steps: - name: Build Summary run: | echo "## Build Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| Component | Status | Result |" >> $GITHUB_STEP_SUMMARY echo "|-----------|--------|--------|" >> $GITHUB_STEP_SUMMARY echo "| Web Client | ${{ needs.build-webclient.result || 'skipped' }} | ${{ contains(needs.build-webclient.result, 'failure') && '❌ Failed' || contains(needs.build-webclient.result, 'success') && '✅ Success' || '⏭️ Skipped' }} |" >> $GITHUB_STEP_SUMMARY echo "| Desktop | ${{ needs.build-desktop.result || 'skipped' }} | ${{ contains(needs.build-desktop.result, 'failure') && '❌ Failed' || contains(needs.build-desktop.result, 'success') && '✅ Success' || '⏭️ Skipped' }} |" >> $GITHUB_STEP_SUMMARY echo "| Android | ${{ needs.build-android.result || 'skipped' }} | ${{ contains(needs.build-android.result, 'failure') && '❌ Failed' || contains(needs.build-android.result, 'success') && '✅ Success' || '⏭️ Skipped' }} |" >> $GITHUB_STEP_SUMMARY echo "| Backend | ${{ needs.build-backend.result || 'skipped' }} | ${{ contains(needs.build-backend.result, 'failure') && '❌ Failed' || contains(needs.build-backend.result, 'success') && '✅ Success' || '⏭️ Skipped' }} |" >> $GITHUB_STEP_SUMMARY echo "| Docker | ${{ needs.build-docker.result || 'skipped' }} | ${{ contains(needs.build-docker.result, 'failure') && '❌ Failed' || contains(needs.build-docker.result, 'success') && '✅ Success' || '⏭️ Skipped' }} |" >> $GITHUB_STEP_SUMMARY