|
| 1 | +name: E2E Bridge Smoke (deterministic, no LLM) |
| 2 | + |
| 3 | +# Boots a headless Unity Editor, starts the Python MCP server's wire path, and |
| 4 | +# drives a fixed sequence of real tool calls with exact assertions |
| 5 | +# (Server/tests/e2e/bridge_smoke.py). Unlike claude-nl-suite.yml this needs |
| 6 | +# NO Anthropic API key -- it is deterministic and cheap, so it can gate PRs and |
| 7 | +# releases. It still needs Unity license secrets to boot the Editor. |
| 8 | + |
| 9 | +on: |
| 10 | + workflow_dispatch: |
| 11 | + pull_request: |
| 12 | + paths: |
| 13 | + - "MCPForUnity/Editor/**" |
| 14 | + - "MCPForUnity/Runtime/**" |
| 15 | + - "Server/src/**" |
| 16 | + - "Server/tests/e2e/**" |
| 17 | + - "tools/local_harness.py" |
| 18 | + - ".github/workflows/e2e-bridge.yml" |
| 19 | + |
| 20 | +permissions: |
| 21 | + contents: read |
| 22 | + |
| 23 | +concurrency: |
| 24 | + group: ${{ github.workflow }}-${{ github.ref }} |
| 25 | + cancel-in-progress: true |
| 26 | + |
| 27 | +env: |
| 28 | + UNITY_IMAGE: unityci/editor:ubuntu-2021.3.45f2-linux-il2cpp-3 |
| 29 | + |
| 30 | +jobs: |
| 31 | + e2e-bridge: |
| 32 | + runs-on: ubuntu-24.04 |
| 33 | + timeout-minutes: 40 |
| 34 | + steps: |
| 35 | + - name: Detect Unity license secrets |
| 36 | + id: detect |
| 37 | + env: |
| 38 | + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} |
| 39 | + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} |
| 40 | + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} |
| 41 | + UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} |
| 42 | + run: | |
| 43 | + set -e |
| 44 | + if [ -n "$UNITY_LICENSE" ] || { [ -n "$UNITY_EMAIL" ] && [ -n "$UNITY_PASSWORD" ] && [ -n "$UNITY_SERIAL" ]; }; then |
| 45 | + echo "unity_ok=true" >> "$GITHUB_OUTPUT" |
| 46 | + else |
| 47 | + echo "unity_ok=false" >> "$GITHUB_OUTPUT" |
| 48 | + echo "::warning::Unity license secrets absent; E2E bridge smoke will be skipped (not failed)." |
| 49 | + fi |
| 50 | +
|
| 51 | + - uses: actions/checkout@v4 |
| 52 | + if: steps.detect.outputs.unity_ok == 'true' |
| 53 | + with: |
| 54 | + fetch-depth: 0 |
| 55 | + |
| 56 | + - uses: astral-sh/setup-uv@v4 |
| 57 | + if: steps.detect.outputs.unity_ok == 'true' |
| 58 | + with: |
| 59 | + python-version: "3.11" |
| 60 | + |
| 61 | + - name: Install MCP server |
| 62 | + if: steps.detect.outputs.unity_ok == 'true' |
| 63 | + run: | |
| 64 | + set -eux |
| 65 | + uv venv |
| 66 | + echo "VIRTUAL_ENV=$GITHUB_WORKSPACE/.venv" >> "$GITHUB_ENV" |
| 67 | + echo "$GITHUB_WORKSPACE/.venv/bin" >> "$GITHUB_PATH" |
| 68 | + uv pip install -e Server |
| 69 | +
|
| 70 | + # --- License staging (mirrors claude-nl-suite.yml) --- |
| 71 | + - name: Decide license sources |
| 72 | + if: steps.detect.outputs.unity_ok == 'true' |
| 73 | + id: lic |
| 74 | + shell: bash |
| 75 | + env: |
| 76 | + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} |
| 77 | + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} |
| 78 | + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} |
| 79 | + UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} |
| 80 | + run: | |
| 81 | + set -eu |
| 82 | + use_ulf=false; use_ebl=false |
| 83 | + [[ -n "${UNITY_LICENSE:-}" ]] && use_ulf=true |
| 84 | + [[ -n "${UNITY_EMAIL:-}" && -n "${UNITY_PASSWORD:-}" && -n "${UNITY_SERIAL:-}" ]] && use_ebl=true |
| 85 | + echo "use_ulf=$use_ulf" >> "$GITHUB_OUTPUT" |
| 86 | + echo "use_ebl=$use_ebl" >> "$GITHUB_OUTPUT" |
| 87 | +
|
| 88 | + - name: Stage Unity .ulf license (from secret) |
| 89 | + if: steps.detect.outputs.unity_ok == 'true' && steps.lic.outputs.use_ulf == 'true' |
| 90 | + id: ulf |
| 91 | + env: |
| 92 | + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} |
| 93 | + shell: bash |
| 94 | + run: | |
| 95 | + set -eu |
| 96 | + mkdir -p "$RUNNER_TEMP/unity-license-ulf" "$RUNNER_TEMP/unity-local/Unity" |
| 97 | + f="$RUNNER_TEMP/unity-license-ulf/Unity_lic.ulf" |
| 98 | + if printf "%s" "$UNITY_LICENSE" | base64 -d - >/dev/null 2>&1; then |
| 99 | + printf "%s" "$UNITY_LICENSE" | base64 -d - > "$f" |
| 100 | + else |
| 101 | + printf "%s" "$UNITY_LICENSE" > "$f" |
| 102 | + fi |
| 103 | + chmod 600 "$f" || true |
| 104 | + if grep -qi '<Signature>' "$f"; then |
| 105 | + cp -f "$f" "$RUNNER_TEMP/unity-local/Unity/Unity_lic.ulf" |
| 106 | + echo "ok=true" >> "$GITHUB_OUTPUT" |
| 107 | + else |
| 108 | + echo "ok=false" >> "$GITHUB_OUTPUT" |
| 109 | + fi |
| 110 | +
|
| 111 | + - name: Activate Unity (EBL via container) |
| 112 | + if: steps.detect.outputs.unity_ok == 'true' && steps.lic.outputs.use_ebl == 'true' |
| 113 | + shell: bash |
| 114 | + env: |
| 115 | + UNITY_IMAGE: ${{ env.UNITY_IMAGE }} |
| 116 | + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} |
| 117 | + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} |
| 118 | + UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} |
| 119 | + run: | |
| 120 | + set -euo pipefail |
| 121 | + mkdir -p "$RUNNER_TEMP/unity-config" "$RUNNER_TEMP/unity-local" |
| 122 | + docker run --rm --network host \ |
| 123 | + -e HOME=/root -e UNITY_EMAIL -e UNITY_PASSWORD -e UNITY_SERIAL \ |
| 124 | + -v "$RUNNER_TEMP/unity-config:/root/.config/unity3d" \ |
| 125 | + -v "$RUNNER_TEMP/unity-local:/root/.local/share/unity3d" \ |
| 126 | + "$UNITY_IMAGE" bash -lc ' |
| 127 | + set -euxo pipefail |
| 128 | + /opt/unity/Editor/Unity -batchmode -nographics -logFile - \ |
| 129 | + -username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" -serial "$UNITY_SERIAL" -quit || true |
| 130 | + ' |
| 131 | +
|
| 132 | + - name: Warm up project (import Library once) |
| 133 | + if: steps.detect.outputs.unity_ok == 'true' |
| 134 | + shell: bash |
| 135 | + env: |
| 136 | + UNITY_IMAGE: ${{ env.UNITY_IMAGE }} |
| 137 | + ULF_OK: ${{ steps.ulf.outputs.ok }} |
| 138 | + run: | |
| 139 | + set -euxo pipefail |
| 140 | + manual_args=() |
| 141 | + if [[ "${ULF_OK:-false}" == "true" ]]; then |
| 142 | + manual_args=(-manualLicenseFile "/root/.local/share/unity3d/Unity/Unity_lic.ulf") |
| 143 | + fi |
| 144 | + docker run --rm --network host \ |
| 145 | + -e HOME=/root \ |
| 146 | + -v "${{ github.workspace }}:${{ github.workspace }}" -w "${{ github.workspace }}" \ |
| 147 | + -v "$RUNNER_TEMP/unity-config:/root/.config/unity3d" \ |
| 148 | + -v "$RUNNER_TEMP/unity-local:/root/.local/share/unity3d" \ |
| 149 | + -v "$RUNNER_TEMP/unity-cache:/root/.cache/unity3d" \ |
| 150 | + "$UNITY_IMAGE" /opt/unity/Editor/Unity -batchmode -nographics -logFile - \ |
| 151 | + -projectPath "${{ github.workspace }}/TestProjects/UnityMCPTests" \ |
| 152 | + "${manual_args[@]}" -quit |
| 153 | +
|
| 154 | + - name: Clean old MCP status |
| 155 | + if: steps.detect.outputs.unity_ok == 'true' |
| 156 | + run: | |
| 157 | + set -eux |
| 158 | + mkdir -p "$GITHUB_WORKSPACE/.unity-mcp" |
| 159 | + rm -f "$GITHUB_WORKSPACE/.unity-mcp"/unity-mcp-status-*.json || true |
| 160 | +
|
| 161 | + - name: Run headless bridge harness (boot + wait + smoke/editmode/playmode) |
| 162 | + if: steps.detect.outputs.unity_ok == 'true' |
| 163 | + shell: bash |
| 164 | + env: |
| 165 | + UNITY_IMAGE: ${{ env.UNITY_IMAGE }} |
| 166 | + ULF_OK: ${{ steps.ulf.outputs.ok }} |
| 167 | + run: | |
| 168 | + set -euxo pipefail |
| 169 | + # In --ci mode the harness drives the DockerLauncher: it runs the same |
| 170 | + # docker container (repo .unity-mcp status dir, docker liveness/teardown, |
| 171 | + # log redaction), waits on the status file, derives the instance, then |
| 172 | + # runs the smoke + EditMode + PlayMode legs over the bridge. |
| 173 | + license_args=() |
| 174 | + if [[ "${ULF_OK:-false}" == "true" ]]; then |
| 175 | + license_args=(--editor-arg -manualLicenseFile \ |
| 176 | + --editor-arg "/root/.local/share/unity3d/Unity/Unity_lic.ulf") |
| 177 | + fi |
| 178 | + python3 tools/local_harness.py --ci \ |
| 179 | + --legs smoke,editmode,playmode \ |
| 180 | + --project-path TestProjects/UnityMCPTests \ |
| 181 | + --reports reports \ |
| 182 | + "${license_args[@]}" |
| 183 | +
|
| 184 | + - name: Unity logs on failure |
| 185 | + if: failure() && steps.detect.outputs.unity_ok == 'true' |
| 186 | + run: docker logs unity-mcp --tail 200 | sed -E 's/((email|serial|license|password|token)[^[:space:]]*)/[REDACTED]/Ig' || true |
| 187 | + |
| 188 | + - name: Upload E2E report |
| 189 | + if: always() && steps.detect.outputs.unity_ok == 'true' |
| 190 | + uses: actions/upload-artifact@v4 |
| 191 | + with: |
| 192 | + name: e2e-bridge-report |
| 193 | + path: reports/junit-*.xml |
| 194 | + if-no-files-found: ignore |
0 commit comments