Skip to content
This repository was archived by the owner on Jan 15, 2026. It is now read-only.

Commit e82a94b

Browse files
committed
Add personal license support
1 parent fc243c6 commit e82a94b

7 files changed

Lines changed: 249 additions & 45 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: test personal license
2+
on:
3+
push:
4+
branches:
5+
- "**"
6+
paths-ignore:
7+
- "**.md"
8+
jobs:
9+
test:
10+
runs-on: ${{ matrix.os }}
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
os:
15+
- ubuntu-18.04
16+
- ubuntu-20.04
17+
- macos-10.15
18+
- macos-11.0
19+
- windows-2019
20+
unity-version:
21+
- 2018.4.1f1
22+
- 2019.4.1f1
23+
- 2020.1.0f1
24+
steps:
25+
- name: Checkout code
26+
uses: actions/checkout@v2
27+
- name: Setup unity
28+
uses: kuler90/setup-unity@v1
29+
with:
30+
unity-version: ${{ matrix.unity-version }}
31+
- name: Activate unity
32+
uses: ./
33+
with:
34+
unity-username: ${{ secrets.UNITY_USERNAME }}
35+
unity-password: ${{ secrets.UNITY_PASSWORD }}
36+
unity-authenticator-key: ${{ secrets.UNITY_AUTHENTICATOR_KEY }}

README.md

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# activate-unity
22

3-
GitHub Action to activate Unity license. License will be automatically returned when the job finishes.
3+
<p align="left">
4+
<a href="https://github.com/kuler90/activate-unity/actions"><img alt="GitHub Actions status" src="https://github.com/kuler90/activate-unity/workflows/test%20personal%20license/badge.svg?branch=master"></a>
5+
</p>
6+
7+
GitHub Action to activate personal or professional Unity license. License will be automatically returned at the end of a job.
48

59
Works on Linux, macOS and Windows.
610

@@ -12,15 +16,27 @@ Path to Unity executable. `UNITY_PATH` env will be used if not provided.
1216

1317
### `unity-username`
1418

15-
Unity activation username.
19+
**Required** Unity account username.
1620

1721
### `unity-password`
1822

19-
Unity activation password.
23+
**Required** Unity account password.
24+
25+
### `unity-authenticator-key`
26+
27+
Unity account [authenticator key](#How-to-obtain-authenticator-key) for Authenticator App (Two Factor Authentication). Used for account verification during Personal license activation.
2028

2129
### `unity-serial`
2230

23-
Unity activation serial key.
31+
Unity license serial key. Used for Plus/Professional license activation.
32+
33+
## How to obtain authenticator key
34+
35+
1. Login to Unity account
36+
2. Go to account settings
37+
3. Activate Two Factor Authentication through Authenticator App
38+
4. On page with QR code click "Can't scan the barcode?" and save key
39+
5. Finish activation
2440

2541
## Example usage
2642

@@ -30,11 +46,19 @@ Unity activation serial key.
3046

3147
- name: Setup Unity
3248
uses: kuler90/setup-unity@v1
49+
with:
50+
unity-modules: android
3351

3452
- name: Activate Unity
3553
uses: kuler90/activate-unity@v1
3654
with:
3755
unity-username: ${{ secrets.UNITY_USERNAME }}
3856
unity-password: ${{ secrets.UNITY_PASSWORD }}
39-
unity-serial: ${{ secrets.UNITY_SERIAL }}
57+
unity-authenticator-key: ${{ secrets.UNITY_AUTHENTICATOR_KEY }}
58+
59+
- name: Build Unity
60+
uses: kuler90/build-unity@v1
61+
with:
62+
build-target: Android
63+
build-path: ./build.apk
4064
```

action.yml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
name: Activate Unity
2-
description: Activate Unity license and return it after job finish
2+
description: Activate Unity license and return it at the end of a job
33
inputs:
44
unity-path:
55
description: Path to Unity executable. UNITY_PATH env will be used if not provided
66
required: false
77
unity-username:
8-
description: Unity activation username
8+
description: Unity account username
99
required: false
1010
unity-password:
11-
description: Unity activation password
11+
description: Unity account password
12+
required: false
13+
unity-authenticator-key:
14+
description: Unity account authenticator key for Authenticator App (Two Factor Authentication). Used for account verification during Personal license activation
1215
required: false
1316
unity-serial:
14-
description: Unity activation serial key
17+
description: Unity license serial key. Used for Plus/Professional license activation
1518
required: false
1619
runs:
1720
using: node12
1821
main: src/activate-license.js
1922
post: src/return-license.js
23+
post-if: inputs.unity-serial != ''
2024
branding:
2125
icon: unlock
2226
color: gray-dark

src/activate-license.js

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
const core = require('@actions/core');
2+
const exec = require('@actions/exec');
3+
const path = require('path');
24
const unity = require('./unity');
35

46
async function run() {
@@ -7,17 +9,19 @@ async function run() {
79
if (!unityPath) {
810
throw new Error('unity path not found');
911
}
10-
const unityUsername = core.getInput('unity-username');
11-
const unityPassword = core.getInput('unity-password');
12+
const unityUsername = core.getInput('unity-username', { required: true });
13+
const unityPassword = core.getInput('unity-password', { required: true });
14+
const unityAuthenticatorKey = core.getInput('unity-authenticator-key');
1215
const unitySerial = core.getInput('unity-serial');
13-
const unityManualLicense = core.getInput('unity-manual-license');
1416

15-
if (unityUsername && unityPassword && unitySerial) {
16-
await unity.activateLicense(unityPath, unityUsername, unityPassword, unitySerial);
17-
} else if (unityManualLicense) {
18-
await unity.activateManualLicense(unityPath, unityManualLicense);
17+
if (unitySerial) {
18+
await unity.activateSerialLicense(unityPath, unityUsername, unityPassword, unitySerial);
1919
} else {
20-
throw new Error('Empty (unity-username and unity-password and unity-serial) or unity-manual-license inputs');
20+
await exec.exec('npm install puppeteer@"^5.x"', [], { cwd: path.join(__dirname, '..') }); // install puppeteer for current platform
21+
const licenseRobot = require('./license-robot');
22+
const licenseRequestFile = await unity.createManualActivationFile(unityPath);
23+
const licenseData = await licenseRobot.getPersonalLicense(licenseRequestFile, unityUsername, unityPassword, unityAuthenticatorKey);
24+
await unity.activateManualLicense(unityPath, licenseData);
2125
}
2226
} catch (error) {
2327
core.setFailed(error.message);

src/license-robot.js

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
const puppeteer = require('puppeteer');
2+
const otplib = require('otplib');
3+
const fs = require('fs');
4+
5+
module.exports = { getPersonalLicense };
6+
7+
let RETRY_COUNT = 2;
8+
let BROWSER_HEADLESS = true;
9+
10+
async function getPersonalLicense(licenseRequestFile, username, password, authenticatorKey) {
11+
const licenseRequestData = fs.readFileSync(licenseRequestFile, 'utf8');
12+
const licenseData = await retry(() => browser_getPersonalLicense(licenseRequestData, username, password, authenticatorKey), RETRY_COUNT);
13+
return licenseData;
14+
}
15+
16+
async function browser_getPersonalLicense(licenseRequestData, username, password, authenticatorKey) {
17+
const browser = await puppeteer.launch({ headless: BROWSER_HEADLESS });
18+
try {
19+
const page = await browser.newPage();
20+
await page.goto('https://license.unity3d.com/manual');
21+
await licensePage_login(page, username, password, authenticatorKey);
22+
try {
23+
await licensePage_attachFileData(page, licenseRequestData);
24+
} catch {
25+
// https://forum.unity.com/threads/i-cant-create-a-unity-license.1001648/
26+
await licensePage_attachFileData(page, convertToUtf16(licenseRequestData));
27+
}
28+
await licensePage_selectType(page);
29+
const licenseData = await licensePage_downloadLicense(page);
30+
return licenseData;
31+
} finally {
32+
await browser.close();
33+
}
34+
}
35+
36+
/**
37+
* @param {import("puppeteer").Page} page
38+
*/
39+
async function licensePage_login(page, username, password, authenticatorKey) {
40+
await page.waitForSelector('#conversations_create_session_form_email');
41+
await page.type('#conversations_create_session_form_email', username);
42+
await page.type('#conversations_create_session_form_password', password);
43+
await page.click('input[name=commit]');
44+
await page.waitForNavigation();
45+
await page.waitForTimeout(1000);
46+
47+
const verifyCodeInput = await page.$('#conversations_tfa_required_form_verify_code');
48+
if (verifyCodeInput) {
49+
if (!authenticatorKey) {
50+
throw new Error('account verification requested but authenticator key is not provided');
51+
}
52+
const otpTimeRemaining = otplib.authenticator.timeRemaining();
53+
if (otpTimeRemaining < 5) {
54+
await page.waitForTimeout((otpTimeRemaining + 2) * 1000); // wait for new code
55+
}
56+
const otpCode = otplib.authenticator.generate(authenticatorKey.replace(/ /g, ''));
57+
await verifyCodeInput.type(otpCode);
58+
await page.click('input[name="conversations_tfa_required_form[submit_verify_code]"]');
59+
await page.waitForNavigation();
60+
await page.waitForTimeout(1000);
61+
}
62+
}
63+
64+
/**
65+
* @param {import("puppeteer").Page} page
66+
*/
67+
async function licensePage_attachFileData(page, licenseRequestData) {
68+
await page.waitForTimeout(2000);
69+
await page.setRequestInterception(true);
70+
page.once("request", interceptedRequest => {
71+
interceptedRequest.continue({
72+
method: "POST",
73+
postData: licenseRequestData,
74+
headers: { "Content-Type": "text/xml" }
75+
});
76+
});
77+
const response = await page.goto('https://license.unity3d.com/genesis/activation/create-transaction');
78+
if (!response.ok()) {
79+
console.log(await response.text());
80+
throw new Error(response.statusText());
81+
}
82+
}
83+
84+
/**
85+
* @param {import("puppeteer").Page} page
86+
*/
87+
async function licensePage_selectType(page) {
88+
await page.waitForTimeout(1000);
89+
page.once("request", interceptedRequest => {
90+
interceptedRequest.continue({
91+
method: "PUT",
92+
postData: JSON.stringify({ transaction: { serial: { type: "personal" } } }),
93+
headers: { "Content-Type": "application/json" }
94+
});
95+
});
96+
const response = await page.goto('https://license.unity3d.com/genesis/activation/update-transaction');
97+
if (!response.ok()) {
98+
console.log(await response.text());
99+
throw new Error(response.statusText());
100+
}
101+
}
102+
103+
/**
104+
* @param {import("puppeteer").Page} page
105+
*/
106+
async function licensePage_downloadLicense(page) {
107+
await page.waitForTimeout(1000);
108+
page.once("request", interceptedRequest => {
109+
interceptedRequest.continue({
110+
method: "POST",
111+
postData: JSON.stringify({}),
112+
headers: { "Content-Type": "application/json" }
113+
});
114+
});
115+
const response = await page.goto('https://license.unity3d.com/genesis/activation/download-license');
116+
if (response.ok()) {
117+
const json = await response.json();
118+
return json['xml'];
119+
} else {
120+
console.log(await response.text());
121+
throw new Error(response.statusText());
122+
}
123+
}
124+
125+
async function retry(func, retryCount) {
126+
while (true) {
127+
try {
128+
return await func();
129+
} catch (error) {
130+
if (retryCount > 0)
131+
retryCount--;
132+
else
133+
throw error;
134+
}
135+
}
136+
}
137+
138+
function convertToUtf16(str) {
139+
var buf = new ArrayBuffer(str.length * 2);
140+
var bufView = new Uint16Array(buf);
141+
for (var i = 0, strLen = str.length; i < strLen; i++) {
142+
bufView[i] = str.charCodeAt(i);
143+
}
144+
return String.fromCharCode.apply(null, new Uint8Array(buf));
145+
}

src/return-license.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ async function run() {
77
if (!unityPath) {
88
throw new Error('unity path not found');
99
}
10-
unity.returnLicense(unityPath);
10+
await unity.returnLicense(unityPath);
1111
} catch (error) {
1212
core.setFailed(error.message);
1313
}

src/unity.js

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,46 @@
11
const exec = require('@actions/exec');
22
const fs = require('fs');
33

4-
module.exports = { activateLicense, activateManualLicense, returnLicense }
4+
module.exports = { createManualActivationFile, activateManualLicense, activateSerialLicense, returnLicense };
55

6-
async function activateLicense(unityPath, username, password, serial) {
7-
await prepareForActivation();
8-
const stdout = await execute(`${unityCmd(unityPath)} -batchmode -nographics -quit -logFile -projectPath ? -username "${username}" -password "${password}" -serial "${serial}"`, true);
6+
async function activateSerialLicense(unityPath, username, password, serial) {
7+
// use '-projectPath ?' for skipping project indexing
8+
const stdout = await executeUnity(unityPath, `-batchmode -nographics -quit -logFile "-" -projectPath "?" -username "${username}" -password "${password}" -serial "${serial}"`);
99
if (!stdout.includes('Next license update check is after')) {
1010
throw new Error('Activation failed');
1111
}
1212
}
1313

14-
async function activateManualLicense(unityPath, manualLicense) {
15-
fs.writeFileSync('license.ulf', manualLicense);
16-
await prepareForActivation();
17-
const stdout = await execute(`${unityCmd(unityPath)} -batchmode -nographics -quit -logFile -projectPath ? -manualLicenseFile license.ulf`);
14+
async function createManualActivationFile(unityPath) {
15+
await executeUnity(unityPath, '-batchmode -nographics -quit -logFile "-" -createManualActivationFile');
16+
return fs.readdirSync('./').find(path => path.endsWith('.alf'));
17+
}
18+
19+
async function activateManualLicense(unityPath, licenseData) {
20+
fs.writeFileSync('license.ulf', licenseData);
21+
const stdout = await executeUnity(unityPath, `-batchmode -nographics -quit -logFile "-" -manualLicenseFile license.ulf`);
1822
if (!stdout.includes('Next license update check is after')) {
1923
throw new Error('Activation failed');
2024
}
2125
}
2226

2327
async function returnLicense(unityPath) {
24-
await execute(`${unityCmd(unityPath)} -batchmode -nographics -quit -logFile -returnlicense`);
25-
}
26-
27-
async function prepareForActivation() {
28-
if (process.platform === 'darwin') {
29-
await execute('sudo mkdir -p "/Library/Application Support/Unity"');
30-
await execute(`sudo chown -R ${process.env.USER} "/Library/Application Support/Unity"`);
31-
}
28+
await executeUnity(unityPath, '-batchmode -nographics -quit -logFile "-" -returnlicense');
3229
}
3330

34-
function unityCmd(unityPath) {
35-
let unityCmd = '';
31+
async function executeUnity(unityPath, args) {
3632
if (process.platform === 'linux') {
37-
unityCmd = `xvfb-run --auto-servernum "${unityPath}"`;
38-
} else if (process.platform === 'darwin') {
39-
unityCmd = `"${unityPath}"`;
40-
} else if (process.platform === 'win32') {
41-
unityCmd = `"${unityPath}"`;
33+
return await execute(`xvfb-run --auto-servernum "${unityPath}" ${args}`, true);
34+
} else {
35+
return await execute(`"${unityPath}" ${args}`, true);
4236
}
43-
return unityCmd;
4437
}
4538

4639
async function execute(command, ignoreReturnCode) {
4740
let stdout = '';
4841
await exec.exec(command, [], {
4942
ignoreReturnCode: ignoreReturnCode,
50-
listeners: {
51-
stdout: buffer => stdout += buffer.toString()
52-
}
43+
listeners: { stdout: buffer => stdout += buffer.toString() }
5344
});
5445
return stdout;
5546
}

0 commit comments

Comments
 (0)