Package Installers (jpackage) #98
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Package Installers (jpackage) | |
| permissions: | |
| contents: write | |
| actions: read | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: "Release version (e.g., 1.2.3)" | |
| required: true | |
| type: string | |
| workflow_call: | |
| inputs: | |
| version: | |
| description: "Release version (e.g., 1.2.3)" | |
| required: true | |
| type: string | |
| workflow_run: | |
| workflows: [ "Create Release" ] | |
| types: | |
| - completed | |
| jobs: | |
| build-package: | |
| if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' || !github.event.workflow_run.conclusion }} | |
| name: Build installers on ${{ matrix.os }} | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 60 | |
| env: | |
| VERSION: ${{ inputs.version || github.event.inputs.version }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # Linux builds - split by installer type for parallelism | |
| - os: ubuntu-latest | |
| arch: native | |
| installer_type: deb | |
| - os: ubuntu-latest | |
| arch: native | |
| installer_type: rpm | |
| # Windows build | |
| - os: windows-latest | |
| arch: native | |
| installer_type: msi | |
| # macOS build - Apple Silicon (also runs on Intel via Rosetta 2) | |
| - os: macos-latest | |
| arch: arm64 | |
| installer_type: pkg | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Verify runner architecture (macOS only) | |
| if: matrix.arch != 'native' | |
| run: | | |
| EXPECTED="${{ matrix.arch }}" | |
| ACTUAL="$(uname -m)" | |
| echo "Expected architecture: $EXPECTED" | |
| echo "Actual architecture: $ACTUAL" | |
| if [ "$EXPECTED" = "arm64" ] && [ "$ACTUAL" != "arm64" ]; then | |
| echo "ERROR: Expected arm64 but runner is $ACTUAL" | |
| exit 1 | |
| fi | |
| if [ "$EXPECTED" = "x86_64" ] && [ "$ACTUAL" != "x86_64" ]; then | |
| echo "ERROR: Expected x86_64 but runner is $ACTUAL" | |
| exit 1 | |
| fi | |
| echo "✓ Architecture verification passed" | |
| shell: bash | |
| - name: Set up Temurin JDK 21 (default/native) | |
| if: matrix.arch == 'native' | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: '21' | |
| cache: 'gradle' | |
| - name: Set up Temurin JDK 21 (Intel x86_64) | |
| if: matrix.arch == 'x86_64' | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: '21' | |
| cache: 'gradle' | |
| architecture: 'x64' | |
| - name: Set up Temurin JDK 21 (Apple Silicon arm64) | |
| if: matrix.arch == 'arm64' | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: '21' | |
| cache: 'gradle' | |
| architecture: 'arm64' | |
| - name: Set up Gradle | |
| uses: gradle/actions/setup-gradle@v5 | |
| - name: Ensure WiX 3.x available (Windows) | |
| if: matrix.os == 'windows-latest' | |
| shell: powershell | |
| run: | | |
| $wix = Get-Command candle.exe -ErrorAction SilentlyContinue | |
| if ($wix) { | |
| Write-Host "WiX already available: $($wix.Path)" | |
| candle.exe -? | Select-Object -First 1 | |
| exit 0 | |
| } else { | |
| choco install wixtoolset -y --no-progress | |
| } | |
| - name: Install Linux packaging prerequisites | |
| if: matrix.os == 'ubuntu-latest' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y rpm fakeroot libfuse2 | |
| - name: Verify Java and jpackage | |
| run: | | |
| java -version | |
| jpackage --version | |
| - name: Build and create installers for this OS (quiet) | |
| run: | | |
| set -e | |
| # Ensure log directory exists | |
| mkdir -p build | |
| # Determine which jpackage task to run based on installer type | |
| case "${{ matrix.installer_type }}" in | |
| deb) | |
| JPACKAGE_TASK="jpackageLinuxDeb" | |
| ;; | |
| rpm) | |
| JPACKAGE_TASK="jpackageLinuxRpm" | |
| ;; | |
| msi) | |
| JPACKAGE_TASK="jpackageWin" | |
| ;; | |
| pkg) | |
| JPACKAGE_TASK="jpackageMacPkg" | |
| ;; | |
| *) | |
| echo "Unknown installer type: ${{ matrix.installer_type }}" | |
| exit 1 | |
| ;; | |
| esac | |
| echo "Running Gradle task: gui:$JPACKAGE_TASK" | |
| # Run Gradle quietly and capture all output to a log. On failure, print only the tail. | |
| # Use --parallel to enable parallel task execution within Gradle | |
| ./gradlew --no-daemon --parallel --stacktrace --quiet --console=plain -Dorg.gradle.warning.mode=none \ | |
| gui:$JPACKAGE_TASK \ | |
| > build/ci-gradle.log 2>&1 \ | |
| || (echo "Gradle build failed — showing last 400 lines of log:" && tail -n 400 build/ci-gradle.log && exit 1) | |
| echo "Gradle build succeeded. (Logs suppressed; see build/ci-gradle.log in the artifact if needed.)" | |
| # Copy installers to build/dist for artifact upload | |
| mkdir -p build/dist | |
| find gui/build/jpackage -type f \( -name "*.deb" -o -name "*.rpm" -o -name "*.msi" -o -name "*.pkg" \) -exec cp {} build/dist/ \; | |
| shell: bash | |
| - name: Re-run macOS with deep jpackage diagnostics (only on macOS) | |
| if: startsWith(matrix.os, 'macos') && failure() | |
| run: | | |
| echo "Re-running macOS packaging with --debug to surface jpackage stderr" | |
| ./gradlew --no-daemon --debug -PciVerbose=true gui:jpackageMacPkg | |
| shell: bash | |
| - name: Rename macOS .pkg files to match release version | |
| if: startsWith(matrix.os, 'macos') | |
| env: | |
| MACOS_ARCH: ${{ matrix.arch }} | |
| run: | | |
| set -e | |
| echo "=== Renaming macOS .pkg files ===" | |
| # Use the version from workflow input or gradle.properties | |
| VERSION="${{ inputs.version || github.event.inputs.version }}" | |
| if [ -z "$VERSION" ]; then | |
| echo "No version from workflow input, reading from gradle.properties" | |
| VERSION=$(grep '^version=' gradle.properties | cut -d'=' -f2 | tr -d '\r') | |
| fi | |
| if [ -z "$VERSION" ]; then | |
| echo "ERROR: Could not determine version for renaming!" | |
| exit 1 | |
| fi | |
| echo "Target version: $VERSION" | |
| echo "" | |
| # Check if build/dist exists | |
| if [ ! -d "build/dist" ]; then | |
| echo "ERROR: build/dist directory does not exist!" | |
| echo "Listing contents of build:" | |
| ls -la build/ || echo "build/ does not exist" | |
| exit 1 | |
| fi | |
| echo "=== Contents of build/dist before renaming: ===" | |
| find build/dist -type f -name '*.pkg' -exec ls -lh {} \; || echo "No .pkg files found" | |
| echo "" | |
| # Find all .pkg files and rename them (using array to avoid subshell issues) | |
| FOUND_FILES=0 | |
| RENAMED_FILES=0 | |
| while IFS= read -r -d '' PKG_ORIG; do | |
| FOUND_FILES=$((FOUND_FILES + 1)) | |
| BASENAME=$(basename "$PKG_ORIG") | |
| echo "Found .pkg file: $PKG_ORIG" | |
| # Check if filename contains version pattern like 1.34.3 | |
| if [[ "$BASENAME" =~ -1\.[0-9]+\.[0-9]+\.pkg$ ]]; then | |
| # Replace 1.x.y with the target version | |
| PKG_NEW=$(echo "$PKG_ORIG" | sed "s/-1\.\([0-9]\+\)\.\([0-9]\+\)\.pkg$/-${VERSION}.pkg/") | |
| echo " Pattern matched! Renaming to: $PKG_NEW" | |
| if mv "$PKG_ORIG" "$PKG_NEW"; then | |
| echo " ✓ Successfully renamed" | |
| RENAMED_FILES=$((RENAMED_FILES + 1)) | |
| else | |
| echo " ✗ Failed to rename!" | |
| exit 1 | |
| fi | |
| # Note: We no longer append arch suffix since we only build one macOS PKG (ARM, works on Intel via Rosetta 2) | |
| else | |
| echo " Skipping (no version 1.x.y pattern found in: $BASENAME)" | |
| fi | |
| echo "" | |
| done < <(find build/dist -type f -name '*.pkg' -print0) | |
| echo "=== Summary ===" | |
| echo "Files found: $FOUND_FILES" | |
| echo "Files renamed: $RENAMED_FILES" | |
| echo "" | |
| if [ $FOUND_FILES -eq 0 ]; then | |
| echo "WARNING: No .pkg files found in build/dist!" | |
| fi | |
| echo "=== Final .pkg files in build/dist: ===" | |
| find build/dist -type f -name '*.pkg' -exec ls -lh {} \; || echo "No .pkg files found" | |
| shell: bash | |
| - name: Import GPG key (Linux/macOS) | |
| if: matrix.os != 'windows-latest' | |
| run: echo "$GPG_PRIVATE_KEY" | gpg --batch --import | |
| env: | |
| GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} | |
| - name: Import GPG key (Windows) | |
| if: matrix.os == 'windows-latest' | |
| shell: pwsh | |
| run: | | |
| Set-Content -Path gpg-key.asc -Value $env:GPG_PRIVATE_KEY -NoNewline | |
| gpg --batch --import gpg-key.asc | |
| env: | |
| GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} | |
| - name: Generate SHA256 checksums (Linux/macOS) | |
| if: matrix.os != 'windows-latest' | |
| run: | | |
| cd build/dist | |
| find . -type f -exec sha256sum {} + > SHA256SUMS | |
| - name: Generate SHA256 checksums (Windows) | |
| if: matrix.os == 'windows-latest' | |
| shell: pwsh | |
| run: | | |
| Set-Location build/dist | |
| Get-ChildItem -Recurse -File | ForEach-Object { | |
| $hash = Get-FileHash $_.FullName -Algorithm SHA256 | |
| "$($hash.Hash) $($_.FullName.Substring((Get-Location).Path.Length + 1))" | Out-File -Encoding ascii -Append SHA256SUMS | |
| } | |
| - name: Sign SHA256SUMS (Linux/macOS) | |
| if: matrix.os != 'windows-latest' | |
| run: | | |
| cd build/dist | |
| gpg --batch --yes --pinentry-mode loopback \ | |
| --passphrase "$GPG_PASSPHRASE" \ | |
| --armor --detach-sign SHA256SUMS | |
| env: | |
| GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} | |
| - name: Sign SHA256SUMS (Windows) | |
| if: matrix.os == 'windows-latest' | |
| shell: pwsh | |
| run: | | |
| Set-Location build/dist | |
| gpg --batch --yes --pinentry-mode loopback --passphrase "$env:GPG_PASSPHRASE" --armor --detach-sign SHA256SUMS | |
| env: | |
| GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} | |
| - name: Upload installers | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: installers-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.installer_type }} | |
| path: | | |
| build/dist/**/* | |
| build/ci-gradle.log | |
| build/dist/SHA256SUMS | |
| build/dist/SHA256SUMS.asc | |
| if-no-files-found: warn | |
| upload-to-release: | |
| name: Upload installers to GitHub Release | |
| needs: build-package | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call' || github.event_name == 'push' || github.event_name == 'workflow_run' | |
| steps: | |
| - name: Download all installer artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: all-installers | |
| - name: Download existing release assets (JARs, sample bots, etc.) | |
| run: | | |
| set -euo pipefail | |
| # Determine version (handle both workflow_dispatch and workflow_call) | |
| VERSION="${{ inputs.version || github.event.inputs.version }}" | |
| if [ -z "$VERSION" ]; then | |
| echo "No version provided, detecting from latest release..." | |
| VERSION=$(curl -sS -H "Authorization: token $GITHUB_TOKEN" \ | |
| "https://api.github.com/repos/${{ github.repository }}/releases/latest" \ | |
| | jq -r '.tag_name' | sed 's/^v//') | |
| fi | |
| if [ -z "$VERSION" ] || [ "$VERSION" == "null" ]; then | |
| echo "ERROR: Could not determine version" | |
| exit 1 | |
| fi | |
| TAG="v${VERSION}" | |
| echo "Downloading assets for release: $TAG" | |
| # Get release info (including draft releases) | |
| releases=$(curl -sS -H "Authorization: token $GITHUB_TOKEN" \ | |
| "https://api.github.com/repos/${{ github.repository }}/releases") | |
| release_info=$(echo "$releases" | jq -r ".[] | select(.tag_name == \"$TAG\")") | |
| if [ -z "$release_info" ]; then | |
| echo "WARNING: Release $TAG not found or has no assets yet. Skipping asset download." | |
| exit 0 | |
| fi | |
| # Show ALL assets in the release for debugging | |
| echo "=== ALL Assets in Release $TAG ===" | |
| echo "$release_info" | jq -r '.assets[] | "\(.name) (\(.size) bytes)"' | |
| echo "" | |
| # Create directory for release assets | |
| mkdir -p all-installers/release-assets | |
| # Extract asset information for debugging | |
| echo "=== Release Assets Found ===" | |
| echo "$release_info" | jq -r '.assets[] | select(.name | test("\\.(jar|zip)$")) | "\(.name) (\(.size) bytes, ID: \(.id))"' | |
| echo "=== Starting Downloads ===" | |
| # Download all non-installer assets (JARs, sample bots) | |
| # Use the asset API URL with Accept header for draft releases | |
| while IFS='|' read -r asset_id asset_name asset_size; do | |
| if [ -n "$asset_id" ]; then | |
| echo "Downloading: $asset_name ($asset_size bytes, ID: $asset_id)" | |
| # Use GitHub API to download asset with proper authentication | |
| # This works for both draft and published releases | |
| if ! curl -fSL \ | |
| -H "Authorization: token $GITHUB_TOKEN" \ | |
| -H "Accept: application/octet-stream" \ | |
| -o "all-installers/release-assets/$asset_name" \ | |
| "https://api.github.com/repos/${{ github.repository }}/releases/assets/$asset_id" 2>&1; then | |
| echo "ERROR: Failed to download $asset_name" | |
| exit 1 | |
| fi | |
| # Show what we got | |
| downloaded_size=$(stat -c%s "all-installers/release-assets/$asset_name" 2>/dev/null || stat -f%z "all-installers/release-assets/$asset_name" 2>/dev/null || echo "unknown") | |
| echo " Downloaded: $downloaded_size bytes" | |
| # Verify size matches | |
| if [ "$downloaded_size" != "$asset_size" ] && [ "$downloaded_size" != "unknown" ]; then | |
| echo " WARNING: Downloaded size ($downloaded_size) doesn't match expected size ($asset_size)" | |
| fi | |
| fi | |
| done < <(echo "$release_info" | jq -r '.assets[] | select(.name | test("\\.(jar|zip)$")) | "\(.id)|\(.name)|\(.size)"') | |
| # Verify downloaded files are not HTML error pages | |
| echo "" | |
| echo "=== Verifying Downloaded Files ===" | |
| file_count=0 | |
| for file in all-installers/release-assets/*; do | |
| if [ -f "$file" ]; then | |
| file_count=$((file_count + 1)) | |
| filename=$(basename "$file") | |
| # Check file type | |
| filetype=$(file -b "$file") | |
| # Check if file is HTML (error page) instead of expected binary | |
| if echo "$filetype" | grep -qi "HTML"; then | |
| echo "❌ ERROR: $filename is HTML (likely an error page)" | |
| echo "File type: $filetype" | |
| echo "Content preview:" | |
| head -n 20 "$file" | |
| exit 1 | |
| fi | |
| # Show size and checksum | |
| size=$(ls -lh "$file" | awk '{print $5}') | |
| checksum=$(sha256sum "$file" | awk '{print $1}') | |
| echo "✓ $filename" | |
| echo " Size: $size" | |
| echo " Type: $filetype" | |
| echo " SHA256: $checksum" | |
| fi | |
| done | |
| if [ $file_count -eq 0 ]; then | |
| echo "WARNING: No files were downloaded!" | |
| else | |
| echo "" | |
| echo "Successfully verified $file_count file(s)" | |
| fi | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: List downloaded files (debug) | |
| run: | | |
| find all-installers | |
| - name: Ensure jq is available (required for URL encoding) | |
| run: | | |
| if ! command -v jq >/dev/null 2>&1; then | |
| sudo apt-get update && sudo apt-get install -y jq | |
| else | |
| echo "jq already installed" | |
| fi | |
| - name: Deduplicate SHA256SUMS files and remove build logs | |
| run: | | |
| # Keep only the first found SHA256SUMS and SHA256SUMS.asc | |
| find all-installers -name 'SHA256SUMS' | head -n 1 | xargs -I{} cp {} all-installers/SHA256SUMS | |
| find all-installers -name 'SHA256SUMS.asc' | head -n 1 | xargs -I{} cp {} all-installers/SHA256SUMS.asc | |
| # Remove all other SHA256SUMS and SHA256SUMS.asc | |
| find all-installers -name 'SHA256SUMS' ! -path 'all-installers/SHA256SUMS' -delete | |
| find all-installers -name 'SHA256SUMS.asc' ! -path 'all-installers/SHA256SUMS.asc' -delete | |
| # Remove ci-gradle.log files (debug logs not needed in release) | |
| find all-installers -name 'ci-gradle.log' -delete | |
| - name: Ensure release is published with correct tag | |
| run: | | |
| set -euo pipefail | |
| # Determine version | |
| VERSION="${{ inputs.version || github.event.inputs.version }}" | |
| if [ -z "$VERSION" ]; then | |
| echo "No version provided via inputs. Attempting to detect from latest release..." | |
| VERSION=$(curl -sS -H "Authorization: token $GITHUB_TOKEN" \ | |
| "https://api.github.com/repos/${{ github.repository }}/releases/latest" \ | |
| | jq -r '.tag_name' | sed 's/^v//') | |
| fi | |
| if [ -z "$VERSION" ] || [ "$VERSION" == "null" ]; then | |
| echo "ERROR: Could not determine release version" | |
| exit 1 | |
| fi | |
| TAG="v${VERSION}" | |
| REPO="${{ github.repository }}" | |
| API_URL="https://api.github.com/repos/$REPO/releases" | |
| echo "Ensuring release $TAG exists and is published..." | |
| # Find release by tag | |
| release_info=$(curl -sS -H "Authorization: token $GITHUB_TOKEN" "$API_URL" | jq -c ".[] | select(.tag_name == \"$TAG\")" | head -n 1) | |
| release_id=$(echo "$release_info" | jq -r '.id // empty') | |
| is_draft=$(echo "$release_info" | jq -r '.draft // empty') | |
| if [ -z "$release_id" ]; then | |
| echo "Release with tag $TAG not found. Creating published release..." | |
| create_response=$(curl -sS -X POST "$API_URL" \ | |
| -H "Authorization: token $GITHUB_TOKEN" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{\"tag_name\": \"$TAG\", \"name\": \"Release $VERSION\", \"draft\": false, \"prerelease\": false}") | |
| echo "Created release: $(echo "$create_response" | jq -r '.html_url')" | |
| elif [ "$is_draft" = "true" ]; then | |
| echo "Release $TAG is a draft (ID: $release_id). Publishing it..." | |
| curl -sS -X PATCH "$API_URL/$release_id" \ | |
| -H "Authorization: token $GITHUB_TOKEN" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{\"draft\": false}" | |
| echo "Release $TAG is now published." | |
| else | |
| echo "Release $TAG is already published (ID: $release_id)." | |
| fi | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| shell: bash | |
| - name: Upload installers to GitHub Release | |
| run: | | |
| set -euo pipefail | |
| # Determine version from inputs or detect from latest release (handle both workflow_dispatch and workflow_call) | |
| VERSION="${{ inputs.version || github.event.inputs.version }}" | |
| if [ -z "$VERSION" ]; then | |
| echo "No version provided via inputs. Attempting to detect from latest release..." | |
| VERSION=$(curl -sS -H "Authorization: token $GITHUB_TOKEN" \ | |
| "https://api.github.com/repos/${{ github.repository }}/releases/latest" \ | |
| | jq -r '.tag_name' | sed 's/^v//') | |
| fi | |
| if [ -z "$VERSION" ] || [ "$VERSION" == "null" ]; then | |
| echo "ERROR: Could not determine release version" | |
| exit 1 | |
| fi | |
| TAG="v${VERSION}" | |
| echo "Working with release version: $VERSION (tag: $TAG)" | |
| # Import GPG key so we can sign a combined SHA256SUMS for ALL artifacts | |
| echo "$GPG_PRIVATE_KEY" > /tmp/gpg-key.asc | |
| gpg --batch --import /tmp/gpg-key.asc | |
| # Create combined SHA256SUMS from all downloaded artifacts (exclude existing SHA files) | |
| cd all-installers | |
| # Generate checksums with only filenames (no paths) | |
| # Note: For .pkg files, we fix the version in the filename (1.x.y -> actual VERSION) | |
| > SHA256SUMS # Clear the file | |
| find . -type f ! -name 'SHA256SUMS*' ! -name 'ci-gradle.log' -print0 | sort -z | while IFS= read -r -d '' file; do | |
| # Calculate checksum and extract just the filename (no path) | |
| filename=$(basename "$file") | |
| checksum=$(sha256sum "$file" | awk '{print $1}') | |
| # Fix .pkg filename in checksum (macOS jpackage uses 1.x.y due to Apple requirements, | |
| # but we want the filename to show the actual version (e.g., 0.x.y) | |
| if [[ "$filename" =~ -1\.[0-9]+\.[0-9]+\.pkg$ ]]; then | |
| fixed_filename=$(echo "$filename" | sed "s/-1\.\([0-9]\+\)\.\([0-9]\+\)\.pkg$/-${VERSION}.pkg/") | |
| echo " Fixing .pkg checksum filename: $filename -> $fixed_filename" | |
| filename="$fixed_filename" | |
| fi | |
| # Detect architecture from the original file basename and append arch to .pkg checksum filename | |
| orig_base=$(basename "$file") | |
| arch="" | |
| if [[ "$orig_base" =~ (x86_64|x86|intel) ]]; then | |
| arch="x86_64" | |
| elif [[ "$orig_base" =~ (arm64|aarch64|arm) ]]; then | |
| arch="arm64" | |
| fi | |
| if [ -n "$arch" ]; then | |
| if [[ "$filename" =~ \.pkg$ ]] && [[ "$filename" != *".$arch.pkg" ]]; then | |
| filename=$(echo "$filename" | sed -E "s/\.pkg$/.$arch.pkg/") | |
| echo " Appending arch to checksum filename: $filename" | |
| fi | |
| fi | |
| printf "%s %s\n" "$checksum" "$filename" >> SHA256SUMS | |
| done | |
| # Sign the combined SHA256SUMS | |
| gpg --batch --yes --pinentry-mode loopback --passphrase "$GPG_PASSPHRASE" --armor --detach-sign SHA256SUMS | |
| # Return to the original directory | |
| cd .. | |
| # Get upload URL for the release | |
| # Note: We query all releases because draft releases may not be fetchable by tag | |
| API_URL="https://api.github.com/repos/${{ github.repository }}/releases" | |
| echo "Fetching releases from: $API_URL" | |
| echo "Looking for release with tag: $TAG" | |
| releases_response=$(curl -sS -H "Authorization: token $GITHUB_TOKEN" "$API_URL") | |
| # Find the release matching our tag (including draft releases) - get only the first match | |
| release_info=$(echo "$releases_response" | jq -c ".[] | select(.tag_name == \"$TAG\")" | head -n 1) | |
| if [ -z "$release_info" ]; then | |
| echo "ERROR: Could not find release with tag '$TAG'" | |
| echo "Available releases:" | |
| echo "$releases_response" | jq -r '.[] | "\(.tag_name) - \(.name) (draft: \(.draft))"' | head -10 | |
| exit 1 | |
| fi | |
| upload_url=$(echo "$release_info" | jq -r '.upload_url' | sed -e 's/{.*//') | |
| release_id=$(echo "$release_info" | jq -r '.id') | |
| is_draft=$(echo "$release_info" | jq -r '.draft') | |
| if [ -z "$upload_url" ] || [ "$upload_url" == "null" ]; then | |
| echo "ERROR: Could not get upload URL for release" | |
| echo "Release info:" | |
| echo "$release_info" | jq . | |
| exit 1 | |
| fi | |
| echo "Found release: ID=$release_id, tag=$TAG, draft=$is_draft" | |
| echo "Upload URL: $upload_url" | |
| # Helper to URI-encode strings (jq required) | |
| urlencode() { jq -nr --arg v "$1" '$v|@uri'; } | |
| # Function to delete ALL installer assets (cleanup stale assets from previous runs) | |
| cleanup_installer_assets() { | |
| echo "=== Cleaning up existing installer assets ===" | |
| local assets=$(curl -sS -H "Authorization: token $GITHUB_TOKEN" \ | |
| "https://api.github.com/repos/${{ github.repository }}/releases/$release_id/assets") | |
| # Find all assets matching installer patterns (.deb, .rpm, .msi, .pkg) | |
| echo "$assets" | jq -r '.[] | select(.name | test("\\.(deb|rpm|msi|pkg)$")) | "\(.id)|\(.name)"' | while IFS='|' read -r asset_id asset_name; do | |
| if [ -n "$asset_id" ] && [ "$asset_id" != "null" ]; then | |
| echo "Deleting stale installer asset: $asset_name (ID: $asset_id)" | |
| curl -sS -X DELETE \ | |
| -H "Authorization: token $GITHUB_TOKEN" \ | |
| "https://api.github.com/repos/${{ github.repository }}/releases/assets/$asset_id" | |
| fi | |
| done | |
| echo "=== Cleanup complete ===" | |
| } | |
| # Function to delete an existing asset if it exists | |
| delete_existing_asset() { | |
| local asset_name="$1" | |
| # Get list of assets for this release | |
| local assets=$(curl -sS -H "Authorization: token $GITHUB_TOKEN" \ | |
| "https://api.github.com/repos/${{ github.repository }}/releases/$release_id/assets") | |
| # Find asset with matching name | |
| local asset_id=$(echo "$assets" | jq -r ".[] | select(.name == \"$asset_name\") | .id") | |
| if [ -n "$asset_id" ] && [ "$asset_id" != "null" ]; then | |
| echo "Deleting existing asset: $asset_name (ID: $asset_id)" | |
| curl -sS -X DELETE \ | |
| -H "Authorization: token $GITHUB_TOKEN" \ | |
| "https://api.github.com/repos/${{ github.repository }}/releases/assets/$asset_id" | |
| echo | |
| fi | |
| } | |
| # Upload function: uploads a file and sets a user-friendly asset name and label | |
| upload_asset() { | |
| local file_path="$1" | |
| local desired_name="$2" | |
| local label="$3" | |
| # Delete existing asset if it exists (prevents 422 errors on re-runs) | |
| delete_existing_asset "$desired_name" | |
| # determine MIME type | |
| mime=$(file --brief --mime-type "$file_path" || echo application/octet-stream) | |
| enc_label=$(urlencode "$label") | |
| # Upload via GitHub uploads API | |
| echo "Uploading $file_path as '$desired_name' (label: $label)" | |
| curl --fail -sS -X POST \ | |
| -H "Authorization: token $GITHUB_TOKEN" \ | |
| -H "Content-Type: $mime" \ | |
| --data-binary @"$file_path" \ | |
| "$upload_url?name=$(urlencode "$desired_name")&label=${enc_label}" | |
| echo | |
| } | |
| # Clean up all old installer assets before uploading new ones | |
| cleanup_installer_assets | |
| # Map files to human-friendly labels and corrected asset names | |
| while IFS= read -r -d '' f; do | |
| rel=${f#./} | |
| base=$(basename "$rel") | |
| # Skip SHA256SUMS files - they will be uploaded explicitly at the end | |
| if [[ "$base" == "SHA256SUMS" || "$base" == "SHA256SUMS.asc" ]]; then | |
| continue | |
| fi | |
| # Default: upload with same name and a generic label | |
| asset_name="$base" | |
| label="${base}" | |
| # Fix common naming issues and set friendly labels | |
| case "$base" in | |
| robocode-tank-royale-gui-*.rpm) | |
| # Convert: name-version-release.arch.rpm -> name-version.arch.rpm (strip release) | |
| # Example: robocode-tank-royale-gui-0.34.2-1.x86_64.rpm -> robocode-tank-royale-gui-0.34.2.x86_64.rpm | |
| # Use perl with lookahead to only replace the '-<digits>.' that precedes the arch and .rpm | |
| asset_name=$(echo "$base" | perl -pe 's/-\d+\.(?=[^.]+\.rpm$)/./') | |
| # We only build one RPM (native on ubuntu-latest = x86_64) | |
| label="GUI for Linux (rpm)" | |
| ;; | |
| robocode-tank-royale-gui_*.deb | robocode-tank-royale-gui-*.deb) | |
| # We only build one DEB (native on ubuntu-latest = amd64) | |
| label="GUI for Linux (deb)" | |
| ;; | |
| robocode-tank-royale-gui-*.msi) | |
| label="GUI for Windows (msi)" | |
| ;; | |
| robocode-tank-royale-gui-*.pkg) | |
| # Fix macOS .pkg version: jpackage uses 1.x.y due to Apple requirements, | |
| # but we want the filename to show the actual version (e.g., 0.x.y) | |
| if [[ "$base" =~ -1\.[0-9]+\.[0-9]+\.pkg$ ]]; then | |
| asset_name=$(echo "$base" | sed "s/-1\.\([0-9]\+\)\.\([0-9]\+\)\.pkg$/-${VERSION}.pkg/") | |
| echo " Fixing macOS .pkg version: $base -> $asset_name" | |
| fi | |
| # Label for macOS (ARM build works on both Apple Silicon and Intel via Rosetta 2) | |
| label="GUI for macOS (pkg) - Apple Silicon - arm64" | |
| ;; | |
| robocode-tankroyale-gui-*.jar) | |
| label="GUI (jar)" | |
| ;; | |
| robocode-tankroyale-server-*.jar) | |
| label="Server (jar)" | |
| ;; | |
| *) | |
| # Detect sample-bots archives by name inspection | |
| lower=$(echo "$base" | tr '[:upper:]' '[:lower:]') | |
| if [[ "$lower" == *sample* && ( "$lower" == *csharp* || "$lower" == *cs* ) ]]; then | |
| label="Sample bots for C# (zip)" | |
| elif [[ "$lower" == *sample* && "$lower" == *java* ]]; then | |
| label="Sample bots for Java (zip)" | |
| elif [[ "$lower" == *sample* && ( "$lower" == *python* || "$lower" == *py* ) ]]; then | |
| label="Sample bots for Python (zip)" | |
| fi | |
| ;; | |
| esac | |
| upload_asset "$f" "$asset_name" "$label" | |
| done < <(find all-installers -type f -print0) | |
| # Finally upload the combined checksums and signature | |
| upload_asset "all-installers/SHA256SUMS" "SHA256SUMS" "SHA256 checksums" | |
| upload_asset "all-installers/SHA256SUMS.asc" "SHA256SUMS.asc" "SHA256 checksums (GPG signature)" | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} | |
| GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} | |
| shell: bash | |