diff --git a/.github/workflows/build-desktop-platforms.yml b/.github/workflows/build-desktop-platforms.yml index 7235817b0..c5901057c 100644 --- a/.github/workflows/build-desktop-platforms.yml +++ b/.github/workflows/build-desktop-platforms.yml @@ -411,3 +411,174 @@ jobs: if-no-files-found: error retention-days: 30 compression-level: 0 + + release: + name: Draft release with all installers + needs: [build-windows, build-macos, build-linux] + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Read project version + id: version + run: | + set -euo pipefail + VERSION=$(grep 'projectVersionName' gradle/libs.versions.toml | head -1 | sed 's/.*= *"\(.*\)"/\1/') + if [ -z "$VERSION" ]; then + echo "ERROR: could not read projectVersionName from gradle/libs.versions.toml" + exit 1 + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=v$VERSION" >> "$GITHUB_OUTPUT" + echo "Resolved release tag: v$VERSION" + shell: bash + + - name: Download all build artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Stage release files (rename arch / compat collisions) + run: | + set -euo pipefail + mkdir -p release-files + + # stage() returns 0 on a successful copy, 1 if the source is missing. + # Callers use the exit status to increment per-group counters so the + # completeness guard at the end can detect missing groups. + stage() { + local src="$1" + local target_name="$2" + if [ -f "$src" ]; then + cp "$src" "release-files/$target_name" + echo "Staged: $target_name" + return 0 + fi + return 1 + } + + windows_count=0 + macos_x64_count=0 + macos_arm64_count=0 + linux_modern_count=0 + linux_debian12_count=0 + linux_appimage_count=0 + linux_arch_count=0 + + # Windows — names already unique (.exe, .msi) + for f in artifacts/windows-installers/*.exe artifacts/windows-installers/*.msi; do + [ -f "$f" ] || continue + stage "$f" "$(basename "$f")" && windows_count=$((windows_count + 1)) || true + done + + # macOS — disambiguate x64 vs arm64 (Compose outputs identical filenames per arch) + for f in artifacts/macos-installers-x64/*.dmg artifacts/macos-installers-x64/*.pkg; do + [ -f "$f" ] || continue + base="$(basename "$f")" + ext="${base##*.}" + stem="${base%.*}" + stage "$f" "${stem}-x64.${ext}" && macos_x64_count=$((macos_x64_count + 1)) || true + done + for f in artifacts/macos-installers-arm64/*.dmg artifacts/macos-installers-arm64/*.pkg; do + [ -f "$f" ] || continue + base="$(basename "$f")" + ext="${base##*.}" + stem="${base%.*}" + stage "$f" "${stem}-arm64.${ext}" && macos_arm64_count=$((macos_arm64_count + 1)) || true + done + + # Linux modern — default Debian/RPM (unprefixed) + for f in artifacts/linux-installers-modern/*.deb artifacts/linux-installers-modern/*.rpm; do + [ -f "$f" ] || continue + stage "$f" "$(basename "$f")" && linux_modern_count=$((linux_modern_count + 1)) || true + done + + # Linux debian12-compat — suffix to avoid collision with modern + for f in artifacts/linux-installers-debian12-compat/*.deb artifacts/linux-installers-debian12-compat/*.rpm; do + [ -f "$f" ] || continue + base="$(basename "$f")" + ext="${base##*.}" + stem="${base%.*}" + stage "$f" "${stem}-debian12.${ext}" && linux_debian12_count=$((linux_debian12_count + 1)) || true + done + + # Linux AppImage + zsync (filenames already include -x86_64) + for f in artifacts/linux-appimage/*.AppImage artifacts/linux-appimage/*.AppImage.zsync; do + [ -f "$f" ] || continue + stage "$f" "$(basename "$f")" && linux_appimage_count=$((linux_appimage_count + 1)) || true + done + + # Linux Arch (.pkg.tar.zst already has version + arch in filename) + for f in artifacts/linux-arch/*.pkg.tar.zst; do + [ -f "$f" ] || continue + stage "$f" "$(basename "$f")" && linux_arch_count=$((linux_arch_count + 1)) || true + done + + echo + echo "Final staged files:" + ls -la release-files/ + echo + echo "Per-group counts: windows=$windows_count macos-x64=$macos_x64_count macos-arm64=$macos_arm64_count linux-modern=$linux_modern_count linux-debian12=$linux_debian12_count linux-appimage=$linux_appimage_count linux-arch=$linux_arch_count" + + # Completeness guard: refuse to ship an incomplete release. Each + # group must produce >= 1 staged file. Without this guard, a build + # regression (e.g. AppImage step silently producing nothing) would + # ship a draft release missing the affected group and we'd discover + # it only when users complained. + missing=() + [ "$windows_count" -eq 0 ] && missing+=("Windows installers (.exe/.msi)") + [ "$macos_x64_count" -eq 0 ] && missing+=("macOS x64 (.dmg/.pkg)") + [ "$macos_arm64_count" -eq 0 ] && missing+=("macOS arm64 (.dmg/.pkg)") + [ "$linux_modern_count" -eq 0 ] && missing+=("Linux modern (.deb/.rpm)") + [ "$linux_debian12_count" -eq 0 ] && missing+=("Linux debian12-compat (.deb/.rpm)") + [ "$linux_appimage_count" -eq 0 ] && missing+=("Linux AppImage (.AppImage/.zsync)") + [ "$linux_arch_count" -eq 0 ] && missing+=("Linux Arch (.pkg.tar.zst)") + + if [ ${#missing[@]} -gt 0 ]; then + echo + echo "ERROR: missing artifacts for the following groups:" + printf " - %s\n" "${missing[@]}" + echo + echo "Refusing to publish an incomplete release." + exit 1 + fi + shell: bash + + - name: Create or update draft release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.version.outputs.tag }} + VERSION: ${{ steps.version.outputs.version }} + run: | + set -euo pipefail + if gh release view "$TAG" >/dev/null 2>&1; then + # Release with this tag exists. Only clobber assets if it's still + # a DRAFT — we never silently overwrite assets on a published + # release. Operator must bump projectVersionName in + # gradle/libs.versions.toml to roll a new tag. + is_draft=$(gh release view "$TAG" --json isDraft -q '.isDraft') + if [ "$is_draft" = "true" ]; then + echo "Draft release $TAG already exists — replacing assets via --clobber..." + gh release upload "$TAG" release-files/* --clobber + else + echo "ERROR: release $TAG is already PUBLISHED." + echo "Refusing to clobber published assets." + echo + echo "If you intended to ship a new version, bump" + echo "projectVersionName in gradle/libs.versions.toml first," + echo "then re-push to generate-installers." + exit 1 + fi + else + echo "Creating new draft release $TAG..." + gh release create "$TAG" \ + --draft \ + --title "$VERSION" \ + --generate-notes \ + release-files/* + fi + shell: bash