name: Release Desktop App on: push: tags: ['v*'] workflow_dispatch: permissions: contents: write concurrency: group: release-${{ github.ref }} cancel-in-progress: false jobs: build: name: Build (${{ matrix.os }} / ${{ matrix.arch }}) runs-on: ${{ matrix.os }} timeout-minutes: 60 strategy: fail-fast: false matrix: include: - os: ubuntu-latest arch: amd64 - os: ubuntu-24.04-arm arch: arm64 - os: windows-latest arch: amd64 - os: macos-latest arch: arm64 - os: macos-26-intel arch: amd64 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_VERSION: ${{ github.ref_name }} steps: - uses: actions/checkout@v7 with: fetch-depth: 0 - name: Normalize version for manual runs if: github.event_name == 'workflow_dispatch' shell: bash run: | set -euo pipefail tag="$(git describe --tags --abbrev=0)" echo "RELEASE_VERSION=$tag" >> "$GITHUB_ENV" # JDK 25 to run Gradle, Linux packaging tools + Xvfb, and Gradle. The Nucleus plugin # downloads the Node.js that electron-builder runs under. - name: Set up build environment uses: NucleusFramework/Nucleus/.github/actions/setup-nucleus@v3.0.0-dev-202611011507 with: java-version: '25' packaging-tools: 'true' setup-gradle: 'true' # setup-nucleus installs Temurin, but the app runs and ships under JBR 25 (Jewel's decorated # window needs it), which the jbrRuntime toolchain in desktopApp/build.gradle.kts resolves. # Install it here so that lookup finds a local JDK instead of downloading one per runner. - name: Set up JetBrains Runtime uses: actions/setup-java@v5 with: distribution: 'jetbrains' java-version: '25' # Import the Developer ID Application cert into a temporary keychain so Nucleus can # codesign + notarize at package time. Its path reaches the macOS signing config via the # MAC_KEYCHAIN_PATH env on the build step below. - name: Set up macOS signing id: signing if: runner.os == 'macOS' uses: NucleusFramework/Nucleus/.github/actions/setup-macos-signing@v3.0.0-dev-202611011507 with: certificate-base64: ${{ secrets.MAC_CERTIFICATES_P12 }} certificate-password: ${{ secrets.MAC_CERTIFICATES_PASSWORD }} - name: Build packages shell: bash env: # Azure Trusted Signing service-principal credentials. # Only consumed on Windows runners by electron-builder when AZURE_TENANT_ID # is set (gating happens in desktopApp/build.gradle.kts). AZURE_TENANT_ID: 'dfe2ba9a-411b-4907-b6b0-59fe59aefc2d' AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} # macOS signing: keychain holding the Developer ID cert (empty off macOS) + the # app-specific notarization password. Both are only read by the macOS package task. MAC_KEYCHAIN_PATH: ${{ steps.signing.outputs.keychain-path }} NOTARY_PWD: ${{ secrets.MAC_NOTARIZATION_PASSWORD }} # Three network paths have flaked here across releases, each clearing on a plain # re-run: the wrapper's Gradle distribution download (it does not retry on its # own), electron-builder fetching its packaging tools, and notarytool's poll of # Apple's notary service dropping while a submission is still In Progress. Retry # those, and only those -- a compile error has to fail on the first attempt # rather than burn three builds on every runner before reporting it. # # Retrying that last one re-submits to Apple unless the notarization task resumes the # saved submission. That costs a round trip, which is still less than the manual job # re-run it replaces (which re-submits too, after recompiling from scratch). run: | transient='Unexpected end of file from server|socket hang up|Connection reset|Connection timed out|Read timed out|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|The Internet connection appears to be offline|Could not (GET|HEAD|resolve)|429 Too Many Requests|502 Bad Gateway|503 Service|Remote host terminated the handshake' # notarytool has also been killed outright mid-submission (exit 138 = SIGBUS). # Anything past 128 is a signal death, not a verdict on what we built. Apple's # "403 ... agreement is missing or has expired" deliberately matches neither: # that one needs a human to go sign something, so it must fail on attempt one. crashed='Exit code: 1(2[89]|[3-6][0-9])' for attempt in 1 2 3; do if [ "$attempt" -gt 1 ]; then echo "::warning::Previous attempt failed in a way that clears on a re-run; retrying (attempt $attempt of 3)." sleep $(( (attempt - 1) * 30 )) fi log="$(mktemp)" status=0 ./gradlew :desktopApp:packageDistributionForCurrentOS --stacktrace --no-daemon 2>&1 | tee "$log" \ || status="${PIPESTATUS[0]}" if [ "$status" -eq 0 ]; then exit 0 fi # A signal death reaches us two ways. Gradle prints "Exit code: 138" when a tool it # ran was killed, which is the beta.20 notarytool case the pattern above catches. But # when Gradle itself is the one killed -- the runner's OOM killer, say -- it gets no # chance to write that line, and the signal shows up only as our own exit status. if [ "$status" -lt 128 ] || [ "$status" -gt 169 ]; then if ! grep -qiE "$transient" "$log" && ! grep -qE "$crashed" "$log"; then echo "::error::Build failed for a reason that a retry will not fix; not retrying." exit 1 fi fi done echo "::error::Build still failing after 3 attempts." exit 1 - uses: actions/upload-artifact@v7 with: name: release-assets-${{ runner.os }}-${{ matrix.arch }} retention-days: 3 path: | desktopApp/build/compose/binaries/**/*.dmg desktopApp/build/compose/binaries/**/*.pkg desktopApp/build/compose/binaries/**/*.exe desktopApp/build/compose/binaries/**/*.msi desktopApp/build/compose/binaries/**/*.appx desktopApp/build/compose/binaries/**/*.deb desktopApp/build/compose/binaries/**/*.rpm desktopApp/build/compose/binaries/**/*.AppImage desktopApp/build/compose/binaries/**/*.snap desktopApp/build/compose/binaries/**/*.flatpak desktopApp/build/compose/binaries/**/*.zip desktopApp/build/compose/binaries/**/*.tar desktopApp/build/compose/binaries/**/*.7z desktopApp/build/compose/binaries/**/*.blockmap desktopApp/build/compose/binaries/**/signing-metadata.json desktopApp/build/compose/binaries/**/packaging-metadata.json # electron-builder update manifests, uploaded to the release as-is. desktopApp/build/compose/binaries/**/latest*.yml desktopApp/build/compose/binaries/**/beta*.yml desktopApp/build/compose/binaries/**/alpha*.yml !desktopApp/build/compose/binaries/**/app/** !desktopApp/build/compose/binaries/**/runtime/** if-no-files-found: error android: name: Build & publish Android (Play) # Opt-in: set repository variable PUBLISH_ANDROID=true once the signing/Play # secrets are configured. Until then this job is skipped so it can't fail releases. if: ${{ vars.PUBLISH_ANDROID == 'true' }} runs-on: ubuntu-latest timeout-minutes: 30 env: RELEASE_VERSION: ${{ github.ref_name }} steps: - uses: actions/checkout@v7 with: fetch-depth: 0 - name: Normalize version for manual runs if: github.event_name == 'workflow_dispatch' shell: bash run: | set -euo pipefail tag="$(git describe --tags --abbrev=0)" echo "RELEASE_VERSION=$tag" >> "$GITHUB_ENV" - uses: actions/setup-java@v5 with: distribution: temurin java-version: '21' # The action's default package list includes 'tools', which cmdline-tools 20.0 no longer # has, so sdkmanager fails. AGP installs the platforms and build-tools it needs itself. - uses: android-actions/setup-android@v4 with: packages: platform-tools # cache-provider: basic keeps caching on the MIT implementation; the default 'enhanced' is # proprietary. See the longer note in ci.yaml. - uses: gradle/actions/setup-gradle@v6 with: cache-provider: basic - name: Decode upload keystore shell: bash run: | set -euo pipefail echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > "$RUNNER_TEMP/upload.jks" echo "ANDROID_KEYSTORE_PATH=$RUNNER_TEMP/upload.jks" >> "$GITHUB_ENV" - name: Build release bundle (.aab) shell: bash env: ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} run: ./gradlew :androidApp:bundleRelease -PiosSkip=true -PlintSkip=true --stacktrace --no-daemon - name: Publish to Play (internal track) uses: r0adkll/upload-google-play@v1 with: serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }} packageName: warlockfe.warlock3 releaseFiles: androidApp/build/outputs/bundle/release/androidApp-release.aab track: internal status: completed publish: name: Publish Release needs: [build] if: ${{ !cancelled() && needs.build.result == 'success' }} runs-on: ubuntu-latest timeout-minutes: 30 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v7 - uses: actions/download-artifact@v8 with: path: artifacts pattern: release-assets-* - name: Determine release type shell: bash run: | set -euo pipefail TAG="${GITHUB_REF_NAME}" echo "TAG=$TAG" >> "$GITHUB_ENV" echo "VERSION=${TAG#v}" >> "$GITHUB_ENV" case "$TAG" in *-alpha*) echo "CHANNEL=alpha" >> "$GITHUB_ENV" ;; *-beta*) echo "CHANNEL=beta" >> "$GITHUB_ENV" ;; *) echo "CHANNEL=latest" >> "$GITHUB_ENV" ;; esac if [[ "$TAG" == *-alpha* || "$TAG" == *-beta* ]]; then echo "RELEASE_TYPE=prerelease" >> "$GITHUB_ENV" else echo "RELEASE_TYPE=release" >> "$GITHUB_ENV" fi # One manifest per OS into artifacts/update-yml/, unioning every arch's installers (Linux # x64 and arm64 share -linux.yml, as do the two macOS arches in -mac.yml). - name: Generate update manifests uses: NucleusFramework/Nucleus/.github/actions/generate-update-yml@v3.0.0-dev-202611011507 with: artifacts-path: artifacts version: ${{ env.VERSION }} channel: ${{ env.CHANNEL }} # Installs from the Potassium era read -linux-arm64.yml on arm64. Their updater # picks the file matching its own arch out of a manifest, so the merged Linux one serves # them as-is. Keep publishing it under the old name until those installs have moved over. - name: Keep the arm64 Linux manifest name for Potassium-era installs shell: bash run: | set -euo pipefail cp "artifacts/update-yml/${CHANNEL}-linux.yml" "artifacts/update-yml/${CHANNEL}-linux-arm64.yml" # Uploads installers plus the manifests in artifacts/update-yml/; per-runner manifests in # the build artifacts are skipped in favor of those. - name: Publish release uses: NucleusFramework/Nucleus/.github/actions/publish-release@v3.0.0-dev-202611011507 with: artifacts-path: artifacts tag: ${{ env.TAG }} release-type: ${{ env.RELEASE_TYPE }}