Make prerelease the default branch: workflows trigger on main; add br… #205
Workflow file for this run
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: Release | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| jobs: | |
| release: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write # Required to create releases | |
| actions: read # Required to download artifacts from other workflows | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Python (release gates) | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.13' | |
| - name: Sync version from git tag | |
| run: python scripts/sync_version.py --tag ${{ github.ref_name }} | |
| - name: Validate version | |
| run: python scripts/validate_version.py | |
| - name: Validate changelog | |
| run: python scripts/validate_changelog.py | |
| - name: Wait for build workflows to complete | |
| run: | | |
| TAG="${{ github.ref }}" | |
| REPO="${{ github.repository }}" | |
| echo "Waiting for build workflows to complete for tag: ${TAG}" | |
| # Extract tag name (remove refs/tags/ prefix) | |
| TAG_NAME="${TAG#refs/tags/}" | |
| # Install jq for JSON parsing (more reliable than grep) | |
| sudo apt-get update && sudo apt-get install -y jq | |
| # Wait for both macOS and Windows build workflows to complete | |
| MAX_WAIT=3600 # 60 minutes max | |
| WAIT_INTERVAL=30 # Check every 30 seconds | |
| ELAPSED=0 | |
| while [ $ELAPSED -lt $MAX_WAIT ]; do | |
| echo "Checking workflow status (elapsed: ${ELAPSED}s)..." | |
| # Check macOS workflow using GitHub API | |
| # For tags, we need to search by ref (tag name) not head_branch | |
| MACOS_RESPONSE=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ | |
| "https://api.github.com/repos/${REPO}/actions/workflows/build-macos.yml/runs?ref=${TAG_NAME}&per_page=1") | |
| # Extract conclusion using jq (more reliable) | |
| MACOS_STATUS=$(echo "$MACOS_RESPONSE" | jq -r '.workflow_runs[0].conclusion // .workflow_runs[0].status // "unknown"' 2>/dev/null || echo "unknown") | |
| # Normalize status values | |
| if [ "$MACOS_STATUS" = "in_progress" ] || [ "$MACOS_STATUS" = "queued" ]; then | |
| MACOS_STATUS="running" | |
| fi | |
| # Check Windows workflow using GitHub API | |
| WINDOWS_RESPONSE=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ | |
| "https://api.github.com/repos/${REPO}/actions/workflows/build-windows.yml/runs?ref=${TAG_NAME}&per_page=1") | |
| # Extract conclusion using jq (more reliable) | |
| WINDOWS_STATUS=$(echo "$WINDOWS_RESPONSE" | jq -r '.workflow_runs[0].conclusion // .workflow_runs[0].status // "unknown"' 2>/dev/null || echo "unknown") | |
| # Normalize status values | |
| if [ "$WINDOWS_STATUS" = "in_progress" ] || [ "$WINDOWS_STATUS" = "queued" ]; then | |
| WINDOWS_STATUS="running" | |
| fi | |
| echo "macOS build status: ${MACOS_STATUS}" | |
| echo "Windows build status: ${WINDOWS_STATUS}" | |
| # Debug: show API response if status is unknown or empty | |
| if [ "$MACOS_STATUS" = "unknown" ] || [ "$WINDOWS_STATUS" = "unknown" ] || [ -z "$MACOS_STATUS" ] || [ -z "$WINDOWS_STATUS" ]; then | |
| echo "Debug - macOS API response (first 500 chars):" | |
| echo "$MACOS_RESPONSE" | head -c 500 | |
| echo "" | |
| echo "Debug - Windows API response (first 500 chars):" | |
| echo "$WINDOWS_RESPONSE" | head -c 500 | |
| echo "" | |
| fi | |
| if [ "${MACOS_STATUS}" = "success" ] && [ "${WINDOWS_STATUS}" = "success" ]; then | |
| echo "Both build workflows completed successfully!" | |
| exit 0 | |
| fi | |
| if [ "${MACOS_STATUS}" = "failure" ] || [ "${WINDOWS_STATUS}" = "failure" ]; then | |
| echo "One or more build workflows failed!" | |
| exit 1 | |
| fi | |
| # If workflows are still running or not found, wait and retry | |
| if [ "${MACOS_STATUS}" = "running" ] || [ "${WINDOWS_STATUS}" = "running" ] || [ "${MACOS_STATUS}" = "null" ] || [ "${WINDOWS_STATUS}" = "null" ]; then | |
| echo "Workflows still running or not found, waiting..." | |
| fi | |
| sleep $WAIT_INTERVAL | |
| ELAPSED=$((ELAPSED + WAIT_INTERVAL)) | |
| done | |
| echo "Timeout waiting for build workflows to complete" | |
| exit 1 | |
| - name: Download macOS artifact | |
| uses: dawidd6/action-download-artifact@v3 | |
| with: | |
| workflow: build-macos.yml | |
| name: macos-dmg | |
| path: artifacts/macos | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| if_no_artifact_found: fail | |
| commit: ${{ github.sha }} | |
| workflow_conclusion: success | |
| allow_forks: false | |
| - name: Download Windows artifact | |
| uses: dawidd6/action-download-artifact@v3 | |
| with: | |
| workflow: build-windows.yml | |
| name: windows-installer | |
| path: artifacts/windows | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| if_no_artifact_found: fail | |
| commit: ${{ github.sha }} | |
| workflow_conclusion: success | |
| allow_forks: false | |
| - name: Create Windows README | |
| run: | | |
| printf '%s\n' \ | |
| 'CuePoint - Installation (unsigned app)' \ | |
| '' \ | |
| 'Step 1: Open CuePoint Setup.exe' \ | |
| '' \ | |
| 'Step 2: After you get a popup called "Windows protected your PC", click "More info"' \ | |
| '' \ | |
| 'Step 3: Click "Run anyway" (this happens because we do not pay 200 euros/year to sign the app with Microsoft)' \ | |
| '' \ | |
| 'Step 4: Install and enjoy' \ | |
| > artifacts/windows/README.txt | |
| echo "Created artifacts/windows/README.txt" | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.13' | |
| - name: Generate THIRD_PARTY_LICENSES.txt for release | |
| run: | | |
| pip install -r requirements-build.txt -q | |
| python scripts/generate_licenses.py --output THIRD_PARTY_LICENSES.txt | |
| if [ ! -f THIRD_PARTY_LICENSES.txt ]; then | |
| echo "ERROR: License bundle generation failed" | |
| exit 1 | |
| fi | |
| - name: Generate SBOM | |
| run: | | |
| mkdir -p dist | |
| python scripts/generate_sbom.py --output dist/sbom.spdx.json | |
| - name: Generate checksums | |
| run: | | |
| python scripts/generate_checksums.py \ | |
| --artifacts $(ls artifacts/macos/*.dmg artifacts/windows/*.exe 2>/dev/null) \ | |
| --output SHA256SUMS \ | |
| --algorithms sha256 | |
| - name: Sign checksum file (optional, design 2.17) | |
| id: sign_checksums | |
| env: | |
| GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} | |
| GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} | |
| run: | | |
| if [ -z "$GPG_PRIVATE_KEY" ]; then | |
| echo "No GPG_PRIVATE_KEY secret; skipping checksum signing." | |
| exit 0 | |
| fi | |
| echo "Configuring GPG for headless CI..." | |
| mkdir -p ~/.gnupg | |
| echo "allow-loopback-pinentry" >> ~/.gnupg/gpg-agent.conf | |
| chmod 600 ~/.gnupg/gpg-agent.conf | |
| echo "Importing GPG key..." | |
| echo "$GPG_PRIVATE_KEY" | gpg --batch --import | |
| echo "Signing SHA256SUMS..." | |
| if [ -n "$GPG_PASSPHRASE" ]; then | |
| python scripts/sign_checksums.py SHA256SUMS --passphrase | |
| else | |
| python scripts/sign_checksums.py SHA256SUMS | |
| fi | |
| if [ -f SHA256SUMS.asc ]; then | |
| echo "Signed: SHA256SUMS.asc" | |
| fi | |
| - name: Sync version from git tag | |
| run: | | |
| echo "Syncing version.py from git tag: ${{ github.ref_name }}" | |
| python scripts/sync_version.py --tag ${{ github.ref_name }} | |
| if [ $? -ne 0 ]; then | |
| echo "Error: Version sync failed" | |
| exit 1 | |
| fi | |
| echo "[OK] Version synced successfully" | |
| - name: Generate release notes (from PRs, design 2.18) | |
| run: python scripts/generate_release_notes.py --tag ${{ github.ref_name }} --output RELEASE_NOTES.md | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| # All tags create a published release. Test tags (e.g. v1.0.3-test1.1) are | |
| # marked prerelease so assets are public (no 404); website uses releases/latest | |
| # which returns only non-prerelease, so it points to stable only. | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v1 | |
| with: | |
| files: | | |
| artifacts/macos/*.dmg | |
| artifacts/windows/*.exe | |
| artifacts/windows/README.txt | |
| dist/sbom.spdx.json | |
| THIRD_PARTY_LICENSES.txt | |
| SHA256SUMS | |
| body_path: RELEASE_NOTES.md | |
| draft: false | |
| prerelease: ${{ contains(github.ref_name, '-test') }} | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Upload checksum signature (if GPG signed) | |
| if: hashFiles('SHA256SUMS.asc') != '' | |
| run: gh release upload ${{ github.ref_name }} SHA256SUMS.asc --clobber | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # --- Test tag only: fetch existing test appcasts from gh-pages (design: two appcast feeds) --- | |
| - name: Fetch Existing Test Appcast Feeds | |
| if: ${{ contains(github.ref_name, '-test') }} | |
| run: | | |
| echo "=== Fetching Existing Test Appcast Feeds ===" | |
| mkdir -p updates/macos/test | |
| mkdir -p updates/windows/test | |
| git fetch origin gh-pages:gh-pages 2>/dev/null || echo "gh-pages branch not found, will create new test appcasts" | |
| if git show origin/gh-pages:updates/macos/test/appcast.xml > updates/macos/test/appcast.xml 2>/dev/null; then | |
| echo "[OK] Fetched existing macOS test appcast" | |
| else | |
| echo "[INFO] No existing macOS test appcast found, will create new one" | |
| rm -f updates/macos/test/appcast.xml | |
| fi | |
| if git show origin/gh-pages:updates/windows/test/appcast.xml > updates/windows/test/appcast.xml 2>/dev/null; then | |
| echo "[OK] Fetched existing Windows test appcast" | |
| else | |
| echo "[INFO] No existing Windows test appcast found, will create new one" | |
| rm -f updates/windows/test/appcast.xml | |
| fi | |
| - name: Generate Test Appcast Feeds | |
| if: ${{ contains(github.ref_name, '-test') }} | |
| run: | | |
| echo "=== Generating Test Appcast Feeds ===" | |
| VERSION="${{ github.ref_name }}" | |
| REPO="${{ github.repository }}" | |
| VERSION_NUM="${VERSION#v}" | |
| echo "Version: $VERSION_NUM" | |
| DMG_FILE=$(ls artifacts/macos/*.dmg 2>/dev/null | head -1) | |
| EXE_FILE=$(ls artifacts/windows/*.exe 2>/dev/null | head -1) | |
| if [ -z "$DMG_FILE" ] || [ -z "$EXE_FILE" ]; then | |
| echo "Warning: DMG or EXE file not found, skipping test appcast generation" | |
| exit 0 | |
| fi | |
| DMG_URL="https://github.com/$REPO/releases/download/$VERSION/$(basename $DMG_FILE)" | |
| EXE_URL="https://github.com/$REPO/releases/download/$VERSION/$(basename $EXE_FILE)" | |
| RELEASE_NOTES_URL="https://github.com/$REPO/releases/tag/$VERSION" | |
| if [ -f "$DMG_FILE" ]; then | |
| python scripts/generate_appcast.py \ | |
| --dmg "$DMG_FILE" \ | |
| --version "$VERSION_NUM" \ | |
| --url "$DMG_URL" \ | |
| --notes "$RELEASE_NOTES_URL" \ | |
| --output updates/macos/test/appcast.xml \ | |
| --append \ | |
| --channel test | |
| echo "[OK] macOS test appcast generated" | |
| fi | |
| if [ -f "$EXE_FILE" ]; then | |
| python scripts/generate_update_feed.py \ | |
| --exe "$EXE_FILE" \ | |
| --version "$VERSION_NUM" \ | |
| --url "$EXE_URL" \ | |
| --notes "$RELEASE_NOTES_URL" \ | |
| --output updates/windows/test/appcast.xml \ | |
| --append \ | |
| --channel test | |
| echo "[OK] Windows test appcast generated" | |
| fi | |
| echo "Test appcast generation complete" | |
| - name: Validate Test Appcast Feeds | |
| if: ${{ contains(github.ref_name, '-test') }} | |
| run: | | |
| echo "=== Validating Test Appcast Feeds ===" | |
| if [ -f "updates/macos/test/appcast.xml" ] && [ -f "updates/windows/test/appcast.xml" ]; then | |
| python scripts/validate_feeds.py \ | |
| --macos updates/macos/test/appcast.xml \ | |
| --windows updates/windows/test/appcast.xml | |
| echo "[OK] Test appcast feeds validated" | |
| else | |
| echo "Warning: Test appcast files not found, skipping validation" | |
| fi | |
| - name: Publish Test Appcast Feeds to GitHub Pages | |
| if: ${{ contains(github.ref_name, '-test') }} | |
| run: | | |
| echo "=== Publishing Test Appcast Feeds (no index) ===" | |
| if [ -f "updates/macos/test/appcast.xml" ] && [ -f "updates/windows/test/appcast.xml" ]; then | |
| python scripts/publish_feeds.py \ | |
| updates/macos/test/appcast.xml updates/windows/test/appcast.xml \ | |
| --branch gh-pages \ | |
| --message "Update appcast feeds (test) for ${{ github.ref_name }}" \ | |
| --github-token "${{ secrets.GITHUB_TOKEN }}" | |
| echo "[OK] Test appcast feeds published to gh-pages branch" | |
| else | |
| echo "Warning: Test appcast files not found, skipping publish" | |
| fi | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Fetch Existing Appcast Feeds | |
| if: ${{ !contains(github.ref_name, '-test') }} | |
| run: | | |
| echo "=== Fetching Existing Appcast Feeds ===" | |
| # Create updates directory structure | |
| mkdir -p updates/macos/stable | |
| mkdir -p updates/windows/stable | |
| # Try to fetch existing appcast files from gh-pages branch | |
| # This allows --append to work correctly | |
| git fetch origin gh-pages:gh-pages 2>/dev/null || echo "gh-pages branch not found, will create new appcasts" | |
| # Checkout existing appcast files if they exist | |
| if git show origin/gh-pages:updates/macos/stable/appcast.xml > updates/macos/stable/appcast.xml 2>/dev/null; then | |
| echo "[OK] Fetched existing macOS appcast" | |
| else | |
| echo "[INFO] No existing macOS appcast found, will create new one" | |
| rm -f updates/macos/stable/appcast.xml | |
| fi | |
| if git show origin/gh-pages:updates/windows/stable/appcast.xml > updates/windows/stable/appcast.xml 2>/dev/null; then | |
| echo "[OK] Fetched existing Windows appcast" | |
| else | |
| echo "[INFO] No existing Windows appcast found, will create new one" | |
| rm -f updates/windows/stable/appcast.xml | |
| fi | |
| - name: Generate Appcast Feeds | |
| if: ${{ !contains(github.ref_name, '-test') }} | |
| run: | | |
| echo "=== Generating Appcast Feeds ===" | |
| VERSION="${{ github.ref_name }}" | |
| REPO="${{ github.repository }}" | |
| # Extract version without 'v' prefix if present | |
| VERSION_NUM="${VERSION#v}" | |
| echo "Version: $VERSION_NUM" | |
| echo "Repository: $REPO" | |
| # Find DMG and EXE files | |
| DMG_FILE=$(ls artifacts/macos/*.dmg 2>/dev/null | head -1) | |
| EXE_FILE=$(ls artifacts/windows/*.exe 2>/dev/null | head -1) | |
| if [ -z "$DMG_FILE" ] || [ -z "$EXE_FILE" ]; then | |
| echo "Warning: DMG or EXE file not found, skipping appcast generation" | |
| echo " DMG: $DMG_FILE" | |
| echo " EXE: $EXE_FILE" | |
| exit 0 | |
| fi | |
| echo "DMG file: $DMG_FILE" | |
| echo "EXE file: $EXE_FILE" | |
| # Generate download URLs | |
| DMG_URL="https://github.com/$REPO/releases/download/$VERSION/$(basename $DMG_FILE)" | |
| EXE_URL="https://github.com/$REPO/releases/download/$VERSION/$(basename $EXE_FILE)" | |
| RELEASE_NOTES_URL="https://github.com/$REPO/releases/tag/$VERSION" | |
| echo "DMG URL: $DMG_URL" | |
| echo "EXE URL: $EXE_URL" | |
| # Generate macOS appcast (with --append to merge with existing) | |
| if [ -f "$DMG_FILE" ]; then | |
| echo "Generating macOS appcast..." | |
| python scripts/generate_appcast.py \ | |
| --dmg "$DMG_FILE" \ | |
| --version "$VERSION_NUM" \ | |
| --url "$DMG_URL" \ | |
| --notes "$RELEASE_NOTES_URL" \ | |
| --output updates/macos/stable/appcast.xml \ | |
| --append \ | |
| --channel stable | |
| echo "[OK] macOS appcast generated" | |
| fi | |
| # Generate Windows appcast (with --append to merge with existing) | |
| if [ -f "$EXE_FILE" ]; then | |
| echo "Generating Windows appcast..." | |
| python scripts/generate_update_feed.py \ | |
| --exe "$EXE_FILE" \ | |
| --version "$VERSION_NUM" \ | |
| --url "$EXE_URL" \ | |
| --notes "$RELEASE_NOTES_URL" \ | |
| --output updates/windows/stable/appcast.xml \ | |
| --append \ | |
| --channel stable | |
| echo "[OK] Windows appcast generated" | |
| fi | |
| echo "Appcast generation complete" | |
| - name: Validate Appcast Feeds | |
| if: ${{ !contains(github.ref_name, '-test') }} | |
| run: | | |
| echo "=== Validating Appcast Feeds ===" | |
| if [ -f "updates/macos/stable/appcast.xml" ] && [ -f "updates/windows/stable/appcast.xml" ]; then | |
| python scripts/validate_feeds.py \ | |
| --macos updates/macos/stable/appcast.xml \ | |
| --windows updates/windows/stable/appcast.xml | |
| echo "[OK] Appcast feeds validated" | |
| else | |
| echo "Warning: Appcast files not found, skipping validation" | |
| fi | |
| - name: Publish Appcast Feeds to GitHub Pages | |
| if: ${{ !contains(github.ref_name, '-test') }} | |
| run: | | |
| echo "=== Publishing Appcast Feeds ===" | |
| # Use publish_feeds.py script for reliable publishing | |
| if [ -f "updates/macos/stable/appcast.xml" ] && [ -f "updates/windows/stable/appcast.xml" ]; then | |
| python scripts/publish_feeds.py \ | |
| updates/macos/stable/appcast.xml updates/windows/stable/appcast.xml \ | |
| --branch gh-pages \ | |
| --message "Update appcast feeds for ${{ github.ref_name }}" \ | |
| --github-token "${{ secrets.GITHUB_TOKEN }}" \ | |
| --index gh-pages-root/index.html | |
| echo "[OK] Appcast feeds published to gh-pages branch" | |
| else | |
| echo "Warning: Appcast files not found, skipping publish" | |
| fi | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |