diff --git a/.github/actions/scripts/utp-ci-assertion-helpers.sh b/.github/actions/scripts/utp-ci-assertion-helpers.sh index 004c34f..888faff 100644 --- a/.github/actions/scripts/utp-ci-assertion-helpers.sh +++ b/.github/actions/scripts/utp-ci-assertion-helpers.sh @@ -1,6 +1,20 @@ #!/usr/bin/env bash # Shared helpers for UTP CI batch validation (.github/actions/scripts/run-utp-tests.sh). # Keep behavior in sync with contract tests: tests/run-utp-tests-contract.sh +# +# Severity checks go through utp-file-has-actionable-severity.cjs → normalizeTelemetryEntry +# so benign Unity noise (multicast WSAEACCES, OpenCL, StackAllocator, etc.) is remapped +# before Error/Exception/Assert are treated as failures. Do not re-special-case messages here. + +_UTP_ASSERT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +_UTP_ACTIONABLE_SEVERITY_JS="${_UTP_ASSERT_DIR}/utp-file-has-actionable-severity.cjs" + +# Returns 0 if the UTP file has an actionable Error/Exception/(optional Assert) after normalize. +utp_file_has_actionable_severity() { + local utp_file="$1" + local severities_re="$2" + node "$_UTP_ACTIONABLE_SEVERITY_JS" "$utp_file" "$severities_re" +} # Returns 0 (true) if this UTP JSON log should fail an *expected-success* scenario. utp_signals_failure_for_expected_success() { @@ -9,18 +23,18 @@ utp_signals_failure_for_expected_success() { case "$test_name" in CompilerWarnings|BuildWarnings) # Engine / allocator assert telemetry is common here; only treat Error/Exception as hard failures. - grep -qi '"severity"[[:space:]]*:[[:space:]]*"\(Error\|Exception\)"' "$utp_file" 2>/dev/null + utp_file_has_actionable_severity "$utp_file" 'Error|Exception' ;; *) - grep -qi '"severity"[[:space:]]*:[[:space:]]*"\(Error\|Exception\|Assert\)"' "$utp_file" 2>/dev/null + utp_file_has_actionable_severity "$utp_file" 'Error|Exception|Assert' ;; esac } -# Returns 0 if UTP log contains any Error/Exception/Assert (used for expected-failure scenarios). +# Returns 0 if UTP log contains any actionable Error/Exception/Assert (expected-failure scenarios). utp_signals_any_severity_problem() { local utp_file="$1" - grep -qi '"severity"[[:space:]]*:[[:space:]]*"\(Error\|Exception\|Assert\)"' "$utp_file" 2>/dev/null + utp_file_has_actionable_severity "$utp_file" 'Error|Exception|Assert' } # Prints first path to an NUnit results file containing , or nothing. diff --git a/.github/actions/scripts/utp-file-has-actionable-severity.cjs b/.github/actions/scripts/utp-file-has-actionable-severity.cjs new file mode 100644 index 0000000..65cda11 --- /dev/null +++ b/.github/actions/scripts/utp-file-has-actionable-severity.cjs @@ -0,0 +1,69 @@ +/** + * CI helper: parse a *-utp-json.log (pretty JSON array from unity-cli, or NDJSON fixtures) + * through normalizeTelemetryEntry so benign elevated severities are remapped, then exit 0 + * if any remaining severity is in the requested set. + * + * Usage: node utp-file-has-actionable-severity.cjs [Error|Exception|Assert] + * Exit 0 = has actionable severity; exit 1 = none; exit 2 = usage/IO/parse error. + * + * Requires `npm run build` so dist/utp.js exists (same as scan-utp-artifacts.cjs). + */ +const fs = require('fs'); +const path = require('path'); + +const repoRoot = path.join(__dirname, '..', '..', '..'); +const distUtp = path.join(repoRoot, 'dist', 'utp.js'); +if (!fs.existsSync(distUtp)) { + console.error(`utp-file-has-actionable-severity: missing ${distUtp} (run npm run build)`); + process.exit(2); +} + +const { normalizeTelemetryEntry } = require(distUtp); + +const filePath = process.argv[2]; +const severitiesArg = process.argv[3] || 'Error|Exception|Assert'; +if (!filePath) { + console.error('Usage: node utp-file-has-actionable-severity.cjs [Error|Exception|Assert]'); + process.exit(2); +} + +const severitySet = new Set(severitiesArg.split('|').map(s => s.trim()).filter(Boolean)); + +function loadEntries(raw) { + const trimmed = raw.trim(); + if (!trimmed) { + return []; + } + try { + const data = JSON.parse(trimmed); + return Array.isArray(data) ? data : [data]; + } catch { + // NDJSON / one compact object per line (contract fixtures) + const out = []; + for (const line of trimmed.split(/\r?\n/)) { + const t = line.trim(); + if (!t) { + continue; + } + out.push(JSON.parse(t)); + } + return out; + } +} + +let entries; +try { + entries = loadEntries(fs.readFileSync(filePath, 'utf8')); +} catch (err) { + console.error(`utp-file-has-actionable-severity: ${err.message || err}`); + process.exit(2); +} + +for (const entry of entries) { + const { utp } = normalizeTelemetryEntry(entry); + if (utp && utp.severity && severitySet.has(String(utp.severity))) { + process.exit(0); + } +} + +process.exit(1); diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 776b545..469d69b 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -17,6 +17,12 @@ jobs: contents: read steps: - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: '24' + cache: npm + - run: npm ci + - run: npm run build - name: UTP batch assertion helpers (contract) run: bash tests/run-utp-tests-contract.sh setup: diff --git a/package-lock.json b/package-lock.json index bdd36b0..944cfeb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,21 @@ { "name": "@rage-against-the-pixel/unity-cli", - "version": "3.0.1", + "version": "3.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@rage-against-the-pixel/unity-cli", - "version": "3.0.1", + "version": "3.0.2", "license": "MIT", "dependencies": { - "@electron/asar": "^4.2.0", - "@rage-against-the-pixel/unity-releases-api": "^1.0.4", + "@electron/asar": "^4.2.1", + "@rage-against-the-pixel/unity-releases-api": "^1.0.5", "commander": "^14.0.3", "glob": "^13.0.6", - "semver": "^7.8.1", + "semver": "^7.8.5", "source-map-support": "^0.5.21", - "tar": "^7.5.15", + "tar": "^7.5.22", "update-notifier": "^7.3.1", "yaml": "^2.9.0" }, @@ -24,11 +24,11 @@ }, "devDependencies": { "@types/jest": "^30.0.0", - "@types/node": "^24.12.4", - "@types/semver": "^7.7.1", + "@types/node": "^24.13.3", + "@types/semver": "^7.8.0", "@types/update-notifier": "^6.0.8", "jest": "^30.4.2", - "ts-jest": "^29.4.11", + "ts-jest": "^29.4.12", "ts-node": "^10.9.2", "typescript": "^6.0.3" }, @@ -103,14 +103,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", - "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -243,13 +243,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.7" + "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -513,18 +513,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", - "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", + "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.7", + "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7", + "@babel/types": "^7.29.8", "debug": "^4.3.1" }, "engines": { @@ -532,9 +532,9 @@ } }, "node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "dev": true, "license": "MIT", "dependencies": { @@ -577,12 +577,11 @@ } }, "node_modules/@electron/asar": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-4.2.0.tgz", - "integrity": "sha512-npW1NW5yy8EB9XY/vEw9sUdgmq0sJEhmSBb6bqyFOAw1CSkrhvAvO6QWlW8CdIMo8VN1lkdF345l/MeW0LrY0Q==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-4.2.1.tgz", + "integrity": "sha512-rGyEe7iy52zxiWuihV/h/AqQrFkx7YnUUJKW1bdufVDHCzIMP/XDrDI/NOzv/LLtzt3NduAzNa475WRmRnmswQ==", "license": "MIT", "dependencies": { - "commander": "^13.1.0", "glob": "^13.0.2", "minimatch": "^10.0.1" }, @@ -593,15 +592,6 @@ "node": ">=22.12.0" } }, - "node_modules/@electron/asar/node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -1275,32 +1265,35 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz", + "integrity": "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.1" + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/Brooooooklyn" }, "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" } }, "node_modules/@pkgr/core": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", "dev": true, "license": "MIT", "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + "node": "^14.18.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/pkgr" @@ -1334,9 +1327,9 @@ "license": "ISC" }, "node_modules/@pnpm/npm-conf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-3.0.2.tgz", - "integrity": "sha512-h104Kh26rR8tm+a3Qkc5S4VLYint3FE48as7+/5oCEcKR2idC/pF1G6AhIXKI+eHPJa/3J9i5z0Al47IeGHPkA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-3.0.3.tgz", + "integrity": "sha512-//0sR/cow/s4ICQaYoAobOl4aU8cjU6x/V24V7XkKotb9+O+3zySIYp146vpaobYHnxa4pZX8NkV54Z5AwbDKA==", "license": "MIT", "dependencies": { "@pnpm/config.env-replace": "^1.1.0", @@ -1348,18 +1341,18 @@ } }, "node_modules/@rage-against-the-pixel/unity-releases-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@rage-against-the-pixel/unity-releases-api/-/unity-releases-api-1.0.4.tgz", - "integrity": "sha512-jt7yrF5fsMI24nZUamV17IwCWUH/Wd8WO/j3akQXyZ1hMmelkTAEMMNwV3qeqW3DGkipHP1tP2gu5WAgA8ZtTw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@rage-against-the-pixel/unity-releases-api/-/unity-releases-api-1.0.5.tgz", + "integrity": "sha512-BNtvhMsJnzIcELLFTum3Iz+7ZCvHSJQSNd0CRSut8SWWwklhttZCtFCV4LZ4/dTgcTPzKMJMwvxzxagWgz+uJQ==", "license": "MIT", "dependencies": { "jose": "5.10.0" } }, "node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.52", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz", + "integrity": "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==", "dev": true, "license": "MIT" }, @@ -1412,9 +1405,9 @@ "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dev": true, "license": "MIT", "optional": true, @@ -1513,19 +1506,19 @@ } }, "node_modules/@types/node": { - "version": "24.12.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", - "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ==", "dev": true, "license": "MIT" }, @@ -1565,9 +1558,9 @@ "license": "MIT" }, "node_modules/@ungap/structured-clone": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", - "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", "dev": true, "license": "ISC" }, @@ -1885,9 +1878,9 @@ ] }, "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, "license": "MIT", "bin": { @@ -2186,9 +2179,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.32", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", - "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==", + "version": "2.11.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.11.tgz", + "integrity": "sha512-/yImnXwyTvgMkhgekLHok/Rx5vO6E0BmStWlSqKWMVm2a2ITuZ1Tn+9bgLS+gZRdZmWtd8nxuhHpdmCUOWsTQQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2222,21 +2215,21 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", "dev": true, "funding": [ { @@ -2254,10 +2247,10 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { @@ -2320,9 +2313,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001793", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", - "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", "dev": true, "funding": [ { @@ -2694,9 +2687,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.361", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz", - "integrity": "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==", + "version": "1.5.399", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.399.tgz", + "integrity": "sha512-lEcqhErbHjXRvd41rnWLpzbyU/IXfIYo7QwaFWmxGeLiLyY2TBCdHnWY88vB+p3ubnihRypDm66panXl7TylLA==", "dev": true, "license": "ISC" }, @@ -4297,9 +4290,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", "dev": true, "license": "MIT", "dependencies": { @@ -4468,12 +4461,12 @@ } }, "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.5" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" @@ -4557,9 +4550,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.46", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", - "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", "dev": true, "license": "MIT", "engines": { @@ -4734,9 +4727,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", - "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -4750,9 +4743,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -4879,9 +4872,9 @@ }, "node_modules/react-is-19": { "name": "react-is", - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz", - "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", "dev": true, "license": "MIT" }, @@ -4946,9 +4939,9 @@ } }, "node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -5171,13 +5164,13 @@ } }, "node_modules/synckit": { - "version": "0.11.12", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", - "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.2.9" + "@pkgr/core": "^0.3.6" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -5187,9 +5180,9 @@ } }, "node_modules/tar": { - "version": "7.5.15", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", - "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -5234,9 +5227,9 @@ "license": "BSD-3-Clause" }, "node_modules/ts-jest": { - "version": "29.4.11", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.11.tgz", - "integrity": "sha512-IrFl7l9AuB/qrNw5quqvAv/hmKMb8dhWOH4jQOGo0Oq8tCeo1O86/iTFG1FaRimgUkF13l4PcepO8ATFT6Ns4g==", + "version": "29.4.12", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.12.tgz", + "integrity": "sha512-Ov6ClY53Fflh6BGAnY2DlTq1hYDrTycz2PVTXBWFW2CU+9zrEqAp9fWdGXl42EXO5RLSFAcAZ2JFKbP+zBTFfw==", "dev": true, "license": "MIT", "dependencies": { @@ -5246,7 +5239,7 @@ "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", - "semver": "^7.8.0", + "semver": "^7.8.5", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, @@ -5403,9 +5396,9 @@ } }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "dev": true, "license": "MIT" }, @@ -5851,9 +5844,9 @@ } }, "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 76f8474..127cc1c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rage-against-the-pixel/unity-cli", - "version": "3.0.1", + "version": "3.0.2", "description": "A command line utility for the Unity Game Engine.", "author": "RageAgainstThePixel", "license": "MIT", @@ -53,23 +53,23 @@ "unlink": "npm unlink @rage-against-the-pixel/unity-cli" }, "dependencies": { - "@electron/asar": "^4.2.0", - "@rage-against-the-pixel/unity-releases-api": "^1.0.4", + "@electron/asar": "^4.2.1", + "@rage-against-the-pixel/unity-releases-api": "^1.0.5", "commander": "^14.0.3", "glob": "^13.0.6", - "semver": "^7.8.1", + "semver": "^7.8.5", "source-map-support": "^0.5.21", - "tar": "^7.5.15", + "tar": "^7.5.22", "update-notifier": "^7.3.1", "yaml": "^2.9.0" }, "devDependencies": { "@types/jest": "^30.0.0", - "@types/node": "^24.12.4", - "@types/semver": "^7.7.1", + "@types/node": "^24.13.3", + "@types/semver": "^7.8.0", "@types/update-notifier": "^6.0.8", "jest": "^30.4.2", - "ts-jest": "^29.4.11", + "ts-jest": "^29.4.12", "ts-node": "^10.9.2", "typescript": "^6.0.3" }, diff --git a/src/logging.ts b/src/logging.ts index 164a802..5d12795 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -1,4 +1,5 @@ import { UTP, Severity } from './utp'; +import { UTP_BENIGN_SEVERITY_REMAPS } from './utp-benign'; import { GitHubActionsLoggerProvider, GitHubAnnotationLevel } from './github-actions-ci'; import { ILoggerProvider, LocalCliLoggerProvider, LoggerAnnotationOptions, MarkdownTarget } from './logger-provider'; @@ -386,18 +387,32 @@ function formatDurationMsForSummary(ms: number | undefined): string { } /** Unity/CI noise shown in logs; omit from workflow summary foldouts and counts. */ -const SUMMARY_NOISE_ACCESS_TOKEN = 'Access token is unavailable; failed to update'; +function buildSummaryNoisePatterns(): RegExp[] { + return UTP_BENIGN_SEVERITY_REMAPS.map(({ fragment }) => { + const escaped = fragment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + // Multicast lines often include "(err: 10013)." — strip the whole clause. + if (fragment.includes('multicast group')) { + return new RegExp(`${escaped}(?:\\s*\\(err:\\s*\\d+\\))?\\.?`, 'gi'); + } + return new RegExp(escaped, 'gi'); + }); +} + +const SUMMARY_NOISE_PATTERNS: RegExp[] = buildSummaryNoisePatterns(); /** * Removes known noise phrases from a log message for summary display. - * Exported for unit tests. + * Exported for unit tests. Fragments come from {@link UTP_BENIGN_SEVERITY_REMAPS}. */ export function stripSummaryNoiseFromLogMessage(message: string): string { const flat = toSingleLineText(message); if (!flat) return ''; - const pattern = SUMMARY_NOISE_ACCESS_TOKEN.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const out = flat.replace(new RegExp(pattern, 'gi'), ' ').replace(/\s+/g, ' ').trim(); - return out; + let out = flat; + for (const pattern of SUMMARY_NOISE_PATTERNS) { + pattern.lastIndex = 0; + out = out.replace(pattern, ' '); + } + return out.replace(/\s+/g, ' ').trim(); } function filterNoiseFromSummaryLogEntries(entries: UTP[]): UTP[] { diff --git a/src/unity-hub.ts b/src/unity-hub.ts index 7fe1bf8..4cb4d1d 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -39,46 +39,108 @@ const MIN_NATIVE_WINDOWS_ARM64_HUB_VERSION = coerce('3.17.0')!; /** Allowed characters in a Debian package version (no shell metacharacters). */ const LINUX_HUB_DEB_VERSION_RE = /^[0-9A-Za-z.+~:-]+$/; +/** Hub 3.20+ Electron Forge deb layout. */ +export const LINUX_HUB_EXECUTABLE_MODERN = '/usr/lib/unityhub/unityhub'; +/** Hub ≤3.19 fpm / electron-builder layout. */ +export const LINUX_HUB_EXECUTABLE_LEGACY = '/opt/unityhub/unityhub'; + +/** + * Resolves the Unity Hub binary on Linux. + * Prefers UNITY_HUB_PATH, then the Hub 3.20+ path, then the legacy /opt path. + * When neither is present (pre-install), defaults to the modern path. + */ +export function resolveLinuxHubExecutable( + envPath: string | undefined = process.env.UNITY_HUB_PATH, + existsSync: (p: string) => boolean = fs.existsSync +): string { + if (envPath !== undefined && envPath.length > 0) { + return envPath; + } + if (existsSync(LINUX_HUB_EXECUTABLE_MODERN)) { + return LINUX_HUB_EXECUTABLE_MODERN; + } + if (existsSync(LINUX_HUB_EXECUTABLE_LEGACY)) { + return LINUX_HUB_EXECUTABLE_LEGACY; + } + return LINUX_HUB_EXECUTABLE_MODERN; +} + /** * Fixed bootstrap for Linux Hub apt repo + update index. No user-controlled interpolation (CodeQL). - * Matches prior `Install` update path (wget | gpg | sudo tee, then sources.list). + * Uses DEB822 .sources (Hub 3.20+) and removes legacy .list to avoid duplicate-source warnings. */ const LINUX_HUB_LINUX_UPDATE_REPO_BOOTSTRAP = `#!/bin/sh set -e wget -qO - https://hub.unity3d.com/linux/keys/public | gpg --dearmor | sudo tee /usr/share/keyrings/Unity_Technologies_ApS.gpg >/dev/null -sudo sh -c 'echo "deb [signed-by=/usr/share/keyrings/Unity_Technologies_ApS.gpg] https://hub.unity3d.com/linux/repos/deb stable main" > /etc/apt/sources.list.d/unityhub.list' +sudo rm -f /etc/apt/sources.list.d/unityhub.list +sudo tee /etc/apt/sources.list.d/unityhub.sources >/dev/null <<'EOF' +Types: deb +URIs: https://hub.unity3d.com/linux/repos/deb +Suites: stable +Components: main +Signed-By: /usr/share/keyrings/Unity_Technologies_ApS.gpg +EOF sudo apt-get update --allow-releaseinfo-change `; /** * First phase of fresh Linux Hub install: machine-id, repo keys, jammy mirror, apt-get update. - * No user-controlled interpolation. + * No user-controlled interpolation. Uses DEB822 .sources (Hub 3.20+). */ const LINUX_HUB_LINUX_INSTALL_BOOTSTRAP = `#!/bin/sh set -e dbus-uuidgen >/etc/machine-id && mkdir -p /var/lib/dbus/ && ln -sf /etc/machine-id /var/lib/dbus/machine-id wget -qO - https://hub.unity3d.com/linux/keys/public | gpg --dearmor | tee /usr/share/keyrings/Unity_Technologies_ApS.gpg >/dev/null -echo "deb [signed-by=/usr/share/keyrings/Unity_Technologies_ApS.gpg] https://hub.unity3d.com/linux/repos/deb stable main" > /etc/apt/sources.list.d/unityhub.list +rm -f /etc/apt/sources.list.d/unityhub.list +tee /etc/apt/sources.list.d/unityhub.sources >/dev/null <<'EOF' +Types: deb +URIs: https://hub.unity3d.com/linux/repos/deb +Suites: stable +Components: main +Signed-By: /usr/share/keyrings/Unity_Technologies_ApS.gpg +EOF echo "deb https://archive.ubuntu.com/ubuntu jammy main universe" | tee /etc/apt/sources.list.d/jammy.list apt-get update `; /** * Post-install cleanup and xvfb / unity-hub wrapper setup. Runs as root; no user interpolation. + * Wrapper resolves Hub 3.20+ (/usr/lib/unityhub) vs legacy (/opt/unityhub) at runtime so apt + * upgrades that move the binary do not leave a stale path (exit 127). */ const LINUX_HUB_LINUX_INSTALL_POST = `#!/bin/sh set -e apt-get clean sed -i 's/^\\(.*DISPLAY=:.*XAUTHORITY=.*\\)\\( "\\$@" \\)2>&1$/\\1\\2/' /usr/bin/xvfb-run -printf '#!/bin/bash\\nxvfb-run --auto-servernum /opt/unityhub/unityhub "$@" 2>/dev/null' | tee /usr/bin/unity-hub >/dev/null -chmod 777 /usr/bin/unity-hub -which unityhub || { echo "Unity Hub installation failed"; exit 1; } -hubPath=$(which unityhub) -if [ -z "$hubPath" ]; then - echo "Failed to install Unity Hub" - exit 1 +command -v unityhub >/dev/null || { echo "Unity Hub installation failed"; exit 1; } +hubPath=$(readlink -f "$(command -v unityhub)" 2>/dev/null || true) +if [ -z "$hubPath" ] || [ ! -x "$hubPath" ]; then + if [ -x /usr/lib/unityhub/unityhub ]; then + hubPath=/usr/lib/unityhub/unityhub + elif [ -x /opt/unityhub/unityhub ]; then + hubPath=/opt/unityhub/unityhub + else + echo "Failed to install Unity Hub" + exit 1 + fi fi -chmod -R 777 "$hubPath" +tee /usr/bin/unity-hub >/dev/null <<'WRAPPER' +#!/bin/bash +if [ -x /usr/lib/unityhub/unityhub ]; then + hubBin=/usr/lib/unityhub/unityhub +elif [ -x /opt/unityhub/unityhub ]; then + hubBin=/opt/unityhub/unityhub +else + hubBin=$(readlink -f "$(command -v unityhub)" 2>/dev/null || true) +fi +if [ -z "$hubBin" ] || [ ! -x "$hubBin" ]; then + echo "Unity Hub binary not found" >&2 + exit 127 +fi +exec xvfb-run --auto-servernum "$hubBin" "$@" 2>/dev/null +WRAPPER +chmod 777 /usr/bin/unity-hub +chmod -R 777 "$(dirname "$hubPath")" `; const LINUX_HUB_LINUX_APT_EXTRAS = [ @@ -93,9 +155,9 @@ const LINUX_HUB_LINUX_APT_EXTRAS = [ export class UnityHub { /** The path to the Unity Hub executable. */ - public readonly executable: string; + public executable!: string; /** The root directory of the Unity Hub installation. */ - public readonly rootDirectory: string; + public rootDirectory!: string; /** The file extension for the Unity editor executable. */ public readonly editorFileExtension: string; @@ -131,8 +193,7 @@ export class UnityHub { this.editorFileExtension = '/Unity.app/Contents/MacOS/Unity'; break; case 'linux': - this.executable = process.env.UNITY_HUB_PATH || '/opt/unityhub/unityhub'; - this.rootDirectory = path.join(this.executable, '../'); + this.refreshLinuxHubPaths(); this.editorFileExtension = '/Editor/Unity'; break; default: @@ -140,6 +201,12 @@ export class UnityHub { } } + /** Re-resolve Linux Hub executable + root after install/upgrade (Hub 3.20 moved under /usr/lib). */ + private refreshLinuxHubPaths(): void { + this.executable = resolveLinuxHubExecutable(); + this.rootDirectory = path.join(this.executable, '../'); + } + /** * Some Hub builds (notably Windows headless) occasionally exit non-zero after streaming usable * `editors --releases` / `editors -i` data. Tolerate only when the captured output parses the same @@ -535,6 +602,9 @@ export class UnityHub { ['apt-get', 'install', '-y', '--no-install-recommends', '--only-upgrade', hubPkg], linuxExecOpts ); + // Refresh xvfb wrapper after upgrades that move /opt → /usr/lib (Hub 3.20+). + await Exec('sudo', ['sh', '-c', LINUX_HUB_LINUX_INSTALL_POST], linuxExecOpts); + this.refreshLinuxHubPaths(); this.logger.info(`Unity Hub updated successfully.`); } else { throw new Error(`Unsupported platform: ${process.platform}`); @@ -544,6 +614,9 @@ export class UnityHub { } } + if (process.platform === 'linux') { + this.refreshLinuxHubPaths(); + } await fs.promises.access(this.executable, fs.constants.X_OK); return this.executable; } @@ -683,6 +756,7 @@ export class UnityHub { linuxExecOpts ); await Exec('sudo', ['sh', '-c', LINUX_HUB_LINUX_INSTALL_POST], linuxExecOpts); + this.refreshLinuxHubPaths(); break; } default: diff --git a/src/unity-logging.ts b/src/unity-logging.ts index 09db7c3..cbb1d25 100644 --- a/src/unity-logging.ts +++ b/src/unity-logging.ts @@ -20,8 +20,10 @@ import { UTPMemoryLeak, UTPPlayerBuildInfo, UTPTestStatus, - normalizeTelemetryEntry + isElevatedUtpSeverity, + normalizeTelemetryEntry, } from './utp'; +import { utpMessageMatchesBenignRemap } from './utp-benign'; /** * Result of the tailLogFile function containing cleanup resources. @@ -1127,26 +1129,6 @@ async function writeUtpTelemetryLog(filePath: string, entries: UTP[], logger: Lo } } -/** - * Editor log messages whose severity has been changed. - * Useful for making certain error messages that are not critical less noisy. - * Key is a substring of the log message, value is the remapped LogLevel. - */ -const remappedEditorLogs: Record = { - 'OpenCL device, baking cannot use GPU lightmapper.': LogLevel.INFO, - 'Failed to find a suitable OpenCL device, baking cannot use GPU lightmapper.': LogLevel.INFO, - '~StackAllocator(ALLOC_TEMP_MAIN) m_LastAlloc not NULL. Did you forget to call FreeAllStackAllocations()?': LogLevel.INFO, -}; - -function getRemappedEditorLogLevel(message: string): LogLevel | undefined { - for (const [fragment, level] of Object.entries(remappedEditorLogs)) { - if (message.includes(fragment)) { - return level; - } - } - return undefined; -} - /** * Tails a log file using fs.watch and ReadStream for efficient reading. * @param logPath The path to the log file to tail. @@ -1253,15 +1235,7 @@ export function TailLogFile(logPath: string, projectPath: string | undefined): L } } - if (utp.message && 'severity' in utp && - (utp.severity === Severity.Error || utp.severity === Severity.Exception || utp.severity === Severity.Assert)) { - let messageLevel: LogLevel = LogLevel.ERROR; - - const remappedLevel = getRemappedEditorLogLevel(utp.message); - if (remappedLevel !== undefined) { - messageLevel = remappedLevel; - } - + if (utp.message && 'severity' in utp && isElevatedUtpSeverity(utp.severity)) { const normalizedPath = normalizeAnnotationPath(utp.file, projectPath); const stacktrace = sanitizeStackTrace(utp.stackTrace); const message = stacktrace == undefined ? utp.message : `${utp.message}\n${stacktrace}`; @@ -1279,20 +1253,16 @@ export function TailLogFile(logPath: string, projectPath: string | undefined): L } } } else { - switch (messageLevel) { - case LogLevel.WARN: - logger.warn(message); - break; - case LogLevel.ERROR: - logger.error(message); - break; - case LogLevel.INFO: - default: - logger.info(message); - break; - } + logger.error(message); } } + } else if (utp.message && utpMessageMatchesBenignRemap(utp.message)) { + // Remapped at normalize time (e.g. multicast WSAEACCES); surface as info, not error. + const stacktrace = sanitizeStackTrace(utp.stackTrace); + const message = stacktrace == undefined ? utp.message : `${utp.message}\n${stacktrace}`; + if (!annotationCommandPrefixRegex.test(message)) { + logger.info(message); + } } else if (Logger.instance.logLevel === LogLevel.UTP) { printUTP(utp); } @@ -1300,8 +1270,21 @@ export function TailLogFile(logPath: string, projectPath: string | undefined): L logger.warn(`Failed to parse telemetry JSON: ${error} -- raw: ${jsonPart}`); } } else { + // Skip plain-log false positives (e.g. "Socket: bind failed, error: …" matching \berror\b). + if (utpMessageMatchesBenignRemap(line)) { + if (Logger.instance.logLevel !== LogLevel.UTP) { + process.stdout.write(`${line}\n`); + } + return; + } const scan = parsePlainLogIssue(line); if (scan) { + if (utpMessageMatchesBenignRemap(scan.message)) { + if (Logger.instance.logLevel !== LogLevel.UTP) { + process.stdout.write(`${line}\n`); + } + return; + } const key = buildIssueKey(scan.file, scan.line, scan.message); if (!seenIssueKeys.has(key)) { seenIssueKeys.add(key); diff --git a/src/utp-benign.ts b/src/utp-benign.ts new file mode 100644 index 0000000..b463997 --- /dev/null +++ b/src/utp-benign.ts @@ -0,0 +1,41 @@ +/** + * Known Unity/editor messages that are non-actionable despite elevated UTP severity. + * Kept in a leaf module (no imports) so normalize, summaries, and CI share one list + * without circular deps between utp.ts and logging.ts. + * + * Severity strings must match {@link Severity} in utp.ts. + */ +export const UTP_BENIGN_SEVERITY_REMAPS: ReadonlyArray<{ + readonly fragment: string; + readonly severity: 'Info' | 'Warning'; +}> = [ + // Longer OpenCL form first so summary strip does not leave a "Failed to find a suitable" prefix. + { fragment: 'Failed to find a suitable OpenCL device, baking cannot use GPU lightmapper.', severity: 'Info' }, + { fragment: 'OpenCL device, baking cannot use GPU lightmapper.', severity: 'Info' }, + { + fragment: + '~StackAllocator(ALLOC_TEMP_MAIN) m_LastAlloc not NULL. Did you forget to call FreeAllStackAllocations()?', + severity: 'Info', + }, + // Windows hosted CI: WSAEACCES (10013) — player-connection multicast / socket bind. Unity falls back. + { fragment: 'Unable to join player connection multicast group', severity: 'Info' }, + { fragment: 'Socket: bind failed', severity: 'Info' }, + { + fragment: 'An attempt was made to access a socket in a way forbidden by its access permissions', + severity: 'Info', + }, + { fragment: 'Access token is unavailable; failed to update', severity: 'Info' }, +]; + +/** True if the message matches a known benign Unity/CI noise fragment. */ +export function utpMessageMatchesBenignRemap(message: string): boolean { + if (!message) { + return false; + } + for (const { fragment } of UTP_BENIGN_SEVERITY_REMAPS) { + if (message.includes(fragment)) { + return true; + } + } + return false; +} diff --git a/src/utp.ts b/src/utp.ts index f917ad5..9ce1dfd 100644 --- a/src/utp.ts +++ b/src/utp.ts @@ -1,4 +1,10 @@ import { Logger } from "./logging"; +import { UTP_BENIGN_SEVERITY_REMAPS } from './utp-benign'; + +export { + UTP_BENIGN_SEVERITY_REMAPS, + utpMessageMatchesBenignRemap, +} from './utp-benign'; export class UTPBase { type?: string; @@ -153,6 +159,30 @@ export enum Severity { Assert = 'Assert' } +/** Severities that normally fail builds / CI expected-success checks. */ +export function isElevatedUtpSeverity(severity: Severity | string | undefined): boolean { + return severity === Severity.Error + || severity === Severity.Exception + || severity === Severity.Assert; +} + +/** + * Downgrades elevated severity on known benign messages. Mutates `utp`. + * @returns true when severity was changed. + */ +export function remapBenignUtpSeverity(utp: UTP): boolean { + if (!utp.message || !isElevatedUtpSeverity(utp.severity)) { + return false; + } + for (const { fragment, severity } of UTP_BENIGN_SEVERITY_REMAPS) { + if (utp.message.includes(fragment)) { + utp.severity = severity as Severity; + return true; + } + } + return false; +} + /** * Root-level JSON keys on UTP objects that this CLI recognizes. Other keys are still parsed * but reported via {@link normalizeTelemetryEntry}'s `unknownTopLevelKeys` for logging. @@ -197,8 +227,9 @@ export interface NormalizeTelemetryResult { } /** - * Normalizes UTP telemetry entries to canonical shapes. Unknown top-level keys are listed - * for the caller to log (with the raw `##utp:` line when tailing logs). + * Normalizes UTP telemetry entries to canonical shapes and remaps known benign elevated + * severities. Unknown top-level keys are listed for the caller to log (with the raw + * `##utp:` line when tailing logs). */ export function normalizeTelemetryEntry(entry: unknown): NormalizeTelemetryResult { if (!entry || typeof entry !== 'object') { @@ -234,6 +265,18 @@ export function normalizeTelemetryEntry(entry: unknown): NormalizeTelemetryResul utp.lineNumber = utp.line; } + // Canonicalize severity string casing from Unity payloads. + if (typeof utp.severity === 'string') { + const matched = (Object.values(Severity) as string[]).find( + s => s.toLowerCase() === (utp.severity as string).toLowerCase() + ); + if (matched) { + utp.severity = matched as Severity; + } + } + + remapBenignUtpSeverity(utp); + if (!utp.type) { Logger.instance.warn('UTP entry missing type property; telemetry entry may be ignored.'); } diff --git a/tests/linux-hub-path.test.ts b/tests/linux-hub-path.test.ts new file mode 100644 index 0000000..68a95eb --- /dev/null +++ b/tests/linux-hub-path.test.ts @@ -0,0 +1,34 @@ +import { + LINUX_HUB_EXECUTABLE_LEGACY, + LINUX_HUB_EXECUTABLE_MODERN, + resolveLinuxHubExecutable, +} from '../src/unity-hub'; + +describe('resolveLinuxHubExecutable', () => { + it('prefers UNITY_HUB_PATH when set', () => { + const exists = jest.fn(() => true); + expect(resolveLinuxHubExecutable('/custom/unityhub', exists)).toBe('/custom/unityhub'); + expect(exists).not.toHaveBeenCalled(); + }); + + it('ignores empty UNITY_HUB_PATH and uses filesystem candidates', () => { + const exists = (p: string) => p === LINUX_HUB_EXECUTABLE_LEGACY; + expect(resolveLinuxHubExecutable('', exists)).toBe(LINUX_HUB_EXECUTABLE_LEGACY); + expect(resolveLinuxHubExecutable(undefined, exists)).toBe(LINUX_HUB_EXECUTABLE_LEGACY); + }); + + it('prefers Hub 3.20+ /usr/lib layout over legacy /opt', () => { + const exists = (p: string) => + p === LINUX_HUB_EXECUTABLE_MODERN || p === LINUX_HUB_EXECUTABLE_LEGACY; + expect(resolveLinuxHubExecutable(undefined, exists)).toBe(LINUX_HUB_EXECUTABLE_MODERN); + }); + + it('falls back to legacy /opt when modern path is missing', () => { + const exists = (p: string) => p === LINUX_HUB_EXECUTABLE_LEGACY; + expect(resolveLinuxHubExecutable(undefined, exists)).toBe(LINUX_HUB_EXECUTABLE_LEGACY); + }); + + it('defaults to modern path when neither layout is installed', () => { + expect(resolveLinuxHubExecutable(undefined, () => false)).toBe(LINUX_HUB_EXECUTABLE_MODERN); + }); +}); diff --git a/tests/logging-summary.test.ts b/tests/logging-summary.test.ts index cef432a..7b6b040 100644 --- a/tests/logging-summary.test.ts +++ b/tests/logging-summary.test.ts @@ -28,6 +28,20 @@ describe('stripSummaryNoiseFromLogMessage', () => { ); expect(stripSummaryNoiseFromLogMessage('Access token is unavailable; failed to update')).toBe(''); }); + + it('removes player-connection multicast noise', () => { + expect(stripSummaryNoiseFromLogMessage('Unable to join player connection multicast group (err: 10013).')).toBe(''); + expect( + stripSummaryNoiseFromLogMessage('Before. Unable to join player connection multicast group (err: 10013). After.') + ).toBe('Before. After.'); + }); + + it('removes OpenCL / access-token noise phrases from the shared remap list', () => { + expect(stripSummaryNoiseFromLogMessage('Access token is unavailable; failed to update')).toBe(''); + expect( + stripSummaryNoiseFromLogMessage('Failed to find a suitable OpenCL device, baking cannot use GPU lightmapper.') + ).toBe(''); + }); }); describe('mergeLogEntriesPreferringSeverity', () => { diff --git a/tests/run-utp-tests-contract.sh b/tests/run-utp-tests-contract.sh index e4531d8..8b2c505 100644 --- a/tests/run-utp-tests-contract.sh +++ b/tests/run-utp-tests-contract.sh @@ -1,8 +1,13 @@ #!/usr/bin/env bash # Contract tests for UTP CI assertion helpers (bash; run on Linux CI or Git Bash). +# Requires dist/utp.js (npm run build) — helpers parse UTP via normalizeTelemetryEntry. set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +if [ ! -f "$ROOT/dist/utp.js" ]; then + echo "Building dist/ for UTP normalize…" >&2 + (cd "$ROOT" && npm run build) +fi # shellcheck source=../.github/actions/scripts/utp-ci-assertion-helpers.sh source "$ROOT/.github/actions/scripts/utp-ci-assertion-helpers.sh" @@ -25,6 +30,71 @@ if ! utp_signals_failure_for_expected_success CompilerWarnings "$tmpdir/warn-err fail "CompilerWarnings + Error should signal failure for expected-success check" fi +# Windows GHA multicast permission noise — remapped to Info by normalizeTelemetryEntry. +printf '%s\n' '{"type":"Log","severity":"Error","message":"Unable to join player connection multicast group (err: 10013)."}' >"$tmpdir/multicast.json" +if utp_signals_failure_for_expected_success CompilerWarnings "$tmpdir/multicast.json"; then + fail "CompilerWarnings + player-connection multicast Error should be remapped (not actionable)" +fi + +# Unity 2019-style sibling WSAEACCES line (same CI failure mode as multicast). +printf '%s\n' '{"type":"Log","severity":"Error","message":"Socket: bind failed, error: An attempt was made to access a socket in a way forbidden by its access permissions."}' >"$tmpdir/socket-bind.json" +if utp_signals_failure_for_expected_success CompilerWarnings "$tmpdir/socket-bind.json"; then + fail "CompilerWarnings + Socket bind failed should be remapped (not actionable)" +fi +if utp_signals_failure_for_expected_success BuildWarnings "$tmpdir/socket-bind.json"; then + fail "BuildWarnings + Socket bind failed should be remapped (not actionable)" +fi +if utp_signals_failure_for_expected_success BuildWarnings "$tmpdir/multicast.json"; then + fail "BuildWarnings + player-connection multicast Error should be remapped (not actionable)" +fi +if utp_signals_failure_for_expected_success EditmodeTestsPassing "$tmpdir/multicast.json"; then + fail "Expected-success scenarios should treat remapped multicast as non-actionable" +fi +if utp_signals_any_severity_problem "$tmpdir/multicast.json"; then + fail "utp_signals_any_severity_problem should treat remapped multicast as non-actionable" +fi + +# Pretty-printed CLI artifact shape (writeUtpTelemetryLog) must parse + remap the same way. +cat >"$tmpdir/multicast-pretty.json" <<'EOF' +[ + { + "type": "LogEntry", + "severity": "Error", + "message": "Unable to join player connection multicast group (err: 10013)." + } +] +EOF +if utp_signals_failure_for_expected_success CompilerWarnings "$tmpdir/multicast-pretty.json"; then + fail "Pretty-printed multicast UTP artifact should remap Error → Info" +fi + +# Mixed: multicast noise + real Error still fails. +printf '%s\n' \ + '{"type":"Log","severity":"Error","message":"Unable to join player connection multicast group (err: 10013)."}' \ + '{"type":"Log","severity":"Error","message":"boom"}' >"$tmpdir/multicast-plus-real.json" +if ! utp_signals_failure_for_expected_success CompilerWarnings "$tmpdir/multicast-plus-real.json"; then + fail "Real Error alongside multicast noise should still signal failure" +fi + +# Pretty mixed array +cat >"$tmpdir/multicast-plus-real-pretty.json" <<'EOF' +[ + { + "type": "LogEntry", + "severity": "Error", + "message": "Unable to join player connection multicast group (err: 10013)." + }, + { + "type": "LogEntry", + "severity": "Error", + "message": "boom" + } +] +EOF +if ! utp_signals_failure_for_expected_success CompilerWarnings "$tmpdir/multicast-plus-real-pretty.json"; then + fail "Pretty-printed real Error alongside multicast should still signal failure" +fi + printf '%s\n' '{"severity":"Assert"}' >"$tmpdir/nonwarn-assert.json" if ! utp_signals_failure_for_expected_success EditmodeTestsPassing "$tmpdir/nonwarn-assert.json"; then fail "Non-warning scenario should still treat Assert as failure for expected-success check" diff --git a/tests/utp-telemetry-fixtures.test.ts b/tests/utp-telemetry-fixtures.test.ts index ef705f7..d1b57b3 100644 --- a/tests/utp-telemetry-fixtures.test.ts +++ b/tests/utp-telemetry-fixtures.test.ts @@ -1,6 +1,10 @@ import * as fs from 'fs'; import * as path from 'path'; -import { normalizeTelemetryEntry, UTP_SUPPORTED_TOP_LEVEL_PROPERTIES } from '../src/utp'; +import { + normalizeTelemetryEntry, + Severity, + UTP_SUPPORTED_TOP_LEVEL_PROPERTIES, +} from '../src/utp'; import { buildTestResultsTableMarkdown, utpToTestResultSummary } from '../src/logging'; import { formatUtpUnrecognizedTopLevelPropertiesMessage } from '../src/unity-logging'; @@ -72,6 +76,68 @@ describe('UTP telemetry fixtures', () => { }); }); +describe('normalizeTelemetryEntry benign severity remaps', () => { + it('remaps player-connection multicast Error to Info', () => { + const { utp } = normalizeTelemetryEntry({ + type: 'LogEntry', + severity: 'Error', + message: 'Unable to join player connection multicast group (err: 10013).', + }); + expect(utp.severity).toBe(Severity.Info); + }); + + it('remaps Socket bind failed / WSAEACCES permission Errors to Info', () => { + const bind = normalizeTelemetryEntry({ + type: 'LogEntry', + severity: 'Error', + message: + 'Socket: bind failed, error: An attempt was made to access a socket in a way forbidden by its access permissions.', + }).utp; + expect(bind.severity).toBe(Severity.Info); + + const plainExtracted = normalizeTelemetryEntry({ + type: 'LogEntry', + severity: Severity.Error, + message: 'An attempt was made to access a socket in a way forbidden by its access permissions.', + }).utp; + expect(plainExtracted.severity).toBe(Severity.Info); + }); + + it('remaps OpenCL and StackAllocator elevated severities to Info', () => { + const opencl = normalizeTelemetryEntry({ + type: 'LogEntry', + severity: Severity.Error, + message: 'Failed to find a suitable OpenCL device, baking cannot use GPU lightmapper.', + }).utp; + expect(opencl.severity).toBe(Severity.Info); + + const stack = normalizeTelemetryEntry({ + type: 'LogEntry', + severity: Severity.Assert, + message: '~StackAllocator(ALLOC_TEMP_MAIN) m_LastAlloc not NULL. Did you forget to call FreeAllStackAllocations()?', + }).utp; + expect(stack.severity).toBe(Severity.Info); + }); + + it('leaves real Errors unchanged', () => { + const { utp } = normalizeTelemetryEntry({ + type: 'Compiler', + severity: 'Error', + message: 'Assets/Foo.cs(1,1): error CS0001: boom', + }); + expect(utp.severity).toBe(Severity.Error); + }); + + it('canonicalizes severity casing', () => { + const { utp } = normalizeTelemetryEntry({ + type: 'LogEntry', + severity: 'error', + message: 'real failure', + }); + expect(utp.severity).toBe(Severity.Error); + }); +}); + describe('formatUtpUnrecognizedTopLevelPropertiesMessage', () => { it('includes unknown key names and the full ##utp line', () => { const line = '##utp:{"type":"Action","extra":1}';