Skip to content

PZPA [Hoppe] [6a8476aa3b1841cef0e6ff6e] #927

PZPA [Hoppe] [6a8476aa3b1841cef0e6ff6e]

PZPA [Hoppe] [6a8476aa3b1841cef0e6ff6e] #927

Workflow file for this run

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."