diff --git a/.github/workflows/build-options.json b/.github/workflows/build-options.json index b167129..8630410 100644 --- a/.github/workflows/build-options.json +++ b/.github/workflows/build-options.json @@ -17,6 +17,15 @@ "6000.1.*", "6000.2" ], + "tests": [ + "None", + "CompilerWarnings", + "CompilerErrors", + "BuildWarnings", + "BuildErrors", + "PlaymodeTestsErrors", + "EditmodeTestsErrors" + ], "include": [ { "os": "ubuntu-latest", diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 06d1bd8..f72d8f1 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -15,11 +15,12 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + checks: write # to publish unit test results via checks github api steps: - uses: actions/checkout@v6 with: sparse-checkout: .github/ - - uses: RageAgainstThePixel/job-builder@v1 + - uses: RageAgainstThePixel/job-builder@development id: setup-jobs with: build-options: ./.github/workflows/build-options.json @@ -32,6 +33,7 @@ jobs: name: build ${{ matrix.jobs.name }} permissions: contents: read + checks: write # required by nested unity-build workflow strategy: matrix: ${{ fromJSON(needs.setup.outputs.jobs) }} fail-fast: false diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 564f901..62ed36d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,4 +1,4 @@ -name: Publish +name: publish on: push: branches: [main] diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index 1e66910..b8323bd 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -17,6 +17,9 @@ jobs: strategy: matrix: ${{ fromJSON(inputs.matrix) }} fail-fast: false + permissions: + contents: read + checks: write # to publish unit test results via checks github api defaults: run: shell: bash @@ -97,12 +100,48 @@ jobs: else echo "Skipping build: Unity version $version does not support the build pipeline package (requires 2019.4+)" fi + - name: Copy selected Unity test + if: ${{ steps.verify-project-path.outputs.RUN_BUILD == 'true' && matrix.tests != '' && matrix.tests != 'None' }} + run: | + set -euo pipefail + test_name="${{ matrix.tests }}" + src="${GITHUB_WORKSPACE}/unity-tests/${test_name}.cs" + if [ ! -f "$src" ]; then + echo "::error::Requested test '$test_name' not found at $src" && exit 1 + fi + + case "$test_name" in + CompilerWarnings|CompilerErrors) + dest="$UNITY_PROJECT_PATH/Assets/UnityCliTests" + ;; + BuildWarnings|BuildErrors) + dest="$UNITY_PROJECT_PATH/Assets/Editor/UnityCliTests" + ;; + PlaymodeTestsErrors) + dest="$UNITY_PROJECT_PATH/Assets/Tests/PlayMode/UnityCliTests" + ;; + EditmodeTestsErrors) + dest="$UNITY_PROJECT_PATH/Assets/Tests/EditMode/UnityCliTests" + ;; + *) + echo "::error::Unknown test selection '$test_name'" && exit 1 + ;; + esac + + mkdir -p "$dest" + cp "$src" "$dest/" + echo "Copied $test_name to $dest" - name: Install OpenUPM and build pipeline package if: ${{ steps.verify-project-path.outputs.RUN_BUILD == 'true' }} working-directory: ${{ env.UNITY_PROJECT_PATH }} run: | npm install -g openupm-cli openupm add com.utilities.buildpipeline + case "${{ matrix.tests }}" in + PlaymodeTestsErrors|EditmodeTestsErrors) + openupm add com.unity.test-framework + ;; + esac - name: Update Android Target Sdk Version if: ${{ matrix.build-target == 'Android' }} run: | @@ -114,9 +153,11 @@ jobs: if: ${{ steps.verify-project-path.outputs.RUN_BUILD == 'true' }} timeout-minutes: 60 run: | + set -euo pipefail # we don't have to specify the project path or unity editor path as unity-cli will use the environment variables unity-cli run --log-name Validate -quit -executeMethod Utilities.Editor.BuildPipeline.UnityPlayerBuildTools.ValidateProject -importTMProEssentialsAsset unity-cli run --log-name Build -buildTarget ${{ matrix.build-target }} -quit -executeMethod Utilities.Editor.BuildPipeline.UnityPlayerBuildTools.StartCommandLineBuild -sceneList Assets/Scenes/SampleScene.unity ${{ matrix.build-args }} + continue-on-error: ${{ matrix.tests != 'None' }} - name: Uninstall Editor if: ${{ matrix.unity-version != 'none' }} run: | @@ -132,12 +173,12 @@ jobs: PACKAGE_MANAGER_LOG_PATH=$(unity-cli package-manager-logs) LICENSING_CLIENT_LOG_PATH=$(unity-cli licensing-client-logs) LICENSING_AUDIT_LOG_PATH=$(unity-cli licensing-audit-logs) - + echo "Hub Log Path: ${HUB_LOG_PATH}" echo "Package Manager Log Path: ${PACKAGE_MANAGER_LOG_PATH}" echo "Licensing Client Log Path: ${LICENSING_CLIENT_LOG_PATH}" echo "Licensing Audit Log Path: ${LICENSING_AUDIT_LOG_PATH}" - + if [ ! -f "${HUB_LOG_PATH}" ]; then echo "::warning:: Hub log file does not exist at ${HUB_LOG_PATH}" # find all info-log.json files in ~/.config/unity3d/ - print their paths @@ -151,18 +192,25 @@ jobs: find ~/.config/ -type f -exec echo "{}" \; echo "::warning:: Hub log file does not exist at any known location" fi - + if [ ! -f "${PACKAGE_MANAGER_LOG_PATH}" ]; then echo "::warning::Package Manager log file does not exist at ${PACKAGE_MANAGER_LOG_PATH}" fi - + if [ ! -f "${LICENSING_CLIENT_LOG_PATH}" ]; then echo "::error::Licensing Client log file does not exist at ${LICENSING_CLIENT_LOG_PATH}" fi - + if [ ! -f "${LICENSING_AUDIT_LOG_PATH}" ]; then echo "::error::Licensing Audit log file does not exist at ${LICENSING_AUDIT_LOG_PATH}" fi + - name: Upload UTP logs + if: always() + uses: actions/upload-artifact@v6 + with: + name: ${{ matrix.os }}-${{ matrix.unity-version }}-${{ matrix.build-target }}-${{ matrix.tests }}-utp-logs + path: '**/*-utp-json.log' + if-no-files-found: ignore - name: Return License if: always() run: unity-cli return-license --license personal diff --git a/.gitignore b/.gitignore index 9a5aced..b34eee6 100644 --- a/.gitignore +++ b/.gitignore @@ -137,3 +137,5 @@ dist # Vite logs files vite.config.js.timestamp-* vite.config.ts.timestamp-* + +.artifacts/ diff --git a/package-lock.json b/package-lock.json index 55a86a3..8796015 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@rage-against-the-pixel/unity-cli", - "version": "1.8.1", + "version": "1.8.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@rage-against-the-pixel/unity-cli", - "version": "1.8.1", + "version": "1.8.2", "license": "MIT", "dependencies": { "@electron/asar": "^4.0.1", @@ -2269,9 +2269,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.7", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.7.tgz", - "integrity": "sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==", + "version": "2.9.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", + "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2411,9 +2411,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001760", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", - "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", + "version": "1.0.30001761", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz", + "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==", "dev": true, "funding": [ { @@ -2724,9 +2724,9 @@ } }, "node_modules/dedent": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", - "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", + "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -5968,9 +5968,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", - "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { diff --git a/package.json b/package.json index 759c2d4..581f58c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rage-against-the-pixel/unity-cli", - "version": "1.8.1", + "version": "1.8.2", "description": "A command line utility for the Unity Game Engine.", "author": "RageAgainstThePixel", "license": "MIT", diff --git a/src/logging.ts b/src/logging.ts index 16a1f15..739760a 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -1,4 +1,5 @@ import * as fs from 'fs'; +import { UTP } from './utp/utp'; export enum LogLevel { DEBUG = 'debug', @@ -258,19 +259,55 @@ export class Logger { } } - public CI_appendWorkflowSummary(telemetry: any[]) { + public CI_appendWorkflowSummary(name: string, telemetry: UTP[]) { + if (telemetry.length === 0) { return; } switch (this._ci) { case 'GITHUB_ACTIONS': { const githubSummary = process.env.GITHUB_STEP_SUMMARY; if (githubSummary) { - let table = `| Key | Value |\n| --- | ----- |\n`; - telemetry.forEach(item => { - table += `| ${item.key} | ${item.value} |\n`; - }); + // for now lets just log the number of items we get per type + const typeCounts: Record = {}; + for (const entry of telemetry) { + const type = entry.type || 'unknown'; + + if (!typeCounts[type]) { + typeCounts[type] = 0; + } + + typeCounts[type]++; + } + + let table = `## ${name} Summary\n\n| Type | Count |\n| --- | ---: |\n`; + for (const [type, count] of Object.entries(typeCounts)) { + table += `| ${type} | ${count} |\n`; + } + + // guard against very large summaries over 1MB. Trim at a row boundary to avoid mangled tables. + const byteLimit = 1024 * 1024; + if (Buffer.byteLength(table, 'utf8') > byteLimit) { + const footer = `\n| ... | ... |\n\n***Summary truncated due to size limits.***\n`; + const footerSize = Buffer.byteLength(footer, 'utf8'); + + const lines = table.split('\n'); + let rebuilt = ''; + + for (const line of lines) { + const nextSize = Buffer.byteLength(rebuilt + line + '\n', 'utf8') + footerSize; + + if (nextSize > byteLimit) { + break; + } + + rebuilt += `${line}\n`; + } + + table = rebuilt + footer; + } fs.appendFileSync(githubSummary, table, { encoding: 'utf8' }); } + break; } } } diff --git a/src/unity-editor.ts b/src/unity-editor.ts index 62d1fd7..bc3feaf 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -277,6 +277,7 @@ export class UnityEditor { const baseEditorEnv: NodeJS.ProcessEnv = { ...process.env, UNITY_THISISABUILDMACHINE: '1', + DISABLE_EMBEDDED_BUILD_PIPELINE_PLUGIN_LOGGING: '1', ...(linuxEnvOverrides ?? {}) }; diff --git a/src/unity-logging.ts b/src/unity-logging.ts index 52cbb9a..d149bb5 100644 --- a/src/unity-logging.ts +++ b/src/unity-logging.ts @@ -973,6 +973,8 @@ export function TailLogFile(logPath: string, projectPath: string | undefined): L if (telemetryFlushed) { return; } telemetryFlushed = true; await writeUtpTelemetryLog(utpLogPath, telemetry, logger); + const parsed = path.parse(logPath); + Logger.instance.CI_appendWorkflowSummary(parsed.name, telemetry); }; const writeStdoutThenTableContent = (content: string, restoreTable: boolean = true): void => { diff --git a/unity-tests/BuildErrors.cs b/unity-tests/BuildErrors.cs new file mode 100644 index 0000000..ce75c3f --- /dev/null +++ b/unity-tests/BuildErrors.cs @@ -0,0 +1,20 @@ +using UnityEditor; +using UnityEditor.Build; +using UnityEditor.Build.Reporting; + +namespace UnityCli.UtpSamples +{ + /// + /// Forces the build pipeline to fail by throwing a BuildFailedException. + /// Place under an Editor folder when copying into a project. + /// + public class BuildErrors : IPreprocessBuildWithReport + { + public int callbackOrder => 0; + + public void OnPreprocessBuild(BuildReport report) + { + throw new System.Exception("Intentional build failure for test matrix coverage."); + } + } +} diff --git a/unity-tests/BuildWarnings.cs b/unity-tests/BuildWarnings.cs new file mode 100644 index 0000000..f365a77 --- /dev/null +++ b/unity-tests/BuildWarnings.cs @@ -0,0 +1,20 @@ +using UnityEditor; +using UnityEditor.Build; +using UnityEditor.Build.Reporting; + +namespace UnityCli.UtpSamples +{ + /// + /// Emits a build-time warning via the build pipeline (no custom UTP JSON logging). + /// Place under an Editor folder when copying into a project. + /// + public class BuildWarnings : IPreprocessBuildWithReport + { + public int callbackOrder => 0; + + public void OnPreprocessBuild(BuildReport report) + { + UnityEngine.Debug.LogWarning("Intentional build warning for test matrix coverage."); + } + } +} diff --git a/unity-tests/CompilerErrors.cs b/unity-tests/CompilerErrors.cs new file mode 100644 index 0000000..056f16e --- /dev/null +++ b/unity-tests/CompilerErrors.cs @@ -0,0 +1,4 @@ +// Intentional compiler error for matrix scenario coverage. +#error Intentional compiler error: CS1029 + +// Note: file is kept minimal so it can be copied into a project to force a build failure. diff --git a/unity-tests/CompilerWarnings.cs b/unity-tests/CompilerWarnings.cs new file mode 100644 index 0000000..c1fc077 --- /dev/null +++ b/unity-tests/CompilerWarnings.cs @@ -0,0 +1,20 @@ +using UnityEngine; + +namespace UnityCli.UtpSamples +{ + /// + /// Introduces a benign compiler warning (unused variable) without emitting custom logs. + /// + public class CompilerWarnings : MonoBehaviour + { + private void Awake() + { + ObsoleteApi(); // CS0618: call to obsolete member + } + + [System.Obsolete("Intentional warning for test matrix coverage", false)] + private static void ObsoleteApi() + { + } + } +} diff --git a/unity-tests/EditmodeTestsErrors.cs b/unity-tests/EditmodeTestsErrors.cs new file mode 100644 index 0000000..4c4d89c --- /dev/null +++ b/unity-tests/EditmodeTestsErrors.cs @@ -0,0 +1,16 @@ +using NUnit.Framework; + +namespace UnityCli.UtpSamples +{ + /// + /// Editmode test that intentionally fails to produce real test failure output. + /// + public class EditmodeTestsErrors + { + [Test] + public void FailsEditmodeSuite() + { + Assert.Fail("Intentional editmode failure for test matrix coverage."); + } + } +} diff --git a/unity-tests/PlaymodeTestsErrors.cs b/unity-tests/PlaymodeTestsErrors.cs new file mode 100644 index 0000000..729b166 --- /dev/null +++ b/unity-tests/PlaymodeTestsErrors.cs @@ -0,0 +1,19 @@ +using System.Collections; +using NUnit.Framework; +using UnityEngine.TestTools; + +namespace UnityCli.UtpSamples +{ + /// + /// Playmode test that intentionally fails to generate real test failure output. + /// + public class PlaymodeTestsErrors + { + [UnityTest] + public IEnumerator FailsPlaymodeSuite() + { + yield return null; + Assert.Fail("Intentional playmode failure for test matrix coverage."); + } + } +}