chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
name: Beta Release (PyPI Pre-release)
|
||||
|
||||
concurrency:
|
||||
group: beta-release
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- beta
|
||||
paths:
|
||||
- "Server/**"
|
||||
- "MCPForUnity/**"
|
||||
|
||||
jobs:
|
||||
unity_tests:
|
||||
name: Unity tests gate
|
||||
if: github.actor != 'github-actions[bot]'
|
||||
uses: ./.github/workflows/unity-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
python_tests:
|
||||
name: Python tests gate
|
||||
if: github.actor != 'github-actions[bot]'
|
||||
uses: ./.github/workflows/python-tests.yml
|
||||
|
||||
update_unity_beta_version:
|
||||
name: Update Unity package to beta version
|
||||
runs-on: ubuntu-latest
|
||||
needs: [unity_tests, python_tests]
|
||||
# Avoid running when the workflow's own automation merges the PR
|
||||
# created by this workflow (prevents a version-bump loop).
|
||||
if: github.actor != 'github-actions[bot]'
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
outputs:
|
||||
unity_beta_version: ${{ steps.version.outputs.unity_beta_version }}
|
||||
version_updated: ${{ steps.commit.outputs.updated }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: beta
|
||||
|
||||
- name: Generate beta version for Unity package
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Read current Unity package version
|
||||
CURRENT_VERSION=$(jq -r '.version' MCPForUnity/package.json)
|
||||
echo "Current Unity package version: $CURRENT_VERSION"
|
||||
|
||||
# Check if already a beta version - increment beta number
|
||||
if [[ "$CURRENT_VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-beta\.([0-9]+)$ ]]; then
|
||||
BASE_VERSION="${BASH_REMATCH[1]}"
|
||||
BETA_NUM="${BASH_REMATCH[2]}"
|
||||
NEXT_BETA=$((BETA_NUM + 1))
|
||||
BETA_VERSION="${BASE_VERSION}-beta.${NEXT_BETA}"
|
||||
echo "Incrementing beta number: $CURRENT_VERSION -> $BETA_VERSION"
|
||||
elif [[ "$CURRENT_VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
|
||||
# Stable version - bump patch and add -beta.1 suffix
|
||||
# This ensures beta is "newer" than stable (9.3.2-beta.1 > 9.3.1)
|
||||
# The release workflow decides final bump type (patch/minor/major)
|
||||
MAJOR="${BASH_REMATCH[1]}"
|
||||
MINOR="${BASH_REMATCH[2]}"
|
||||
PATCH="${BASH_REMATCH[3]}"
|
||||
NEXT_PATCH=$((PATCH + 1))
|
||||
BETA_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}-beta.1"
|
||||
echo "Converting stable to beta: $CURRENT_VERSION -> $BETA_VERSION"
|
||||
else
|
||||
echo "Error: Could not parse version '$CURRENT_VERSION'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Always output the computed version
|
||||
echo "unity_beta_version=$BETA_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Only skip update if computed version matches current (no change needed)
|
||||
if [[ "$BETA_VERSION" == "$CURRENT_VERSION" ]]; then
|
||||
echo "Version unchanged, skipping update"
|
||||
echo "needs_update=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Version will be updated: $CURRENT_VERSION -> $BETA_VERSION"
|
||||
echo "needs_update=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Update Unity package.json with beta version
|
||||
if: steps.version.outputs.needs_update == 'true'
|
||||
env:
|
||||
BETA_VERSION: ${{ steps.version.outputs.unity_beta_version }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Update package.json version
|
||||
jq --arg v "$BETA_VERSION" '.version = $v' MCPForUnity/package.json > tmp.json
|
||||
mv tmp.json MCPForUnity/package.json
|
||||
echo "Updated MCPForUnity/package.json:"
|
||||
jq '.version' MCPForUnity/package.json
|
||||
|
||||
- name: Commit to temporary branch and create PR
|
||||
id: commit
|
||||
if: steps.version.outputs.needs_update == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
BETA_VERSION: ${{ steps.version.outputs.unity_beta_version }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email "actions@github.com"
|
||||
|
||||
if git diff --quiet MCPForUnity/package.json; then
|
||||
echo "No changes to commit"
|
||||
echo "updated=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create a temporary branch for the version update
|
||||
BRANCH="beta-version-${BETA_VERSION}-${GITHUB_RUN_ID}"
|
||||
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
|
||||
|
||||
git checkout -b "$BRANCH"
|
||||
git add MCPForUnity/package.json
|
||||
git commit -m "chore: update Unity package to beta version ${BETA_VERSION}"
|
||||
git push origin "$BRANCH"
|
||||
|
||||
# Check if PR already exists
|
||||
if gh pr view "$BRANCH" >/dev/null 2>&1; then
|
||||
echo "PR already exists for $BRANCH"
|
||||
PR_NUMBER=$(gh pr view "$BRANCH" --json number -q '.number')
|
||||
else
|
||||
PR_URL=$(gh pr create \
|
||||
--base beta \
|
||||
--head "$BRANCH" \
|
||||
--title "chore: update Unity package to beta version ${BETA_VERSION}" \
|
||||
--body "Automated beta version bump for the Unity package.")
|
||||
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
|
||||
PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$')
|
||||
fi
|
||||
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
|
||||
echo "updated=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Auto-merge version bump PR
|
||||
if: steps.commit.outputs.updated == 'true' && steps.commit.outputs.pr_number != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ steps.commit.outputs.pr_number }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh pr merge "$PR_NUMBER" --merge --delete-branch
|
||||
|
||||
publish_pypi_prerelease:
|
||||
name: Publish beta to PyPI (pre-release)
|
||||
runs-on: ubuntu-latest
|
||||
needs: [unity_tests, python_tests]
|
||||
# Avoid double-publish when the bot merges the version bump PR
|
||||
if: github.actor != 'github-actions[bot]'
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/mcpforunityserver
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: beta
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
version: "latest"
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "Server/uv.lock"
|
||||
|
||||
- name: Generate beta version
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
RAW_VERSION=$(grep -oP '(?<=version = ")[^"]+' Server/pyproject.toml)
|
||||
echo "Raw version: $RAW_VERSION"
|
||||
|
||||
# Check if already a beta/prerelease version
|
||||
if [[ "$RAW_VERSION" =~ (a|b|rc|\.dev|\.post)[0-9]+$ ]]; then
|
||||
IS_PRERELEASE=true
|
||||
# Strip the prerelease suffix to get base version
|
||||
BASE_VERSION=$(echo "$RAW_VERSION" | sed -E 's/(a|b|rc|\.dev|\.post)[0-9]+$//')
|
||||
else
|
||||
IS_PRERELEASE=false
|
||||
BASE_VERSION="$RAW_VERSION"
|
||||
fi
|
||||
|
||||
# Validate we have a proper X.Y.Z format
|
||||
if ! [[ "$BASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Error: Could not parse version '$RAW_VERSION' -> '$BASE_VERSION'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IFS='.' read -r MAJOR MINOR PATCH <<< "$BASE_VERSION"
|
||||
|
||||
# Only bump patch if coming from stable; keep same base if already prerelease
|
||||
if [[ "$IS_PRERELEASE" == "true" ]]; then
|
||||
# Already on a beta series - keep the same base version
|
||||
NEXT_PATCH="$PATCH"
|
||||
echo "Already prerelease, keeping base: $BASE_VERSION"
|
||||
else
|
||||
# Stable version - bump patch to ensure beta is "newer"
|
||||
NEXT_PATCH=$((PATCH + 1))
|
||||
echo "Stable version, bumping patch: $PATCH -> $NEXT_PATCH"
|
||||
fi
|
||||
|
||||
BETA_NUMBER="$(date +%Y%m%d%H%M%S)"
|
||||
BETA_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}b${BETA_NUMBER}"
|
||||
echo "Base version: $BASE_VERSION"
|
||||
echo "Beta version: $BETA_VERSION"
|
||||
echo "beta_version=$BETA_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Update version for beta release
|
||||
env:
|
||||
BETA_VERSION: ${{ steps.version.outputs.beta_version }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sed -i "s/^version = .*/version = \"${BETA_VERSION}\"/" Server/pyproject.toml
|
||||
echo "Updated pyproject.toml:"
|
||||
grep "^version" Server/pyproject.toml
|
||||
|
||||
- name: Build a binary wheel and a source tarball
|
||||
shell: bash
|
||||
run: uv build
|
||||
working-directory: ./Server
|
||||
|
||||
- name: Publish distribution to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: Server/dist/
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,97 @@
|
||||
name: Docs — Build & Deploy
|
||||
|
||||
# Builds the Docusaurus site under /website and, on push to beta, deploys
|
||||
# to GitHub Pages at https://coplaydev.github.io/unity-mcp/.
|
||||
#
|
||||
# PR runs only build (no deploy) as a preview-validation step.
|
||||
# Re-deploys also fire when the Python tool/resource registry changes,
|
||||
# so M3's auto-generated reference pages stay fresh.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [beta]
|
||||
paths:
|
||||
- website/**
|
||||
- docs/**
|
||||
- Server/src/services/tools/**
|
||||
- Server/src/services/resources/**
|
||||
- Server/src/services/registry/**
|
||||
- .github/workflows/docs-deploy.yml
|
||||
pull_request:
|
||||
branches: [beta, main]
|
||||
paths:
|
||||
- website/**
|
||||
- docs/**
|
||||
- Server/src/services/tools/**
|
||||
- Server/src/services/resources/**
|
||||
- Server/src/services/registry/**
|
||||
- .github/workflows/docs-deploy.yml
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build site
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# Full history so Docusaurus's showLastUpdateTime / showLastUpdateAuthor
|
||||
# can resolve real per-file commit metadata. Shallow clones make every
|
||||
# page report the latest commit instead.
|
||||
fetch-depth: 0
|
||||
# No push back from this workflow — the deploy uses the Pages
|
||||
# OIDC token issued by actions/deploy-pages, not the repo token.
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
cache-dependency-path: website/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: website
|
||||
run: npm ci
|
||||
|
||||
- name: Setup Pages
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/beta'
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Build
|
||||
working-directory: website
|
||||
env:
|
||||
# Inject the cookieless GoatCounter beacon when provisioned. The site
|
||||
# gates on process.env.GOATCOUNTER_CODE, which Actions does NOT
|
||||
# auto-expose — the Actions variable must be mapped in explicitly.
|
||||
GOATCOUNTER_CODE: ${{ vars.GOATCOUNTER_CODE }}
|
||||
run: npm run build
|
||||
|
||||
- name: Upload Pages artifact
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/beta'
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: website/build
|
||||
|
||||
deploy:
|
||||
name: Deploy to GitHub Pages
|
||||
needs: build
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/beta'
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Deploy
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
@@ -0,0 +1,72 @@
|
||||
name: Docs — Reference Drift Check
|
||||
|
||||
# Fails a PR if the committed /website/docs/reference/ output disagrees
|
||||
# with what tools/generate_docs_reference.py would produce from the live
|
||||
# Python tool/resource registry. Contributors should regenerate locally:
|
||||
#
|
||||
# cd Server && uv run python ../tools/generate_docs_reference.py
|
||||
#
|
||||
# or install the pre-commit hook to make this automatic:
|
||||
#
|
||||
# tools/install-hooks.sh
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [beta, main]
|
||||
paths:
|
||||
- Server/src/services/tools/**
|
||||
- Server/src/services/resources/**
|
||||
- Server/src/services/registry/**
|
||||
- website/docs/reference/**
|
||||
- tools/generate_docs_reference.py
|
||||
- .github/workflows/docs-generate.yml
|
||||
push:
|
||||
branches: [beta]
|
||||
paths:
|
||||
- Server/src/services/tools/**
|
||||
- Server/src/services/resources/**
|
||||
- Server/src/services/registry/**
|
||||
- website/docs/reference/**
|
||||
- tools/generate_docs_reference.py
|
||||
- .github/workflows/docs-generate.yml
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check docs reference is fresh
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install 3.10
|
||||
|
||||
- name: Sync Server deps
|
||||
working-directory: Server
|
||||
run: uv sync
|
||||
|
||||
- name: Drift check
|
||||
working-directory: Server
|
||||
run: uv run python ../tools/generate_docs_reference.py --check
|
||||
|
||||
- name: Tool-count sanity
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Match the decorator at start-of-line (skips imports and docstring mentions).
|
||||
decorator_count=$(grep -rhE "^@mcp_for_unity_tool\(" Server/src/services/tools/ | wc -l | tr -d ' ')
|
||||
# md_count excludes group landing pages (index.md) and the catalog root.
|
||||
md_count=$(find website/docs/reference/tools -name '*.md' -not -name 'index.md' | wc -l | tr -d ' ')
|
||||
echo "decorator_count=$decorator_count, md_count=$md_count"
|
||||
if [[ "$decorator_count" != "$md_count" ]]; then
|
||||
echo "Mismatch: $decorator_count @mcp_for_unity_tool decorators vs $md_count reference pages." >&2
|
||||
echo "Run: cd Server && uv run python ../tools/generate_docs_reference.py" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,194 @@
|
||||
name: E2E Bridge Smoke (deterministic, no LLM)
|
||||
|
||||
# Boots a headless Unity Editor, starts the Python MCP server's wire path, and
|
||||
# drives a fixed sequence of real tool calls with exact assertions
|
||||
# (Server/tests/e2e/bridge_smoke.py). Unlike claude-nl-suite.yml this needs
|
||||
# NO Anthropic API key -- it is deterministic and cheap, so it can gate PRs and
|
||||
# releases. It still needs Unity license secrets to boot the Editor.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- "MCPForUnity/Editor/**"
|
||||
- "MCPForUnity/Runtime/**"
|
||||
- "Server/src/**"
|
||||
- "Server/tests/e2e/**"
|
||||
- "tools/local_harness.py"
|
||||
- ".github/workflows/e2e-bridge.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
UNITY_IMAGE: unityci/editor:ubuntu-2021.3.45f2-linux-il2cpp-3
|
||||
|
||||
jobs:
|
||||
e2e-bridge:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 40
|
||||
steps:
|
||||
- name: Detect Unity license secrets
|
||||
id: detect
|
||||
env:
|
||||
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
|
||||
run: |
|
||||
set -e
|
||||
if [ -n "$UNITY_LICENSE" ] || { [ -n "$UNITY_EMAIL" ] && [ -n "$UNITY_PASSWORD" ] && [ -n "$UNITY_SERIAL" ]; }; then
|
||||
echo "unity_ok=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "unity_ok=false" >> "$GITHUB_OUTPUT"
|
||||
echo "::warning::Unity license secrets absent; E2E bridge smoke will be skipped (not failed)."
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
if: steps.detect.outputs.unity_ok == 'true'
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: astral-sh/setup-uv@v4
|
||||
if: steps.detect.outputs.unity_ok == 'true'
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install MCP server
|
||||
if: steps.detect.outputs.unity_ok == 'true'
|
||||
run: |
|
||||
set -eux
|
||||
uv venv
|
||||
echo "VIRTUAL_ENV=$GITHUB_WORKSPACE/.venv" >> "$GITHUB_ENV"
|
||||
echo "$GITHUB_WORKSPACE/.venv/bin" >> "$GITHUB_PATH"
|
||||
uv pip install -e Server
|
||||
|
||||
# --- License staging (mirrors claude-nl-suite.yml) ---
|
||||
- name: Decide license sources
|
||||
if: steps.detect.outputs.unity_ok == 'true'
|
||||
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:-}" && -n "${UNITY_SERIAL:-}" ]] && use_ebl=true
|
||||
echo "use_ulf=$use_ulf" >> "$GITHUB_OUTPUT"
|
||||
echo "use_ebl=$use_ebl" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Stage Unity .ulf license (from secret)
|
||||
if: steps.detect.outputs.unity_ok == 'true' && 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 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)
|
||||
if: steps.detect.outputs.unity_ok == 'true' && 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
|
||||
/opt/unity/Editor/Unity -batchmode -nographics -logFile - \
|
||||
-username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" -serial "$UNITY_SERIAL" -quit || true
|
||||
'
|
||||
|
||||
- name: Warm up project (import Library once)
|
||||
if: steps.detect.outputs.unity_ok == '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
|
||||
|
||||
- name: Clean old MCP status
|
||||
if: steps.detect.outputs.unity_ok == 'true'
|
||||
run: |
|
||||
set -eux
|
||||
mkdir -p "$GITHUB_WORKSPACE/.unity-mcp"
|
||||
rm -f "$GITHUB_WORKSPACE/.unity-mcp"/unity-mcp-status-*.json || true
|
||||
|
||||
- name: Run headless bridge harness (boot + wait + smoke/editmode/playmode)
|
||||
if: steps.detect.outputs.unity_ok == 'true'
|
||||
shell: bash
|
||||
env:
|
||||
UNITY_IMAGE: ${{ env.UNITY_IMAGE }}
|
||||
ULF_OK: ${{ steps.ulf.outputs.ok }}
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
# In --ci mode the harness drives the DockerLauncher: it runs the same
|
||||
# docker container (repo .unity-mcp status dir, docker liveness/teardown,
|
||||
# log redaction), waits on the status file, derives the instance, then
|
||||
# runs the smoke + EditMode + PlayMode legs over the bridge.
|
||||
license_args=()
|
||||
if [[ "${ULF_OK:-false}" == "true" ]]; then
|
||||
license_args=(--editor-arg -manualLicenseFile \
|
||||
--editor-arg "/root/.local/share/unity3d/Unity/Unity_lic.ulf")
|
||||
fi
|
||||
python3 tools/local_harness.py --ci \
|
||||
--legs smoke,editmode,playmode \
|
||||
--project-path TestProjects/UnityMCPTests \
|
||||
--reports reports \
|
||||
"${license_args[@]}"
|
||||
|
||||
- name: Unity logs on failure
|
||||
if: failure() && steps.detect.outputs.unity_ok == 'true'
|
||||
run: docker logs unity-mcp --tail 200 | sed -E 's/((email|serial|license|password|token)[^[:space:]]*)/[REDACTED]/Ig' || true
|
||||
|
||||
- name: Upload E2E report
|
||||
if: always() && steps.detect.outputs.unity_ok == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e-bridge-report
|
||||
path: reports/junit-*.xml
|
||||
if-no-files-found: ignore
|
||||
@@ -0,0 +1,20 @@
|
||||
name: github-repo-stats
|
||||
|
||||
on:
|
||||
# schedule:
|
||||
# Run this once per day, towards the end of the day for keeping the most
|
||||
# recent data point most meaningful (hours are interpreted in UTC).
|
||||
#- cron: "0 23 * * *"
|
||||
workflow_dispatch: # Allow for running this manually.
|
||||
|
||||
jobs:
|
||||
j1:
|
||||
if: github.repository == 'CoplayDev/unity-mcp'
|
||||
name: github-repo-stats
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: run-ghrs
|
||||
# Use latest release.
|
||||
uses: jgehrcke/github-repo-stats@RELEASE
|
||||
with:
|
||||
ghtoken: ${{ secrets.ghrs_github_api_token }}
|
||||
@@ -0,0 +1,81 @@
|
||||
name: Python Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
# Exclude beta and main: those branches re-trigger this workflow via
|
||||
# workflow_call from beta-release.yml / release.yml. Without the exclusion,
|
||||
# a push to beta that touches Server/** fires this workflow twice for the
|
||||
# same SHA, and GitHub auto-cancels the duplicate.
|
||||
branches-ignore: [beta, main]
|
||||
paths:
|
||||
- Server/**
|
||||
- tools/**
|
||||
- .github/workflows/python-tests.yml
|
||||
pull_request:
|
||||
branches: [main, beta]
|
||||
paths:
|
||||
- Server/**
|
||||
- tools/**
|
||||
- .github/workflows/python-tests.yml
|
||||
workflow_dispatch: {}
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
description: "Git ref to test (defaults to the triggering ref)."
|
||||
type: string
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run Python Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install 3.10
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd Server
|
||||
uv sync
|
||||
uv pip install -e ".[dev]"
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: |
|
||||
cd Server
|
||||
uv run pytest tests/ -v --tb=short --cov --cov-report=xml --cov-report=html --cov-report=term
|
||||
|
||||
- name: Run local harness unit tests (hermetic, no Unity)
|
||||
run: |
|
||||
cd Server
|
||||
uv run python -m pytest "$GITHUB_WORKSPACE/tools/tests/" -v --tb=short
|
||||
|
||||
- name: Upload coverage reports
|
||||
uses: codecov/codecov-action@v4
|
||||
if: always()
|
||||
with:
|
||||
files: ./Server/coverage.xml
|
||||
flags: python
|
||||
name: python-coverage
|
||||
fail_ci_if_error: false
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: pytest-results
|
||||
path: |
|
||||
Server/.pytest_cache/
|
||||
Server/tests/
|
||||
Server/coverage.xml
|
||||
Server/htmlcov/
|
||||
@@ -0,0 +1,557 @@
|
||||
name: Release
|
||||
|
||||
concurrency:
|
||||
group: release-main
|
||||
cancel-in-progress: false
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_bump:
|
||||
description: "Version bump type (none = release beta version as-is)"
|
||||
type: choice
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
- none
|
||||
default: patch
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
unity_tests:
|
||||
name: Unity tests gate
|
||||
uses: ./.github/workflows/unity-tests.yml
|
||||
with:
|
||||
ref: beta
|
||||
secrets: inherit
|
||||
|
||||
python_tests:
|
||||
name: Python tests gate
|
||||
uses: ./.github/workflows/python-tests.yml
|
||||
with:
|
||||
ref: beta
|
||||
|
||||
bump:
|
||||
name: Bump version, tag, and create release
|
||||
runs-on: ubuntu-latest
|
||||
needs: [unity_tests, python_tests]
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
outputs:
|
||||
new_version: ${{ steps.compute.outputs.new_version }}
|
||||
tag: ${{ steps.tag.outputs.tag }}
|
||||
bump_branch: ${{ steps.bump_branch.outputs.name }}
|
||||
steps:
|
||||
- name: Ensure workflow is running on main
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "${GITHUB_REF_NAME}" != "main" ]]; then
|
||||
echo "This workflow must be run on the main branch. Current ref: ${GITHUB_REF_NAME}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Show current versions
|
||||
id: preview
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "============================================"
|
||||
echo "CURRENT VERSION STATUS"
|
||||
echo "============================================"
|
||||
|
||||
# Get main version
|
||||
MAIN_VERSION=$(jq -r '.version' "MCPForUnity/package.json")
|
||||
MAIN_PYPI=$(grep -oP '(?<=version = ")[^"]+' Server/pyproject.toml)
|
||||
echo "Main branch:"
|
||||
echo " Unity package: $MAIN_VERSION"
|
||||
echo " PyPI server: $MAIN_PYPI"
|
||||
echo ""
|
||||
|
||||
# Get beta version
|
||||
git fetch origin beta
|
||||
BETA_VERSION=$(git show origin/beta:MCPForUnity/package.json | jq -r '.version')
|
||||
BETA_PYPI=$(git show origin/beta:Server/pyproject.toml | grep -oP '(?<=version = ")[^"]+')
|
||||
echo "Beta branch:"
|
||||
echo " Unity package: $BETA_VERSION"
|
||||
echo " PyPI server: $BETA_PYPI"
|
||||
echo ""
|
||||
|
||||
# Compute stripped version (used for "none" bump option)
|
||||
STRIPPED=$(echo "$BETA_VERSION" | sed -E 's/-[a-zA-Z]+\.[0-9]+$//')
|
||||
echo "stripped_version=$STRIPPED" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Show what will happen
|
||||
BUMP="${{ inputs.version_bump }}"
|
||||
echo "Selected bump type: $BUMP"
|
||||
echo "After stripping beta suffix: $STRIPPED"
|
||||
|
||||
if [[ "$BUMP" == "none" ]]; then
|
||||
echo "Release version will be: $STRIPPED"
|
||||
else
|
||||
IFS='.' read -r MA MI PA <<< "$STRIPPED"
|
||||
case "$BUMP" in
|
||||
major) ((MA+=1)); MI=0; PA=0 ;;
|
||||
minor) ((MI+=1)); PA=0 ;;
|
||||
patch) ((PA+=1)) ;;
|
||||
esac
|
||||
echo "Release version will be: $MA.$MI.$PA"
|
||||
fi
|
||||
echo "============================================"
|
||||
|
||||
- name: Merge beta into main
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email "actions@github.com"
|
||||
|
||||
# Fetch beta branch
|
||||
git fetch origin beta
|
||||
|
||||
# Check if beta has changes not in main
|
||||
if git merge-base --is-ancestor origin/beta HEAD; then
|
||||
echo "beta is already merged into main. Nothing to merge."
|
||||
else
|
||||
echo "Merging beta into main..."
|
||||
git merge origin/beta --no-edit -m "chore: merge beta into main for release"
|
||||
echo "Beta merged successfully."
|
||||
fi
|
||||
|
||||
- name: Strip beta suffix from version if present
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CURRENT_VERSION=$(jq -r '.version' "MCPForUnity/package.json")
|
||||
echo "Current version: $CURRENT_VERSION"
|
||||
|
||||
# Strip beta/alpha/rc suffix if present (e.g., "9.4.0-beta.1" -> "9.4.0")
|
||||
if [[ "$CURRENT_VERSION" == *"-"* ]]; then
|
||||
STABLE_VERSION=$(echo "$CURRENT_VERSION" | sed -E 's/-[a-zA-Z]+\.[0-9]+$//')
|
||||
# Validate we have a proper X.Y.Z format after stripping
|
||||
if ! [[ "$STABLE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Error: Could not parse version '$CURRENT_VERSION' -> '$STABLE_VERSION'" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Stripping prerelease suffix: $CURRENT_VERSION -> $STABLE_VERSION"
|
||||
jq --arg v "$STABLE_VERSION" '.version = $v' MCPForUnity/package.json > tmp.json
|
||||
mv tmp.json MCPForUnity/package.json
|
||||
|
||||
# Also update pyproject.toml
|
||||
sed -i "s/^version = .*/version = \"${STABLE_VERSION}\"/" Server/pyproject.toml
|
||||
else
|
||||
echo "Version is already stable: $CURRENT_VERSION"
|
||||
fi
|
||||
|
||||
- name: Compute new version
|
||||
id: compute
|
||||
env:
|
||||
PREVIEWED_STRIPPED: ${{ steps.preview.outputs.stripped_version }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BUMP="${{ inputs.version_bump }}"
|
||||
CURRENT_VERSION=$(jq -r '.version' "MCPForUnity/package.json")
|
||||
echo "Current version: $CURRENT_VERSION"
|
||||
|
||||
# Sanity check: ensure current version matches what was previewed
|
||||
if [[ "$CURRENT_VERSION" != "$PREVIEWED_STRIPPED" ]]; then
|
||||
echo "Warning: Current version ($CURRENT_VERSION) differs from previewed ($PREVIEWED_STRIPPED)"
|
||||
echo "This may indicate an unexpected merge result. Proceeding with current version."
|
||||
fi
|
||||
|
||||
if [[ "$BUMP" == "none" ]]; then
|
||||
# Use the previewed stripped version to ensure consistency with what user saw
|
||||
NEW_VERSION="$PREVIEWED_STRIPPED"
|
||||
else
|
||||
IFS='.' read -r MA MI PA <<< "$CURRENT_VERSION"
|
||||
case "$BUMP" in
|
||||
major)
|
||||
((MA+=1)); MI=0; PA=0
|
||||
;;
|
||||
minor)
|
||||
((MI+=1)); PA=0
|
||||
;;
|
||||
patch)
|
||||
((PA+=1))
|
||||
;;
|
||||
*)
|
||||
echo "Unknown version_bump: $BUMP" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
NEW_VERSION="$MA.$MI.$PA"
|
||||
fi
|
||||
|
||||
echo "New version: $NEW_VERSION"
|
||||
echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "current_version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Compute tag
|
||||
id: tag
|
||||
env:
|
||||
NEW_VERSION: ${{ steps.compute.outputs.new_version }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "tag=v${NEW_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Update files to new version
|
||||
env:
|
||||
NEW_VERSION: ${{ steps.compute.outputs.new_version }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
echo "Updating all version references to $NEW_VERSION"
|
||||
python3 tools/update_versions.py --version "$NEW_VERSION"
|
||||
|
||||
- name: Commit version bump to a temporary branch
|
||||
id: bump_branch
|
||||
env:
|
||||
NEW_VERSION: ${{ steps.compute.outputs.new_version }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BRANCH="release/v${NEW_VERSION}"
|
||||
echo "name=$BRANCH" >> "$GITHUB_OUTPUT"
|
||||
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email "actions@github.com"
|
||||
git checkout -b "$BRANCH"
|
||||
git add MCPForUnity/package.json manifest.json "Server/pyproject.toml" Server/README.md
|
||||
if git diff --cached --quiet; then
|
||||
echo "No version changes to commit."
|
||||
else
|
||||
git commit -m "chore: bump version to ${NEW_VERSION}"
|
||||
fi
|
||||
|
||||
echo "Pushing bump branch $BRANCH"
|
||||
git push origin "$BRANCH"
|
||||
|
||||
- name: Create PR for version bump into main
|
||||
id: bump_pr
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
NEW_VERSION: ${{ steps.compute.outputs.new_version }}
|
||||
BRANCH: ${{ steps.bump_branch.outputs.name }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PR_URL=$(gh pr create \
|
||||
--base main \
|
||||
--head "$BRANCH" \
|
||||
--title "chore: bump version to ${NEW_VERSION}" \
|
||||
--body "Automated version bump to ${NEW_VERSION}.")
|
||||
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
|
||||
PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$')
|
||||
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Enable auto-merge and merge PR
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ steps.bump_pr.outputs.pr_number }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Enable auto-merge (requires repo setting "Allow auto-merge")
|
||||
gh pr merge "$PR_NUMBER" --merge --auto || true
|
||||
# Wait for PR to be merged (poll up to 2 minutes)
|
||||
for i in {1..24}; do
|
||||
STATE=$(gh pr view "$PR_NUMBER" --json state -q '.state')
|
||||
if [[ "$STATE" == "MERGED" ]]; then
|
||||
echo "PR merged successfully."
|
||||
exit 0
|
||||
fi
|
||||
echo "Waiting for PR to merge... (state: $STATE)"
|
||||
sleep 5
|
||||
done
|
||||
echo "PR did not merge in time. Attempting direct merge..."
|
||||
gh pr merge "$PR_NUMBER" --merge
|
||||
|
||||
- name: Fetch merged main and create tag
|
||||
env:
|
||||
TAG: ${{ steps.tag.outputs.tag }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch origin main
|
||||
git checkout main
|
||||
git pull origin main
|
||||
|
||||
echo "Preparing to create tag $TAG"
|
||||
|
||||
if git ls-remote --tags origin | grep -q "refs/tags/$TAG$"; then
|
||||
echo "Tag $TAG already exists on remote. Refusing to release." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git tag -a "$TAG" -m "Version ${TAG#v}"
|
||||
git push origin "$TAG"
|
||||
|
||||
- name: Clean up release branch
|
||||
if: always()
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
BRANCH: ${{ steps.bump_branch.outputs.name }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git push origin --delete "$BRANCH" || true
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ steps.tag.outputs.tag }}
|
||||
name: ${{ steps.tag.outputs.tag }}
|
||||
generate_release_notes: true
|
||||
|
||||
sync_beta:
|
||||
name: Merge main back into beta via PR
|
||||
needs:
|
||||
- bump
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout beta
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: beta
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Prepare sync branch from beta with merged main
|
||||
id: sync_branch
|
||||
env:
|
||||
NEW_VERSION: ${{ needs.bump.outputs.new_version }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email "actions@github.com"
|
||||
|
||||
# Fetch both branches so we can build a merge commit in CI.
|
||||
git fetch origin main beta
|
||||
if git merge-base --is-ancestor origin/main origin/beta; then
|
||||
echo "beta is already up to date with main. Skipping sync."
|
||||
echo "skipped=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SYNC_BRANCH="sync/main-v${NEW_VERSION}-into-beta-${GITHUB_RUN_ID}"
|
||||
echo "name=$SYNC_BRANCH" >> "$GITHUB_OUTPUT"
|
||||
echo "skipped=false" >> "$GITHUB_OUTPUT"
|
||||
|
||||
git checkout -b "$SYNC_BRANCH" origin/beta
|
||||
|
||||
if git merge origin/main --no-ff --no-commit; then
|
||||
echo "main merged cleanly into sync branch."
|
||||
else
|
||||
echo "Merge conflicts detected. Attempting expected conflict resolution for beta version files."
|
||||
CONFLICTS=$(git diff --name-only --diff-filter=U || true)
|
||||
if [[ -n "$CONFLICTS" ]]; then
|
||||
echo "$CONFLICTS"
|
||||
fi
|
||||
|
||||
# Keep beta-side prerelease versions if these files conflict.
|
||||
for file in MCPForUnity/package.json Server/pyproject.toml; do
|
||||
if git ls-files -u -- "$file" | grep -q .; then
|
||||
echo "Keeping beta version for $file"
|
||||
git checkout --ours -- "$file"
|
||||
git add "$file"
|
||||
fi
|
||||
done
|
||||
|
||||
REMAINING=$(git diff --name-only --diff-filter=U || true)
|
||||
if [[ -n "$REMAINING" ]]; then
|
||||
echo "Unexpected unresolved conflicts remain:"
|
||||
echo "$REMAINING"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
git commit -m "chore: sync main (v${NEW_VERSION}) into beta"
|
||||
|
||||
# After releasing X.Y.Z on main, beta should move to X.Y.(Z+1)-beta.1.
|
||||
IFS='.' read -r MAJOR MINOR PATCH <<< "$NEW_VERSION"
|
||||
NEXT_PATCH=$((PATCH + 1))
|
||||
NEXT_BETA_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}-beta.1"
|
||||
echo "beta_version=$NEXT_BETA_VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "Setting beta version to $NEXT_BETA_VERSION"
|
||||
|
||||
CURRENT_BETA_VERSION=$(jq -r '.version' MCPForUnity/package.json)
|
||||
if [[ "$CURRENT_BETA_VERSION" != "$NEXT_BETA_VERSION" ]]; then
|
||||
jq --arg v "$NEXT_BETA_VERSION" '.version = $v' MCPForUnity/package.json > tmp.json
|
||||
mv tmp.json MCPForUnity/package.json
|
||||
git add MCPForUnity/package.json
|
||||
git commit -m "chore: set beta version to ${NEXT_BETA_VERSION} after release v${NEW_VERSION}"
|
||||
else
|
||||
echo "Beta version already at target: $NEXT_BETA_VERSION"
|
||||
fi
|
||||
|
||||
echo "Pushing sync branch $SYNC_BRANCH"
|
||||
git push origin "$SYNC_BRANCH"
|
||||
|
||||
- name: Create PR to merge sync branch into beta
|
||||
if: steps.sync_branch.outputs.skipped != 'true'
|
||||
id: sync_pr
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
NEW_VERSION: ${{ needs.bump.outputs.new_version }}
|
||||
NEXT_BETA_VERSION: ${{ steps.sync_branch.outputs.beta_version }}
|
||||
SYNC_BRANCH: ${{ steps.sync_branch.outputs.name }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PR_URL=$(gh pr create \
|
||||
--base beta \
|
||||
--head "$SYNC_BRANCH" \
|
||||
--title "chore: sync main (v${NEW_VERSION}) into beta" \
|
||||
--body "Automated sync of main back into beta after release v${NEW_VERSION}, including beta version set to ${NEXT_BETA_VERSION}.")
|
||||
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
|
||||
PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$')
|
||||
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Merge sync PR
|
||||
if: steps.sync_branch.outputs.skipped != 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ steps.sync_pr.outputs.pr_number }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Best effort: auto-merge if repository settings allow it.
|
||||
gh pr merge "$PR_NUMBER" --merge --auto --delete-branch || true
|
||||
|
||||
# Retry direct merge for up to 2 minutes while checks settle.
|
||||
for i in {1..24}; do
|
||||
STATE=$(gh pr view "$PR_NUMBER" --json state -q '.state')
|
||||
if [[ "$STATE" == "MERGED" ]]; then
|
||||
echo "Sync PR merged successfully."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if gh pr merge "$PR_NUMBER" --merge --delete-branch >/dev/null 2>&1; then
|
||||
echo "Sync PR merged successfully."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Waiting for sync PR to become mergeable... (state: $STATE)"
|
||||
sleep 5
|
||||
done
|
||||
|
||||
echo "Sync PR did not merge in time."
|
||||
gh pr view "$PR_NUMBER" --json state,mergeStateStatus,isDraft -q '{state: .state, mergeStateStatus: .mergeStateStatus, isDraft: .isDraft}'
|
||||
exit 1
|
||||
|
||||
publish_docker:
|
||||
name: Publish Docker image
|
||||
needs:
|
||||
- bump
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.bump.outputs.tag }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: ./.github/actions/publish-docker
|
||||
with:
|
||||
docker_username: ${{ secrets.DOCKER_USERNAME }}
|
||||
docker_password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
image: ${{ secrets.DOCKER_USERNAME }}/mcp-for-unity-server
|
||||
version: ${{ needs.bump.outputs.new_version }}
|
||||
include_branch_tags: "false"
|
||||
context: .
|
||||
dockerfile: Server/Dockerfile
|
||||
platforms: linux/amd64
|
||||
|
||||
publish_pypi:
|
||||
name: Publish Python distribution to PyPI
|
||||
needs:
|
||||
- bump
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/mcpforunityserver
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.bump.outputs.tag }}
|
||||
fetch-depth: 0
|
||||
|
||||
# Inlined from .github/actions/publish-pypi to avoid nested composite action issue
|
||||
# with pypa/gh-action-pypi-publish (see https://github.com/pypa/gh-action-pypi-publish/issues/338)
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
version: "latest"
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "Server/uv.lock"
|
||||
|
||||
- name: Build a binary wheel and a source tarball
|
||||
shell: bash
|
||||
run: uv build
|
||||
working-directory: ./Server
|
||||
|
||||
- name: Publish distribution to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: Server/dist/
|
||||
|
||||
publish_mcpb:
|
||||
name: Generate and publish MCPB bundle
|
||||
needs:
|
||||
- bump
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.bump.outputs.tag }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Generate MCPB bundle
|
||||
env:
|
||||
NEW_VERSION: ${{ needs.bump.outputs.new_version }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 tools/generate_mcpb.py "$NEW_VERSION" \
|
||||
--output "unity-mcp-${NEW_VERSION}.mcpb" \
|
||||
--icon docs/images/coplay-logo.png
|
||||
|
||||
- name: Upload MCPB to release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ needs.bump.outputs.tag }}
|
||||
files: unity-mcp-${{ needs.bump.outputs.new_version }}.mcpb
|
||||
@@ -0,0 +1,25 @@
|
||||
name: Adoption stats (maintainer-only)
|
||||
# Posts unified PyPI install + docs-traffic numbers to the workflow run summary,
|
||||
# which is visible ONLY to repo collaborators. Nothing is published publicly.
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 6 * * *'
|
||||
workflow_dispatch: {}
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
stats:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with: { node-version: 20 }
|
||||
- name: Fetch + summarize stats (private to collaborators)
|
||||
env:
|
||||
# public stars/forks work with the default token; unique cloners/viewers
|
||||
# need a maintainer PAT with Administration: read (STATS_GITHUB_TOKEN).
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
STATS_GITHUB_TOKEN: ${{ secrets.STATS_GITHUB_TOKEN }}
|
||||
GOATCOUNTER_TOKEN: ${{ secrets.GOATCOUNTER_TOKEN }}
|
||||
GOATCOUNTER_SITE: ${{ secrets.GOATCOUNTER_SITE }}
|
||||
run: node website/scripts/fetch-stats.mjs >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -0,0 +1,78 @@
|
||||
name: Docs — Sync Release Notes
|
||||
|
||||
# Keeps website/docs/releases.md and the README's "Recent Updates" block
|
||||
# in sync with the GitHub Releases API. Source of truth is GitHub Releases.
|
||||
#
|
||||
# Triggers (intentionally narrow):
|
||||
# - release.published / edited / unpublished / deleted: the only time
|
||||
# the synced files can legitimately go stale. Fires on every release
|
||||
# event so the docs reflect the new version within ~30 seconds.
|
||||
# - workflow_dispatch: manual escape hatch (re-run after a one-off
|
||||
# GitHub UI edit to a release body, or to backfill after a downtime).
|
||||
#
|
||||
# Why no pull_request / schedule triggers:
|
||||
# - PR-time drift-check would fail outsider PRs that touched README for
|
||||
# unrelated reasons (e.g. typo fixes) and the contributor has no push
|
||||
# access to fix it. The synced files are maintained by the release
|
||||
# pipeline, not by PR authors — drift can't logically be introduced
|
||||
# by a PR that the workflow couldn't already handle on release.
|
||||
# - A daily cron would mask the source-of-truth (release events) and
|
||||
# produce mystery commits unattached to a release. Better to let
|
||||
# workflow_dispatch handle the rare out-of-band UI edit.
|
||||
#
|
||||
# Behavior:
|
||||
# - Commits directly to `beta` with [skip ci] in the message — the
|
||||
# change is mechanical (re-render from API output), pre-validated,
|
||||
# and confined to two well-known files.
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published, edited, unpublished, deleted]
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: docs-sync-releases
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: Sync release notes
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout beta
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: beta
|
||||
# Full history so the commit lands on a fresh ref tip.
|
||||
fetch-depth: 0
|
||||
# `sync` needs to push back, so the token must persist here.
|
||||
persist-credentials: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Sync release notes
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: python tools/sync_release_notes.py
|
||||
|
||||
- name: Commit & push (if anything changed)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if git diff --quiet -- website/docs/releases.md README.md; then
|
||||
echo "No drift — release notes already up to date."
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add website/docs/releases.md README.md
|
||||
git commit -m "docs: sync release notes from GitHub Releases [skip ci]"
|
||||
git push origin beta
|
||||
@@ -0,0 +1,265 @@
|
||||
name: Unity Tests
|
||||
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
description: "Git ref to test (defaults to the triggering ref)."
|
||||
type: string
|
||||
required: false
|
||||
default: ""
|
||||
push:
|
||||
# Exclude beta and main: those branches re-trigger this workflow via
|
||||
# workflow_call from beta-release.yml / release.yml. Without the exclusion,
|
||||
# a push to beta that touches MCPForUnity/** fires this workflow twice
|
||||
# for the same SHA, and GitHub's auto-generated concurrency group
|
||||
# (`unity-tests-refs/heads/beta`) cancels the older run with the noisy
|
||||
# "higher priority waiting request" annotation.
|
||||
branches-ignore: [beta, main]
|
||||
paths:
|
||||
- TestProjects/UnityMCPTests/**
|
||||
- MCPForUnity/Editor/**
|
||||
- MCPForUnity/Runtime/**
|
||||
- .github/workflows/unity-tests.yml
|
||||
# Same-repo PRs get a unity-tests status check on every open / push via this trigger
|
||||
# (mirrors python-tests.yml). Fork PRs ALSO fire this trigger but run in the fork's
|
||||
# context without secrets — the detect step downstream writes unity_ok=false and the
|
||||
# job exits clean with a "missing license secrets" notice so the status check still
|
||||
# appears. Maintainers apply 'safe-to-test' to invoke pull_request_target below for
|
||||
# a real fork-PR test run.
|
||||
pull_request:
|
||||
branches: [main, beta]
|
||||
paths:
|
||||
- TestProjects/UnityMCPTests/**
|
||||
- MCPForUnity/Editor/**
|
||||
- MCPForUnity/Runtime/**
|
||||
- .github/workflows/unity-tests.yml
|
||||
# Fork PRs: maintainer applies the 'safe-to-test' label after reviewing
|
||||
# the diff. The workflow runs with UNITY_LICENSE in scope against the
|
||||
# PR's head SHA. Re-pushed commits do NOT auto-trigger — maintainer must
|
||||
# remove and re-apply the label to re-run after additional review.
|
||||
pull_request_target:
|
||||
types: [labeled]
|
||||
branches: [main, beta]
|
||||
paths:
|
||||
- TestProjects/UnityMCPTests/**
|
||||
- MCPForUnity/Editor/**
|
||||
- MCPForUnity/Runtime/**
|
||||
- .github/workflows/unity-tests.yml
|
||||
|
||||
# Dedup runs for the same branch across push / pull_request / pull_request_target / workflow_call.
|
||||
# Same-repo PRs would otherwise fire both push (on the branch SHA) AND pull_request (on the PR);
|
||||
# concurrency keeps only the newer in-flight run per branch.
|
||||
concurrency:
|
||||
group: unity-tests-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
matrix:
|
||||
name: Compute Unity version matrix
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
# Gate (mirrored by testAllModes below):
|
||||
# - Always run for non-PR triggers (push / workflow_call / workflow_dispatch).
|
||||
# - Fork PRs: require 'safe-to-test' to be applied (existing secret-safety gate);
|
||||
# 'full-matrix' may be added on top to opt into the full 4-version matrix.
|
||||
# - In-repo PRs: only re-run via pull_request_target when 'full-matrix' is the
|
||||
# label that just fired (the push-event run already covered the default leg).
|
||||
if: >
|
||||
github.event_name != 'pull_request_target' ||
|
||||
(
|
||||
github.event.pull_request.head.repo.full_name != github.repository &&
|
||||
contains(github.event.pull_request.labels.*.name, 'safe-to-test') &&
|
||||
(github.event.label.name == 'safe-to-test' || github.event.label.name == 'full-matrix')
|
||||
) ||
|
||||
(
|
||||
github.event.pull_request.head.repo.full_name == github.repository &&
|
||||
github.event.label.name == 'full-matrix'
|
||||
)
|
||||
outputs:
|
||||
versions: ${{ steps.set.outputs.versions }}
|
||||
steps:
|
||||
- name: Checkout version manifest
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.event.pull_request.head.sha || github.ref }}
|
||||
sparse-checkout: tools/unity-versions.json
|
||||
sparse-checkout-cone-mode: false
|
||||
persist-credentials: false
|
||||
- name: Select versions for this trigger
|
||||
id: set
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
GH_REF: ${{ github.ref }}
|
||||
FULL_MATRIX_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'full-matrix') }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Full matrix on: beta push, workflow_call (release pipelines), workflow_dispatch,
|
||||
# or any PR (pull_request OR pull_request_target) labeled with 'full-matrix'.
|
||||
# Default (single defaultVersion from tools/unity-versions.json) otherwise — fast PR feedback.
|
||||
if [[ "$EVENT_NAME" == "workflow_dispatch" ]] || \
|
||||
[[ "$EVENT_NAME" == "workflow_call" ]] || \
|
||||
{ [[ "$EVENT_NAME" == "push" ]] && [[ "$GH_REF" == "refs/heads/beta" ]]; } || \
|
||||
{ { [[ "$EVENT_NAME" == "pull_request" ]] || [[ "$EVENT_NAME" == "pull_request_target" ]]; } && [[ "$FULL_MATRIX_LABEL" == "true" ]]; }; then
|
||||
versions=$(jq -c '[.versions[].id]' tools/unity-versions.json)
|
||||
echo "Trigger '$EVENT_NAME' on ref '$GH_REF' (full_matrix_label=$FULL_MATRIX_LABEL) → full matrix: $versions"
|
||||
else
|
||||
versions=$(jq -c '[.defaultVersion]' tools/unity-versions.json)
|
||||
echo "Trigger '$EVENT_NAME' on ref '$GH_REF' → default only: $versions"
|
||||
fi
|
||||
echo "versions=$versions" >> "$GITHUB_OUTPUT"
|
||||
|
||||
testAllModes:
|
||||
name: Test in ${{ matrix.testMode }} on Unity ${{ matrix.unityVersion }}
|
||||
needs: matrix
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
if: >
|
||||
github.event_name != 'pull_request_target' ||
|
||||
(
|
||||
github.event.pull_request.head.repo.full_name != github.repository &&
|
||||
contains(github.event.pull_request.labels.*.name, 'safe-to-test') &&
|
||||
(github.event.label.name == 'safe-to-test' || github.event.label.name == 'full-matrix')
|
||||
) ||
|
||||
(
|
||||
github.event.pull_request.head.repo.full_name == github.repository &&
|
||||
github.event.label.name == 'full-matrix'
|
||||
)
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
projectPath:
|
||||
- TestProjects/UnityMCPTests
|
||||
testMode:
|
||||
- editmode
|
||||
unityVersion: ${{ fromJson(needs.matrix.outputs.versions) }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
ref: ${{ inputs.ref || github.event.pull_request.head.sha || github.ref }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Detect Unity license secrets
|
||||
id: detect
|
||||
env:
|
||||
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
|
||||
run: |
|
||||
set -e
|
||||
if [ -n "$UNITY_LICENSE" ] || { [ -n "$UNITY_EMAIL" ] && [ -n "$UNITY_PASSWORD" ] && [ -n "$UNITY_SERIAL" ]; }; then
|
||||
echo "unity_ok=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "unity_ok=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Skip Unity tests (missing license secrets)
|
||||
if: steps.detect.outputs.unity_ok != 'true'
|
||||
run: |
|
||||
echo "Unity license secrets missing; skipping Unity tests."
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ matrix.projectPath }}/Library
|
||||
key: Library-${{ matrix.projectPath }}-${{ matrix.unityVersion }}
|
||||
restore-keys: |
|
||||
Library-${{ matrix.projectPath }}-
|
||||
Library-
|
||||
|
||||
# Run domain reload tests first (they're [Explicit] so need explicit category)
|
||||
- name: Run domain reload tests
|
||||
if: steps.detect.outputs.unity_ok == 'true'
|
||||
uses: game-ci/unity-test-runner@v4
|
||||
id: domain-tests
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
||||
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
|
||||
with:
|
||||
projectPath: ${{ matrix.projectPath }}
|
||||
unityVersion: ${{ matrix.unityVersion }}
|
||||
testMode: ${{ matrix.testMode }}
|
||||
customParameters: -testCategory domain_reload
|
||||
|
||||
- name: Run tests
|
||||
if: steps.detect.outputs.unity_ok == 'true'
|
||||
uses: game-ci/unity-test-runner@v4
|
||||
id: tests
|
||||
continue-on-error: true
|
||||
env:
|
||||
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
|
||||
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
|
||||
with:
|
||||
projectPath: ${{ matrix.projectPath }}
|
||||
unityVersion: ${{ matrix.unityVersion }}
|
||||
testMode: ${{ matrix.testMode }}
|
||||
|
||||
- name: Check test results
|
||||
if: steps.detect.outputs.unity_ok == 'true'
|
||||
env:
|
||||
ARTIFACTS_PATH: ${{ steps.tests.outputs.artifactsPath }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# `|| true` so a missing $ARTIFACTS_PATH (Unity crashed before producing any) doesn't trip
|
||||
# `pipefail` and skip the explicit empty-check diagnostic below.
|
||||
RESULTS_XML=$(find "$ARTIFACTS_PATH" -name '*.xml' 2>/dev/null | head -1 || true)
|
||||
if [ -z "$RESULTS_XML" ]; then
|
||||
echo "::error::No test results XML found — Unity may have crashed"
|
||||
exit 1
|
||||
fi
|
||||
python3 - "$RESULTS_XML" <<'PY'
|
||||
import sys, xml.etree.ElementTree as ET
|
||||
# Escape workflow-command payloads so test-controlled XML (under pull_request_target this
|
||||
# is fork-supplied) can't break annotation rendering or inject extra workflow commands.
|
||||
# https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
|
||||
def esc_data(s):
|
||||
return s.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")
|
||||
def esc_prop(s):
|
||||
return esc_data(s).replace(":", "%3A").replace(",", "%2C")
|
||||
root = ET.parse(sys.argv[1]).getroot()
|
||||
totals = root.attrib
|
||||
passed = totals.get("passed", "?")
|
||||
failed = totals.get("failed", "?")
|
||||
total = totals.get("total", "?")
|
||||
incon = totals.get("inconclusive", "?")
|
||||
skipped = totals.get("skipped", "?")
|
||||
print(f"Results: {passed} passed, {failed} failed, {incon} inconclusive, {skipped} skipped (total: {total})")
|
||||
fails = [tc for tc in root.iter("test-case") if tc.attrib.get("result") == "Failed"]
|
||||
if not fails:
|
||||
sys.exit(0)
|
||||
# Surface every failure inline so a CI watcher doesn't need to download the NUnit XML artifact.
|
||||
for tc in fails:
|
||||
name = tc.attrib.get("fullname") or tc.attrib.get("name") or "<unknown>"
|
||||
f = tc.find("failure")
|
||||
msg = (f.findtext("message") or "").strip() if f is not None else ""
|
||||
stack = (f.findtext("stack-trace") or "").strip() if f is not None else ""
|
||||
# First line of the message becomes the GitHub annotation title.
|
||||
first_line = msg.splitlines()[0] if msg else "(no message)"
|
||||
# GitHub annotations don't render multi-line bodies, so emit the full failure inside a collapsible group.
|
||||
print(f"::error title=Failed: {esc_prop(name)}::{esc_data(first_line)}")
|
||||
print(f"::group::Failure details — {esc_data(name)}")
|
||||
if msg:
|
||||
print("Message:")
|
||||
print(msg)
|
||||
if stack:
|
||||
print("Stack trace:")
|
||||
print(stack)
|
||||
print("::endgroup::")
|
||||
print(f"::error::{len(fails)} test(s) failed")
|
||||
sys.exit(1)
|
||||
PY
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always() && steps.detect.outputs.unity_ok == 'true' && steps.tests.outcome != 'skipped'
|
||||
with:
|
||||
name: Test results for ${{ matrix.testMode }} on Unity ${{ matrix.unityVersion }}
|
||||
path: ${{ steps.tests.outputs.artifactsPath }}
|
||||
Reference in New Issue
Block a user