diff --git a/dist/index.js b/dist/index.js index a33732a6..1ccffaab 100644 Binary files a/dist/index.js and b/dist/index.js differ diff --git a/dist/index.js.map b/dist/index.js.map index d34a948f..a5fc6f39 100644 Binary files a/dist/index.js.map and b/dist/index.js.map differ diff --git a/dist/platforms/ubuntu/steps/activate.sh b/dist/platforms/ubuntu/steps/activate.sh index 8802de9c..81eb2ffc 100755 --- a/dist/platforms/ubuntu/steps/activate.sh +++ b/dist/platforms/ubuntu/steps/activate.sh @@ -74,6 +74,21 @@ elif [[ -n "$UNITY_SERIAL" && -n "$UNITY_EMAIL" && -n "$UNITY_PASSWORD" ]]; then # Store the exit code from the verify command UNITY_EXIT_CODE=$? +elif [[ -n "$UNITY_LICENSING_SERVER" ]]; then + # + # Custom Unity License Server + # + echo "Adding licensing server config" + + /opt/unity/Editor/Data/Resources/Licensing/Client/Unity.Licensing.Client --acquire-floating > license.txt #is this accessible in a env variable? + PARSEDFILE=$(grep -oP '\".*?\"' < license.txt | tr -d '"') + export FLOATING_LICENSE + FLOATING_LICENSE=$(sed -n 2p <<< "$PARSEDFILE") + FLOATING_LICENSE_TIMEOUT=$(sed -n 4p <<< "$PARSEDFILE") + + echo "Acquired floating license: \"$FLOATING_LICENSE\" with timeout $FLOATING_LICENSE_TIMEOUT" + # Store the exit code from the verify command + UNITY_EXIT_CODE=$? else # # NO LICENSE ACTIVATION STRATEGY MATCHED diff --git a/dist/platforms/ubuntu/steps/return_license.sh b/dist/platforms/ubuntu/steps/return_license.sh index c5bb721f..f0f68b58 100755 --- a/dist/platforms/ubuntu/steps/return_license.sh +++ b/dist/platforms/ubuntu/steps/return_license.sh @@ -4,7 +4,14 @@ echo "Changing to \"$ACTIVATE_LICENSE_PATH\" directory." pushd "$ACTIVATE_LICENSE_PATH" -if [[ -n "$UNITY_SERIAL" ]]; then + +if [[ -n "$UNITY_LICENSING_SERVER" ]]; then # + # + # Return any floating license used. + # + echo "Returning floating license: \"$FLOATING_LICENSE\"" + /opt/unity/Editor/Data/Resources/Licensing/Client/Unity.Licensing.Client --return-floating "$FLOATING_LICENSE" +elif [[ -n "$UNITY_SERIAL" ]]; then # # PROFESSIONAL (SERIAL) LICENSE MODE # diff --git a/dist/unity-config/services-config.json.template b/dist/unity-config/services-config.json.template new file mode 100644 index 00000000..5a868f1b --- /dev/null +++ b/dist/unity-config/services-config.json.template @@ -0,0 +1,7 @@ +{ + "licensingServiceBaseUrl": "%URL%", + "enableEntitlementLicensing": true, + "enableFloatingApi": true, + "clientConnectTimeoutSec": 5, + "clientHandshakeTimeoutSec": 10 +} diff --git a/src/model/build-parameters.test.ts b/src/model/build-parameters.test.ts index 1cb37ae5..c41befba 100644 --- a/src/model/build-parameters.test.ts +++ b/src/model/build-parameters.test.ts @@ -5,21 +5,17 @@ import BuildParameters from './build-parameters'; import Input from './input'; import Platform from './platform'; -// Todo - Don't use process.env directly, that's what the input model class is for. const testLicense = '\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \nm0Db8UK+ktnOLJBtHybkfetpcKo=o/pUbSQAukz7+ZYAWhnA0AJbIlyyCPL7bKVEM2lVqbrXt7cyey+umkCXamuOgsWPVUKBMkXtMH8L\n5etLmD0getWIhTGhzOnDCk+gtIPfL4jMo9tkEuOCROQAXCci23VFscKcrkB+3X6h4wEOtA2APhOY\nB+wvC794o8/82ffjP79aVAi57rp3Wmzx+9pe9yMwoJuljAy2sc2tIMgdQGWVmOGBpQm3JqsidyzI\nJWG2kjnc7pDXK9pwYzXoKiqUqqrut90d+kQqRyv7MSZXR50HFqD/LI69h68b7P8Bjo3bPXOhNXGR\n9YCoemH6EkfCJxp2gIjzjWW+l2Hj2EsFQi8YXw=='; -process.env.UNITY_LICENSE = testLicense; - -const determineVersion = jest.spyOn(Versioning, 'determineBuildVersion').mockImplementation(async () => '1.3.37'); -const determineUnityVersion = jest - .spyOn(UnityVersioning, 'determineUnityVersion') - .mockImplementation(() => '2019.2.11f1'); -const determineSdkManagerParameters = jest - .spyOn(AndroidVersioning, 'determineSdkManagerParameters') - .mockImplementation(() => 'platforms;android-30'); afterEach(() => { jest.clearAllMocks(); + jest.restoreAllMocks(); +}); + +beforeEach(() => { + jest.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', () => { @@ -29,48 +25,54 @@ describe('BuildParameters', () => { }); it('determines the version only once', async () => { + jest.spyOn(Versioning, 'determineBuildVersion').mockImplementation(async () => '1.3.37'); await BuildParameters.create(); - expect(determineVersion).toHaveBeenCalledTimes(1); + await expect(Versioning.determineBuildVersion).toHaveBeenCalledTimes(1); }); it('determines the unity version only once', async () => { + jest.spyOn(UnityVersioning, 'determineUnityVersion').mockImplementation(() => '2019.2.11f1'); await BuildParameters.create(); - expect(determineUnityVersion).toHaveBeenCalledTimes(1); + await expect(UnityVersioning.determineUnityVersion).toHaveBeenCalledTimes(1); }); it('returns the android version code with provided input', async () => { const mockValue = '42'; jest.spyOn(Input, 'androidVersionCode', 'get').mockReturnValue(mockValue); - expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidVersionCode: mockValue })); + await expect(BuildParameters.create()).resolves.toEqual( + expect.objectContaining({ androidVersionCode: mockValue }), + ); }); it('returns the android version code from version by default', async () => { const mockValue = ''; jest.spyOn(Input, 'androidVersionCode', 'get').mockReturnValue(mockValue); - expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidVersionCode: 1003037 })); + await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidVersionCode: 1003037 })); }); it('determines the android sdk manager parameters only once', async () => { + jest.spyOn(AndroidVersioning, 'determineSdkManagerParameters').mockImplementation(() => 'platforms;android-30'); await BuildParameters.create(); - expect(determineSdkManagerParameters).toHaveBeenCalledTimes(1); + await expect(AndroidVersioning.determineSdkManagerParameters).toHaveBeenCalledTimes(1); }); it('returns the targetPlatform', async () => { const mockValue = 'somePlatform'; jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockValue); - expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ targetPlatform: mockValue })); + await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ targetPlatform: mockValue })); }); it('returns the project path', async () => { const mockValue = 'path/to/project'; + jest.spyOn(UnityVersioning, 'determineUnityVersion').mockImplementation(() => '2019.2.11f1'); jest.spyOn(Input, 'projectPath', 'get').mockReturnValue(mockValue); - expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ projectPath: mockValue })); + await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ projectPath: mockValue })); }); it('returns the build name', async () => { const mockValue = 'someBuildName'; jest.spyOn(Input, 'buildName', 'get').mockReturnValue(mockValue); - expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildName: mockValue })); + await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildName: mockValue })); }); it('returns the build path', async () => { @@ -79,13 +81,18 @@ describe('BuildParameters', () => { const expectedBuildPath = `${mockPath}/${mockPlatform}`; jest.spyOn(Input, 'buildsPath', 'get').mockReturnValue(mockPath); jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockPlatform); - expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildPath: expectedBuildPath })); + await expect(BuildParameters.create()).resolves.toEqual( + expect.objectContaining({ buildPath: expectedBuildPath }), + ); }); it('returns the build file', async () => { const mockValue = 'someBuildName'; + const mockPlatform = 'somePlatform'; + jest.spyOn(Input, 'buildName', 'get').mockReturnValue(mockValue); - expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildFile: mockValue })); + jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(mockPlatform); + await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildFile: mockValue })); }); test.each([Platform.types.StandaloneWindows, Platform.types.StandaloneWindows64])( @@ -93,7 +100,7 @@ describe('BuildParameters', () => { async (targetPlatform) => { jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform); jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform); - expect(BuildParameters.create()).resolves.toEqual( + await expect(BuildParameters.create()).resolves.toEqual( expect.objectContaining({ buildFile: `${targetPlatform}.exe` }), ); }, @@ -103,7 +110,7 @@ describe('BuildParameters', () => { jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform); jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform); jest.spyOn(Input, 'androidAppBundle', 'get').mockReturnValue(false); - expect(BuildParameters.create()).resolves.toEqual( + await expect(BuildParameters.create()).resolves.toEqual( expect.objectContaining({ buildFile: `${targetPlatform}.apk` }), ); }); @@ -112,7 +119,7 @@ describe('BuildParameters', () => { jest.spyOn(Input, 'targetPlatform', 'get').mockReturnValue(targetPlatform); jest.spyOn(Input, 'buildName', 'get').mockReturnValue(targetPlatform); jest.spyOn(Input, 'androidAppBundle', 'get').mockReturnValue(true); - expect(BuildParameters.create()).resolves.toEqual( + await expect(BuildParameters.create()).resolves.toEqual( expect.objectContaining({ buildFile: `${targetPlatform}.aab` }), ); }); @@ -120,51 +127,82 @@ describe('BuildParameters', () => { it('returns the build method', async () => { const mockValue = 'Namespace.ClassName.BuildMethod'; jest.spyOn(Input, 'buildMethod', 'get').mockReturnValue(mockValue); - expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildMethod: mockValue })); + await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ buildMethod: mockValue })); }); it('returns the android keystore name', async () => { const mockValue = 'keystore.keystore'; jest.spyOn(Input, 'androidKeystoreName', 'get').mockReturnValue(mockValue); - expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidKeystoreName: mockValue })); + await expect(BuildParameters.create()).resolves.toEqual( + expect.objectContaining({ androidKeystoreName: mockValue }), + ); }); it('returns the android keystore base64-encoded content', async () => { const mockValue = 'secret'; jest.spyOn(Input, 'androidKeystoreBase64', 'get').mockReturnValue(mockValue); - expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidKeystoreBase64: mockValue })); + await expect(BuildParameters.create()).resolves.toEqual( + expect.objectContaining({ androidKeystoreBase64: mockValue }), + ); }); it('returns the android keystore pass', async () => { const mockValue = 'secret'; jest.spyOn(Input, 'androidKeystorePass', 'get').mockReturnValue(mockValue); - expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidKeystorePass: mockValue })); + await expect(BuildParameters.create()).resolves.toEqual( + expect.objectContaining({ androidKeystorePass: mockValue }), + ); }); it('returns the android keyalias name', async () => { const mockValue = 'secret'; jest.spyOn(Input, 'androidKeyaliasName', 'get').mockReturnValue(mockValue); - expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidKeyaliasName: mockValue })); + await expect(BuildParameters.create()).resolves.toEqual( + expect.objectContaining({ androidKeyaliasName: mockValue }), + ); }); it('returns the android keyalias pass', async () => { const mockValue = 'secret'; jest.spyOn(Input, 'androidKeyaliasPass', 'get').mockReturnValue(mockValue); - expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ androidKeyaliasPass: mockValue })); + await expect(BuildParameters.create()).resolves.toEqual( + expect.objectContaining({ androidKeyaliasPass: mockValue }), + ); }); it('returns the android target sdk version', async () => { const mockValue = 'AndroidApiLevelAuto'; jest.spyOn(Input, 'androidTargetSdkVersion', 'get').mockReturnValue(mockValue); - expect(BuildParameters.create()).resolves.toEqual( + await expect(BuildParameters.create()).resolves.toEqual( expect.objectContaining({ androidTargetSdkVersion: mockValue }), ); }); + it('returns the unity licensing server address', async () => { + const mockValue = 'http://example.com'; + jest.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'; jest.spyOn(Input, 'customParameters', 'get').mockReturnValue(mockValue); - expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ customParameters: mockValue })); + await expect(BuildParameters.create()).resolves.toEqual(expect.objectContaining({ customParameters: mockValue })); }); }); }); diff --git a/src/model/build-parameters.ts b/src/model/build-parameters.ts index 4dcb3854..27c1aad9 100644 --- a/src/model/build-parameters.ts +++ b/src/model/build-parameters.ts @@ -14,6 +14,7 @@ class BuildParameters { public editorVersion!: string; public customImage!: string; public unitySerial!: string; + public unityLicensingServer!: string; public runnerTempPath: string | undefined; public targetPlatform!: string; public projectPath!: string; @@ -76,24 +77,26 @@ class BuildParameters { // Todo - Don't use process.env directly, that's what the input model class is for. // --- let unitySerial = ''; - if (!process.env.UNITY_SERIAL && Input.githubInputEnabled) { - // No serial was present, so it is a personal license that we need to convert - if (!process.env.UNITY_LICENSE) { - throw new Error(`Missing Unity License File and no Serial was found. If this + if (Input.unityLicensingServer === '') { + if (!process.env.UNITY_SERIAL && Input.githubInputEnabled) { + // No serial was present, so it is a personal license that we need to convert + if (!process.env.UNITY_LICENSE) { + throw new Error(`Missing Unity License File and no Serial was found. If this is a personal license, make sure to follow the activation steps and set the UNITY_LICENSE GitHub secret or enter a Unity serial number inside the UNITY_SERIAL GitHub secret.`); + } + unitySerial = this.getSerialFromLicenseFile(process.env.UNITY_LICENSE); + } else { + unitySerial = process.env.UNITY_SERIAL!; } - unitySerial = this.getSerialFromLicenseFile(process.env.UNITY_LICENSE); - } else { - unitySerial = process.env.UNITY_SERIAL!; } return { editorVersion, customImage: Input.customImage, unitySerial, - + unityLicensingServer: Input.unityLicensingServer, runnerTempPath: process.env.RUNNER_TEMP, targetPlatform: Input.targetPlatform, projectPath: Input.projectPath, diff --git a/src/model/cloud-runner/cloud-runner.test.ts b/src/model/cloud-runner/cloud-runner.test.ts index b8e51ed3..4f7dc265 100644 --- a/src/model/cloud-runner/cloud-runner.test.ts +++ b/src/model/cloud-runner/cloud-runner.test.ts @@ -91,6 +91,7 @@ describe('Cloud Runner', () => { delete Cli.options; }, 1000000); } + it('Local cloud runner returns commands', async () => { // Build parameters Cli.options = { @@ -119,6 +120,7 @@ describe('Cloud Runner', () => { Input.githubInputEnabled = true; delete Cli.options; }, 1000000); + it('Test cloud runner returns commands', async () => { // Build parameters Cli.options = { diff --git a/src/model/docker.ts b/src/model/docker.ts index 497b3f05..ef991357 100644 --- a/src/model/docker.ts +++ b/src/model/docker.ts @@ -38,6 +38,7 @@ class Docker { --volume "${actionFolder}/default-build-script:/UnityBuilderAction:z" \ --volume "${actionFolder}/platforms/ubuntu/steps:/steps:z" \ --volume "${actionFolder}/platforms/ubuntu/entrypoint.sh:/entrypoint.sh:z" \ + --volume "${actionFolder}/unity-config:/usr/share/unity3d/config/:z" \ ${sshAgent ? `--volume ${sshAgent}:/ssh-agent` : ''} \ ${sshAgent ? '--volume /home/runner/.ssh/known_hosts:/root/.ssh/known_hosts:ro' : ''} \ ${image} \ diff --git a/src/model/image-environment-factory.ts b/src/model/image-environment-factory.ts index 15513fda..581b7bff 100644 --- a/src/model/image-environment-factory.ts +++ b/src/model/image-environment-factory.ts @@ -31,6 +31,7 @@ class ImageEnvironmentFactory { { name: 'UNITY_EMAIL', value: process.env.UNITY_EMAIL }, { name: 'UNITY_PASSWORD', value: process.env.UNITY_PASSWORD }, { name: 'UNITY_SERIAL', value: parameters.unitySerial }, + { name: 'UNITY_LICENSING_SERVER', value: parameters.unityLicensingServer }, { name: 'UNITY_VERSION', value: parameters.editorVersion }, { name: 'USYM_UPLOAD_AUTH_TOKEN', value: process.env.USYM_UPLOAD_AUTH_TOKEN }, { name: 'PROJECT_PATH', value: parameters.projectPath }, diff --git a/src/model/input-readers/git-repo.test.ts b/src/model/input-readers/git-repo.test.ts index 1e96db60..11d05dd9 100644 --- a/src/model/input-readers/git-repo.test.ts +++ b/src/model/input-readers/git-repo.test.ts @@ -1,8 +1,24 @@ import { GitRepoReader } from './git-repo'; +import { CloudRunnerSystem } from '../cloud-runner/services/cloud-runner-system'; +import Input from '../input'; describe(`git repo tests`, () => { it(`Branch value parsed from CLI to not contain illegal characters`, async () => { expect(await GitRepoReader.GetBranch()).not.toContain(`\n`); expect(await GitRepoReader.GetBranch()).not.toContain(` `); }); + + it(`returns valid branch name when using https`, async () => { + const mockValue = 'https://github.com/example/example.git'; + await jest.spyOn(CloudRunnerSystem, 'Run').mockReturnValue(Promise.resolve(mockValue)); + await jest.spyOn(Input, 'cloudRunnerCluster', 'get').mockReturnValue('not-local'); + expect(await GitRepoReader.GetRemote()).toEqual(`example/example`); + }); + + it(`returns valid branch name when using ssh`, async () => { + const mockValue = 'git@github.com:example/example.git'; + await jest.spyOn(CloudRunnerSystem, 'Run').mockReturnValue(Promise.resolve(mockValue)); + await jest.spyOn(Input, 'cloudRunnerCluster', 'get').mockReturnValue('not-local'); + expect(await GitRepoReader.GetRemote()).toEqual(`example/example`); + }); }); diff --git a/src/model/input-readers/git-repo.ts b/src/model/input-readers/git-repo.ts index 3372089e..1db63010 100644 --- a/src/model/input-readers/git-repo.ts +++ b/src/model/input-readers/git-repo.ts @@ -14,7 +14,7 @@ export class GitRepoReader { CloudRunnerLogger.log(`value ${value}`); assert(value.includes('github.com')); - return value.split('github.com/')[1].split('.git')[0]; + return value.split('github.com')[1].split('.git')[0].slice(1); } public static async GetBranch() { diff --git a/src/model/input.ts b/src/model/input.ts index d3bb8116..6589f4d2 100644 --- a/src/model/input.ts +++ b/src/model/input.ts @@ -117,6 +117,10 @@ class Input { return Input.getInput('buildsPath') || 'build'; } + static get unityLicensingServer() { + return Input.getInput('unityLicensingServer') || ''; + } + static get buildMethod() { return Input.getInput('buildMethod') || ''; // Processed in docker file } diff --git a/src/model/platform-setup.ts b/src/model/platform-setup.ts index 7ef9d938..ced5b753 100644 --- a/src/model/platform-setup.ts +++ b/src/model/platform-setup.ts @@ -1,9 +1,13 @@ +import fs from 'fs'; +import * as core from '@actions/core'; import { BuildParameters } from '.'; import { SetupMac, SetupWindows } from './platform-setup/'; import ValidateWindows from './platform-validation/validate-windows'; class PlatformSetup { static async setup(buildParameters: BuildParameters, actionFolder: string) { + PlatformSetup.SetupShared(buildParameters, actionFolder); + switch (process.platform) { case 'win32': ValidateWindows.validate(buildParameters); @@ -16,6 +20,20 @@ class PlatformSetup { // Add other baseOS's here } } + + private static SetupShared(buildParameters: BuildParameters, actionFolder: string) { + const servicesConfigPath = `${actionFolder}/unity-config/services-config.json`; + const servicesConfigPathTemplate = `${servicesConfigPath}.template`; + if (!fs.existsSync(servicesConfigPathTemplate)) { + core.error(`Missing services config ${servicesConfigPathTemplate}`); + + return; + } + + let servicesConfig = fs.readFileSync(servicesConfigPathTemplate).toString(); + servicesConfig = servicesConfig.replace('%URL%', buildParameters.unityLicensingServer); + fs.writeFileSync(servicesConfigPath, servicesConfig); + } } export default PlatformSetup; diff --git a/src/model/platform-setup/setup-mac.ts b/src/model/platform-setup/setup-mac.ts index beb55195..10f7cfd3 100644 --- a/src/model/platform-setup/setup-mac.ts +++ b/src/model/platform-setup/setup-mac.ts @@ -52,6 +52,7 @@ class SetupMac { process.env.ACTION_FOLDER = actionFolder; process.env.UNITY_VERSION = buildParameters.editorVersion; process.env.UNITY_SERIAL = buildParameters.unitySerial; + process.env.UNITY_LICENSING_SERVER = buildParameters.unityLicensingServer; process.env.PROJECT_PATH = buildParameters.projectPath; process.env.BUILD_TARGET = buildParameters.targetPlatform; process.env.BUILD_NAME = buildParameters.buildName;