Skip to content

Fix wrong mac path for License Server Activation when using Unity 6.3+ - #842

Open
alexis-gervacio wants to merge 3 commits into
game-ci:mainfrom
alexis-gervacio:fix/unity-6.3-licensing-server
Open

Fix wrong mac path for License Server Activation when using Unity 6.3+#842
alexis-gervacio wants to merge 3 commits into
game-ci:mainfrom
alexis-gervacio:fix/unity-6.3-licensing-server

Conversation

@alexis-gervacio

@alexis-gervacio alexis-gervacio commented Jul 4, 2026

Copy link
Copy Markdown

Changes

  • As per the Unity docs, the path for the Licensing Client on macOS changed for Editor versions 6000.3 or later:
macOS (Editor versions 6000.3 or later):
<UnityEditorDir>/Contents/Helpers/UnityLicensingClient.app/Contents/MacOS/

macOS (Editor versions 2021.3.19f1 through 6000.2):
<UnityEditorDir>/Contents/Frameworks/UnityLicensingClient.app/Contents/MacOS/

The paths for activation in the activate and return_license scripts were hardcoded to:

/Applications/Unity/Hub/Editor/$UNITY_VERSION/Unity.app/Contents/Frameworks/UnityLicensingClient.app/Contents/MacOS/Unity.Licensing.Client \

But for Unity 6.3+, the /Frameworks/ substring changed to /Helpers/ instead. This PR detects the Unity version and then changes the Licensing Client path depending on that.

  • ➡️ Impact: Fixes the use of Licensing Servers with game-ci/unity-builder on Unity Editor versions of 6.3+ on macs (e.g. used for iOS builds)

Related Issues

  • N/A

Related PRs

  • N/A

Successful Workflow Run Link

PRs don't have access to secrets so you will need to provide a link to a successful run of the workflows from your own
repo.

  • Can't share the private repo results but I'll screenshot the relevant pieces that have no sensitive info
BEFORE
Run game-ci/unity-builder@v4
  with:
    targetPlatform: iOS
    buildName: iOS-Device
    buildsPath: Build
    versioning: None
    unityVersion: 6000.3.5f2
    androidExportType: androidPackage
    androidSymbolType: none
    runAsHostUser: false
    dockerIsolationMode: default
    containerRegistryRepository: unityci/editor
    containerRegistryImageVersion: 3
    awsStackName: game-ci
    providerStrategy: local
    kubeVolumeSize: 5Gi
    watchToEnd: true
    cacheUnityInstallationOnMac: false
    dockerWorkspacePath: /github/workspace
    skipActivation: false
  env:
    UNITY_LICENSE_SERVER_URL: ***
    UNITY_VERSION: 6000.3.5f2
    UNITY_VERSION_CHANGESET: 3fa8bc678cb0
    BUILD_VERSION: 0.9.0-rc.495
    UNITY_LICENSING_SERVER: ***
    UNITY_CI: true

🔴 Error when trying to build with game-ci/unity-builder on Unity 6.3, build target iOS

/bin/bash /Users/runner/work/_actions/game-ci/unity-builder/v4/dist/platforms/mac/entrypoint.sh
Creating Unity License Directory
Changing to "/Users/runner/work/_actions/game-ci/unity-builder/v4/dist/BlankProject" directory.
~/work/_actions/game-ci/unity-builder/v4/dist/BlankProject ~/work/vrify-unity/vrify-unity
Adding licensing server config
/Users/runner/work/_actions/game-ci/unity-builder/v4/dist/platforms/mac/steps/activate.sh: line 39: /Applications/Unity/Hub/Editor/6000.3.5f2/Unity.app/Contents/Frameworks/UnityLicensingClient.app/Contents/MacOS/Unity.Licensing.Client: No such file or directory
Unclassified error occured while trying to activate license.
Exit code was: 127
Error: There was an error while trying to activate the Unity license.
Error: Build failed with exit code 127

AFTER

Floating license was properly acquired

acquired floating license

License properly returned

return floating license

Checklist

  • Read the contribution guide and accept the
    code of conduct
  • Docs (If new inputs or outputs have been added or changes to behavior that should be documented. Please make a PR
    in the documentation repo)
  • Readme (updated or not needed)
  • Tests (added, updated or not needed)

Summary by CodeRabbit

  • Tests
    • Expanded build integrity checks to cover the built macOS Unity licensing scripts.
    • Added verification that the generated scripts include the expected documentation link.
    • Added coverage for version-gated behavior to ensure the correct Unity licensing app path is resolved for both below and at/above the gate.
    • Continued validation that package-lock.json is not present.

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown

Cat Gif

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds integrity tests for built macOS licensing scripts, checking script contents and verifying that Unity licensing client resolution switches at the version gate.

Changes

Integrity Test Additions

Layer / File(s) Summary
Test harness and mock editor setup
src/integrity.test.ts
Expands the test imports, adds executable-file helpers, creates a mocked Unity Editor layout, and adds helpers that patch UNITY_EDITOR_DIR, run the built script, and capture the resolved client path.
mac licensing script assertions
src/integrity.test.ts
Adds a version-gate assertion helper and a test suite that checks activate.sh and return_license.sh for the licensing docs URL and the expected Frameworks-versus-Helpers client path selection.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: GabLeRoux

Poem

A rabbit hopped through scripts at dawn,
With tiny paws and tests all drawn.
It sniffed the path, then gave a grin,
"Frameworks first, then Helpers in!"
Hop, hop — the licensing trail is gone.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the macOS licensing-client path fix for Unity 6.3+.
Description check ✅ Passed The description follows the template and includes changes, related items, workflow evidence, and checklist details.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
src/integrity.test.ts (3)

2-3: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Merge with existing node:fs/promises import.

stat is already imported from node:fs/promises on Line 2; add readFile to that same import instead of a separate statement.

♻️ Proposed fix
-import { stat } from 'node:fs/promises';
-import { readFile } from 'node:fs/promises';
+import { stat, readFile } from 'node:fs/promises';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/integrity.test.ts` around lines 2 - 3, `src/integrity.test.ts` has
duplicate imports from `node:fs/promises`; merge `readFile` into the existing
import that already brings in `stat` instead of keeping a separate statement.
Update the top-level import block in `integrity.test.ts` so both `stat` and
`readFile` come from the same `node:fs/promises` import.

13-37: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate assertion sets between the two tests.

The activate.sh and return_license.sh tests assert an identical set of five substrings, differing only in the script path being read. Consider a small data-driven helper to avoid repeating the assertion list.

♻️ Proposed refactor
+  const expectedLicensingClientSnippets = [
+    'UNITY_LICENSING_CLIENT_SUBDIR="Frameworks"',
+    'UNITY_LICENSING_CLIENT_SUBDIR="Helpers"',
+    '^6000\\.([3-9]|[1-9][0-9])',
+    'https://docs.unity.com/en-us/licensing-server/client-config',
+    '"$UNITY_EDITOR_DIR/Contents/$UNITY_LICENSING_CLIENT_SUBDIR/UnityLicensingClient.app/Contents/MacOS/Unity.Licensing.Client"',
+  ];
+
   describe('mac licensing scripts', () => {
-    it('activate script switches Unity Licensing Client path for Unity 6000.3+', async () => {
-      const activateScriptPath = `${process.cwd()}/dist/platforms/mac/steps/activate.sh`;
-      const activateScript = await readFile(activateScriptPath, 'utf8');
-
-      expect(activateScript).toContain('UNITY_LICENSING_CLIENT_SUBDIR="Frameworks"');
-      expect(activateScript).toContain('UNITY_LICENSING_CLIENT_SUBDIR="Helpers"');
-      expect(activateScript).toContain('^6000\\.([3-9]|[1-9][0-9])');
-      expect(activateScript).toContain('https://docs.unity.com/en-us/licensing-server/client-config');
-      expect(activateScript).toContain(
-        '"$UNITY_EDITOR_DIR/Contents/$UNITY_LICENSING_CLIENT_SUBDIR/UnityLicensingClient.app/Contents/MacOS/Unity.Licensing.Client"',
-      );
-    });
-
-    it('return license script uses the same Unity version-gated path logic', async () => {
-      const returnLicenseScriptPath = `${process.cwd()}/dist/platforms/mac/steps/return_license.sh`;
-      const returnLicenseScript = await readFile(returnLicenseScriptPath, 'utf8');
-
-      expect(returnLicenseScript).toContain('UNITY_LICENSING_CLIENT_SUBDIR="Frameworks"');
-      expect(returnLicenseScript).toContain('UNITY_LICENSING_CLIENT_SUBDIR="Helpers"');
-      expect(returnLicenseScript).toContain('^6000\\.([3-9]|[1-9][0-9])');
-      expect(returnLicenseScript).toContain('https://docs.unity.com/en-us/licensing-server/client-config');
-      expect(returnLicenseScript).toContain(
-        '"$UNITY_EDITOR_DIR/Contents/$UNITY_LICENSING_CLIENT_SUBDIR/UnityLicensingClient.app/Contents/MacOS/Unity.Licensing.Client"',
-      );
-    });
+    it.each([
+      ['activate.sh', 'dist/platforms/mac/steps/activate.sh'],
+      ['return_license.sh', 'dist/platforms/mac/steps/return_license.sh'],
+    ])('%s uses the Unity version-gated licensing client path logic', async (_name, relPath) => {
+      const script = await readFile(`${process.cwd()}/${relPath}`, 'utf8');
+      for (const snippet of expectedLicensingClientSnippets) {
+        expect(script).toContain(snippet);
+      }
+    });
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/integrity.test.ts` around lines 13 - 37, The two integrity tests in
src/integrity.test.ts duplicate the same five substring assertions for
activateScript and returnLicenseScript. Refactor the repeated assertion set into
a small shared helper or data-driven loop, and call it from both test cases
while keeping the differing script path reads in place. Use the existing test
names and variables activateScript and returnLicenseScript to locate the
duplicated logic.

12-38: 🎯 Functional Correctness | 🔵 Trivial | ⚖️ Poor tradeoff

Tests only verify string presence, not that the version-gated branch actually selects the right path.

Both tests assert that UNITY_LICENSING_CLIENT_SUBDIR="Frameworks", UNITY_LICENSING_CLIENT_SUBDIR="Helpers", the version regex, and the path-construction template all appear somewhere in the script text. This would still pass even if the conditional logic connecting the version check to the subdir selection were broken (e.g., always falling through to "Frameworks", or the regex never matching), since the strings exist in the file regardless of which branch is taken.

Given the PR's stated goal of covering "path-selection branches," consider actually executing the built script (e.g., via child_process.execFile with a stubbed $UNITY_EDITOR_DIR/mocked Unity version input) and asserting on the resolved path, rather than just grepping literal substrings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/integrity.test.ts` around lines 12 - 38, The tests in integrity.test.ts
only check that literal strings exist in activateScript and returnLicenseScript,
so they do not prove the version-gated path-selection branch works. Update the
mac licensing script tests to exercise the actual branching logic in the built
scripts, using the existing activateScriptPath and returnLicenseScriptPath
checks as a guide. Execute the scripts with controlled inputs (for example a
stubbed UNITY_EDITOR_DIR and a mock Unity version) and assert the resolved
UnityLicensingClient path for both sides of the version gate, instead of only
grepping for substrings.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/integrity.test.ts`:
- Around line 2-3: `src/integrity.test.ts` has duplicate imports from
`node:fs/promises`; merge `readFile` into the existing import that already
brings in `stat` instead of keeping a separate statement. Update the top-level
import block in `integrity.test.ts` so both `stat` and `readFile` come from the
same `node:fs/promises` import.
- Around line 13-37: The two integrity tests in src/integrity.test.ts duplicate
the same five substring assertions for activateScript and returnLicenseScript.
Refactor the repeated assertion set into a small shared helper or data-driven
loop, and call it from both test cases while keeping the differing script path
reads in place. Use the existing test names and variables activateScript and
returnLicenseScript to locate the duplicated logic.
- Around line 12-38: The tests in integrity.test.ts only check that literal
strings exist in activateScript and returnLicenseScript, so they do not prove
the version-gated path-selection branch works. Update the mac licensing script
tests to exercise the actual branching logic in the built scripts, using the
existing activateScriptPath and returnLicenseScriptPath checks as a guide.
Execute the scripts with controlled inputs (for example a stubbed
UNITY_EDITOR_DIR and a mock Unity version) and assert the resolved
UnityLicensingClient path for both sides of the version gate, instead of only
grepping for substrings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f1213bdb-6a38-4aee-b0ea-9a6660321a82

📥 Commits

Reviewing files that changed from the base of the PR and between d829bfc and ab9dcf5.

⛔ Files ignored due to path filters (2)
  • dist/platforms/mac/steps/activate.sh is excluded by !**/dist/**
  • dist/platforms/mac/steps/return_license.sh is excluded by !**/dist/**
📒 Files selected for processing (1)
  • src/integrity.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
src/integrity.test.ts (3)

94-96: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No error context captured on script failure.

If the patched script exits non-zero, execFileAsync throws an ExecException whose stdout/stderr are attached but not surfaced anywhere, and the subsequent readFile(capturePathFile, ...) line is never reached. Debugging a failing integrity test would then require re-running locally rather than reading CI output.

♻️ Optional improvement to surface stdout/stderr
-  await execFileAsync('bash', [localScriptPath], { env: environment });
-
-  return (await readFile(capturePathFile, 'utf8')).trim();
+  try {
+    await execFileAsync('bash', [localScriptPath], { env: environment });
+  } catch (error) {
+    const { stdout, stderr } = error as { stdout?: string; stderr?: string };
+    throw new Error(`Script execution failed.\nstdout: ${stdout}\nstderr: ${stderr}`, { cause: error });
+  }
+
+  return (await readFile(capturePathFile, 'utf8')).trim();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/integrity.test.ts` around lines 94 - 96, The integrity test helper
currently lets execFileAsync fail without surfacing the ExecException
stdout/stderr, so failures lack context. Update the helper in integrity.test.ts
around execFileAsync and readFile(capturePathFile, ...) to catch script
execution errors, include the attached stdout/stderr in the thrown/asserted
failure message, and then rethrow so the test output is actionable; use the
existing localScriptPath and capturePathFile flow to place the fix.

72-77: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Brittle string-replace patch for UNITY_EDITOR_DIR.

The patch relies on an exact literal match of UNITY_EDITOR_DIR="/Applications/Unity/Hub/Editor/$UNITY_VERSION/Unity.app" in the built script. If the source script's quoting/spacing/variable name changes slightly, .replace silently no-ops, the script then tries to use the real /Applications/... path in the sandbox, and the failure mode (execFileAsync throwing on a nonexistent path, or the assertion just failing) won't clearly indicate the root cause is a stale test fixture rather than a script bug.

Consider asserting that the replacement actually took effect (e.g., if (patchedScript === scriptSource) throw new Error(...)) so a mismatch fails fast with an informative message.


57-97: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Temp directories created via mkdtemp are never cleaned up.

Each call to executeAndCaptureResolvedClientPath creates a fresh temp workspace under os.tmpdir() (mock editor tree, capture file, patched script) but nothing ever removes root. expectVersionGateBehavior calls this twice per test, across two test cases, leaving stray directories on disk after every test run (worse in CI where tmp isn't always cleared between jobs).

♻️ Proposed cleanup
+import { rm } from 'node:fs/promises';
+
 async function executeAndCaptureResolvedClientPath(
   sourceScriptPath: string,
   unityVersion: string,
   kind: ScriptKind,
 ): Promise<string> {
   const root = await mkdtemp(join(tmpdir(), 'unity-builder-integrity-'));
+  try {
     ...
-  await execFileAsync('bash', [localScriptPath], { env: environment });
-
-  return (await readFile(capturePathFile, 'utf8')).trim();
+    await execFileAsync('bash', [localScriptPath], { env: environment });
+    return (await readFile(capturePathFile, 'utf8')).trim();
+  } finally {
+    await rm(root, { recursive: true, force: true });
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/integrity.test.ts` around lines 57 - 97, The temp workspace created in
executeAndCaptureResolvedClientPath is never removed, so each mkdtemp call
leaves behind the mock editor, scripts, and capture files under the temp root.
Add cleanup for the root directory after the bash exec/read completes, and make
sure expectVersionGateBehavior still works when
executeAndCaptureResolvedClientPath is called multiple times by using a
try/finally around the existing mkdtemp/mkdir/writeFile/execFileAsync flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/integrity.test.ts`:
- Around line 94-96: The integrity test helper currently lets execFileAsync fail
without surfacing the ExecException stdout/stderr, so failures lack context.
Update the helper in integrity.test.ts around execFileAsync and
readFile(capturePathFile, ...) to catch script execution errors, include the
attached stdout/stderr in the thrown/asserted failure message, and then rethrow
so the test output is actionable; use the existing localScriptPath and
capturePathFile flow to place the fix.
- Around line 57-97: The temp workspace created in
executeAndCaptureResolvedClientPath is never removed, so each mkdtemp call
leaves behind the mock editor, scripts, and capture files under the temp root.
Add cleanup for the root directory after the bash exec/read completes, and make
sure expectVersionGateBehavior still works when
executeAndCaptureResolvedClientPath is called multiple times by using a
try/finally around the existing mkdtemp/mkdir/writeFile/execFileAsync flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5c30ee87-5ad5-41f5-a80e-bb17a1edfb6e

📥 Commits

Reviewing files that changed from the base of the PR and between ab9dcf5 and 1d177e4.

📒 Files selected for processing (1)
  • src/integrity.test.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant