@@ -324,6 +324,36 @@ runs:
324324 exit 1
325325 fi
326326
327+ cat > simdeck-daemon-supervisor.sh <<'EOF'
328+ #!/usr/bin/env bash
329+ set +e
330+ terminating=0
331+ child=""
332+ trap 'terminating=1; if [[ -n "${child}" ]]; then kill "${child}" 2>/dev/null; wait "${child}" 2>/dev/null; fi; exit 0' TERM INT HUP
333+
334+ while true; do
335+ "$@" >> simdeck-daemon.log 2>&1 &
336+ child="$!"
337+ echo "${child}" > simdeck-child.pid
338+ wait "${child}"
339+ status="$?"
340+ child=""
341+
342+ if [[ "${terminating}" -eq 1 ]]; then
343+ exit 0
344+ fi
345+ if [[ "${status}" -eq 75 || "${status}" -ge 128 ]]; then
346+ echo "[simdeck-action-supervisor] daemon exited with status ${status}; restarting" >> simdeck-daemon.log
347+ sleep 1
348+ continue
349+ fi
350+
351+ echo "[simdeck-action-supervisor] daemon exited with status ${status}; not restarting" >> simdeck-daemon.log
352+ exit "${status}"
353+ done
354+ EOF
355+ chmod +x simdeck-daemon-supervisor.sh
356+
327357 SIMDECK_VIDEO_CODEC=software \
328358 SIMDECK_ANDROID_VIDEO_CODEC=software \
329359 SIMDECK_ALLOWED_ORIGINS='*' \
@@ -334,6 +364,7 @@ runs:
334364 SIMDECK_REALTIME_MIN_BITRATE="${stream_min_bitrate}" \
335365 SIMDECK_REALTIME_BITS_PER_PIXEL="${stream_bits_per_pixel}" \
336366 SIMDECK_LOCAL_STREAM_FPS="${stream_fps}" \
367+ ./simdeck-daemon-supervisor.sh \
337368 simdeck daemon run \
338369 --project-root "${GITHUB_WORKSPACE}" \
339370 --metadata-path "${metadata_path}" \
@@ -343,8 +374,7 @@ runs:
343374 --stream-quality "${SIMDECK_STREAM_PROFILE}" \
344375 --local-stream-fps "${stream_fps}" \
345376 --access-token "${access_token}" \
346- --pairing-code 000000 \
347- > simdeck-daemon.log 2>&1 &
377+ --pairing-code 000000 &
348378 echo "$!" > simdeck.pid
349379
350380 cloudflared tunnel --url "http://127.0.0.1:${SIMDECK_PORT}" --protocol http2 --no-autoupdate > cloudflared.log 2>&1 &
@@ -444,40 +474,106 @@ runs:
444474 sha="${{ steps.pr.outputs.sha }}"
445475 if [[ -n "${ARTIFACT_NAME_INPUT}" ]]; then
446476 artifact_name="${ARTIFACT_NAME_INPUT}"
477+ artifact_candidates="${artifact_name}"
447478 else
448479 artifact_name="${ARTIFACT_PREFIX}-${sha}"
480+ artifact_candidates="${artifact_name}"
481+ if [[ "${ARTIFACT_PREFIX}" != "${artifact_name}" ]]; then
482+ artifact_candidates+=$'\n'"${ARTIFACT_PREFIX}"
483+ fi
449484 fi
485+ printf '%s\n' "${artifact_candidates}" > /tmp/simdeck-artifact-candidates.txt
486+ artifact_candidates_summary="$(python3 - <<'PY'
487+ with open("/tmp/simdeck-artifact-candidates.txt", "r", encoding="utf-8") as handle:
488+ names = [line.strip() for line in handle if line.strip()]
489+ print(", ".join(f"`{name}`" for name in names))
490+ PY
491+ )"
492+ echo "SIMDECK_ARTIFACT_NAME=${artifact_name}" >> "${GITHUB_ENV}"
493+ echo "SIMDECK_ARTIFACT_CANDIDATES=${artifact_candidates_summary}" >> "${GITHUB_ENV}"
450494 mkdir -p downloaded-app
451495 rm -f /tmp/simdeck-artifact-download.status app-download.log
452496
453497 (
454498 set +e
455499 {
456- run_id=""
457- for attempt in {1..30}; do
458- run_id="$(gh api -X GET "repos/${REPO}/actions/artifacts?name=${artifact_name}&per_page=100" \
459- --jq '.artifacts[] | select(.expired == false) | .workflow_run.id' \
460- | head -n 1 || true)"
461-
462- if [[ -z "${run_id}" && -n "${BUILD_WORKFLOW}" ]]; then
463- run_id="$(gh api --paginate "repos/${REPO}/actions/workflows/${BUILD_WORKFLOW}/runs?per_page=100" \
464- --jq ".workflow_runs[] | select(.head_sha == \"${sha}\" and .conclusion == \"success\") | .id" \
465- | head -n 1 || true)"
500+ find_artifact_by_run() {
501+ local candidate_run_id="$1"
502+ gh api -X GET "repos/${REPO}/actions/runs/${candidate_run_id}/artifacts?per_page=100" |
503+ python3 -c '
504+ import json
505+ import sys
506+
507+ with open("/tmp/simdeck-artifact-candidates.txt", "r", encoding="utf-8") as handle:
508+ names = [line.strip() for line in handle if line.strip()]
509+
510+ data = json.load(sys.stdin)
511+ for name in names:
512+ for artifact in data.get("artifacts", []):
513+ if artifact.get("expired") is False and artifact.get("name") == name:
514+ print(name)
515+ raise SystemExit(0)
516+ raise SystemExit(1)
517+ ' || true
518+ }
519+
520+ find_artifact_record() {
521+ local artifact_record artifact_name_for_run candidate candidate_run_id
522+
523+ while IFS= read -r candidate; do
524+ [[ -z "${candidate}" ]] && continue
525+ artifact_record="$(gh api -X GET "repos/${REPO}/actions/artifacts?name=${candidate}&per_page=100" |
526+ SIMDECK_ARTIFACT_SHA="${sha}" python3 -c '
527+ import json
528+ import os
529+ import sys
530+
531+ data = json.load(sys.stdin)
532+ sha = os.environ["SIMDECK_ARTIFACT_SHA"]
533+ for artifact in data.get("artifacts", []):
534+ run = artifact.get("workflow_run") or {}
535+ if artifact.get("expired") is False and run.get("head_sha") == sha:
536+ print("{}\t{}".format(run.get("id"), artifact.get("name")))
537+ break
538+ ' || true)"
539+ if [[ -n "${artifact_record}" ]]; then
540+ printf '%s\n' "${artifact_record}"
541+ return 0
542+ fi
543+ done < /tmp/simdeck-artifact-candidates.txt
544+
545+ if [[ -n "${BUILD_WORKFLOW}" ]]; then
546+ while IFS= read -r candidate_run_id; do
547+ [[ -z "${candidate_run_id}" ]] && continue
548+ artifact_name_for_run="$(find_artifact_by_run "${candidate_run_id}")"
549+ if [[ -n "${artifact_name_for_run}" ]]; then
550+ printf '%s\t%s\n' "${candidate_run_id}" "${artifact_name_for_run}"
551+ return 0
552+ fi
553+ done < <(gh api --paginate "repos/${REPO}/actions/workflows/${BUILD_WORKFLOW}/runs?per_page=100" \
554+ --jq ".workflow_runs[] | select(.head_sha == \"${sha}\" and .conclusion == \"success\") | .id" || true)
466555 fi
467556
468- if [[ -n "${run_id}" ]]; then
469- echo "Using build workflow run ${run_id} for ${sha}"
470- gh run download "${run_id}" --repo "${REPO}" --name "${artifact_name}" --dir downloaded-app
557+ return 1
558+ }
559+
560+ for attempt in {1..30}; do
561+ artifact_record="$(find_artifact_record || true)"
562+
563+ if [[ -n "${artifact_record}" ]]; then
564+ IFS=$'\t' read -r run_id download_artifact_name <<< "${artifact_record}"
565+ echo "Using build workflow run ${run_id} artifact '${download_artifact_name}' for ${sha}"
566+ gh run download "${run_id}" --repo "${REPO}" --name "${download_artifact_name}" --dir downloaded-app
471567 exit_code="$?"
472568 echo "${exit_code}" > /tmp/simdeck-artifact-download.status
473569 exit "${exit_code}"
474570 fi
475571
476- echo "Waiting for artifact '${artifact_name}' for PR head ${sha} (${attempt}/30)"
572+ echo "Waiting for artifact (${artifact_candidates_summary}) for PR head ${sha} (${attempt}/30)"
477573 sleep 20
478574 done
479575
480- echo "No successful '${artifact_name}' artifact was found for PR head ${sha}." >&2
576+ echo "No successful unexpired artifact (${artifact_candidates_summary}) was found for PR head ${sha}." >&2
481577 echo "1" > /tmp/simdeck-artifact-download.status
482578 exit 1
483579 } > app-download.log 2>&1
@@ -543,6 +639,37 @@ runs:
543639 cat app-download.log
544640 status="$(cat /tmp/simdeck-artifact-download.status 2>/dev/null || echo 1)"
545641 if [[ "${status}" -ne 0 ]]; then
642+ echo "SIMDECK_SESSION_START_FAILED=1" >> "${GITHUB_ENV}"
643+ commit_sha="${{ steps.pr.outputs.sha }}"
644+ artifact_candidates="${SIMDECK_ARTIFACT_CANDIDATES:-${SIMDECK_ARTIFACT_NAME:-${ARTIFACT_PREFIX}-${commit_sha}}}"
645+ mention=""
646+ if [[ -n "${COMMAND_COMMENT_AUTHOR:-}" ]]; then
647+ mention="@${COMMAND_COMMENT_AUTHOR} "
648+ fi
649+
650+ cat > comment.md <<'EOF'
651+ __MENTION__SimDeck Android session could not start for commit `__COMMIT_SHA__`.
652+
653+ No unexpired APK artifact was available for this PR head. Expected one of: __ARTIFACT_CANDIDATES__.
654+
655+ Re-run the build workflow or push a new commit, then comment `simdeck run android` again.
656+ EOF
657+
658+ body="$(cat comment.md)"
659+ body="${body/__MENTION__/${mention}}"
660+ body="${body/__COMMIT_SHA__/${commit_sha}}"
661+ body="${body/__ARTIFACT_CANDIDATES__/${artifact_candidates}}"
662+ for attempt in {1..5}; do
663+ if [[ -n "${SIMDECK_STATUS_COMMENT_ID:-}" ]]; then
664+ if gh api -X PATCH "repos/${REPO}/issues/comments/${SIMDECK_STATUS_COMMENT_ID}" -f body="${body}"; then
665+ break
666+ fi
667+ elif comment_id="$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" -f body="${body}" --jq '.id')"; then
668+ echo "SIMDECK_STATUS_COMMENT_ID=${comment_id}" >> "${GITHUB_ENV}"
669+ break
670+ fi
671+ sleep $((attempt * 5))
672+ done
546673 exit "${status}"
547674 fi
548675
@@ -592,6 +719,7 @@ runs:
592719 if [[ -f /tmp/sim-boot-start && -f /tmp/sim-boot-end ]]; then
593720 echo "Android emulator boot took $(( $(cat /tmp/sim-boot-end) - $(cat /tmp/sim-boot-start) )) seconds."
594721 fi
722+ echo "SIMDECK_SESSION_OPEN=1" >> "${GITHUB_ENV}"
595723
596724 - name : Update status comment after app launch
597725 shell : bash
@@ -621,14 +749,39 @@ runs:
621749 set -euo pipefail
622750 udid="${{ steps.android.outputs.udid }}"
623751 end=$((SECONDS + KEEPALIVE_SECONDS))
752+ SIMDECK_DAEMON_HEALTH_GRACE_SECONDS="${SIMDECK_DAEMON_HEALTH_GRACE_SECONDS:-90}"
753+ health_failure_started=""
754+
755+ note_daemon_health_failure() {
756+ local reason="$1"
757+ if [[ -z "${health_failure_started}" ]]; then
758+ health_failure_started="${SECONDS}"
759+ fi
760+
761+ if (( SECONDS - health_failure_started >= SIMDECK_DAEMON_HEALTH_GRACE_SECONDS )); then
762+ echo "${reason} for ${SIMDECK_DAEMON_HEALTH_GRACE_SECONDS}s; stopping session." >&2
763+ cat simdeck-list-error.log >&2 || true
764+ cat simdeck-health.log >&2 || true
765+ cat simdeck-daemon.log >&2 || true
766+ return 1
767+ fi
768+
769+ echo "${reason}; waiting for SimDeck daemon supervisor to recover."
770+ sleep 5
771+ return 0
772+ }
624773
625774 while (( SECONDS < end )); do
626775 if [[ -f simdeck.pid ]] && ! kill -0 "$(cat simdeck.pid)" 2>/dev/null; then
627776 echo "SimDeck daemon process exited; stopping session."
777+ cat simdeck-daemon.log >&2 || true
628778 exit 1
629779 fi
630780
631- list_json="$(simdeck --server-url "http://127.0.0.1:${SIMDECK_PORT}" list --format json)"
781+ if ! list_json="$(simdeck --server-url "http://127.0.0.1:${SIMDECK_PORT}" list --format json 2>simdeck-list-error.log)"; then
782+ note_daemon_health_failure "SimDeck daemon list check failed" || exit 1
783+ continue
784+ fi
632785 if ! SIMDECK_LIST_JSON="${list_json}" python3 - "${udid}" <<'PY'
633786 import json
634787 import os
@@ -647,7 +800,12 @@ runs:
647800 exit 0
648801 fi
649802
650- curl -fsS "http://127.0.0.1:${SIMDECK_PORT}/api/health?simdeckToken=${{ steps.stream.outputs.access_token }}" >/dev/null
803+ if ! curl -fsS "http://127.0.0.1:${SIMDECK_PORT}/api/health?simdeckToken=${{ steps.stream.outputs.access_token }}" >/dev/null 2>simdeck-health.log; then
804+ note_daemon_health_failure "SimDeck daemon health check failed" || exit 1
805+ continue
806+ fi
807+
808+ health_failure_started=""
651809 sleep 15
652810 done
653811
@@ -668,7 +826,7 @@ runs:
668826 simdeck daemon stop
669827
670828 - name : Update status comment at end
671- if : always()
829+ if : always() && env.SIMDECK_SESSION_OPEN == '1'
672830 shell : bash
673831 run : |
674832 set -euo pipefail
0 commit comments