|
| 1 | +################################################################################################### |
| 2 | +# Build Unity SpaceCraft – GitHub Actions (Ubuntu runner) |
| 3 | +# |
| 4 | +# Overview |
| 5 | +# - Builds the Unity SpaceCraft project for WebGL on GitHub-hosted Linux runners. |
| 6 | +# - Auto-detects Unity version from ProjectVersion.txt (override via input). |
| 7 | +# - Supports Production/Development profiles or a fully custom build method. |
| 8 | +# - Caches the Unity Library folder for faster imports/compiles between runs. |
| 9 | +# - Downloads real LFS asset blobs for the checked-out commit (required for Unity). |
| 10 | +# - Uploads build artifacts for later download/deploy. |
| 11 | +# |
| 12 | +# Requirements (repo secrets) |
| 13 | +# - UNITY_LICENSE: Unity license file contents (game-ci format) or ULF text. |
| 14 | +# - UNITY_EMAIL: Unity account email (for legacy activation flows in builder). |
| 15 | +# - UNITY_PASSWORD: Unity account password (for legacy activation flows in builder). |
| 16 | +# |
| 17 | +# Inputs (workflow_dispatch) |
| 18 | +# - unityVersion: Unity Editor version (e.g., 2022.3.45f1). Use 'auto' to parse ProjectVersion.txt. |
| 19 | +# - projectPath: Path to the Unity project. Defaults to Unity/SpaceCraft. |
| 20 | +# - targetPlatform: Build target (WebGL). Extendable if needed. |
| 21 | +# - buildProfile: Convenience selector mapping to build methods (Production/Development). |
| 22 | +# - buildMethod: Explicit Unity C# method string, overrides buildProfile. Example: |
| 23 | +# SpaceCraft.Editor.Builds.BuildWebGLProduction |
| 24 | +# |
| 25 | +# Build method expectations (Unity side) |
| 26 | +# - The selected build method should be a static C# function callable by Unity with: |
| 27 | +# - No arguments (or signature supported by Unity batchmode). |
| 28 | +# - It should configure build options and write the WebGL build output into the project Build/ path |
| 29 | +# or another deterministic directory consumed by artifact upload below. |
| 30 | +# - Example namespace/class/method naming is provided here as a convention; implement accordingly. |
| 31 | +# |
| 32 | +# Caching strategy |
| 33 | +# - Caches the Library/ folder per (OS, Unity version, Packages/manifest.json hash) to reduce reimport. |
| 34 | +# - On cache hits, compilations and imports should be much faster. |
| 35 | +# - We intentionally do not cache Temp/ or final Build/ outputs; those are uploaded as artifacts. |
| 36 | +# |
| 37 | +# LFS and checkout |
| 38 | +# - actions/checkout is configured with lfs: true and fetch-depth: 1 |
| 39 | +# - Ensures real LFS blobs for the current commit are available to the build. |
| 40 | +# - Uses shallow history for speed (no need for full Git history in CI). |
| 41 | +# |
| 42 | +# Artifacts |
| 43 | +# - Uploads the default game-ci build/ directory and the project Build/ directory. |
| 44 | +# - Adjust artifact paths if your build method writes to a different location. |
| 45 | +# |
| 46 | +# Invocation examples |
| 47 | +# - Production (auto-detect Unity, default project path): |
| 48 | +# unityVersion: auto |
| 49 | +# buildProfile: WebGLProductionBuild |
| 50 | +# - Development: |
| 51 | +# unityVersion: auto |
| 52 | +# buildProfile: WebGLDevelopmentBuild |
| 53 | +# - Explicit method (overrides profile mapping): |
| 54 | +# buildMethod: SpaceCraft.Editor.Builds.BuildWebGLProduction |
| 55 | +# |
| 56 | +# Notes |
| 57 | +# - This workflow focuses on GitHub-hosted runners (no self-hosted required). |
| 58 | +# - For further speed-ups, consider enabling editor caching in the action if appropriate and supported |
| 59 | +# by your environment, or splitting content generation into a separate, cacheable step. |
| 60 | +################################################################################################### |
| 61 | +name: Build Unity SpaceCraft |
| 62 | + |
| 63 | +on: |
| 64 | + workflow_dispatch: |
| 65 | + inputs: |
| 66 | + unityVersion: |
| 67 | + description: "Unity Editor version (e.g. 2022.3.45f1). Use 'auto' to detect from ProjectVersion.txt" |
| 68 | + required: false |
| 69 | + default: "auto" |
| 70 | + type: string |
| 71 | + projectPath: |
| 72 | + description: "Unity project path" |
| 73 | + required: false |
| 74 | + default: "Unity/SpaceCraft" |
| 75 | + type: string |
| 76 | + targetPlatform: |
| 77 | + description: "Unity target platform" |
| 78 | + required: false |
| 79 | + default: "WebGL" |
| 80 | + type: choice |
| 81 | + options: |
| 82 | + - WebGL |
| 83 | + buildProfile: |
| 84 | + description: "Build profile token (maps to a Unity build method)" |
| 85 | + required: false |
| 86 | + default: "WebGLProductionBuild" |
| 87 | + type: choice |
| 88 | + options: |
| 89 | + - WebGLProductionBuild |
| 90 | + - WebGLDevelopmentBuild |
| 91 | + buildMethod: |
| 92 | + description: "Explicit Unity build method (overrides buildProfile). Example: SpaceCraft.Editor.Builds.BuildWebGLProduction" |
| 93 | + required: false |
| 94 | + default: "" |
| 95 | + type: string |
| 96 | + |
| 97 | +jobs: |
| 98 | + build: |
| 99 | + name: Build ${{ inputs.targetPlatform }} (${{ inputs.buildProfile }}) |
| 100 | + runs-on: ubuntu-latest |
| 101 | + timeout-minutes: 90 |
| 102 | + |
| 103 | + env: |
| 104 | + PROJECT_PATH: ${{ inputs.projectPath }} |
| 105 | + TARGET_PLATFORM: ${{ inputs.targetPlatform }} |
| 106 | + |
| 107 | + steps: |
| 108 | + - name: Checkout |
| 109 | + uses: actions/checkout@v4 |
| 110 | + with: |
| 111 | + lfs: true |
| 112 | + fetch-depth: 1 |
| 113 | + |
| 114 | + - name: Ensure LFS files are materialized (smudged) |
| 115 | + shell: bash |
| 116 | + run: | |
| 117 | + set -euo pipefail |
| 118 | + echo "[lfs] git lfs version:" && git lfs version || true |
| 119 | + echo "[lfs] Pulling LFS objects (broad)" |
| 120 | + git lfs pull --exclude="" --include="" || true |
| 121 | + echo "[lfs] Forcing checkout (smudge) of LFS pointers" |
| 122 | + git lfs checkout || true |
| 123 | +
|
| 124 | + - name: Detect Unity version and resolve project path |
| 125 | + id: detect |
| 126 | + shell: bash |
| 127 | + run: | |
| 128 | + set -euo pipefail |
| 129 | + echo "[detect] GITHUB_WORKSPACE=${{ github.workspace }}" |
| 130 | + echo "[detect] inputs.projectPath='${{ inputs.projectPath }}' inputs.unityVersion='${{ inputs.unityVersion }}'" |
| 131 | + echo "[detect] Workspace root listing:" |
| 132 | + ls -la "${{ github.workspace }}" || true |
| 133 | +
|
| 134 | + # If version provided, honor it and still resolve project path for later steps |
| 135 | + REQ_VER="${{ inputs.unityVersion }}" |
| 136 | + PROJ_PATH_IN="${{ inputs.projectPath }}" |
| 137 | + CANDIDATE_FILE="${{ github.workspace }}/${PROJ_PATH_IN}/ProjectSettings/ProjectVersion.txt" |
| 138 | + echo "[detect] Initial candidate file: $CANDIDATE_FILE" |
| 139 | + echo "[detect] Listing input project path: '${PROJ_PATH_IN}'" |
| 140 | + ls -la "${{ github.workspace }}/${PROJ_PATH_IN}" || true |
| 141 | +
|
| 142 | + if [[ ! -f "$CANDIDATE_FILE" ]]; then |
| 143 | + echo "[detect] Input projectPath not found: $CANDIDATE_FILE" >&2 |
| 144 | + echo "[detect] Searching for ProjectVersion.txt under workspace..." |
| 145 | + MAPFILE -t FOUND < <(find "${{ github.workspace }}" -type f -path "*/ProjectSettings/ProjectVersion.txt" | sort) |
| 146 | + echo "[detect] Found ${#FOUND[@]} candidates" |
| 147 | + if [[ ${#FOUND[@]} -eq 0 ]]; then |
| 148 | + echo "[detect] No ProjectVersion.txt found in repository" >&2 |
| 149 | + exit 1 |
| 150 | + fi |
| 151 | + CANDIDATE_FILE="${FOUND[0]}" |
| 152 | + echo "[detect] Using detected ProjectVersion.txt: $CANDIDATE_FILE" |
| 153 | + echo "[detect] Candidates (up to 10):" && printf '%s\n' "${FOUND[@]:0:10}" |
| 154 | + fi |
| 155 | +
|
| 156 | + # Derive resolved project path (folder containing ProjectSettings) |
| 157 | + RESOLVED_PROJECT_PATH="$(cd "$(dirname "$CANDIDATE_FILE")/.." && pwd)" |
| 158 | + # Convert to path relative to workspace for downstream steps |
| 159 | + RESOLVED_PROJECT_PATH_REL="${RESOLVED_PROJECT_PATH#"${{ github.workspace }}/"}" |
| 160 | + echo "projectPath=$RESOLVED_PROJECT_PATH_REL" >> "$GITHUB_OUTPUT" |
| 161 | + echo "[detect] RESOLVED_PROJECT_PATH=$RESOLVED_PROJECT_PATH" |
| 162 | + echo "[detect] RESOLVED_PROJECT_PATH_REL=$RESOLVED_PROJECT_PATH_REL" |
| 163 | + echo "[detect] ProjectSettings listing:" && ls -la "$RESOLVED_PROJECT_PATH/ProjectSettings" || true |
| 164 | + echo "[detect] cat ProjectVersion.txt (pre-smudge):" && cat "$CANDIDATE_FILE" || true |
| 165 | +
|
| 166 | + # If file still looks like an LFS pointer, try to smudge just this file and re-check |
| 167 | + if head -n 1 "$CANDIDATE_FILE" | grep -q '^version https://git-lfs.github.com/spec/v1'; then |
| 168 | + echo "[detect] ProjectVersion.txt appears to be an LFS pointer; attempting smudge" |
| 169 | + git lfs checkout -- "$CANDIDATE_FILE" || true |
| 170 | + git lfs pull --include="$RESOLVED_PROJECT_PATH_REL/ProjectSettings/ProjectVersion.txt" --exclude="" || true |
| 171 | + echo "[detect] cat ProjectVersion.txt (post-smudge):" && cat "$CANDIDATE_FILE" || true |
| 172 | + fi |
| 173 | +
|
| 174 | + if [[ "$REQ_VER" != "auto" && -n "$REQ_VER" ]]; then |
| 175 | + echo "[detect] Using provided unityVersion: $REQ_VER" |
| 176 | + echo "version=$REQ_VER" >> "$GITHUB_OUTPUT" |
| 177 | + exit 0 |
| 178 | + fi |
| 179 | +
|
| 180 | + VERSION=$(grep -E "^m_EditorVersion:\s*" "$CANDIDATE_FILE" | sed -E 's/^m_EditorVersion:\s*([^[:space:]]+).*/\1/') |
| 181 | + if [[ -z "$VERSION" ]]; then |
| 182 | + echo "[detect] Failed to parse Unity version from ProjectVersion.txt" >&2 |
| 183 | + exit 1 |
| 184 | + fi |
| 185 | + echo "[detect] Detected Unity version: $VERSION" |
| 186 | + echo "version=$VERSION" >> "$GITHUB_OUTPUT" |
| 187 | +
|
| 188 | + - name: Log build configuration |
| 189 | + shell: bash |
| 190 | + run: | |
| 191 | + echo "[config] projectPath='${{ steps.detect.outputs.projectPath }}'" |
| 192 | + echo "[config] unityVersion='${{ steps.detect.outputs.version }}'" |
| 193 | + echo "[config] targetPlatform='${{ inputs.targetPlatform }}'" |
| 194 | + echo "[config] buildProfile='${{ inputs.buildProfile }}'" |
| 195 | +
|
| 196 | + - name: Validate Unity activation secrets |
| 197 | + shell: bash |
| 198 | + run: | |
| 199 | + set -euo pipefail |
| 200 | + missing=0 |
| 201 | + if [[ -z "${{ secrets.UNITY_LICENSE }}" ]]; then echo "[secrets] UNITY_LICENSE is missing" >&2; missing=1; fi |
| 202 | + if [[ -z "${{ secrets.UNITY_EMAIL }}" ]]; then echo "[secrets] UNITY_EMAIL is missing" >&2; missing=1; fi |
| 203 | + if [[ -z "${{ secrets.UNITY_PASSWORD }}" ]]; then echo "[secrets] UNITY_PASSWORD is missing" >&2; missing=1; fi |
| 204 | + if [[ "$missing" -ne 0 ]]; then |
| 205 | + echo "[secrets] One or more required secrets are missing. Per GameCI v4 Personal license activation, provide UNITY_LICENSE, UNITY_EMAIL, UNITY_PASSWORD." >&2 |
| 206 | + exit 1 |
| 207 | + fi |
| 208 | +
|
| 209 | + - name: Cache Unity Library (project import cache) |
| 210 | + uses: actions/cache@v4 |
| 211 | + with: |
| 212 | + path: | |
| 213 | + ${{ steps.detect.outputs.projectPath }}/Library |
| 214 | + key: ${{ runner.os }}-unity-library-${{ steps.detect.outputs.version }}-${{ hashFiles(format('{0}/Packages/manifest.json', steps.detect.outputs.projectPath)) }} |
| 215 | + restore-keys: | |
| 216 | + ${{ runner.os }}-unity-library-${{ steps.detect.outputs.version }}- |
| 217 | +
|
| 218 | + - name: Compute build method |
| 219 | + id: compute |
| 220 | + shell: bash |
| 221 | + run: | |
| 222 | + set -euo pipefail |
| 223 | + if [[ -n "${{ inputs.buildMethod }}" ]]; then |
| 224 | + METHOD="${{ inputs.buildMethod }}" |
| 225 | + else |
| 226 | + case "${{ inputs.buildProfile }}" in |
| 227 | + WebGLProductionBuild) |
| 228 | + METHOD="Build.WebGL_Prod" |
| 229 | + ;; |
| 230 | + WebGLDevelopmentBuild) |
| 231 | + METHOD="Build.WebGL_Dev" |
| 232 | + ;; |
| 233 | + *) |
| 234 | + echo "Unknown buildProfile: ${{ inputs.buildProfile }}" >&2 |
| 235 | + exit 1 |
| 236 | + ;; |
| 237 | + esac |
| 238 | + fi |
| 239 | + echo "buildMethod=$METHOD" >> "$GITHUB_OUTPUT" |
| 240 | + echo "[compute] Selected build method: $METHOD" |
| 241 | +
|
| 242 | + - name: Prepare logs directory |
| 243 | + shell: bash |
| 244 | + run: | |
| 245 | + mkdir -p Logs |
| 246 | + mkdir -p "${{ steps.detect.outputs.projectPath }}/Logs" |
| 247 | +
|
| 248 | + # Do not pre-create dist; Unity writes under project path, we stage after |
| 249 | + |
| 250 | + - name: Unity - Builder |
| 251 | + uses: game-ci/unity-builder@v4 |
| 252 | + env: |
| 253 | + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} |
| 254 | + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} |
| 255 | + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} |
| 256 | + with: |
| 257 | + projectPath: ${{ steps.detect.outputs.projectPath }} |
| 258 | + targetPlatform: ${{ inputs.targetPlatform }} |
| 259 | + unityVersion: ${{ steps.detect.outputs.version }} |
| 260 | + buildMethod: ${{ steps.compute.outputs.buildMethod }} |
| 261 | + customParameters: -logFile Logs/Editor.log -stackTraceLogType Full |
| 262 | + runAsHostUser: true |
| 263 | + |
| 264 | + |
| 265 | + - name: Setup Node.js |
| 266 | + if: success() |
| 267 | + uses: actions/setup-node@v4 |
| 268 | + with: |
| 269 | + node-version: '20' |
| 270 | + |
| 271 | + - name: Install controller dependencies |
| 272 | + if: success() |
| 273 | + working-directory: WebSites/controller |
| 274 | + shell: bash |
| 275 | + run: | |
| 276 | + set -euo pipefail |
| 277 | + echo "[controller] PWD=$(pwd) contents:" && ls -la |
| 278 | + if [[ -f package-lock.json ]]; then |
| 279 | + echo "[controller] Using npm ci" |
| 280 | + npm ci |
| 281 | + else |
| 282 | + echo "[controller] package-lock.json not found; using npm install" |
| 283 | + npm install |
| 284 | + fi |
| 285 | +
|
| 286 | + - name: Install pnpm for build script |
| 287 | + if: success() |
| 288 | + working-directory: WebSites/controller |
| 289 | + shell: bash |
| 290 | + run: | |
| 291 | + echo "[controller] Ensuring pnpm is available in $(pwd)" |
| 292 | + npm i -g pnpm@8 |
| 293 | +
|
| 294 | + - name: Build controller |
| 295 | + if: success() |
| 296 | + working-directory: WebSites/controller |
| 297 | + shell: bash |
| 298 | + run: | |
| 299 | + echo "[controller] Building in $(pwd)" |
| 300 | + npm run build |
| 301 | +
|
| 302 | + - name: Assemble flat artifact (no wrapper directory) |
| 303 | + if: success() |
| 304 | + shell: bash |
| 305 | + run: | |
| 306 | + set -euo pipefail |
| 307 | + STAGE="dist" |
| 308 | + SRC_BUILDS="${{ steps.detect.outputs.projectPath }}/Builds/SpaceCraft" |
| 309 | + echo "[assemble] Expecting Unity output at $SRC_BUILDS" |
| 310 | + test -d "$SRC_BUILDS" || { echo "[assemble] ERROR: Unity output missing at $SRC_BUILDS" >&2; exit 1; } |
| 311 | + rm -rf "$STAGE" |
| 312 | + mkdir -p "$STAGE/SpaceCraft" "$STAGE/controller" |
| 313 | + rsync -a "$SRC_BUILDS/" "$STAGE/SpaceCraft/" |
| 314 | + # Top-level site index |
| 315 | + if [[ -f WebSites/index.html ]]; then |
| 316 | + cp -f WebSites/index.html "$STAGE/index.html" |
| 317 | + fi |
| 318 | + # Controller site |
| 319 | + if [[ -f WebSites/controller/index.html ]]; then |
| 320 | + cp -f WebSites/controller/index.html "$STAGE/controller/index.html" |
| 321 | + fi |
| 322 | + if [[ -d WebSites/controller/lib ]]; then |
| 323 | + cp -R WebSites/controller/lib "$STAGE/controller/lib" |
| 324 | + fi |
| 325 | + if [[ -d WebSites/controller/build ]]; then |
| 326 | + cp -R WebSites/controller/build "$STAGE/controller/build" |
| 327 | + fi |
| 328 | + echo "[assemble] Final artifact layout:" |
| 329 | + (cd "$STAGE" && find . -maxdepth 2 -type f | sed 's#^./##') |
| 330 | +
|
| 331 | + - name: Package site.zip (flat contents) |
| 332 | + if: success() |
| 333 | + shell: bash |
| 334 | + run: | |
| 335 | + set -euo pipefail |
| 336 | + rm -f site.zip |
| 337 | + (cd dist && zip -r ../site.zip .) |
| 338 | + echo "[package] Created site.zip" |
| 339 | +
|
| 340 | + - name: Upload site.zip artifact |
| 341 | + if: success() |
| 342 | + uses: actions/upload-artifact@v4 |
| 343 | + with: |
| 344 | + name: site |
| 345 | + path: site.zip |
0 commit comments