Skip to content

Make prerelease the default branch: workflows trigger on main; add br… #205

Make prerelease the default branch: workflows trigger on main; add br…

Make prerelease the default branch: workflows trigger on main; add br… #205

Workflow file for this run

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 }}