@@ -411,3 +411,174 @@ jobs:
411411 if-no-files-found : error
412412 retention-days : 30
413413 compression-level : 0
414+
415+ release :
416+ name : Draft release with all installers
417+ needs : [build-windows, build-macos, build-linux]
418+ runs-on : ubuntu-latest
419+ permissions :
420+ contents : write
421+
422+ steps :
423+ - name : Checkout code
424+ uses : actions/checkout@v4
425+
426+ - name : Read project version
427+ id : version
428+ run : |
429+ set -euo pipefail
430+ VERSION=$(grep 'projectVersionName' gradle/libs.versions.toml | head -1 | sed 's/.*= *"\(.*\)"/\1/')
431+ if [ -z "$VERSION" ]; then
432+ echo "ERROR: could not read projectVersionName from gradle/libs.versions.toml"
433+ exit 1
434+ fi
435+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
436+ echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
437+ echo "Resolved release tag: v$VERSION"
438+ shell : bash
439+
440+ - name : Download all build artifacts
441+ uses : actions/download-artifact@v4
442+ with :
443+ path : artifacts
444+
445+ - name : Stage release files (rename arch / compat collisions)
446+ run : |
447+ set -euo pipefail
448+ mkdir -p release-files
449+
450+ # stage() returns 0 on a successful copy, 1 if the source is missing.
451+ # Callers use the exit status to increment per-group counters so the
452+ # completeness guard at the end can detect missing groups.
453+ stage() {
454+ local src="$1"
455+ local target_name="$2"
456+ if [ -f "$src" ]; then
457+ cp "$src" "release-files/$target_name"
458+ echo "Staged: $target_name"
459+ return 0
460+ fi
461+ return 1
462+ }
463+
464+ windows_count=0
465+ macos_x64_count=0
466+ macos_arm64_count=0
467+ linux_modern_count=0
468+ linux_debian12_count=0
469+ linux_appimage_count=0
470+ linux_arch_count=0
471+
472+ # Windows — names already unique (.exe, .msi)
473+ for f in artifacts/windows-installers/*.exe artifacts/windows-installers/*.msi; do
474+ [ -f "$f" ] || continue
475+ stage "$f" "$(basename "$f")" && windows_count=$((windows_count + 1)) || true
476+ done
477+
478+ # macOS — disambiguate x64 vs arm64 (Compose outputs identical filenames per arch)
479+ for f in artifacts/macos-installers-x64/*.dmg artifacts/macos-installers-x64/*.pkg; do
480+ [ -f "$f" ] || continue
481+ base="$(basename "$f")"
482+ ext="${base##*.}"
483+ stem="${base%.*}"
484+ stage "$f" "${stem}-x64.${ext}" && macos_x64_count=$((macos_x64_count + 1)) || true
485+ done
486+ for f in artifacts/macos-installers-arm64/*.dmg artifacts/macos-installers-arm64/*.pkg; do
487+ [ -f "$f" ] || continue
488+ base="$(basename "$f")"
489+ ext="${base##*.}"
490+ stem="${base%.*}"
491+ stage "$f" "${stem}-arm64.${ext}" && macos_arm64_count=$((macos_arm64_count + 1)) || true
492+ done
493+
494+ # Linux modern — default Debian/RPM (unprefixed)
495+ for f in artifacts/linux-installers-modern/*.deb artifacts/linux-installers-modern/*.rpm; do
496+ [ -f "$f" ] || continue
497+ stage "$f" "$(basename "$f")" && linux_modern_count=$((linux_modern_count + 1)) || true
498+ done
499+
500+ # Linux debian12-compat — suffix to avoid collision with modern
501+ for f in artifacts/linux-installers-debian12-compat/*.deb artifacts/linux-installers-debian12-compat/*.rpm; do
502+ [ -f "$f" ] || continue
503+ base="$(basename "$f")"
504+ ext="${base##*.}"
505+ stem="${base%.*}"
506+ stage "$f" "${stem}-debian12.${ext}" && linux_debian12_count=$((linux_debian12_count + 1)) || true
507+ done
508+
509+ # Linux AppImage + zsync (filenames already include -x86_64)
510+ for f in artifacts/linux-appimage/*.AppImage artifacts/linux-appimage/*.AppImage.zsync; do
511+ [ -f "$f" ] || continue
512+ stage "$f" "$(basename "$f")" && linux_appimage_count=$((linux_appimage_count + 1)) || true
513+ done
514+
515+ # Linux Arch (.pkg.tar.zst already has version + arch in filename)
516+ for f in artifacts/linux-arch/*.pkg.tar.zst; do
517+ [ -f "$f" ] || continue
518+ stage "$f" "$(basename "$f")" && linux_arch_count=$((linux_arch_count + 1)) || true
519+ done
520+
521+ echo
522+ echo "Final staged files:"
523+ ls -la release-files/
524+ echo
525+ 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"
526+
527+ # Completeness guard: refuse to ship an incomplete release. Each
528+ # group must produce >= 1 staged file. Without this guard, a build
529+ # regression (e.g. AppImage step silently producing nothing) would
530+ # ship a draft release missing the affected group and we'd discover
531+ # it only when users complained.
532+ missing=()
533+ [ "$windows_count" -eq 0 ] && missing+=("Windows installers (.exe/.msi)")
534+ [ "$macos_x64_count" -eq 0 ] && missing+=("macOS x64 (.dmg/.pkg)")
535+ [ "$macos_arm64_count" -eq 0 ] && missing+=("macOS arm64 (.dmg/.pkg)")
536+ [ "$linux_modern_count" -eq 0 ] && missing+=("Linux modern (.deb/.rpm)")
537+ [ "$linux_debian12_count" -eq 0 ] && missing+=("Linux debian12-compat (.deb/.rpm)")
538+ [ "$linux_appimage_count" -eq 0 ] && missing+=("Linux AppImage (.AppImage/.zsync)")
539+ [ "$linux_arch_count" -eq 0 ] && missing+=("Linux Arch (.pkg.tar.zst)")
540+
541+ if [ ${#missing[@]} -gt 0 ]; then
542+ echo
543+ echo "ERROR: missing artifacts for the following groups:"
544+ printf " - %s\n" "${missing[@]}"
545+ echo
546+ echo "Refusing to publish an incomplete release."
547+ exit 1
548+ fi
549+ shell : bash
550+
551+ - name : Create or update draft release
552+ env :
553+ GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
554+ TAG : ${{ steps.version.outputs.tag }}
555+ VERSION : ${{ steps.version.outputs.version }}
556+ run : |
557+ set -euo pipefail
558+ if gh release view "$TAG" >/dev/null 2>&1; then
559+ # Release with this tag exists. Only clobber assets if it's still
560+ # a DRAFT — we never silently overwrite assets on a published
561+ # release. Operator must bump projectVersionName in
562+ # gradle/libs.versions.toml to roll a new tag.
563+ is_draft=$(gh release view "$TAG" --json isDraft -q '.isDraft')
564+ if [ "$is_draft" = "true" ]; then
565+ echo "Draft release $TAG already exists — replacing assets via --clobber..."
566+ gh release upload "$TAG" release-files/* --clobber
567+ else
568+ echo "ERROR: release $TAG is already PUBLISHED."
569+ echo "Refusing to clobber published assets."
570+ echo
571+ echo "If you intended to ship a new version, bump"
572+ echo "projectVersionName in gradle/libs.versions.toml first,"
573+ echo "then re-push to generate-installers."
574+ exit 1
575+ fi
576+ else
577+ echo "Creating new draft release $TAG..."
578+ gh release create "$TAG" \
579+ --draft \
580+ --title "$VERSION" \
581+ --generate-notes \
582+ release-files/*
583+ fi
584+ shell : bash
0 commit comments