Repository navigation
PZPA [Hoppe] [6a8476aa3b1841cef0e6ff6e] #927
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: Agent | |
| run-name: PZPA [${{ github.event.inputs.id }}] [${{ github.event.inputs.objectId }}] | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| id: | |
| description: "Run ID" | |
| required: true | |
| objectId: | |
| description: "Object ID of the run" | |
| required: true | |
| files: | |
| description: "Input files" | |
| required: true | |
| toggles: | |
| description: "Toggles" | |
| required: false | |
| default: '{"autostart":true,"autoplay":true,"practice":false,"adjustOffset":false,"render":true,"newTab":true,"inApp":0}' | |
| preferences: | |
| description: "Player preferences" | |
| required: false | |
| default: '{"aspectRatio":null,"backgroundBlur":1,"backgroundLuminance":0.5,"chartFlipping":0,"chartOffset":0,"fcApIndicator":true,"goodJudgment":160,"hitSoundVolume":0.75,"lineThickness":1,"musicVolume":1,"noteSize":1,"perfectJudgment":80,"simultaneousNoteHint":true,"timeScale":1}' | |
| mediaOptions: | |
| description: "Renderer media options" | |
| required: false | |
| default: '{"frameRate":60,"overrideResolution":[1620,1080],"resultsLoopsToRender":1,"videoCodec":"libx264","videoBitrate":6000,"audioBitrate":320,"vsync":true,"exportPath":""}' | |
| webhookUrl: | |
| description: "Webhook URL" | |
| required: false | |
| timezone: | |
| description: "Timezone" | |
| required: false | |
| default: "Asia/Shanghai" | |
| useSnapshot: | |
| description: "Use latest snapshot" | |
| required: false | |
| type: boolean | |
| default: false | |
| useBrowser: | |
| description: "Use browser" | |
| required: false | |
| type: boolean | |
| default: true | |
| title: | |
| description: "Title" | |
| required: false | |
| level: | |
| description: "Level" | |
| required: false | |
| defaults: | |
| run: | |
| shell: bash | |
| jobs: | |
| agent: | |
| name: Run PhiZone Player | |
| runs-on: macos-latest | |
| steps: | |
| - name: Set timezone | |
| uses: szenius/set-timezone@v2.0 | |
| with: | |
| timezoneLinux: ${{ github.event.inputs.timezone }} | |
| timezoneMacos: ${{ github.event.inputs.timezone }} | |
| timezoneWindows: ${{ github.event.inputs.timezone }} | |
| - name: Notify webhook about job initialization | |
| if: ${{ github.event.inputs.webhookUrl }} | |
| env: | |
| RUN_ID: ${{ github.run_id }} | |
| WEBHOOK_URL: ${{ github.event.inputs.webhookUrl }} | |
| run: | | |
| set -e | |
| run_id="$RUN_ID" | |
| webhook_url="$WEBHOOK_URL" | |
| curl -X POST -H "Content-Type: application/json" -d "{ | |
| \"run_id\": \"$run_id\", | |
| \"status\": \"initializing\", | |
| \"progress\": 0 | |
| }" "$webhook_url" 2>/dev/null || echo "Failed to send initialization notification" | |
| - name: Checkout current repository | |
| uses: actions/checkout@v6 | |
| - name: Obtain Player executable download URL | |
| id: get_url | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const useSnapshot = '${{ github.event.inputs.useSnapshot }}' === 'true'; | |
| let release; | |
| if (useSnapshot) { | |
| // Get latest prerelease with macOS ARM64 executable asset | |
| const { data: releases } = await github.rest.repos.listReleases({ | |
| owner: 'PhiZone', | |
| repo: 'player', | |
| per_page: 100 | |
| }); | |
| release = releases.find(r => | |
| r.prerelease && | |
| r.assets.some(a => a.name.endsWith('-macos-arm64')) | |
| ); | |
| if (!release) throw new Error('No prerelease with macOS ARM64 executable asset found'); | |
| } else { | |
| // Get latest stable release | |
| const { data: latestRelease } = await github.rest.repos.getLatestRelease({ | |
| owner: 'PhiZone', | |
| repo: 'player', | |
| }); | |
| release = latestRelease; | |
| } | |
| const tag = release.tag_name; | |
| const version = tag.replace(/^v/, ''); | |
| const assetName = `PhiZone.Player-${version}-macos-arm64-debug`; | |
| const asset = release.assets.find(a => a.name === assetName); | |
| if (!asset) throw new Error(`Asset ${assetName} not found`); | |
| core.setOutput('asset_url', asset.browser_download_url); | |
| const webZipName = `PhiZone.Player_${version}.zip`; | |
| const webZipAsset = release.assets.find(a => a.name === webZipName); | |
| if (webZipAsset) { | |
| core.setOutput('web_zip_url', webZipAsset.browser_download_url); | |
| } else { | |
| core.warning(`Web build ZIP ${webZipName} not found in release`); | |
| } | |
| - name: Download Player executable | |
| run: | | |
| curl -L "${{ steps.get_url.outputs.asset_url }}" -o player | |
| chmod +x player | |
| - name: Setup FFmpeg | |
| uses: AnimMouse/setup-ffmpeg@v1 | |
| with: | |
| version: 71 | |
| - name: Prepare media options | |
| id: prepare_options | |
| env: | |
| MEDIA_OPTIONS_INPUT: ${{ github.event.inputs.mediaOptions }} | |
| run: | | |
| set -e | |
| mkdir -p ./outputs | |
| outputs_path=$(realpath ./outputs) | |
| echo "outputs_path=$outputs_path" >> $GITHUB_OUTPUT | |
| mediaOptions="$MEDIA_OPTIONS_INPUT" | |
| if [ -n "$mediaOptions" ]; then | |
| # Parse JSON, override exportPath, and serialize back (compact output) | |
| mediaOptions=$(echo "$mediaOptions" | jq -c --arg path "$outputs_path" '. + {exportPath: $path}') | |
| echo "mediaOptions=$mediaOptions" >> $GITHUB_OUTPUT | |
| else | |
| echo "mediaOptions=" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Download and prepare input files | |
| id: prepare_files | |
| env: | |
| FILES_INPUT: ${{ github.event.inputs.files }} | |
| RUN_ID: ${{ github.run_id }} | |
| WEBHOOK_URL: ${{ github.event.inputs.webhookUrl }} | |
| run: | | |
| set -e | |
| retry_curl() { | |
| local out_path="$1" | |
| local url="$2" | |
| local max_retries=10 | |
| local attempt=1 | |
| while [ "$attempt" -le "$max_retries" ]; do | |
| if curl -L -f --connect-timeout 15 --max-time 300 -o "$out_path" "$url"; then | |
| return 0 | |
| fi | |
| exit_code=$? | |
| echo "curl failed (exit $exit_code) attempt ${attempt}/${max_retries}: $url" >&2 | |
| if [ "$attempt" -ge "$max_retries" ]; then | |
| return "$exit_code" | |
| fi | |
| sleep_time=$(( attempt * 2 )) | |
| if [ "$sleep_time" -gt 20 ]; then | |
| sleep_time=20 | |
| fi | |
| echo "Retrying in ${sleep_time}s..." >&2 | |
| sleep "$sleep_time" | |
| attempt=$(( attempt + 1 )) | |
| done | |
| } | |
| files_input="$FILES_INPUT" | |
| run_id="$RUN_ID" | |
| webhook_url="$WEBHOOK_URL" | |
| if [ -n "$webhook_url" ]; then | |
| curl -X POST -H "Content-Type: application/json" -d "{ | |
| \"run_id\": \"$run_id\", | |
| \"status\": \"downloading_assets\", | |
| \"progress\": 0 | |
| }" "$webhook_url" 2>/dev/null || echo "Failed to send download notification" | |
| echo "" | |
| fi | |
| # Create downloads directory | |
| mkdir -p ./downloads | |
| downloads_path=$(realpath ./downloads) | |
| # Accept either a JSON array (e.g. ["a","b"]) or a newline-separated list. | |
| files_raw="" | |
| if printf '%s' "$files_input" | jq -e . >/dev/null 2>&1; then | |
| # It's valid JSON: extract array elements joined by newlines | |
| files_raw=$(echo "$files_input" | jq -r '.[]') | |
| else | |
| files_raw="$files_input" | |
| fi | |
| # Read lines into array (safe for spaces in names) | |
| IFS=$'\n' read -r -d '' -a files_array < <(printf '%s\0' "$files_raw") | |
| # Remove trailing '\r' from each element | |
| for i in "${!files_array[@]}"; do | |
| files_array[$i]="${files_array[$i]%$'\r'}" | |
| done | |
| # Download files and collect local paths | |
| local_files=() | |
| file_counter=1 | |
| for file in "${files_array[@]}"; do | |
| [ -z "$file" ] && continue | |
| echo "Processing: $file" | |
| if [[ "$file" =~ ^https?:// ]]; then | |
| # It's a URL, download it with UUID filename | |
| # Extract extension from URL (remove query parameters first) | |
| clean_url="${file%%\?*}" | |
| extension="" | |
| # Try to get extension from the clean URL | |
| if [[ "$clean_url" =~ \.[a-zA-Z0-9]+$ ]]; then | |
| extension="${clean_url##*.}" | |
| else | |
| # Default to zip if no extension found | |
| extension="zip" | |
| fi | |
| # Generate UUID for filename (using timestamp + random for uniqueness) | |
| uuid=$(date +%s%N | shasum | head -c 8) | |
| filename="${uuid}.${extension}" | |
| local_path="$downloads_path/$filename" | |
| echo "Downloading $file to $local_path" | |
| if retry_curl "$local_path" "$file"; then | |
| echo "Successfully downloaded: $filename" | |
| local_files+=("$local_path") | |
| else | |
| echo "Failed to download: $file" | |
| exit 1 | |
| fi | |
| else | |
| # It's already a local path | |
| echo "Using local file: $file" | |
| local_files+=("$file") | |
| fi | |
| ((file_counter++)) | |
| done | |
| # Convert array to newline-separated string for output | |
| printf '%s\n' "${local_files[@]}" > ./files.txt | |
| echo "local_files_path=$(realpath ./files.txt)" >> $GITHUB_OUTPUT | |
| echo "Prepared ${#local_files[@]} local files:" | |
| for file in "${local_files[@]}"; do | |
| echo " - $file" | |
| done | |
| - name: Prepare browser for CI | |
| if: ${{ github.event.inputs.useBrowser == 'true' }} | |
| run: | | |
| set -e | |
| # Create First Run sentinel file to suppress first-run dialog | |
| mkdir -p "$HOME/Library/Application Support/Google/Chrome" | |
| touch "$HOME/Library/Application Support/Google/Chrome/First Run" | |
| # Create a wrapper script to invoke the preinstalled Chrome binary directly | |
| # (bypasses macOS `open` and avoids spaces-in-path issues) | |
| cat > ./chrome-wrapper <<'WRAPPER' | |
| #!/bin/bash | |
| exec "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" "$@" | |
| WRAPPER | |
| chmod +x ./chrome-wrapper | |
| echo "Browser preparation completed." | |
| - name: Setup Node.js | |
| if: ${{ github.event.inputs.useBrowser == 'true' }} | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| - name: Download and extract Player web build | |
| if: ${{ github.event.inputs.useBrowser == 'true' && steps.get_url.outputs.web_zip_url }} | |
| run: | | |
| set -e | |
| curl -L "${{ steps.get_url.outputs.web_zip_url }}" -o player-web.zip | |
| mkdir -p ./player-web | |
| unzip -q player-web.zip -d ./player-web | |
| rm player-web.zip | |
| echo "Player web build extracted successfully." | |
| - name: Start local web server | |
| if: ${{ github.event.inputs.useBrowser == 'true' && steps.get_url.outputs.web_zip_url }} | |
| run: | | |
| set -e | |
| node serve.mjs ./player-web 3000 & | |
| echo $! > ./server_pid | |
| # Wait for server to be ready | |
| for i in $(seq 1 30); do | |
| if curl -s -o /dev/null http://localhost:3000/; then | |
| echo "Local web server is ready on http://localhost:3000" | |
| break | |
| fi | |
| sleep 1 | |
| done | |
| - name: Start debug screenshot capture | |
| run: | | |
| mkdir -p ./screenshots | |
| ( | |
| sleep 30 | |
| for i in $(seq 1 10); do | |
| screencapture -x "./screenshots/screenshot_$(printf '%02d' $i)_$(date +%H%M%S).png" 2>/dev/null || true | |
| sleep 30 | |
| done | |
| ) & | |
| echo $! > ./screenshot_pid | |
| echo "Debug screenshot capture started (PID: $!), capturing every 30s for 5 minutes" | |
| - name: Run Player executable | |
| env: | |
| REPORT_INTERVAL: "100" | |
| __XPC_JSC_useSharedArrayBuffer: "1" | |
| RUN_ID: ${{ github.run_id }} | |
| WEBHOOK_URL: ${{ github.event.inputs.webhookUrl }} | |
| LOCAL_FILES_PATH: ${{ steps.prepare_files.outputs.local_files_path }} | |
| TOGGLES: ${{ github.event.inputs.toggles }} | |
| PREFERENCES: ${{ github.event.inputs.preferences }} | |
| MEDIA_OPTIONS: ${{ steps.prepare_options.outputs.mediaOptions }} | |
| USE_BROWSER: ${{ github.event.inputs.useBrowser }} | |
| TITLE: ${{ github.event.inputs.title }} | |
| LEVEL: ${{ github.event.inputs.level }} | |
| run: | | |
| set -e | |
| local_files_path="$LOCAL_FILES_PATH" | |
| toggles="$TOGGLES" | |
| preferences="$PREFERENCES" | |
| mediaOptions="$MEDIA_OPTIONS" | |
| useBrowser="$USE_BROWSER" | |
| title="$TITLE" | |
| level="$LEVEL" | |
| run_id="$RUN_ID" | |
| webhook_url="$WEBHOOK_URL" | |
| if [ -n "$webhook_url" ]; then | |
| curl -X POST -H "Content-Type: application/json" -d "{ | |
| \"run_id\": \"$run_id\", | |
| \"status\": \"starting\", | |
| \"progress\": 0 | |
| }" "$webhook_url" 2>/dev/null || echo "Failed to send start notification" | |
| echo "" | |
| fi | |
| # Read local file paths from the prepared list | |
| files_array=() | |
| while IFS= read -r line || [ -n "$line" ]; do | |
| files_array+=("$line") | |
| done < "$local_files_path" | |
| # Build the command with proper argument handling | |
| cmd_args=() | |
| # Add file arguments | |
| for file in "${files_array[@]}"; do | |
| [ -z "$file" ] && continue | |
| cmd_args+=("$file") | |
| done | |
| # Add optional arguments with proper quoting | |
| if [ -n "$toggles" ]; then | |
| cmd_args+=("--toggles" "$toggles") | |
| fi | |
| if [ -n "$preferences" ]; then | |
| cmd_args+=("--preferences" "$preferences") | |
| fi | |
| if [ -n "$mediaOptions" ]; then | |
| cmd_args+=("--mediaOptions" "$mediaOptions") | |
| fi | |
| if [ "$useBrowser" = "true" ]; then | |
| chrome_wrapper=$(realpath ./chrome-wrapper) | |
| cmd_args+=("--browser" "$chrome_wrapper --no-first-run --no-default-browser-check --disable-search-engine-choice-screen" "--url" "http://localhost:3000/") | |
| fi | |
| if [ -n "$title" ]; then | |
| cmd_args+=("--title" "$title") | |
| fi | |
| if [ -n "$level" ]; then | |
| cmd_args+=("--level" "$level") | |
| fi | |
| cmd_args+=("--automate") | |
| # Start memory monitoring in background | |
| ( | |
| while true; do | |
| # Memory usage (macOS: vm_stat and sysctl) | |
| mem_total=$(sysctl -n hw.memsize) | |
| mem_free=$(vm_stat | awk '/Pages free/ {print $3}' | tr -d '.') | |
| mem_inactive=$(vm_stat | awk '/Pages inactive/ {print $3}' | tr -d '.') | |
| page_size=$(vm_stat | head -1 | awk '{print $8}') | |
| mem_free_bytes=$(( (mem_free + mem_inactive) * page_size )) | |
| mem_occupied_bytes=$((mem_total - mem_free_bytes)) | |
| mem_total_gb=$(echo "scale=2; $mem_total/1024/1024/1024" | bc) | |
| mem_occupied_gb=$(echo "scale=2; $mem_occupied_bytes/1024/1024/1024" | bc) | |
| mem_percent=$(echo "scale=1; ($mem_occupied_bytes*100)/$mem_total" | bc) | |
| echo -e "\nMemory: ${mem_occupied_gb} GiB / ${mem_total_gb} GiB (${mem_percent}%)" | |
| sleep 60 | |
| done | |
| ) & | |
| memory_monitor_pid=$! | |
| # Execute Player | |
| echo "Executing: ./player ${cmd_args[*]}" | |
| ./player "${cmd_args[@]}" | |
| player_exit_code=$? | |
| # Stop monitoring processes | |
| kill $memory_monitor_pid 2>/dev/null || true | |
| wait $memory_monitor_pid 2>/dev/null || true | |
| echo "Player execution completed with exit code: $player_exit_code" | |
| exit $player_exit_code | |
| - name: Upload debug screenshots | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: debug-screenshots | |
| path: ./screenshots | |
| if-no-files-found: ignore | |
| - name: Notify webhook about artifact upload | |
| if: ${{ github.event.inputs.webhookUrl }} | |
| env: | |
| RUN_ID: ${{ github.run_id }} | |
| WEBHOOK_URL: ${{ github.event.inputs.webhookUrl }} | |
| run: | | |
| set -e | |
| run_id="$RUN_ID" | |
| webhook_url="$WEBHOOK_URL" | |
| curl -X POST -H "Content-Type: application/json" -d "{ | |
| \"run_id\": \"$run_id\", | |
| \"status\": \"uploading_artifact\", | |
| \"progress\": 0 | |
| }" "$webhook_url" 2>/dev/null || echo "Failed to send upload notification" | |
| - name: Upload output artifact | |
| id: upload | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: artifact | |
| path: ${{ steps.prepare_options.outputs.outputs_path }} | |
| - name: Notify webhook about job completion | |
| if: ${{ github.event.inputs.webhookUrl }} | |
| env: | |
| RUN_ID: ${{ github.run_id }} | |
| ARTIFACT_ID: ${{ steps.upload.outputs.artifact-id }} | |
| WEBHOOK_URL: ${{ github.event.inputs.webhookUrl }} | |
| run: | | |
| set -e | |
| run_id="$RUN_ID" | |
| artifact_id="$ARTIFACT_ID" | |
| webhook_url="$WEBHOOK_URL" | |
| curl -X POST -H "Content-Type: application/json" -d "{ | |
| \"run_id\": \"$run_id\", | |
| \"status\": \"completed\", | |
| \"progress\": 1, | |
| \"artifact_id\": \"$artifact_id\" | |
| }" "$webhook_url" 2>/dev/null || echo "Failed to send completion notification" | |
| - name: Notify webhook about job failure | |
| if: ${{ github.event.inputs.webhookUrl && failure() }} | |
| env: | |
| RUN_ID: ${{ github.run_id }} | |
| WEBHOOK_URL: ${{ github.event.inputs.webhookUrl }} | |
| run: | | |
| set -e | |
| run_id="$RUN_ID" | |
| webhook_url="$WEBHOOK_URL" | |
| curl -X POST -H "Content-Type: application/json" -d "{ | |
| \"run_id\": \"$run_id\", | |
| \"status\": \"failed\", | |
| \"progress\": 1 | |
| }" "$webhook_url" 2>/dev/null || echo "Failed to send failure notification" | |
| - name: Notify webhook about job cancellation | |
| if: ${{ github.event.inputs.webhookUrl && cancelled() }} | |
| env: | |
| RUN_ID: ${{ github.run_id }} | |
| WEBHOOK_URL: ${{ github.event.inputs.webhookUrl }} | |
| run: | | |
| set -e | |
| run_id="$RUN_ID" | |
| webhook_url="$WEBHOOK_URL" | |
| curl -X POST -H "Content-Type: application/json" -d "{ | |
| \"run_id\": \"$run_id\", | |
| \"status\": \"cancelled\", | |
| \"progress\": 1 | |
| }" "$webhook_url" 2>/dev/null || echo "Failed to send cancellation notification" | |
| - name: Post-execution cleanup | |
| if: always() | |
| run: | | |
| set -e | |
| echo "Starting post-execution cleanup..." | |
| # Stop local web server | |
| if [ -f ./server_pid ]; then | |
| kill $(cat ./server_pid) 2>/dev/null || true | |
| rm -f ./server_pid | |
| fi | |
| # Clean up Player web build | |
| rm -rf ./player-web 2>/dev/null || true | |
| # Clean up downloaded files | |
| echo "Cleaning downloaded files..." | |
| rm -rf ./downloads 2>/dev/null || true | |
| # Clean up generated outputs | |
| echo "Cleaning local output files..." | |
| rm -rf ./outputs 2>/dev/null || true | |
| rm -rf ./files.txt 2>/dev/null || true | |
| # Clean up Player executable | |
| rm -f ./player 2>/dev/null || true | |
| # Clean up Tauri app data | |
| if [ -d "$APPDATA" ]; then | |
| rm -rf "$APPDATA/cn.phizone.player" 2>/dev/null || true | |
| fi | |
| if [ -d "$LOCALAPPDATA" ]; then | |
| rm -rf "$LOCALAPPDATA/cn.phizone.player" 2>/dev/null || true | |
| fi | |
| # Clean up any WebView2 processes and cache | |
| echo "Cleaning WebView2 cache post-execution..." | |
| if [ -d "$LOCALAPPDATA/Microsoft/Edge" ]; then | |
| rm -rf "$LOCALAPPDATA/Microsoft/Edge/User Data/Default/Cache" 2>/dev/null || true | |
| rm -rf "$LOCALAPPDATA/Microsoft/Edge/User Data/Default/Code Cache" 2>/dev/null || true | |
| rm -rf "$LOCALAPPDATA/Microsoft/Edge/User Data/Default/GPUCache" 2>/dev/null || true | |
| rm -rf "$LOCALAPPDATA/Microsoft/Edge/User Data/Default/Service Worker" 2>/dev/null || true | |
| rm -rf "$LOCALAPPDATA/Microsoft/Edge/User Data/Default/IndexedDB" 2>/dev/null || true | |
| rm -rf "$LOCALAPPDATA/Microsoft/Edge/User Data/Default/Local Storage" 2>/dev/null || true | |
| rm -rf "$LOCALAPPDATA/Microsoft/Edge/User Data/Default/Session Storage" 2>/dev/null || true | |
| fi | |
| echo "Post-execution cleanup completed." |