mirror of
https://github.com/game-ci/unity-builder.git
synced 2026-05-31 13:56:13 -07:00
* fix: remove concurrency block from reusable workflow to prevent deadlock
When integrity-check.yml calls validate-orchestrator-integration.yml via
workflow_call, both workflows resolve github.workflow to the same name
("Integrity"), creating identical concurrency groups. GitHub detects this
as a deadlock and cancels the run.
Fix: remove concurrency from the reusable workflow entirely — the caller
already manages concurrency for the group.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add linux64RemoveExecutableExtension parameter (default: false)
Adds configurable control over the `.x86_64` file extension for
StandaloneLinux64 builds. Default is `false` (keep the extension),
matching Unity's native behavior.
Set `linux64RemoveExecutableExtension: true` to restore the
extensionless behavior from v4.
Rebased from kitlith's original PR #726. Default flipped for v5.
Closes #722
Co-Authored-By: kitlith <kitlith@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: bump production dependencies (minor/patch)
- @actions/cache ^4.0.0 → ^4.1.0
- @actions/github ^6.0.0 → ^6.0.1
- commander ^9.0.0 → ^9.5.0
- nanoid ^3.3.1 → ^3.3.12
- reflect-metadata ^0.1.13 → ^0.2.2
- semver ^7.5.2 → ^7.7.4
- yaml ^2.2.2 → ^2.8.4
All minor/patch bumps. Major bumps (@actions/core 3.x, nanoid 5.x ESM)
deferred to a separate PR.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: remove legacy CLI bootstrap and unused deps
Remove InitCliMode/RunCli, @CliFunction decorator, and
CliFunctionsRepository. The only registered CLI mode was `print-input`
which is unused — all real CLI functionality lives in the orchestrator
repo now.
This drops 3 dependencies:
- commander-ts (decorator-based CLI, needed reflect-metadata)
- reflect-metadata (peer dep of commander-ts)
- commander (only used for OptionValues type)
Cli.options, Cli.isCliMode, and Cli.query remain — the orchestrator
plugin sets these directly without commander.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: rename Cli to PluginOptions, remove cli directory
Cli class was a legacy name from when unity-builder had its own CLI.
Now it's just an options bridge for plugins — renamed to PluginOptions
with a backwards-compatible Cli alias for the orchestrator.
Moved from src/model/cli/cli.ts to src/model/plugin-options.ts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(ci): fix orchestrator integration test failures
Two issues:
1. jest → vitest: the repo migrated to vitest but the integration
workflow still called `npx jest`. Changed to `npx vitest run`.
2. Git checkout corruption: when the orchestrator branch matching the
PR doesn't exist, the first checkout fails leaving a corrupted .git
directory. The fallback step then hits `fatal: ambiguous argument
'HEAD'`. Fix: add `clean: true` to all fallback checkout steps so
they wipe the broken state before re-cloning.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(ci): wipe .git before fallback checkout, fix remaining jest syntax
- Add `rm -rf .git` step before fallback checkout to clear corrupted
state when branch-matching checkout fails
- Fix unit test step: replace jest --testPathPattern with vitest
positional filters (same fix as orchestrator PR #18)
- Replace all --detectOpenHandles --forceExit --runInBand with
vitest --no-file-parallelism
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: simplify plugin mode check
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: kitlith <kitlith@users.noreply.github.com>
255 lines
14 KiB
TypeScript
255 lines
14 KiB
TypeScript
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, test, vi } from 'vitest';
|
|
import Versioning from './versioning';
|
|
import UnityVersioning from './unity-versioning';
|
|
import AndroidVersioning from './android-versioning';
|
|
import BuildParameters from './build-parameters';
|
|
import Input from './input';
|
|
import Platform from './platform';
|
|
|
|
const testLicense =
|
|
'<?xml version="1.0" encoding="UTF-8"?><root>\n <License id="Terms">\n <MachineBindings>\n <Binding Key="1" Value="576562626572264761624c65526f7578"/>\n <Binding Key="2" Value="576562626572264761624c65526f7578"/>\n </MachineBindings>\n <MachineID Value="D7nTUnjNAmtsUMcnoyrqkgIbYdM="/>\n <SerialHash Value="2033b8ac3e6faa3742ca9f0bfae44d18f2a96b80"/>\n <Features>\n <Feature Value="33"/>\n <Feature Value="1"/>\n <Feature Value="12"/>\n <Feature Value="2"/>\n <Feature Value="24"/>\n <Feature Value="3"/>\n <Feature Value="36"/>\n <Feature Value="17"/>\n <Feature Value="19"/>\n <Feature Value="62"/>\n </Features>\n <DeveloperData Value="AQAAAEY0LUJHUlgtWEQ0RS1aQ1dWLUM1SlctR0RIQg=="/>\n <SerialMasked Value="F4-BGRX-XD4E-ZCWV-C5JW-XXXX"/>\n <StartDate Value="2021-02-08T00:00:00"/>\n <UpdateDate Value="2021-02-09T00:34:57"/>\n <InitialActivationDate Value="2021-02-08T00:34:56"/>\n <LicenseVersion Value="6.x"/>\n <ClientProvidedVersion Value="2018.4.30f1"/>\n <AlwaysOnline Value="false"/>\n <Entitlements>\n <Entitlement Ns="unity_editor" Tag="UnityPersonal" Type="EDITOR" ValidTo="9999-12-31T00:00:00"/>\n <Entitlement Ns="unity_editor" Tag="DarkSkin" Type="EDITOR_FEATURE" ValidTo="9999-12-31T00:00:00"/>\n </Entitlements>\n </License>\n<Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"/><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><Reference URI="#Terms"><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><DigestValue>m0Db8UK+ktnOLJBtHybkfetpcKo=</DigestValue></Reference></SignedInfo><SignatureValue>o/pUbSQAukz7+ZYAWhnA0AJbIlyyCPL7bKVEM2lVqbrXt7cyey+umkCXamuOgsWPVUKBMkXtMH8L\n5etLmD0getWIhTGhzOnDCk+gtIPfL4jMo9tkEuOCROQAXCci23VFscKcrkB+3X6h4wEOtA2APhOY\nB+wvC794o8/82ffjP79aVAi57rp3Wmzx+9pe9yMwoJuljAy2sc2tIMgdQGWVmOGBpQm3JqsidyzI\nJWG2kjnc7pDXK9pwYzXoKiqUqqrut90d+kQqRyv7MSZXR50HFqD/LI69h68b7P8Bjo3bPXOhNXGR\n9YCoemH6EkfCJxp2gIjzjWW+l2Hj2EsFQi8YXw==</SignatureValue></Signature></root>';
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
vi.spyOn(Versioning, 'determineBuildVersion').mockImplementation(async () => '1.3.37');
|
|
process.env.UNITY_LICENSE = testLicense; // Todo - Don't use process.env directly, that's what the input model class is for.
|
|
});
|
|
|
|
describe('BuildParameters', () => {
|
|
describe('create', () => {
|
|
it('does not throw', async () => {
|
|
await expect(BuildParameters.create()).resolves.not.toThrow();
|
|
});
|
|
|
|
it('determines the version only once', async () => {
|
|
vi.spyOn(Versioning, 'determineBuildVersion').mockImplementation(async () => '1.3.37');
|
|
await BuildParameters.create();
|
|
await expect(Versioning.determineBuildVersion).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('determines the unity version only once', async () => {
|
|
vi.spyOn(UnityVersioning, 'determineUnityVersion').mockImplementation(() => '2019.2.11f1');
|
|
await BuildParameters.create();
|
|
expect(UnityVersioning.determineUnityVersion).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('returns the android version code with provided input', async () => {
|
|
const mockValue = '42';
|
|
vi.spyOn(Input, 'androidVersionCode', 'get').mockReturnValue(mockValue);
|
|
await expect(BuildParameters.create()).resolves.toEqual(
|
|
expect.objectContaining({ androidVersionCode: mockValue }),
|
|
);
|
|
});
|
|
|
|
it('returns the android version code from version by default', async () => {
|
|
const mockValue = '';
|
|
vi.spyOn(Input, 'androidVersionCode', 'get').mockReturnValue(mockValue);
|
|
await expect(BuildParameters.create()).resolves.toEqual(
|
|
expect.objectContaining({ androidVersionCode: '1003037' }),
|
|
);
|
|
});
|
|
|
|
it('determines the android sdk manager parameters only once', async () => {
|
|
vi.spyOn(AndroidVersioning, 'determineSdkManagerParameters').mockImplementation(
|
|
() => 'platforms;android-30',
|
|
);
|
|
await BuildParameters.create();
|
|
expect(AndroidVersioning.determineSdkManagerParameters).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('returns the targetPlatform', async () => {
|
|
const mockValue = 'somePlatform';
|
|
vi.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockValue);
|
|
await expect(BuildParameters.create()).resolves.toEqual(
|
|
expect.objectContaining({ targetPlatform: mockValue }),
|
|
);
|
|
});
|
|
|
|
it('returns the project path', async () => {
|
|
const mockValue = 'path/to/project';
|
|
vi.spyOn(UnityVersioning, 'determineUnityVersion').mockImplementation(() => '2019.2.11f1');
|
|
vi.spyOn(Input, 'projectPath', 'get').mockReturnValue(mockValue);
|
|
await expect(BuildParameters.create()).resolves.toEqual(
|
|
expect.objectContaining({ projectPath: mockValue }),
|
|
);
|
|
});
|
|
|
|
it('returns the build profile', async () => {
|
|
const mockValue = 'path/to/build_profile.asset';
|
|
vi.spyOn(Input, 'buildProfile', 'get').mockReturnValue(mockValue);
|
|
await expect(BuildParameters.create()).resolves.toEqual(
|
|
expect.objectContaining({ buildProfile: mockValue }),
|
|
);
|
|
});
|
|
|
|
it('returns the build name', async () => {
|
|
const mockValue = 'someBuildName';
|
|
vi.spyOn(Input, 'buildName', 'get').mockReturnValue(mockValue);
|
|
await expect(BuildParameters.create()).resolves.toEqual(
|
|
expect.objectContaining({ buildName: mockValue }),
|
|
);
|
|
});
|
|
|
|
it('returns the build path', async () => {
|
|
const mockPath = 'somePath';
|
|
const mockPlatform = 'somePlatform';
|
|
const expectedBuildPath = `${mockPath}/${mockPlatform}`;
|
|
vi.spyOn(Input, 'buildsPath', 'get').mockReturnValue(mockPath);
|
|
vi.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockPlatform);
|
|
await expect(BuildParameters.create()).resolves.toEqual(
|
|
expect.objectContaining({ buildPath: expectedBuildPath }),
|
|
);
|
|
});
|
|
|
|
it('returns the build file', async () => {
|
|
const mockValue = 'someBuildName';
|
|
const mockPlatform = 'somePlatform';
|
|
|
|
vi.spyOn(Input, 'buildName', 'get').mockReturnValue(mockValue);
|
|
vi.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockPlatform);
|
|
await expect(BuildParameters.create()).resolves.toEqual(
|
|
expect.objectContaining({ buildFile: mockValue }),
|
|
);
|
|
});
|
|
|
|
test.each`
|
|
targetPlatform | expectedExtension | androidExportType | linux64RemoveExecutableExtension
|
|
${Platform.types.Android} | ${'.apk'} | ${'androidPackage'} | ${false}
|
|
${Platform.types.Android} | ${'.aab'} | ${'androidAppBundle'} | ${true}
|
|
${Platform.types.Android} | ${''} | ${'androidStudioProject'} | ${false}
|
|
${Platform.types.StandaloneWindows} | ${'.exe'} | ${'n/a'} | ${true}
|
|
${Platform.types.StandaloneWindows64} | ${'.exe'} | ${'n/a'} | ${false}
|
|
${Platform.types.StandaloneLinux64} | ${'.x86_64'} | ${'n/a'} | ${false}
|
|
${Platform.types.StandaloneLinux64} | ${''} | ${'n/a'} | ${true}
|
|
`(
|
|
'appends $expectedExtension for $targetPlatform with linux64RemoveExecutableExtension=$linux64RemoveExecutableExtension',
|
|
async ({ targetPlatform, expectedExtension, androidExportType, linux64RemoveExecutableExtension }) => {
|
|
vi.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform);
|
|
vi.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform);
|
|
vi.spyOn(Input, 'androidExportType', 'get').mockReturnValue(androidExportType);
|
|
vi.spyOn(Input, 'linux64RemoveExecutableExtension', 'get').mockReturnValue(linux64RemoveExecutableExtension);
|
|
await expect(BuildParameters.create()).resolves.toEqual(
|
|
expect.objectContaining({ buildFile: `${targetPlatform}${expectedExtension}` }),
|
|
);
|
|
},
|
|
);
|
|
|
|
test.each`
|
|
targetPlatform | androidSymbolType
|
|
${Platform.types.Android} | ${'none'}
|
|
${Platform.types.Android} | ${'public'}
|
|
${Platform.types.Android} | ${'debugging'}
|
|
${Platform.types.StandaloneWindows} | ${'none'}
|
|
${Platform.types.StandaloneWindows64} | ${'none'}
|
|
`(
|
|
'androidSymbolType is set to $androidSymbolType when targetPlatform is $targetPlatform and input targetSymbolType is $androidSymbolType',
|
|
async ({ targetPlatform, androidSymbolType }) => {
|
|
vi.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform);
|
|
vi.spyOn(Input, 'androidSymbolType', 'get').mockReturnValue(androidSymbolType);
|
|
vi.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform);
|
|
await expect(BuildParameters.create()).resolves.toEqual(
|
|
expect.objectContaining({ androidSymbolType }),
|
|
);
|
|
},
|
|
);
|
|
|
|
it('returns the build method', async () => {
|
|
const mockValue = 'Namespace.ClassName.BuildMethod';
|
|
vi.spyOn(Input, 'buildMethod', 'get').mockReturnValue(mockValue);
|
|
await expect(BuildParameters.create()).resolves.toEqual(
|
|
expect.objectContaining({ buildMethod: mockValue }),
|
|
);
|
|
});
|
|
|
|
it('returns the android keystore name', async () => {
|
|
const mockValue = 'keystore.keystore';
|
|
vi.spyOn(Input, 'androidKeystoreName', 'get').mockReturnValue(mockValue);
|
|
await expect(BuildParameters.create()).resolves.toEqual(
|
|
expect.objectContaining({ androidKeystoreName: mockValue }),
|
|
);
|
|
});
|
|
|
|
it('returns the android keystore base64-encoded content', async () => {
|
|
const mockValue = 'secret';
|
|
vi.spyOn(Input, 'androidKeystoreBase64', 'get').mockReturnValue(mockValue);
|
|
await expect(BuildParameters.create()).resolves.toEqual(
|
|
expect.objectContaining({ androidKeystoreBase64: mockValue }),
|
|
);
|
|
});
|
|
|
|
it('returns the android keystore pass', async () => {
|
|
const mockValue = 'secret';
|
|
vi.spyOn(Input, 'androidKeystorePass', 'get').mockReturnValue(mockValue);
|
|
await expect(BuildParameters.create()).resolves.toEqual(
|
|
expect.objectContaining({ androidKeystorePass: mockValue }),
|
|
);
|
|
});
|
|
|
|
it('returns the android keyalias name', async () => {
|
|
const mockValue = 'secret';
|
|
vi.spyOn(Input, 'androidKeyaliasName', 'get').mockReturnValue(mockValue);
|
|
await expect(BuildParameters.create()).resolves.toEqual(
|
|
expect.objectContaining({ androidKeyaliasName: mockValue }),
|
|
);
|
|
});
|
|
|
|
it('returns the android keyalias pass', async () => {
|
|
const mockValue = 'secret';
|
|
vi.spyOn(Input, 'androidKeyaliasPass', 'get').mockReturnValue(mockValue);
|
|
await expect(BuildParameters.create()).resolves.toEqual(
|
|
expect.objectContaining({ androidKeyaliasPass: mockValue }),
|
|
);
|
|
});
|
|
|
|
it('returns the android target sdk version', async () => {
|
|
const mockValue = 'AndroidApiLevelAuto';
|
|
vi.spyOn(Input, 'androidTargetSdkVersion', 'get').mockReturnValue(mockValue);
|
|
await expect(BuildParameters.create()).resolves.toEqual(
|
|
expect.objectContaining({ androidTargetSdkVersion: mockValue }),
|
|
);
|
|
});
|
|
|
|
it('returns the unity licensing server address', async () => {
|
|
const mockValue = 'http://example.com';
|
|
vi.spyOn(Input, 'unityLicensingServer', 'get').mockReturnValue(mockValue);
|
|
await expect(BuildParameters.create()).resolves.toEqual(
|
|
expect.objectContaining({ unityLicensingServer: mockValue }),
|
|
);
|
|
});
|
|
|
|
it('throws error when no unity license provider provided', async () => {
|
|
delete process.env.UNITY_LICENSE; // Need to delete this as it is set for every test currently
|
|
await expect(BuildParameters.create()).rejects.toThrowError();
|
|
});
|
|
|
|
it('return serial when no license server is provided', async () => {
|
|
const mockValue = '123';
|
|
delete process.env.UNITY_LICENSE; // Need to delete this as it is set for every test currently
|
|
process.env.UNITY_SERIAL = mockValue;
|
|
await expect(BuildParameters.create()).resolves.toEqual(
|
|
expect.objectContaining({ unitySerial: mockValue }),
|
|
);
|
|
delete process.env.UNITY_SERIAL;
|
|
});
|
|
|
|
it('returns the custom parameters', async () => {
|
|
const mockValue = '-profile SomeProfile -someBoolean -someValue exampleValue';
|
|
vi.spyOn(Input, 'customParameters', 'get').mockReturnValue(mockValue);
|
|
await expect(BuildParameters.create()).resolves.toEqual(
|
|
expect.objectContaining({ customParameters: mockValue }),
|
|
);
|
|
});
|
|
|
|
it.each([true, false])('returns the flag for useHostNetwork when %s', async (mockValue) => {
|
|
vi.spyOn(Input, 'useHostNetwork', 'get').mockReturnValue(mockValue);
|
|
await expect(BuildParameters.create()).resolves.toEqual(
|
|
expect.objectContaining({ useHostNetwork: mockValue }),
|
|
);
|
|
});
|
|
});
|
|
});
|