name: Generate Updater Manifest on: workflow_run: workflows: - release-macos - release-linux - "Linux ARM64 (aarch64) Build" types: - completed concurrency: group: updater-manifest-${{ github.event.workflow_run.head_branch }} cancel-in-progress: true jobs: generate-manifest: # Only run on successful tag-triggered builds (not manual dispatches) if: > github.event.workflow_run.conclusion == 'success' && startsWith(github.event.workflow_run.head_branch, 'v') runs-on: ubuntu-latest permissions: contents: write steps: - name: Resolve version from tag id: version # `head_branch` on a `workflow_run` event is attacker-controllable # (an outside contributor could push a branch with an injection # payload as its name). Route it through an env var and validate # the shape before letting it flow into shell or $GITHUB_OUTPUT. env: HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} run: | set -euo pipefail if ! printf '%s' "$HEAD_BRANCH" | grep -Eq '^v[0-9][0-9A-Za-z._-]{0,63}$'; then echo "::error::Rejected head_branch for manifest job: $HEAD_BRANCH" exit 1 fi TAG="$HEAD_BRANCH" VERSION="${TAG#v}" echo "tag=$TAG" >> "$GITHUB_OUTPUT" echo "version=$VERSION" >> "$GITHUB_OUTPUT" - name: Wait for all platform builds to finish env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ steps.version.outputs.tag }} run: | # Wait up to 20 minutes for .sig files from all platforms REQUIRED_SIGS=("aarch64.app.tar.gz.sig" "x64.app.tar.gz.sig" "AppImage.sig") MAX_ATTEMPTS=40 SLEEP_SECONDS=30 for attempt in $(seq 1 $MAX_ATTEMPTS); do ASSETS=$(gh release view "$TAG" --repo "${{ github.repository }}" --json assets -q '.assets[].name' 2>/dev/null || echo "") FOUND_ALL=true for sig_suffix in "${REQUIRED_SIGS[@]}"; do if ! echo "$ASSETS" | grep -q "$sig_suffix"; then FOUND_ALL=false break fi done if [ "$FOUND_ALL" = true ]; then echo "All signature files found on attempt $attempt" break fi if [ "$attempt" -eq "$MAX_ATTEMPTS" ]; then echo "Warning: Not all .sig files found after $MAX_ATTEMPTS attempts. Generating manifest with available assets." break fi echo "Attempt $attempt/$MAX_ATTEMPTS: waiting for all .sig files..." sleep $SLEEP_SECONDS done - name: Generate latest.json env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ steps.version.outputs.tag }} VERSION: ${{ steps.version.outputs.version }} run: | REPO="${{ github.repository }}" RELEASE_URL="https://github.com/${REPO}/releases/download/${TAG}" # Get release info RELEASE_JSON=$(gh release view "$TAG" --repo "$REPO" --json body,publishedAt) NOTES=$(echo "$RELEASE_JSON" | jq -r '.body // ""') PUB_DATE=$(echo "$RELEASE_JSON" | jq -r '.publishedAt // ""') # Download all .sig files mkdir -p sigs ASSETS=$(gh release view "$TAG" --repo "$REPO" --json assets -q '.assets[].name') for asset in $ASSETS; do if [[ "$asset" == *.sig ]]; then gh release download "$TAG" --repo "$REPO" --pattern "$asset" --dir sigs fi done ls -la sigs/ # Build platform entries # Helper: read sig content for a glob pattern, return empty string if not found read_sig() { local sig_file for pattern in "$@"; do sig_file=$(find sigs -name "$pattern" -print -quit 2>/dev/null) if [ -n "$sig_file" ] && [ -f "$sig_file" ]; then cat "$sig_file" return fi done echo "" } # Find asset names matching patterns (extended regex) find_asset() { local pattern="$1" echo "$ASSETS" | grep -E "$pattern" | head -1 } # Build platforms JSON PLATFORMS="{}" # macOS aarch64 (.app.tar.gz for updater) MACOS_ARM_TAR=$(find_asset "aarch64.app.tar.gz$") MACOS_ARM_SIG=$(read_sig "*aarch64.app.tar.gz.sig") if [ -n "$MACOS_ARM_TAR" ] && [ -n "$MACOS_ARM_SIG" ]; then PLATFORMS=$(echo "$PLATFORMS" | jq --arg url "${RELEASE_URL}/${MACOS_ARM_TAR}" --arg sig "$MACOS_ARM_SIG" \ '. + {"darwin-aarch64": {"signature": $sig, "url": $url}}') fi # macOS x86_64 (.app.tar.gz for updater) MACOS_X64_TAR=$(find_asset "(x64|x86_64).app.tar.gz$") MACOS_X64_SIG=$(read_sig "*x64.app.tar.gz.sig" "*x86_64.app.tar.gz.sig") if [ -n "$MACOS_X64_TAR" ] && [ -n "$MACOS_X64_SIG" ]; then PLATFORMS=$(echo "$PLATFORMS" | jq --arg url "${RELEASE_URL}/${MACOS_X64_TAR}" --arg sig "$MACOS_X64_SIG" \ '. + {"darwin-x86_64": {"signature": $sig, "url": $url}}') fi # Linux x86_64 AppImage LINUX_X64_APPIMAGE=$(find_asset "(amd64|x86_64).AppImage$") LINUX_X64_SIG=$(read_sig "*amd64.AppImage.sig" "*x86_64.AppImage.sig") if [ -n "$LINUX_X64_APPIMAGE" ] && [ -n "$LINUX_X64_SIG" ]; then PLATFORMS=$(echo "$PLATFORMS" | jq --arg url "${RELEASE_URL}/${LINUX_X64_APPIMAGE}" --arg sig "$LINUX_X64_SIG" \ '. + {"linux-x86_64": {"signature": $sig, "url": $url}}') fi # Linux aarch64 AppImage LINUX_ARM_APPIMAGE=$(find_asset "aarch64.AppImage$") LINUX_ARM_SIG=$(read_sig "*aarch64.AppImage.sig") if [ -n "$LINUX_ARM_APPIMAGE" ] && [ -n "$LINUX_ARM_SIG" ]; then PLATFORMS=$(echo "$PLATFORMS" | jq --arg url "${RELEASE_URL}/${LINUX_ARM_APPIMAGE}" --arg sig "$LINUX_ARM_SIG" \ '. + {"linux-aarch64": {"signature": $sig, "url": $url}}') fi # Assemble final latest.json jq -n \ --arg version "$VERSION" \ --arg notes "$NOTES" \ --arg pub_date "$PUB_DATE" \ --argjson platforms "$PLATFORMS" \ '{version: $version, notes: $notes, pub_date: $pub_date, platforms: $platforms}' \ > latest.json echo "Generated latest.json:" cat latest.json - name: Upload latest.json to release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ steps.version.outputs.tag }} run: | # Delete existing latest.json if present (from a previous run) gh release delete-asset "$TAG" latest.json --repo "${{ github.repository }}" --yes 2>/dev/null || true # Upload the new one gh release upload "$TAG" latest.json --repo "${{ github.repository }}"