Unity builder v4 Parity Update (#246)

- Updated activation strategy to align with builder strategy for more stable activation with retry logic
- Added `containerRegistryImageVersion` and `containerRegistryRepository` for improved custom image support
- Added `runAsHostUser` to fix self-hosted runner issues
- Scripts folder cleanup
- Added error/warning annotations for activation issues
- Improved serial masking
- Image environment factory addition and docker refactoring
This commit is contained in:
Andrew Kahr
2023-12-12 22:12:36 -08:00
committed by GitHub
parent c80d9094f5
commit ce074787fb
56 changed files with 3044 additions and 599 deletions

View File

@@ -27,8 +27,17 @@ export async function run() {
dockerMemoryLimit,
dockerIsolationMode,
unityLicensingServer,
runAsHostUser,
containerRegistryRepository,
containerRegistryImageVersion,
unitySerial,
} = Input.getFromUser();
const baseImage = new ImageTag({ editorVersion, customImage });
const baseImage = new ImageTag({
editorVersion,
customImage,
containerRegistryRepository,
containerRegistryImageVersion,
});
const runnerContext = Action.runnerContext();
try {
@@ -53,6 +62,8 @@ export async function run() {
dockerMemoryLimit,
dockerIsolationMode,
unityLicensingServer,
runAsHostUser,
unitySerial,
...runnerContext,
});
} finally {

View File

@@ -1,3 +1,4 @@
import ImageEnvironmentFactory from './image-environment-factory';
import { existsSync, mkdirSync, readFileSync, rmSync } from 'fs';
import LicensingServerSetup from './licensing-server-setup';
import type { RunnerContext } from './action';
@@ -53,25 +54,15 @@ const Docker = {
getLinuxCommand(image, parameters): string {
const {
actionFolder,
editorVersion,
workspace,
projectPath,
customParameters,
testMode,
coverageOptions,
artifactsPath,
useHostNetwork,
sshAgent,
sshPublicKeysDirectoryPath,
packageMode,
packageName,
gitPrivateToken,
githubToken,
runnerTemporaryPath,
chownFilesTo,
dockerCpuLimit,
dockerMemoryLimit,
unityLicensingServer,
} = parameters;
const githubHome = path.join(runnerTemporaryPath, '_github_home');
@@ -84,91 +75,52 @@ const Docker = {
).join(';');
return `docker run \
--workdir /github/workspace \
--cidfile "${cidfile}" \
--rm \
--env UNITY_LICENSE \
--env UNITY_LICENSE_FILE \
--env UNITY_EMAIL \
--env UNITY_PASSWORD \
--env UNITY_SERIAL \
--env UNITY_LICENSING_SERVER="${unityLicensingServer}" \
--env UNITY_VERSION="${editorVersion}" \
--env PROJECT_PATH="${projectPath}" \
--env CUSTOM_PARAMETERS="${customParameters}" \
--env TEST_PLATFORMS="${testPlatforms}" \
--env COVERAGE_OPTIONS="${coverageOptions}" \
--env COVERAGE_RESULTS_PATH="CodeCoverage" \
--env ARTIFACTS_PATH="${artifactsPath}" \
--env PACKAGE_MODE="${packageMode}" \
--env PACKAGE_NAME="${packageName}" \
--env GITHUB_REF \
--env GITHUB_SHA \
--env GITHUB_REPOSITORY \
--env GITHUB_ACTOR \
--env GITHUB_WORKFLOW \
--env GITHUB_HEAD_REF \
--env GITHUB_BASE_REF \
--env GITHUB_EVENT_NAME \
--env GITHUB_WORKSPACE="/github/workspace" \
--env GITHUB_ACTION \
--env GITHUB_EVENT_PATH \
--env RUNNER_OS \
--env RUNNER_TOOL_CACHE \
--env RUNNER_TEMP \
--env RUNNER_WORKSPACE \
--env GIT_PRIVATE_TOKEN="${gitPrivateToken}" \
--env CHOWN_FILES_TO="${chownFilesTo}" \
--env GIT_CONFIG_EXTENSIONS \
${sshAgent ? '--env SSH_AUTH_SOCK=/ssh-agent' : ''} \
--volume "${githubHome}:/root:z" \
--volume "${githubWorkflow}:/github/workflow:z" \
--volume "${workspace}:/github/workspace:z" \
--volume "${actionFolder}/test-standalone-scripts:/UnityStandaloneScripts:z" \
--volume "${actionFolder}/steps:/steps:z" \
--volume "${actionFolder}/entrypoint.sh:/entrypoint.sh:z" \
--volume "${actionFolder}/unity-config:/usr/share/unity3d/config/:z" \
--cpus=${dockerCpuLimit} \
--memory=${dockerMemoryLimit} \
${sshAgent ? `--volume ${sshAgent}:/ssh-agent` : ''} \
${
sshAgent && !sshPublicKeysDirectoryPath
? `--volume /home/runner/.ssh/known_hosts:/root/.ssh/known_hosts:ro`
: ''
} \
${
sshPublicKeysDirectoryPath
? `--volume ${sshPublicKeysDirectoryPath}:/root/.ssh:ro`
: ''
} \
${useHostNetwork ? '--net=host' : ''} \
${githubToken ? '--env USE_EXIT_CODE=false' : '--env USE_EXIT_CODE=true'} \
${image} \
/bin/bash -c /entrypoint.sh`;
--workdir /github/workspace \
--cidfile "${cidfile}" \
--rm \
${ImageEnvironmentFactory.getEnvVarString(parameters)} \
--env GIT_CONFIG_EXTENSIONS \
--env TEST_PLATFORMS="${testPlatforms}" \
--env GITHUB_WORKSPACE="/github/workspace" \
${sshAgent ? '--env SSH_AUTH_SOCK=/ssh-agent' : ''} \
--volume "${githubHome}:/root:z" \
--volume "${githubWorkflow}:/github/workflow:z" \
--volume "${workspace}:/github/workspace:z" \
--volume "${actionFolder}/test-standalone-scripts:/UnityStandaloneScripts:z" \
--volume "${actionFolder}/platforms/ubuntu:/steps:z" \
--volume "${actionFolder}/unity-config:/usr/share/unity3d/config/:z" \
--volume "${actionFolder}/BlankProject":"/BlankProject:z" \
--cpus=${dockerCpuLimit} \
--memory=${dockerMemoryLimit} \
${sshAgent ? `--volume ${sshAgent}:/ssh-agent` : ''} \
${
sshAgent && !sshPublicKeysDirectoryPath
? `--volume /home/runner/.ssh/known_hosts:/root/.ssh/known_hosts:ro`
: ''
} \
${
sshPublicKeysDirectoryPath
? `--volume ${sshPublicKeysDirectoryPath}:/root/.ssh:ro`
: ''
} \
${useHostNetwork ? '--net=host' : ''} \
${githubToken ? '--env USE_EXIT_CODE=false' : '--env USE_EXIT_CODE=true'} \
${image} \
/bin/bash -c "/steps/entrypoint.sh`;
},
getWindowsCommand(image, parameters): string {
const {
actionFolder,
editorVersion,
workspace,
projectPath,
customParameters,
testMode,
coverageOptions,
artifactsPath,
useHostNetwork,
sshAgent,
packageMode,
packageName,
gitPrivateToken,
githubToken,
runnerTemporaryPath,
chownFilesTo,
dockerCpuLimit,
dockerMemoryLimit,
dockerIsolationMode,
unityLicensingServer,
} = parameters;
const githubHome = path.join(runnerTemporaryPath, '_github_home');
@@ -181,48 +133,19 @@ const Docker = {
).join(';');
return `docker run \
--workdir /github/workspace \
--workdir c:/github/workspace \
--cidfile "${cidfile}" \
--rm \
--env UNITY_LICENSE \
--env UNITY_LICENSE_FILE \
--env UNITY_EMAIL \
--env UNITY_PASSWORD \
--env UNITY_SERIAL \
--env UNITY_LICENSING_SERVER="${unityLicensingServer}" \
--env UNITY_VERSION="${editorVersion}" \
--env PROJECT_PATH="${projectPath}" \
--env CUSTOM_PARAMETERS="${customParameters}" \
${ImageEnvironmentFactory.getEnvVarString(parameters)} \
--env TEST_PLATFORMS="${testPlatforms}" \
--env COVERAGE_OPTIONS="${coverageOptions}" \
--env COVERAGE_RESULTS_PATH="CodeCoverage" \
--env ARTIFACTS_PATH="${artifactsPath}" \
--env PACKAGE_MODE="${packageMode}" \
--env PACKAGE_NAME="${packageName}" \
--env GITHUB_REF \
--env GITHUB_SHA \
--env GITHUB_REPOSITORY \
--env GITHUB_ACTOR \
--env GITHUB_WORKFLOW \
--env GITHUB_HEAD_REF \
--env GITHUB_BASE_REF \
--env GITHUB_EVENT_NAME \
--env GITHUB_WORKSPACE="/github/workspace" \
--env GITHUB_ACTION \
--env GITHUB_EVENT_PATH \
--env RUNNER_OS \
--env RUNNER_TOOL_CACHE \
--env RUNNER_TEMP \
--env RUNNER_WORKSPACE \
--env GIT_PRIVATE_TOKEN="${gitPrivateToken}" \
--env CHOWN_FILES_TO="${chownFilesTo}" \
--env GITHUB_WORKSPACE="c:/github/workspace" \
${sshAgent ? '--env SSH_AUTH_SOCK=c:/ssh-agent' : ''} \
--volume "${actionFolder}/test-standalone-scripts":"c:/UnityStandaloneScripts" \
--volume "${githubHome}":"c:/root" \
--volume "${githubWorkflow}":"c:/github/workflow" \
--volume "${workspace}":"c:/github/workspace" \
--volume "${actionFolder}/steps":"c:/steps" \
--volume "${actionFolder}":"c:/dist" \
--volume "${actionFolder}/platforms/windows":"c:/steps" \
--volume "${actionFolder}/BlankProject":"c:/BlankProject" \
${sshAgent ? `--volume ${sshAgent}:c:/ssh-agent` : ''} \
${
sshAgent
@@ -235,7 +158,7 @@ const Docker = {
${useHostNetwork ? '--net=host' : ''} \
${githubToken ? '--env USE_EXIT_CODE=false' : '--env USE_EXIT_CODE=true'} \
${image} \
powershell c:/dist/entrypoint.ps1`;
powershell c:/steps/entrypoint.ps1`;
},
};

View File

@@ -0,0 +1,80 @@
class ImageEnvironmentFactory {
public static getEnvVarString(parameters) {
const environmentVariables = ImageEnvironmentFactory.getEnvironmentVariables(parameters);
let string = '';
for (const p of environmentVariables) {
if (p.value === '' || p.value === undefined) {
continue;
}
if (p.name !== 'ANDROID_KEYSTORE_BASE64' && p.value.toString().includes(`\n`)) {
string += `--env ${p.name} `;
process.env[p.name] = p.value.toString();
continue;
}
string += `--env ${p.name}="${p.value}" `;
}
return string;
}
public static getEnvironmentVariables(parameters) {
let environmentVariables = [
{ 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 },
{ name: 'COVERAGE_OPTIONS', value: parameters.coverageOptions },
{ name: 'COVERAGE_RESULTS_PATH', value: 'CodeCoverage' },
{ name: 'ARTIFACTS_PATH', value: parameters.artifactsPath },
{ name: 'PACKAGE_MODE', value: parameters.packageMode },
{ name: 'PACKAGE_NAME', value: parameters.packageName },
{ name: 'GIT_PRIVATE_TOKEN', value: parameters.gitPrivateToken },
{ name: 'VERSION', value: parameters.buildVersion },
{ name: 'CUSTOM_PARAMETERS', value: parameters.customParameters },
{ name: 'RUN_AS_HOST_USER', value: parameters.runAsHostUser },
{ name: 'CHOWN_FILES_TO', value: parameters.chownFilesTo },
{ name: 'GITHUB_REF', value: process.env.GITHUB_REF },
{ name: 'GITHUB_SHA', value: process.env.GITHUB_SHA },
{ name: 'GITHUB_REPOSITORY', value: process.env.GITHUB_REPOSITORY },
{ name: 'GITHUB_ACTOR', value: process.env.GITHUB_ACTOR },
{ name: 'GITHUB_WORKFLOW', value: process.env.GITHUB_WORKFLOW },
{ name: 'GITHUB_HEAD_REF', value: process.env.GITHUB_HEAD_REF },
{ name: 'GITHUB_BASE_REF', value: process.env.GITHUB_BASE_REF },
{ name: 'GITHUB_EVENT_NAME', value: process.env.GITHUB_EVENT_NAME },
{ name: 'GITHUB_ACTION', value: process.env.GITHUB_ACTION },
{ name: 'GITHUB_EVENT_PATH', value: process.env.GITHUB_EVENT_PATH },
{ name: 'RUNNER_OS', value: process.env.RUNNER_OS },
{ name: 'RUNNER_TOOL_CACHE', value: process.env.RUNNER_TOOL_CACHE },
{ name: 'RUNNER_TEMP', value: process.env.RUNNER_TEMP },
{ name: 'RUNNER_WORKSPACE', value: process.env.RUNNER_WORKSPACE },
];
for (const variable of environmentVariables) {
if (
environmentVariables.some(
x => variable !== undefined && variable.name !== undefined && x.name === variable.name,
) === undefined
) {
environmentVariables = environmentVariables.filter(x => x !== variable);
}
}
if (parameters.sshAgent) {
environmentVariables.push({ name: 'SSH_AUTH_SOCK', value: '/ssh-agent' });
}
return environmentVariables;
}
}
export default ImageEnvironmentFactory;

View File

@@ -1,17 +1,15 @@
import ImageTag from './image-tag';
jest.spyOn(ImageTag, 'getImagePlatformPrefix').mockReturnValue('ubuntu');
describe('ImageTag', () => {
const some = {
editorVersion: '2099.9.f9f9',
targetPlatform: 'Test',
builderPlatform: '',
containerRegistryRepository: 'unityci/editor',
containerRegistryImageVersion: '3',
};
const defaults = {
repository: 'unityci',
name: 'editor',
image: 'unityci/editor',
};
@@ -24,8 +22,7 @@ describe('ImageTag', () => {
it('accepts parameters and sets the right properties', () => {
const image = new ImageTag(some);
expect(image.repository).toStrictEqual('unityci');
expect(image.name).toStrictEqual('editor');
expect(image.repository).toStrictEqual('unityci/editor');
expect(image.editorVersion).toStrictEqual(some.editorVersion);
expect(image.targetPlatform).toStrictEqual(some.targetPlatform);
expect(image.targetPlatformSuffix).toStrictEqual(some.builderPlatform);
@@ -48,30 +45,65 @@ describe('ImageTag', () => {
const image = new ImageTag({
editorVersion: '2099.1.1111',
targetPlatform: some.targetPlatform,
containerRegistryRepository: 'unityci/editor',
containerRegistryImageVersion: '3',
});
expect(image.toString()).toStrictEqual(`${defaults.image}:ubuntu-2099.1.1111-3`);
switch (process.platform) {
case 'win32':
expect(image.toString()).toStrictEqual(`${defaults.image}:windows-2099.1.1111-3`);
break;
case 'linux':
expect(image.toString()).toStrictEqual(`${defaults.image}:ubuntu-2099.1.1111-3`);
break;
}
});
it('returns customImage if given', () => {
const image = new ImageTag({
editorVersion: '2099.1.1111',
targetPlatform: some.targetPlatform,
customImage: `${defaults.image}:2099.1.1111@347598437689743986`,
containerRegistryRepository: 'unityci/editor',
containerRegistryImageVersion: '3',
});
expect(image.toString()).toStrictEqual(image.customImage);
});
it('returns the specific build platform', () => {
const image = new ImageTag({ editorVersion: '2022.3.7f1', targetPlatform: 'WebGL' });
const image = new ImageTag({
editorVersion: '2022.3.7f1',
targetPlatform: 'WebGL',
containerRegistryRepository: 'unityci/editor',
containerRegistryImageVersion: '3',
});
expect(image.toString()).toStrictEqual(`${defaults.image}:ubuntu-2022.3.7f1-webgl-3`);
switch (process.platform) {
case 'win32':
expect(image.toString()).toStrictEqual(`${defaults.image}:windows-2022.3.7f1-webgl-3`);
break;
case 'linux':
expect(image.toString()).toStrictEqual(`${defaults.image}:ubuntu-2022.3.7f1-webgl-3`);
break;
}
});
it('returns no specific build platform for generic targetPlatforms', () => {
const image = new ImageTag({ targetPlatform: 'NoTarget' });
const image = new ImageTag({
editorVersion: '2019.2.11f1',
targetPlatform: 'NoTarget',
containerRegistryRepository: 'unityci/editor',
containerRegistryImageVersion: '3',
});
expect(image.toString()).toStrictEqual(`${defaults.image}:ubuntu-2022.3.7f1-3`);
switch (process.platform) {
case 'win32':
expect(image.toString()).toStrictEqual(`${defaults.image}:windows-2019.2.11f1-3`);
break;
case 'linux':
expect(image.toString()).toStrictEqual(`${defaults.image}:ubuntu-2019.2.11f1-3`);
break;
}
});
});
});

View File

@@ -3,7 +3,6 @@ import Platform from './platform';
class ImageTag {
public customImage?: string;
public repository: string;
public name: string;
public editorVersion: string;
public targetPlatform: string;
public targetPlatformSuffix: string;
@@ -15,6 +14,8 @@ class ImageTag {
editorVersion = '2022.3.7f1',
targetPlatform = ImageTag.getImagePlatformType(process.platform),
customImage,
containerRegistryRepository,
containerRegistryImageVersion,
} = imageProperties;
if (!ImageTag.versionPattern.test(editorVersion)) {
@@ -25,13 +26,12 @@ class ImageTag {
this.customImage = customImage;
// Or
this.repository = 'unityci';
this.name = 'editor';
this.repository = containerRegistryRepository;
this.editorVersion = editorVersion;
this.targetPlatform = targetPlatform;
this.targetPlatformSuffix = ImageTag.getTargetPlatformSuffix(targetPlatform, editorVersion);
this.imagePlatformPrefix = ImageTag.getImagePlatformPrefix(process.platform);
this.imageRollingVersion = 3;
this.imageRollingVersion = Number(containerRegistryImageVersion);
}
static get versionPattern() {
@@ -146,7 +146,7 @@ class ImageTag {
}
get image() {
return `${this.repository}/${this.name}`.replace(/^\/+/, '');
return `${this.repository}`.replace(/^\/+/, '');
}
toString() {

View File

@@ -7,6 +7,19 @@ const mockedFsExistsSync = jest.spyOn(fs, 'existsSync');
const mockedFsReadFileSync = jest.spyOn(fs, 'readFileSync');
describe('Input', () => {
let originalEnvironment;
beforeEach(() => {
// Store original process.env
originalEnvironment = process.env;
process.env['UNITY_SERIAL'] = 'F4-1234-1234-1234';
});
afterEach(() => {
// Restore original process.env
process.env = originalEnvironment;
});
describe('getFromUser', () => {
it('does not throw', () => {
expect(() => Input.getFromUser()).not.toThrow();

View File

@@ -2,28 +2,29 @@ import UnityVersionParser from './unity-version-parser';
import fs from 'fs';
import { getInput } from '@actions/core';
import os from 'os';
import * as core from '@actions/core';
const Input = {
get testModes() {
class Input {
static get testModes() {
return ['all', 'playmode', 'editmode', 'standalone'];
},
}
isValidFolderName(folderName) {
static isValidFolderName(folderName) {
const validFolderName = new RegExp(/^(\.|\.\/)?(\.?[\w~]+([ _-]?[\w~]+)*\/?)*$/);
return validFolderName.test(folderName);
},
}
isValidGlobalFolderName(folderName) {
static isValidGlobalFolderName(folderName) {
const validFolderName = new RegExp(/^(\.|\.\/|\/)?(\.?[\w~]+([ _-]?[\w~]+)*\/?)*$/);
return validFolderName.test(folderName);
},
}
/**
* When in package mode, we need to scrape the package's name from its package.json file
*/
getPackageNameFromPackageJson(packagePath) {
static getPackageNameFromPackageJson(packagePath) {
const packageJsonPath = `${packagePath}/package.json`;
if (!fs.existsSync(packageJsonPath)) {
throw new Error(`Invalid projectPath - Cannot find package.json at ${packageJsonPath}`);
@@ -54,25 +55,40 @@ const Input = {
}
return rawPackageName;
},
}
private static getSerialFromLicenseFile(license: string) {
const startKey = `<DeveloperData Value="`;
const endKey = `"/>`;
const startIndex = license.indexOf(startKey) + startKey.length;
if (startIndex < 0) {
throw new Error(`License File was corrupted, unable to locate serial`);
}
const endIndex = license.indexOf(endKey, startIndex);
// Slice off the first 4 characters as they are garbage values
return Buffer.from(license.slice(startIndex, endIndex), 'base64').toString('binary').slice(4);
}
/**
* When in package mode, we need to ensure that the Tests folder is present
*/
verifyTestsFolderIsPresent(packagePath) {
static verifyTestsFolderIsPresent(packagePath) {
if (!fs.existsSync(`${packagePath}/Tests`)) {
throw new Error(
`Invalid projectPath - Cannot find package tests folder at ${packagePath}/Tests`,
);
}
},
}
getFromUser() {
public static getFromUser() {
// Input variables specified in workflow using "with" prop.
const unityVersion = getInput('unityVersion') || 'auto';
const customImage = getInput('customImage') || '';
const rawProjectPath = getInput('projectPath') || '.';
const unityLicensingServer = getInput('unityLicensingServer') || '';
const unityLicense = getInput('unityLicense') || (process.env['UNITY_LICENSE'] ?? '');
let unitySerial = process.env['UNITY_SERIAL'] ?? '';
const customParameters = getInput('customParameters') || '';
const testMode = (getInput('testMode') || 'all').toLowerCase();
const coverageOptions = getInput('coverageOptions') || '';
@@ -105,6 +121,10 @@ const Input = {
`${Math.floor((os.totalmem() / bytesInMegabyte) * memoryMultiplier)}m`;
const dockerIsolationMode = getInput('dockerIsolationMode') || 'default';
const runAsHostUser = getInput('runAsHostUser') || 'false';
const containerRegistryRepository = getInput('containerRegistryRepository') || 'unityci/editor';
const containerRegistryImageVersion = getInput('containerRegistryImageVersion') || '3';
// Validate input
if (!this.testModes.includes(testMode)) {
throw new Error(`Invalid testMode ${testMode}`);
@@ -153,6 +173,28 @@ const Input = {
this.verifyTestsFolderIsPresent(projectPath);
}
if (runAsHostUser !== 'true' && runAsHostUser !== 'false') {
throw new Error(`Invalid runAsHostUser "${runAsHostUser}"`);
}
if (unityLicensingServer === '' && !unitySerial) {
// No serial was present, so it is a personal license that we need to convert
if (!unityLicense) {
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(unityLicense);
}
if (unitySerial !== undefined && unitySerial.length === 27) {
core.setSecret(unitySerial);
core.setSecret(`${unitySerial.slice(0, -4)}XXXX`);
}
// Sanitise other input
const artifactsPath = rawArtifactsPath.replace(/\/$/, '');
const sshPublicKeysDirectoryPath = rawSshPublicKeysDirectoryPath.replace(/\/$/, '');
@@ -182,8 +224,12 @@ const Input = {
dockerMemoryLimit,
dockerIsolationMode,
unityLicensingServer,
runAsHostUser,
containerRegistryRepository,
containerRegistryImageVersion,
unitySerial,
};
},
};
}
}
export default Input;