name: Claude GameObject API Tests (Unity live)

on: [workflow_dispatch]

permissions:
  contents: read
  checks: write

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

env:
  UNITY_IMAGE: unityci/editor:ubuntu-2021.3.45f2-linux-il2cpp-3

jobs:
  go-suite:
    runs-on: ubuntu-24.04
    timeout-minutes: 45
    env:
      JUNIT_OUT: reports/junit-go-suite.xml
      MD_OUT: reports/junit-go-suite.md

    steps:
      # ---------- Secrets check ----------
      - name: Detect secrets (outputs)
        id: detect
        env:
          UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
          UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
          UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
          UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          set -e
          if [ -n "$ANTHROPIC_API_KEY" ]; then echo "anthropic_ok=true" >> "$GITHUB_OUTPUT"; else echo "anthropic_ok=false" >> "$GITHUB_OUTPUT"; fi
          if [ -n "$UNITY_LICENSE" ] || { [ -n "$UNITY_EMAIL" ] && [ -n "$UNITY_PASSWORD" ]; }; then
            echo "unity_ok=true" >> "$GITHUB_OUTPUT"
          else
            echo "unity_ok=false" >> "$GITHUB_OUTPUT"
          fi

      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      # ---------- Python env for MCP server (uv) ----------
      - uses: astral-sh/setup-uv@v4
        with:
          python-version: "3.11"

      - name: Install MCP server
        run: |
          set -eux
          uv venv
          echo "VIRTUAL_ENV=$GITHUB_WORKSPACE/.venv" >> "$GITHUB_ENV"
          echo "$GITHUB_WORKSPACE/.venv/bin" >> "$GITHUB_PATH"
          if [ -f Server/pyproject.toml ]; then
            uv pip install -e Server
          elif [ -f Server/requirements.txt ]; then
            uv pip install -r Server/requirements.txt
          else
            echo "No MCP Python deps found (skipping)"
          fi

      # --- Licensing ---
      - name: Decide license sources
        id: lic
        shell: bash
        env:
          UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
          UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
          UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
          UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
        run: |
          set -eu
          use_ulf=false; use_ebl=false
          [[ -n "${UNITY_LICENSE:-}" ]] && use_ulf=true
          [[ -n "${UNITY_EMAIL:-}" && -n "${UNITY_PASSWORD:-}" ]] && use_ebl=true
          echo "use_ulf=$use_ulf" >> "$GITHUB_OUTPUT"
          echo "use_ebl=$use_ebl" >> "$GITHUB_OUTPUT"
          echo "has_serial=$([[ -n "${UNITY_SERIAL:-}" ]] && echo true || echo false)" >> "$GITHUB_OUTPUT"

      - name: Stage Unity .ulf license (from secret)
        if: steps.lic.outputs.use_ulf == 'true'
        id: ulf
        env:
          UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
        shell: bash
        run: |
          set -eu
          mkdir -p "$RUNNER_TEMP/unity-license-ulf" "$RUNNER_TEMP/unity-local/Unity"
          f="$RUNNER_TEMP/unity-license-ulf/Unity_lic.ulf"
          if printf "%s" "$UNITY_LICENSE" | base64 -d - >/dev/null 2>&1; then
            printf "%s" "$UNITY_LICENSE" | base64 -d - > "$f"
          else
            printf "%s" "$UNITY_LICENSE" > "$f"
          fi
          chmod 600 "$f" || true
          if head -c 100 "$f" | grep -qi '<\?xml'; then
            mkdir -p "$RUNNER_TEMP/unity-config/Unity/licenses"
            mv "$f" "$RUNNER_TEMP/unity-config/Unity/licenses/UnityEntitlementLicense.xml"
            echo "ok=false" >> "$GITHUB_OUTPUT"
          elif grep -qi '<Signature>' "$f"; then
            cp -f "$f" "$RUNNER_TEMP/unity-local/Unity/Unity_lic.ulf"
            echo "ok=true" >> "$GITHUB_OUTPUT"
          else
            echo "ok=false" >> "$GITHUB_OUTPUT"
          fi

      - name: Activate Unity (EBL via container - host-mount)
        if: steps.lic.outputs.use_ebl == 'true'
        shell: bash
        env:
          UNITY_IMAGE: ${{ env.UNITY_IMAGE }}
          UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
          UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
          UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
        run: |
          set -euo pipefail
          mkdir -p "$RUNNER_TEMP/unity-config" "$RUNNER_TEMP/unity-local"
          docker run --rm --network host \
            -e HOME=/root \
            -e UNITY_EMAIL -e UNITY_PASSWORD -e UNITY_SERIAL \
            -v "$RUNNER_TEMP/unity-config:/root/.config/unity3d" \
            -v "$RUNNER_TEMP/unity-local:/root/.local/share/unity3d" \
            "$UNITY_IMAGE" bash -lc '
              set -euxo pipefail
              if [[ -n "${UNITY_SERIAL:-}" ]]; then
                /opt/unity/Editor/Unity -batchmode -nographics -logFile - \
                  -username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" -serial "$UNITY_SERIAL" -quit || true
              else
                /opt/unity/Editor/Unity -batchmode -nographics -logFile - \
                  -username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" -quit || true
              fi
              ls -la /root/.config/unity3d/Unity/licenses || true
            '

          if ! find "$RUNNER_TEMP/unity-config" -type f -iname "*.xml" | grep -q .; then
            if [[ "${{ steps.ulf.outputs.ok }}" == "true" ]]; then
              echo "EBL entitlement not found; proceeding with ULF-only (ok=true)."
            else
              echo "No entitlement produced and no valid ULF; cannot continue." >&2
              exit 1
            fi
          fi

      # ---------- Warm up project ----------
      - name: Warm up project (import Library once)
        if: steps.detect.outputs.anthropic_ok == 'true' && (steps.lic.outputs.use_ulf == 'true' || steps.lic.outputs.use_ebl == 'true')
        shell: bash
        env:
          UNITY_IMAGE: ${{ env.UNITY_IMAGE }}
          ULF_OK: ${{ steps.ulf.outputs.ok }}
        run: |
          set -euxo pipefail
          manual_args=()
          if [[ "${ULF_OK:-false}" == "true" ]]; then
            manual_args=(-manualLicenseFile "/root/.local/share/unity3d/Unity/Unity_lic.ulf")
          fi
          docker run --rm --network host \
            -e HOME=/root \
            -v "${{ github.workspace }}:${{ github.workspace }}" -w "${{ github.workspace }}" \
            -v "$RUNNER_TEMP/unity-config:/root/.config/unity3d" \
            -v "$RUNNER_TEMP/unity-local:/root/.local/share/unity3d" \
            -v "$RUNNER_TEMP/unity-cache:/root/.cache/unity3d" \
            "$UNITY_IMAGE" /opt/unity/Editor/Unity -batchmode -nographics -logFile - \
              -projectPath "${{ github.workspace }}/TestProjects/UnityMCPTests" \
              "${manual_args[@]}" \
              -quit

      # ---------- Clean old MCP status ----------
      - name: Clean old MCP status
        run: |
          set -eux
          mkdir -p "$GITHUB_WORKSPACE/.unity-mcp"
          rm -f "$GITHUB_WORKSPACE/.unity-mcp"/unity-mcp-status-*.json || true

      # ---------- Start headless Unity ----------
      - name: Start Unity (persistent bridge)
        if: steps.detect.outputs.anthropic_ok == 'true' && (steps.lic.outputs.use_ulf == 'true' || steps.lic.outputs.use_ebl == 'true')
        shell: bash
        env:
          UNITY_IMAGE: ${{ env.UNITY_IMAGE }}
          ULF_OK: ${{ steps.ulf.outputs.ok }}
        run: |
          set -euxo pipefail
          manual_args=()
          if [[ "${ULF_OK:-false}" == "true" ]]; then
            manual_args=(-manualLicenseFile "/root/.local/share/unity3d/Unity/Unity_lic.ulf")
          fi

          mkdir -p "$GITHUB_WORKSPACE/.unity-mcp"
          docker rm -f unity-mcp >/dev/null 2>&1 || true
          docker run -d --name unity-mcp --network host \
            -e HOME=/root \
            -e UNITY_MCP_ALLOW_BATCH=1 \
            -e UNITY_MCP_STATUS_DIR="${{ github.workspace }}/.unity-mcp" \
            -e UNITY_MCP_BIND_HOST=127.0.0.1 \
            -v "${{ github.workspace }}:${{ github.workspace }}" -w "${{ github.workspace }}" \
            -v "$RUNNER_TEMP/unity-config:/root/.config/unity3d" \
            -v "$RUNNER_TEMP/unity-local:/root/.local/share/unity3d" \
            -v "$RUNNER_TEMP/unity-cache:/root/.cache/unity3d" \
            "$UNITY_IMAGE" /opt/unity/Editor/Unity -batchmode -nographics -logFile /root/.config/unity3d/Editor.log \
              -stackTraceLogType Full \
              -projectPath "${{ github.workspace }}/TestProjects/UnityMCPTests" \
              "${manual_args[@]}" \
              -executeMethod MCPForUnity.Editor.McpCiBoot.StartStdioForCi

      # ---------- Wait for Unity bridge ----------
      - name: Wait for Unity bridge (robust)
        if: steps.detect.outputs.anthropic_ok == 'true' && (steps.lic.outputs.use_ulf == 'true' || steps.lic.outputs.use_ebl == 'true')
        shell: bash
        run: |
          set -euo pipefail
          deadline=$((SECONDS+600))
          fatal_after=$((SECONDS+120))
          ok_pat='(Bridge|MCP(For)?Unity|AutoConnect).*(listening|ready|started|port|bound)'
          license_fatal='No valid Unity|License is not active|cannot load ULF|Signature element not found|Token not found|0 entitlement|Entitlement.*(failed|denied)|License (activation|return|renewal).*(failed|expired|denied)'

          while [ $SECONDS -lt $deadline ]; do
            logs="$(docker logs unity-mcp 2>&1 || true)"
            port="$(jq -r '.unity_port // empty' "$GITHUB_WORKSPACE"/.unity-mcp/unity-mcp-status-*.json 2>/dev/null | head -n1 || true)"
            if [[ -n "${port:-}" ]] && timeout 1 bash -lc "exec 3<>/dev/tcp/127.0.0.1/$port"; then
              echo "Bridge ready on port $port"
              docker exec unity-mcp chmod -R a+rwx "$GITHUB_WORKSPACE/.unity-mcp" || chmod -R a+rwx "$GITHUB_WORKSPACE/.unity-mcp" || true
              exit 0
            fi
            if echo "$logs" | grep -qiE "$ok_pat"; then
              echo "Bridge ready (log markers)"
              docker exec unity-mcp chmod -R a+rwx "$GITHUB_WORKSPACE/.unity-mcp" || chmod -R a+rwx "$GITHUB_WORKSPACE/.unity-mcp" || true
              exit 0
            fi
            if [ $SECONDS -ge $fatal_after ] && echo "$logs" | grep -qiE "$license_fatal"; then
              echo "::error::Fatal licensing signal detected after warm-up"
              echo "$logs" | tail -n 200 | sed -E 's/((email|serial|license|password|token)[^[:space:]]*)/[REDACTED]/Ig'
              exit 1
            fi
            st="$(docker inspect -f '{{.State.Status}}' unity-mcp 2>/dev/null || true)"
            if [[ "$st" != "running" ]]; then
              echo "::error::Unity container exited during wait"; docker logs unity-mcp --tail 200 | sed -E 's/((email|serial|license|password|token)[^[:space:]]*)/[REDACTED]/Ig'
              exit 1
            fi
            sleep 2
          done
          echo "::error::Bridge not ready before deadline"
          docker logs unity-mcp --tail 200 | sed -E 's/((email|serial|license|password|token)[^[:space:]]*)/[REDACTED]/Ig'
          exit 1

      - name: Pin Claude tool permissions
        run: |
          set -eux
          mkdir -p .claude
          cat > .claude/settings.json <<'JSON'
          {
            "permissions": {
              "allow": [
                "mcp__unity",
                "Edit(reports/**)",
                "MultiEdit(reports/**)"
              ],
              "deny": [
                "Bash",
                "WebFetch",
                "WebSearch",
                "Task",
                "TodoWrite",
                "NotebookEdit",
                "NotebookRead"
              ]
            }
          }
          JSON

      - name: Prepare reports
        run: |
          set -eux
          rm -f reports/*.xml reports/*.md || true
          mkdir -p reports

      - name: Create report skeletons
        run: |
          set -eu
          cat > "$JUNIT_OUT" <<'XML'
          <?xml version="1.0" encoding="UTF-8"?>
          <testsuites><testsuite name="UnityMCP.GO-T" tests="1" failures="1" errors="0" skipped="0" time="0">
            <testcase name="GO-Suite.Bootstrap" classname="UnityMCP.GO-T">
              <failure message="bootstrap">Bootstrap placeholder; suite will append real tests.</failure>
            </testcase>
          </testsuite></testsuites>
          XML
          printf '# Unity GameObject API Test Results\n\n' > "$MD_OUT"

      - name: Verify Unity bridge status
        run: |
          set -euxo pipefail
          shopt -s nullglob
          status_files=("$GITHUB_WORKSPACE"/.unity-mcp/unity-mcp-status-*.json)
          if ((${#status_files[@]})); then
            first_status="${status_files[0]}"
            fname="$(basename "$first_status")"
            hash_part="${fname%.json}"; hash_part="${hash_part#unity-mcp-status-}"
            proj="$(jq -r '.project_name // empty' "$first_status" || true)"
            if [[ -n "${proj:-}" && -n "${hash_part:-}" ]]; then
              echo "UNITY_MCP_DEFAULT_INSTANCE=${proj}@${hash_part}" >> "$GITHUB_ENV"
              echo "Default instance set to ${proj}@${hash_part}"
            fi
          fi

      - name: Write MCP config
        run: |
          set -eux
          mkdir -p .claude
          python3 - <<'PY'
          import json
          import os
          import textwrap
          from pathlib import Path

          workspace = os.environ["GITHUB_WORKSPACE"]
          default_inst = os.environ.get("UNITY_MCP_DEFAULT_INSTANCE", "").strip()

          cfg = {
              "mcpServers": {
                  "unity": {
                      "args": [
                          "run",
                          "--active",
                          "--directory",
                          "Server",
                          "mcp-for-unity",
                          "--transport",
                          "stdio",
                      ],
                      "transport": {"type": "stdio"},
                      "env": {
                          "PYTHONUNBUFFERED": "1",
                          "MCP_LOG_LEVEL": "debug",
                          "UNITY_PROJECT_ROOT": f"{workspace}/TestProjects/UnityMCPTests",
                          "UNITY_MCP_STATUS_DIR": f"{workspace}/.unity-mcp",
                          "UNITY_MCP_HOST": "127.0.0.1",
                      },
                  }
              }
          }

          unity = cfg["mcpServers"]["unity"]
          if default_inst:
              unity["env"]["UNITY_MCP_DEFAULT_INSTANCE"] = default_inst
              if "--default-instance" not in unity["args"]:
                  unity["args"] += ["--default-instance", default_inst]

          runner_script = Path(".claude/run-unity-mcp.sh")
          workspace_path = Path(workspace)
          uv_candidate = workspace_path / ".venv" / "bin" / "uv"
          uv_cmd = uv_candidate.as_posix() if uv_candidate.exists() else "uv"
          script = textwrap.dedent(f"""\
              #!/usr/bin/env bash
              set -euo pipefail
              LOG="{workspace}/.unity-mcp/mcp-server-startup-debug.log"
              mkdir -p "$(dirname "$LOG")"
              echo "" >> "$LOG"
              echo "[ $(date -Iseconds) ] Starting unity MCP server" >> "$LOG"
              exec {uv_cmd} "$@" 2>> "$LOG"
              """)
          runner_script.write_text(script)
          runner_script.chmod(0o755)

          unity["command"] = runner_script.resolve().as_posix()

          path = Path(".claude/mcp.json")
          path.write_text(json.dumps(cfg, indent=2) + "\n")
          print(f"Wrote {path} and {runner_script}")
          PY

      # ---------- Run Claude GO pass ----------
      - name: Run Claude GO pass
        uses: anthropics/claude-code-base-action@beta
        if: steps.detect.outputs.anthropic_ok == 'true' && steps.detect.outputs.unity_ok == 'true'
        continue-on-error: true
        env:
          UNITY_MCP_DEFAULT_INSTANCE: ${{ env.UNITY_MCP_DEFAULT_INSTANCE }}
        with:
          use_node_cache: false
          prompt_file: .claude/prompts/nl-gameobject-suite.md
          mcp_config: .claude/mcp.json
          settings: .claude/settings.json
          allowed_tools: "mcp__unity,Edit(reports/**),MultiEdit(reports/**)"
          disallowed_tools: "Bash,WebFetch,WebSearch,Task,TodoWrite,NotebookEdit,NotebookRead"
          model: claude-haiku-4-5-20251001
          fallback_model: claude-sonnet-4-5-20250929
          append_system_prompt: |
            You are running the GameObject API tests.
            - Emit exactly GO-0, GO-1, GO-2, GO-3, GO-4, GO-5.
            - Write each to reports/${ID}_results.xml.
            - Stop after GO-5_results.xml is written.
          timeout_minutes: "25"
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}

      # ---------- Backfill missing tests ----------
      - name: Backfill missing GO tests
        if: always()
        shell: bash
        run: |
          python3 - <<'PY'
          from pathlib import Path
          import xml.etree.ElementTree as ET
          import re

          DESIRED = ["GO-0","GO-1","GO-2","GO-3","GO-4","GO-5"]
          seen = set()

          def id_from_filename(p: Path):
            n = p.name
            m = re.match(r'GO-?(\d+)_results\.xml$', n, re.I)
            if m:
              return f"GO-{int(m.group(1))}"
            return None

          for p in Path("reports").glob("*_results.xml"):
            fid = id_from_filename(p)
            if fid in DESIRED:
              seen.add(fid)

          Path("reports").mkdir(parents=True, exist_ok=True)
          for d in DESIRED:
            if d in seen:
              continue
            frag = Path(f"reports/{d}_results.xml")
            tc = ET.Element("testcase", {"classname":"UnityMCP.GO-T", "name": d})
            fail = ET.SubElement(tc, "failure", {"message":"not produced"})
            fail.text = "The agent did not emit a fragment for this test."
            ET.ElementTree(tc).write(frag, encoding="utf-8", xml_declaration=False)
            print(f"backfill: {d}")
          PY

      # ---------- Merge fragments into JUnit ----------
      - name: Assemble JUnit
        if: always()
        shell: bash
        run: |
          python3 - <<'PY'
          from pathlib import Path
          import xml.etree.ElementTree as ET
          import re, os

          def localname(tag: str) -> str:
              return tag.rsplit('}', 1)[-1] if '}' in tag else tag

          src = Path(os.environ.get('JUNIT_OUT', 'reports/junit-go-suite.xml'))
          if not src.exists():
              raise SystemExit(0)

          tree = ET.parse(src)
          root = tree.getroot()
          suite = root.find('./*') if localname(root.tag) == 'testsuites' else root
          if suite is None:
              raise SystemExit(0)

          def id_from_filename(p: Path):
              n = p.name
              m = re.match(r'GO-?(\d+)_results\.xml$', n, re.I)
              if m:
                  return f"GO-{int(m.group(1))}"
              return None

          fragments = sorted(Path('reports').glob('GO-*_results.xml'))
          added = 0

          for frag in fragments:
              try:
                  froot = ET.parse(frag).getroot()
                  if localname(froot.tag) == 'testcase':
                      suite.append(froot)
                      added += 1
              except Exception as e:
                  print(f"Warning: Could not parse fragment {frag}: {e}")

          if added:
              for tc in list(suite.findall('.//testcase')):
                  if (tc.get('name') or '') == 'GO-Suite.Bootstrap':
                      suite.remove(tc)
              testcases = suite.findall('.//testcase')
              failures_cnt = sum(1 for tc in testcases if (tc.find('failure') is not None or tc.find('error') is not None))
              suite.set('tests', str(len(testcases)))
              suite.set('failures', str(failures_cnt))
              suite.set('errors', '0')
              suite.set('skipped', '0')
              tree.write(src, encoding='utf-8', xml_declaration=True)
              print(f"Appended {added} testcase(s).")
          PY

      # ---------- Build markdown summary ----------
      - name: Build markdown summary
        if: always()
        shell: bash
        run: |
          python3 - <<'PY'
          import xml.etree.ElementTree as ET
          from pathlib import Path
          import os, html, re

          def localname(tag: str) -> str:
              return tag.rsplit('}', 1)[-1] if '}' in tag else tag

          src = Path(os.environ.get('JUNIT_OUT', 'reports/junit-go-suite.xml'))
          md_out = Path(os.environ.get('MD_OUT', 'reports/junit-go-suite.md'))
          md_out.parent.mkdir(parents=True, exist_ok=True)

          if not src.exists():
              md_out.write_text("# Unity GameObject API Test Results\n\n(No JUnit found)\n", encoding='utf-8')
              raise SystemExit(0)

          tree = ET.parse(src)
          root = tree.getroot()
          suite = root.find('./*') if localname(root.tag) == 'testsuites' else root
          cases = [] if suite is None else list(suite.findall('.//testcase'))

          desired = ['GO-0','GO-1','GO-2','GO-3','GO-4','GO-5']
          default_titles = {
              'GO-0': 'Hierarchy with ComponentTypes',
              'GO-1': 'Find GameObjects Tool',
              'GO-2': 'GameObject Resource Read',
              'GO-3': 'Components Resource Read',
              'GO-4': 'Manage Components Tool',
              'GO-5': 'Deprecation Warnings',
          }

          def id_from_case(tc):
              n = (tc.get('name') or '')
              m = re.match(r'\s*(GO-\d+)\b', n)
              if m:
                  return m.group(1)
              return None

          id_status = {}
          for tc in cases:
              tid = id_from_case(tc)
              if not tid or tid not in desired or tid in id_status:
                  continue
              ok = (tc.find('failure') is None and tc.find('error') is None)
              id_status[tid] = ok

          total = len(cases)
          failures = sum(1 for tc in cases if (tc.find('failure') is not None or tc.find('error') is not None))
          passed = total - failures

          lines = [
              '# Unity GameObject API Test Results',
              '',
              f'Totals: {passed} passed, {failures} failed, {total} total',
              '',
              '## Test Checklist'
          ]
          for p in desired:
              st = id_status.get(p, None)
              label = f"{p} — {default_titles.get(p, '')}"
              lines.append(f"- [x] {label}" if st is True else (f"- [ ] {label} (fail)" if st is False else f"- [ ] {label} (not run)"))
          lines.append('')

          lines.append('## Test Details')
          for tc in cases:
              tid = id_from_case(tc)
              if not tid:
                  continue
              title = tc.get('name') or tid
              ok = (tc.find('failure') is None and tc.find('error') is None)
              badge = "PASS" if ok else "FAIL"
              lines.append(f"### {title} — {badge}")
              so = tc.find('system-out')
              text = '' if so is None or so.text is None else html.unescape(so.text.strip())
              if text:
                  lines += ['```', text[:2000], '```']
              else:
                  lines.append('(no system-out)')
              node = tc.find('failure') or tc.find('error')
              if node is not None:
                  msg = (node.get('message') or '').strip()
                  if msg:
                      lines.append(f"- Message: {msg}")
              lines.append('')

          md_out.write_text('\n'.join(lines), encoding='utf-8')
          PY

      - name: GO details -> Job Summary
        if: always()
        run: |
          echo "## Unity GameObject API Tests — Summary" >> $GITHUB_STEP_SUMMARY
          python3 - <<'PY' >> $GITHUB_STEP_SUMMARY
          from pathlib import Path
          p = Path('reports/junit-go-suite.md')
          if p.exists():
              text = p.read_bytes().decode('utf-8', 'replace')
              print(text[:65000])
          else:
              print("_No markdown report found._")
          PY

      - name: Publish JUnit report
        if: always()
        uses: mikepenz/action-junit-report@v5
        with:
          report_paths: "${{ env.JUNIT_OUT }}"
          include_passed: true
          detailed_summary: true
          annotate_notice: true
          require_tests: false
          fail_on_parse_error: true

      - name: Upload artifacts
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: claude-go-suite-artifacts
          path: |
            ${{ env.JUNIT_OUT }}
            ${{ env.MD_OUT }}
            reports/*_results.xml
          retention-days: 7

      # ---------- Cleanup ----------
      - name: Stop Unity
        if: always()
        run: |
          docker logs --tail 400 unity-mcp | sed -E 's/((email|serial|license|password|token)[^[:space:]]*)/[REDACTED]/ig' || true
          docker rm -f unity-mcp || true

      - name: Return Pro license (if used)
        if: always() && steps.lic.outputs.use_ebl == 'true' && steps.lic.outputs.has_serial == 'true'
        uses: game-ci/unity-return-license@v2
        continue-on-error: true
        env:
          UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
          UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
          UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}

