mirror of
https://github.com/game-ci/unity-builder.git
synced 2026-06-06 14:10:17 -07:00
feat(orchestrator): CI platform providers — Remote PowerShell, GitHub Actions, GitLab CI, Ansible
Add four new providers that delegate builds to external CI platforms: - remote-powershell: Execute on remote machines via WinRM/SSH - github-actions: Dispatch workflow_dispatch on target repository - gitlab-ci: Trigger pipeline via GitLab API - ansible: Run playbooks against managed inventory Each follows the CI-as-a-provider pattern: trigger remote job, pass build parameters, stream logs, report status. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
64
action.yml
64
action.yml
@@ -182,8 +182,8 @@ inputs:
|
||||
required: false
|
||||
default: ''
|
||||
description:
|
||||
'[Orchestrator] Run a custom job instead of the standard build automation for orchestrator (in yaml format with the
|
||||
keys image, secrets (name, value object array), command line string)'
|
||||
'[Orchestrator] Run a custom job instead of the standard build automation for orchestrator (in yaml format with
|
||||
the keys image, secrets (name, value object array), command line string)'
|
||||
awsStackName:
|
||||
default: 'game-ci'
|
||||
required: false
|
||||
@@ -279,6 +279,66 @@ inputs:
|
||||
description:
|
||||
'[Orchestrator] Specifies the repo for the unity builder. Useful if you forked the repo for testing, features, or
|
||||
fixes.'
|
||||
remotePowershellHost:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[Orchestrator] Remote PowerShell host (hostname or IP) for the remote-powershell provider'
|
||||
remotePowershellCredential:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[Orchestrator] Remote PowerShell credential (username:password or certificate path)'
|
||||
remotePowershellTransport:
|
||||
default: 'wsman'
|
||||
required: false
|
||||
description: '[Orchestrator] Remote PowerShell transport protocol (wsman or ssh)'
|
||||
githubActionsRepo:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[Orchestrator] Target repository (owner/repo) for the github-actions provider'
|
||||
githubActionsWorkflow:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[Orchestrator] Workflow filename or ID to dispatch for the github-actions provider'
|
||||
githubActionsToken:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[Orchestrator] PAT with actions:write scope for the github-actions provider'
|
||||
githubActionsRef:
|
||||
default: 'main'
|
||||
required: false
|
||||
description: '[Orchestrator] Branch/ref to run the workflow on for the github-actions provider'
|
||||
gitlabProjectId:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[Orchestrator] GitLab project ID or URL-encoded path for the gitlab-ci provider'
|
||||
gitlabTriggerToken:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[Orchestrator] Pipeline trigger token for the gitlab-ci provider'
|
||||
gitlabApiUrl:
|
||||
default: 'https://gitlab.com'
|
||||
required: false
|
||||
description: '[Orchestrator] GitLab API URL (for self-hosted instances) for the gitlab-ci provider'
|
||||
gitlabRef:
|
||||
default: 'main'
|
||||
required: false
|
||||
description: '[Orchestrator] Branch/ref to trigger the pipeline on for the gitlab-ci provider'
|
||||
ansibleInventory:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[Orchestrator] Path to Ansible inventory file or dynamic inventory script'
|
||||
ansiblePlaybook:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[Orchestrator] Path to Ansible playbook for Unity builds'
|
||||
ansibleExtraVars:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[Orchestrator] Additional Ansible variables as JSON'
|
||||
ansibleVaultPassword:
|
||||
default: ''
|
||||
required: false
|
||||
description: '[Orchestrator] Path to Ansible vault password file'
|
||||
|
||||
outputs:
|
||||
volume:
|
||||
|
||||
781
dist/index.js
generated
vendored
781
dist/index.js
generated
vendored
@@ -375,6 +375,25 @@ class BuildParameters {
|
||||
cacheUnityInstallationOnMac: input_1.default.cacheUnityInstallationOnMac,
|
||||
unityHubVersionOnMac: input_1.default.unityHubVersionOnMac,
|
||||
dockerWorkspacePath: input_1.default.dockerWorkspacePath,
|
||||
// Remote PowerShell provider
|
||||
remotePowershellHost: input_1.default.remotePowershellHost,
|
||||
remotePowershellCredential: input_1.default.remotePowershellCredential,
|
||||
remotePowershellTransport: input_1.default.remotePowershellTransport,
|
||||
// GitHub Actions provider
|
||||
githubActionsRepo: input_1.default.githubActionsRepo,
|
||||
githubActionsWorkflow: input_1.default.githubActionsWorkflow,
|
||||
githubActionsToken: input_1.default.githubActionsToken,
|
||||
githubActionsRef: input_1.default.githubActionsRef,
|
||||
// GitLab CI provider
|
||||
gitlabProjectId: input_1.default.gitlabProjectId,
|
||||
gitlabTriggerToken: input_1.default.gitlabTriggerToken,
|
||||
gitlabApiUrl: input_1.default.gitlabApiUrl,
|
||||
gitlabRef: input_1.default.gitlabRef,
|
||||
// Ansible provider
|
||||
ansibleInventory: input_1.default.ansibleInventory,
|
||||
ansiblePlaybook: input_1.default.ansiblePlaybook,
|
||||
ansibleExtraVars: input_1.default.ansibleExtraVars,
|
||||
ansibleVaultPassword: input_1.default.ansibleVaultPassword,
|
||||
};
|
||||
}
|
||||
static parseBuildFile(filename, platform, androidExportType) {
|
||||
@@ -1826,6 +1845,63 @@ class Input {
|
||||
static get skipActivation() {
|
||||
return Input.getInput('skipActivation')?.toLowerCase() ?? 'false';
|
||||
}
|
||||
// ### ### ###
|
||||
// Remote PowerShell provider
|
||||
// ### ### ###
|
||||
static get remotePowershellHost() {
|
||||
return Input.getInput('remotePowershellHost') ?? '';
|
||||
}
|
||||
static get remotePowershellCredential() {
|
||||
return Input.getInput('remotePowershellCredential') ?? '';
|
||||
}
|
||||
static get remotePowershellTransport() {
|
||||
return Input.getInput('remotePowershellTransport') ?? 'wsman';
|
||||
}
|
||||
// ### ### ###
|
||||
// GitHub Actions provider
|
||||
// ### ### ###
|
||||
static get githubActionsRepo() {
|
||||
return Input.getInput('githubActionsRepo') ?? '';
|
||||
}
|
||||
static get githubActionsWorkflow() {
|
||||
return Input.getInput('githubActionsWorkflow') ?? '';
|
||||
}
|
||||
static get githubActionsToken() {
|
||||
return Input.getInput('githubActionsToken') ?? '';
|
||||
}
|
||||
static get githubActionsRef() {
|
||||
return Input.getInput('githubActionsRef') ?? 'main';
|
||||
}
|
||||
// ### ### ###
|
||||
// GitLab CI provider
|
||||
// ### ### ###
|
||||
static get gitlabProjectId() {
|
||||
return Input.getInput('gitlabProjectId') ?? '';
|
||||
}
|
||||
static get gitlabTriggerToken() {
|
||||
return Input.getInput('gitlabTriggerToken') ?? '';
|
||||
}
|
||||
static get gitlabApiUrl() {
|
||||
return Input.getInput('gitlabApiUrl') ?? 'https://gitlab.com';
|
||||
}
|
||||
static get gitlabRef() {
|
||||
return Input.getInput('gitlabRef') ?? 'main';
|
||||
}
|
||||
// ### ### ###
|
||||
// Ansible provider
|
||||
// ### ### ###
|
||||
static get ansibleInventory() {
|
||||
return Input.getInput('ansibleInventory') ?? '';
|
||||
}
|
||||
static get ansiblePlaybook() {
|
||||
return Input.getInput('ansiblePlaybook') ?? '';
|
||||
}
|
||||
static get ansibleExtraVars() {
|
||||
return Input.getInput('ansibleExtraVars') ?? '';
|
||||
}
|
||||
static get ansibleVaultPassword() {
|
||||
return Input.getInput('ansibleVaultPassword') ?? '';
|
||||
}
|
||||
static ToEnvVarFormat(input) {
|
||||
if (input.toUpperCase() === input) {
|
||||
return input;
|
||||
@@ -2501,6 +2577,10 @@ const core = __importStar(__nccwpck_require__(42186));
|
||||
const test_1 = __importDefault(__nccwpck_require__(6389));
|
||||
const local_1 = __importDefault(__nccwpck_require__(48195));
|
||||
const docker_1 = __importDefault(__nccwpck_require__(91739));
|
||||
const remote_powershell_1 = __importDefault(__nccwpck_require__(90732));
|
||||
const github_actions_1 = __importDefault(__nccwpck_require__(57511));
|
||||
const gitlab_ci_1 = __importDefault(__nccwpck_require__(28103));
|
||||
const ansible_1 = __importDefault(__nccwpck_require__(72073));
|
||||
const provider_loader_1 = __importDefault(__nccwpck_require__(50822));
|
||||
const github_1 = __importDefault(__nccwpck_require__(83654));
|
||||
const shared_workspace_locking_1 = __importDefault(__nccwpck_require__(54222));
|
||||
@@ -2619,6 +2699,18 @@ class Orchestrator {
|
||||
case 'local':
|
||||
Orchestrator.Provider = new local_1.default();
|
||||
break;
|
||||
case 'remote-powershell':
|
||||
Orchestrator.Provider = new remote_powershell_1.default(Orchestrator.buildParameters);
|
||||
break;
|
||||
case 'github-actions':
|
||||
Orchestrator.Provider = new github_actions_1.default(Orchestrator.buildParameters);
|
||||
break;
|
||||
case 'gitlab-ci':
|
||||
Orchestrator.Provider = new gitlab_ci_1.default(Orchestrator.buildParameters);
|
||||
break;
|
||||
case 'ansible':
|
||||
Orchestrator.Provider = new ansible_1.default(Orchestrator.buildParameters);
|
||||
break;
|
||||
default:
|
||||
// Try to load provider using the dynamic loader for unknown providers
|
||||
try {
|
||||
@@ -2768,6 +2860,163 @@ Orchestrator.validateAwsTemplates = false;
|
||||
exports["default"] = Orchestrator;
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 72073:
|
||||
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
|
||||
|
||||
"use strict";
|
||||
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
const orchestrator_system_1 = __nccwpck_require__(9744);
|
||||
const orchestrator_logger_1 = __importDefault(__nccwpck_require__(32549));
|
||||
const provider_resource_1 = __nccwpck_require__(72538);
|
||||
/**
|
||||
* Ansible provider — executes Unity builds via Ansible playbooks
|
||||
* against managed inventory.
|
||||
*
|
||||
* Use case: Teams with existing Ansible infrastructure for server
|
||||
* management who want to leverage their inventory for build distribution.
|
||||
*/
|
||||
class AnsibleProvider {
|
||||
constructor(buildParameters) {
|
||||
this.buildParameters = buildParameters;
|
||||
this.inventory = buildParameters.ansibleInventory || '';
|
||||
this.playbook = buildParameters.ansiblePlaybook || '';
|
||||
this.extraVariables = buildParameters.ansibleExtraVars || '';
|
||||
this.vaultPassword = buildParameters.ansibleVaultPassword || '';
|
||||
}
|
||||
async setupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildGuid,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray) {
|
||||
orchestrator_logger_1.default.log(`[Ansible] Setting up playbook execution`);
|
||||
if (!this.inventory) {
|
||||
throw new Error('ansibleInventory is required for the ansible provider');
|
||||
}
|
||||
// Verify ansible is available
|
||||
try {
|
||||
const version = await orchestrator_system_1.OrchestratorSystem.Run('ansible --version | head -1');
|
||||
orchestrator_logger_1.default.log(`[Ansible] ${version.trim()}`);
|
||||
}
|
||||
catch (error) {
|
||||
throw new Error(`Ansible not found on PATH: ${error.message || error}`);
|
||||
}
|
||||
// Verify inventory exists
|
||||
try {
|
||||
await orchestrator_system_1.OrchestratorSystem.Run(`test -e "${this.inventory}"`);
|
||||
}
|
||||
catch {
|
||||
throw new Error(`Inventory not found: ${this.inventory}`);
|
||||
}
|
||||
}
|
||||
async runTaskInWorkflow(buildGuid, image, commands, mountdir, workingdir, environment, secrets) {
|
||||
orchestrator_logger_1.default.log(`[Ansible] Running playbook against inventory ${this.inventory}`);
|
||||
if (!this.playbook) {
|
||||
throw new Error('ansiblePlaybook is required — no default playbook is provided yet. ' +
|
||||
'Provide a playbook that accepts build_guid, build_image, build_commands, mount_dir, and working_dir variables.');
|
||||
}
|
||||
// Build extra-vars JSON
|
||||
// These use snake_case because they are Ansible variable names passed to playbooks
|
||||
const playbookVariables = {
|
||||
// eslint-disable-next-line camelcase
|
||||
build_guid: buildGuid,
|
||||
// eslint-disable-next-line camelcase
|
||||
build_image: image,
|
||||
// eslint-disable-next-line camelcase
|
||||
build_commands: commands,
|
||||
// eslint-disable-next-line camelcase
|
||||
mount_dir: mountdir,
|
||||
// eslint-disable-next-line camelcase
|
||||
working_dir: workingdir,
|
||||
};
|
||||
for (const element of environment) {
|
||||
playbookVariables[element.name.toLowerCase()] = element.value;
|
||||
}
|
||||
// Merge user-provided extra vars
|
||||
if (this.extraVariables) {
|
||||
try {
|
||||
const userVariables = JSON.parse(this.extraVariables);
|
||||
Object.assign(playbookVariables, userVariables);
|
||||
}
|
||||
catch {
|
||||
orchestrator_logger_1.default.logWarning(`[Ansible] Failed to parse ansibleExtraVars as JSON, using as-is`);
|
||||
}
|
||||
}
|
||||
const extraVariablesJson = JSON.stringify(playbookVariables).replace(/'/g, "'\\''");
|
||||
// Build ansible-playbook command
|
||||
const commandParts = [
|
||||
'ansible-playbook',
|
||||
`-i "${this.inventory}"`,
|
||||
`"${this.playbook}"`,
|
||||
`-e '${extraVariablesJson}'`,
|
||||
'--no-color',
|
||||
];
|
||||
if (this.vaultPassword) {
|
||||
commandParts.push(`--vault-password-file "${this.vaultPassword}"`);
|
||||
}
|
||||
// Add secret variables as extra environment
|
||||
const environmentPrefix = secrets
|
||||
.map((secret) => `${secret.EnvironmentVariable}='${secret.ParameterValue}'`)
|
||||
.join(' ');
|
||||
const fullCommand = environmentPrefix ? `${environmentPrefix} ${commandParts.join(' ')}` : commandParts.join(' ');
|
||||
try {
|
||||
const output = await orchestrator_system_1.OrchestratorSystem.Run(fullCommand);
|
||||
orchestrator_logger_1.default.log(`[Ansible] Playbook completed successfully`);
|
||||
return output;
|
||||
}
|
||||
catch (error) {
|
||||
orchestrator_logger_1.default.logWarning(`[Ansible] Playbook failed: ${error.message || error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async cleanupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray) {
|
||||
orchestrator_logger_1.default.log(`[Ansible] Cleanup complete`);
|
||||
}
|
||||
async garbageCollect(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
filter,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
previewOnly,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
olderThan,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
fullCache,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
baseDependencies) {
|
||||
return '';
|
||||
}
|
||||
async listResources() {
|
||||
if (!this.inventory)
|
||||
return [];
|
||||
const resource = new provider_resource_1.ProviderResource();
|
||||
resource.Name = this.inventory;
|
||||
return [resource];
|
||||
}
|
||||
async listWorkflow() {
|
||||
return [];
|
||||
}
|
||||
async watchWorkflow() {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
exports["default"] = AnsibleProvider;
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 41878:
|
||||
@@ -4926,6 +5175,384 @@ find ${sharedFolder} -maxdepth 1 -type f -name "test-*" -exec cp -a {} /github/w
|
||||
exports["default"] = LocalDockerOrchestrator;
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 57511:
|
||||
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
|
||||
|
||||
"use strict";
|
||||
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
const orchestrator_system_1 = __nccwpck_require__(9744);
|
||||
const orchestrator_logger_1 = __importDefault(__nccwpck_require__(32549));
|
||||
const provider_resource_1 = __nccwpck_require__(72538);
|
||||
const provider_workflow_1 = __nccwpck_require__(60511);
|
||||
/**
|
||||
* GitHub Actions provider — triggers builds as workflow_dispatch events
|
||||
* on a target repository via the GitHub API.
|
||||
*
|
||||
* Use case: Distribute builds across orgs, use specialized runner pools,
|
||||
* or trigger builds in repos with Unity licenses.
|
||||
*/
|
||||
class GitHubActionsProvider {
|
||||
constructor(buildParameters) {
|
||||
this.runId = 0;
|
||||
this.buildParameters = buildParameters;
|
||||
this.repo = buildParameters.githubActionsRepo || '';
|
||||
this.workflow = buildParameters.githubActionsWorkflow || '';
|
||||
this.token = buildParameters.githubActionsToken || '';
|
||||
this.ref = buildParameters.githubActionsRef || 'main';
|
||||
}
|
||||
async setupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildGuid,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray) {
|
||||
orchestrator_logger_1.default.log(`[GitHubActions] Setting up workflow dispatch to ${this.repo}`);
|
||||
if (!this.repo || !this.workflow) {
|
||||
throw new Error('githubActionsRepo and githubActionsWorkflow are required for the github-actions provider');
|
||||
}
|
||||
if (!this.token) {
|
||||
throw new Error('githubActionsToken is required (PAT with actions:write scope)');
|
||||
}
|
||||
// Verify repository and workflow exist
|
||||
try {
|
||||
const result = await orchestrator_system_1.OrchestratorSystem.Run(`GH_TOKEN=${this.token} gh api repos/${this.repo}/actions/workflows/${this.workflow} --jq '.id'`);
|
||||
orchestrator_logger_1.default.log(`[GitHubActions] Workflow verified: ${this.workflow} (ID: ${result.trim()})`);
|
||||
}
|
||||
catch (error) {
|
||||
throw new Error(`Failed to verify workflow ${this.workflow} in ${this.repo}: ${error.message || error}`);
|
||||
}
|
||||
}
|
||||
async runTaskInWorkflow(buildGuid, image, commands, mountdir, workingdir, environment,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
secrets) {
|
||||
orchestrator_logger_1.default.log(`[GitHubActions] Dispatching workflow ${this.workflow} on ${this.repo}@${this.ref}`);
|
||||
// Build inputs payload
|
||||
const inputs = {
|
||||
buildGuid,
|
||||
image,
|
||||
commands: Buffer.from(commands).toString('base64'),
|
||||
mountdir,
|
||||
workingdir,
|
||||
};
|
||||
// Add environment variables as a JSON input
|
||||
if (environment.length > 0) {
|
||||
inputs.environment = JSON.stringify(environment.map((element) => ({ name: element.name, value: element.value })));
|
||||
}
|
||||
// Record the time before dispatch to identify the run
|
||||
const beforeDispatch = new Date().toISOString();
|
||||
// Dispatch the workflow
|
||||
const inputsJson = JSON.stringify(inputs).replace(/'/g, "'\\''");
|
||||
try {
|
||||
await orchestrator_system_1.OrchestratorSystem.Run(`GH_TOKEN=${this.token} gh api repos/${this.repo}/actions/workflows/${this.workflow}/dispatches -X POST -f ref='${this.ref}' -f "inputs=${inputsJson}"`);
|
||||
orchestrator_logger_1.default.log(`[GitHubActions] Workflow dispatched`);
|
||||
}
|
||||
catch (error) {
|
||||
throw new Error(`Failed to dispatch workflow: ${error.message || error}`);
|
||||
}
|
||||
// Poll for the run to appear
|
||||
orchestrator_logger_1.default.log(`[GitHubActions] Waiting for workflow run to start...`);
|
||||
let attempts = 0;
|
||||
const maxAttempts = 30;
|
||||
while (attempts < maxAttempts) {
|
||||
attempts++;
|
||||
await new Promise((resolve) => setTimeout(resolve, 10000));
|
||||
try {
|
||||
const runsJson = await orchestrator_system_1.OrchestratorSystem.Run(`GH_TOKEN=${this.token} gh api "repos/${this.repo}/actions/workflows/${this.workflow}/runs?created=>${beforeDispatch}&per_page=5" --jq '.workflow_runs[0] | {id, status, conclusion}'`, true);
|
||||
const run = JSON.parse(runsJson.trim());
|
||||
if (run.id) {
|
||||
this.runId = run.id;
|
||||
orchestrator_logger_1.default.log(`[GitHubActions] Run started: ${this.runId} (status: ${run.status})`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Run not yet available
|
||||
}
|
||||
}
|
||||
if (!this.runId) {
|
||||
throw new Error(`Workflow run did not start within ${maxAttempts * 10}s`);
|
||||
}
|
||||
// Poll until completion and stream logs
|
||||
let status = 'in_progress';
|
||||
while (status === 'in_progress' || status === 'queued') {
|
||||
await new Promise((resolve) => setTimeout(resolve, 15000));
|
||||
try {
|
||||
const statusJson = await orchestrator_system_1.OrchestratorSystem.Run(`GH_TOKEN=${this.token} gh api repos/${this.repo}/actions/runs/${this.runId} --jq '{status, conclusion}'`, true);
|
||||
const result = JSON.parse(statusJson.trim());
|
||||
status = result.status;
|
||||
if (status === 'completed') {
|
||||
orchestrator_logger_1.default.log(`[GitHubActions] Run ${this.runId} completed: ${result.conclusion}`);
|
||||
if (result.conclusion !== 'success') {
|
||||
throw new Error(`Workflow run failed with conclusion: ${result.conclusion}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
orchestrator_logger_1.default.log(`[GitHubActions] Run ${this.runId} status: ${status}`);
|
||||
}
|
||||
catch (error) {
|
||||
if (error.message && error.message.includes('conclusion')) {
|
||||
throw error;
|
||||
}
|
||||
orchestrator_logger_1.default.logWarning(`[GitHubActions] Status check error: ${error.message || error}`);
|
||||
}
|
||||
}
|
||||
// Fetch logs
|
||||
try {
|
||||
const logs = await orchestrator_system_1.OrchestratorSystem.Run(`GH_TOKEN=${this.token} gh run view ${this.runId} --repo ${this.repo} --log`, true);
|
||||
return logs;
|
||||
}
|
||||
catch {
|
||||
return `Run ${this.runId} completed successfully (logs unavailable)`;
|
||||
}
|
||||
}
|
||||
async cleanupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray) {
|
||||
orchestrator_logger_1.default.log(`[GitHubActions] Cleanup complete (no resources to tear down)`);
|
||||
}
|
||||
async garbageCollect(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
filter,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
previewOnly,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
olderThan,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
fullCache,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
baseDependencies) {
|
||||
return '';
|
||||
}
|
||||
async listResources() {
|
||||
if (!this.repo || !this.token)
|
||||
return [];
|
||||
try {
|
||||
const runnersJson = await orchestrator_system_1.OrchestratorSystem.Run(`GH_TOKEN=${this.token} gh api repos/${this.repo}/actions/runners --jq '.runners[] | .name'`, true);
|
||||
return runnersJson
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.map((name) => {
|
||||
const resource = new provider_resource_1.ProviderResource();
|
||||
resource.Name = name.trim();
|
||||
return resource;
|
||||
});
|
||||
}
|
||||
catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
async listWorkflow() {
|
||||
if (!this.repo || !this.token)
|
||||
return [];
|
||||
try {
|
||||
const runsJson = await orchestrator_system_1.OrchestratorSystem.Run(`GH_TOKEN=${this.token} gh api repos/${this.repo}/actions/runs?per_page=10 --jq '.workflow_runs[] | .name'`, true);
|
||||
return runsJson
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.map((name) => {
|
||||
const workflow = new provider_workflow_1.ProviderWorkflow();
|
||||
workflow.Name = name.trim();
|
||||
return workflow;
|
||||
});
|
||||
}
|
||||
catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
async watchWorkflow() {
|
||||
if (!this.runId)
|
||||
return 'No active run to watch';
|
||||
try {
|
||||
return await orchestrator_system_1.OrchestratorSystem.Run(`GH_TOKEN=${this.token} gh run watch ${this.runId} --repo ${this.repo}`, true);
|
||||
}
|
||||
catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
exports["default"] = GitHubActionsProvider;
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 28103:
|
||||
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
|
||||
|
||||
"use strict";
|
||||
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
const orchestrator_system_1 = __nccwpck_require__(9744);
|
||||
const orchestrator_logger_1 = __importDefault(__nccwpck_require__(32549));
|
||||
const provider_workflow_1 = __nccwpck_require__(60511);
|
||||
/**
|
||||
* GitLab CI provider — triggers builds as GitLab CI pipelines
|
||||
* via the GitLab API.
|
||||
*
|
||||
* Use case: Teams using GitLab CI, hybrid GitHub/GitLab setups,
|
||||
* or GitLab runners with Unity licenses.
|
||||
*/
|
||||
class GitLabCIProvider {
|
||||
constructor(buildParameters) {
|
||||
this.pipelineId = 0;
|
||||
this.buildParameters = buildParameters;
|
||||
this.projectId = buildParameters.gitlabProjectId || '';
|
||||
this.triggerToken = buildParameters.gitlabTriggerToken || '';
|
||||
this.apiUrl = (buildParameters.gitlabApiUrl || 'https://gitlab.com').replace(/\/+$/, '');
|
||||
this.ref = buildParameters.gitlabRef || 'main';
|
||||
}
|
||||
async setupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildGuid,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray) {
|
||||
orchestrator_logger_1.default.log(`[GitLabCI] Setting up pipeline trigger for project ${this.projectId}`);
|
||||
if (!this.projectId || !this.triggerToken) {
|
||||
throw new Error('gitlabProjectId and gitlabTriggerToken are required for the gitlab-ci provider');
|
||||
}
|
||||
// Verify project access
|
||||
const encodedProject = encodeURIComponent(this.projectId);
|
||||
try {
|
||||
await orchestrator_system_1.OrchestratorSystem.Run(`curl -sf -H "PRIVATE-TOKEN: ${this.triggerToken}" "${this.apiUrl}/api/v4/projects/${encodedProject}" -o /dev/null`);
|
||||
orchestrator_logger_1.default.log(`[GitLabCI] Project access verified`);
|
||||
}
|
||||
catch (error) {
|
||||
throw new Error(`Failed to access GitLab project ${this.projectId}: ${error.message || error}`);
|
||||
}
|
||||
}
|
||||
async runTaskInWorkflow(buildGuid, image, commands, mountdir, workingdir, environment,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
secrets) {
|
||||
orchestrator_logger_1.default.log(`[GitLabCI] Triggering pipeline on project ${this.projectId}@${this.ref}`);
|
||||
const encodedProject = encodeURIComponent(this.projectId);
|
||||
// Build variables for the pipeline
|
||||
const pipelineVariables = [
|
||||
`-f "variables[BUILD_GUID]=${buildGuid}"`,
|
||||
`-f "variables[BUILD_IMAGE]=${image}"`,
|
||||
`-f "variables[BUILD_COMMANDS]=${Buffer.from(commands).toString('base64')}"`,
|
||||
`-f "variables[MOUNT_DIR]=${mountdir}"`,
|
||||
`-f "variables[WORKING_DIR]=${workingdir}"`,
|
||||
];
|
||||
for (const element of environment) {
|
||||
pipelineVariables.push(`-f "variables[${element.name}]=${element.value}"`);
|
||||
}
|
||||
// Trigger pipeline
|
||||
try {
|
||||
const response = await orchestrator_system_1.OrchestratorSystem.Run(`curl -sf -X POST "${this.apiUrl}/api/v4/projects/${encodedProject}/trigger/pipeline" -f "token=${this.triggerToken}" -f "ref=${this.ref}" ${pipelineVariables.join(' ')}`);
|
||||
const pipeline = JSON.parse(response);
|
||||
this.pipelineId = pipeline.id;
|
||||
orchestrator_logger_1.default.log(`[GitLabCI] Pipeline triggered: ${this.pipelineId} (status: ${pipeline.status})`);
|
||||
}
|
||||
catch (error) {
|
||||
throw new Error(`Failed to trigger pipeline: ${error.message || error}`);
|
||||
}
|
||||
// Poll until completion
|
||||
let status = 'pending';
|
||||
const terminalStatuses = new Set(['success', 'failed', 'canceled', 'skipped']);
|
||||
while (!terminalStatuses.has(status)) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 15000));
|
||||
try {
|
||||
const statusResponse = await orchestrator_system_1.OrchestratorSystem.Run(`curl -sf -H "PRIVATE-TOKEN: ${this.triggerToken}" "${this.apiUrl}/api/v4/projects/${encodedProject}/pipelines/${this.pipelineId}"`, true);
|
||||
const pipelineStatus = JSON.parse(statusResponse);
|
||||
status = pipelineStatus.status;
|
||||
orchestrator_logger_1.default.log(`[GitLabCI] Pipeline ${this.pipelineId} status: ${status}`);
|
||||
}
|
||||
catch (error) {
|
||||
orchestrator_logger_1.default.logWarning(`[GitLabCI] Status check error: ${error.message || error}`);
|
||||
}
|
||||
}
|
||||
if (status !== 'success') {
|
||||
throw new Error(`Pipeline ${this.pipelineId} finished with status: ${status}`);
|
||||
}
|
||||
// Fetch job logs
|
||||
try {
|
||||
const jobsResponse = await orchestrator_system_1.OrchestratorSystem.Run(`curl -sf -H "PRIVATE-TOKEN: ${this.triggerToken}" "${this.apiUrl}/api/v4/projects/${encodedProject}/pipelines/${this.pipelineId}/jobs"`, true);
|
||||
const jobs = JSON.parse(jobsResponse);
|
||||
const logs = [];
|
||||
for (const job of jobs) {
|
||||
try {
|
||||
const jobLog = await orchestrator_system_1.OrchestratorSystem.Run(`curl -sf -H "PRIVATE-TOKEN: ${this.triggerToken}" "${this.apiUrl}/api/v4/projects/${encodedProject}/jobs/${job.id}/trace"`, true);
|
||||
logs.push(`=== Job: ${job.name} (${job.status}) ===\n${jobLog}`);
|
||||
}
|
||||
catch {
|
||||
logs.push(`=== Job: ${job.name} (${job.status}) === (logs unavailable)`);
|
||||
}
|
||||
}
|
||||
return logs.join('\n\n');
|
||||
}
|
||||
catch {
|
||||
return `Pipeline ${this.pipelineId} completed successfully (logs unavailable)`;
|
||||
}
|
||||
}
|
||||
async cleanupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray) {
|
||||
orchestrator_logger_1.default.log(`[GitLabCI] Cleanup complete`);
|
||||
}
|
||||
async garbageCollect(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
filter,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
previewOnly,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
olderThan,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
fullCache,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
baseDependencies) {
|
||||
return '';
|
||||
}
|
||||
async listResources() {
|
||||
return [];
|
||||
}
|
||||
async listWorkflow() {
|
||||
if (!this.projectId || !this.triggerToken)
|
||||
return [];
|
||||
try {
|
||||
const encodedProject = encodeURIComponent(this.projectId);
|
||||
const response = await orchestrator_system_1.OrchestratorSystem.Run(`curl -sf -H "PRIVATE-TOKEN: ${this.triggerToken}" "${this.apiUrl}/api/v4/projects/${encodedProject}/pipelines?per_page=10"`, true);
|
||||
return JSON.parse(response).map((pipeline) => {
|
||||
const workflow = new provider_workflow_1.ProviderWorkflow();
|
||||
workflow.Name = `Pipeline #${pipeline.id} (${pipeline.status})`;
|
||||
return workflow;
|
||||
});
|
||||
}
|
||||
catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
async watchWorkflow() {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
exports["default"] = GitLabCIProvider;
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 66990:
|
||||
@@ -7146,6 +7773,20 @@ class ProviderLoader {
|
||||
exports.ProviderLoader = ProviderLoader;
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 72538:
|
||||
/***/ ((__unused_webpack_module, exports) => {
|
||||
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
exports.ProviderResource = void 0;
|
||||
class ProviderResource {
|
||||
}
|
||||
exports.ProviderResource = ProviderResource;
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 26162:
|
||||
@@ -7267,6 +7908,146 @@ function logProviderSource(source, parsed) {
|
||||
exports.logProviderSource = logProviderSource;
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 60511:
|
||||
/***/ ((__unused_webpack_module, exports) => {
|
||||
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
exports.ProviderWorkflow = void 0;
|
||||
class ProviderWorkflow {
|
||||
}
|
||||
exports.ProviderWorkflow = ProviderWorkflow;
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 90732:
|
||||
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
|
||||
|
||||
"use strict";
|
||||
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
const orchestrator_system_1 = __nccwpck_require__(9744);
|
||||
const orchestrator_logger_1 = __importDefault(__nccwpck_require__(32549));
|
||||
const provider_resource_1 = __nccwpck_require__(72538);
|
||||
/**
|
||||
* Remote PowerShell provider — executes Unity builds on remote machines
|
||||
* via PowerShell Remoting (WinRM or SSH).
|
||||
*
|
||||
* Use case: Teams with dedicated build machines not part of a CI system.
|
||||
*/
|
||||
class RemotePowershellProvider {
|
||||
constructor(buildParameters) {
|
||||
this.sessionId = '';
|
||||
this.buildParameters = buildParameters;
|
||||
this.host = buildParameters.remotePowershellHost || '';
|
||||
this.transport = buildParameters.remotePowershellTransport || 'wsman';
|
||||
this.credential = buildParameters.remotePowershellCredential || '';
|
||||
}
|
||||
async setupWorkflow(buildGuid,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray) {
|
||||
orchestrator_logger_1.default.log(`[RemotePowershell] Setting up remote session to ${this.host} via ${this.transport}`);
|
||||
if (!this.host) {
|
||||
throw new Error('remotePowershellHost is required for the remote-powershell provider');
|
||||
}
|
||||
// Test connectivity
|
||||
const testCommand = this.buildPwshCommand(`Test-WSMan -ComputerName "${this.host}" -ErrorAction Stop`);
|
||||
try {
|
||||
await orchestrator_system_1.OrchestratorSystem.Run(testCommand);
|
||||
orchestrator_logger_1.default.log(`[RemotePowershell] Connection test passed`);
|
||||
}
|
||||
catch (error) {
|
||||
throw new Error(`Failed to connect to remote host ${this.host}: ${error.message || error}`);
|
||||
}
|
||||
this.sessionId = buildGuid;
|
||||
orchestrator_logger_1.default.log(`[RemotePowershell] Session ${this.sessionId} ready`);
|
||||
}
|
||||
async runTaskInWorkflow(buildGuid, image, commands, mountdir, workingdir, environment, secrets) {
|
||||
orchestrator_logger_1.default.log(`[RemotePowershell] Executing task on ${this.host}`);
|
||||
// Build environment variable block for remote session
|
||||
const environmentBlock = environment.map((element) => `$env:${element.name} = '${element.value}'`).join('; ');
|
||||
const secretBlock = secrets
|
||||
.map((secret) => `$env:${secret.EnvironmentVariable} = '${secret.ParameterValue}'`)
|
||||
.join('; ');
|
||||
// Wrap commands for remote execution
|
||||
const remoteScript = [environmentBlock, secretBlock, `Set-Location "${workingdir}"`, commands]
|
||||
.filter(Boolean)
|
||||
.join('; ');
|
||||
const invokeCommand = this.buildInvokeCommand(remoteScript);
|
||||
try {
|
||||
const output = await orchestrator_system_1.OrchestratorSystem.Run(invokeCommand);
|
||||
orchestrator_logger_1.default.log(`[RemotePowershell] Task completed successfully`);
|
||||
return output;
|
||||
}
|
||||
catch (error) {
|
||||
orchestrator_logger_1.default.logWarning(`[RemotePowershell] Task failed: ${error.message || error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async cleanupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray) {
|
||||
orchestrator_logger_1.default.log(`[RemotePowershell] Cleaning up session ${this.sessionId}`);
|
||||
// Remote sessions are stateless per invocation — no cleanup needed
|
||||
}
|
||||
async garbageCollect(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
filter,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
previewOnly,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
olderThan,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
fullCache,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
baseDependencies) {
|
||||
orchestrator_logger_1.default.log(`[RemotePowershell] Garbage collection not supported for remote PowerShell provider`);
|
||||
return '';
|
||||
}
|
||||
async listResources() {
|
||||
const resource = new provider_resource_1.ProviderResource();
|
||||
resource.Name = this.host;
|
||||
return [resource];
|
||||
}
|
||||
async listWorkflow() {
|
||||
return [];
|
||||
}
|
||||
async watchWorkflow() {
|
||||
return '';
|
||||
}
|
||||
buildPwshCommand(script) {
|
||||
return `pwsh -NoProfile -NonInteractive -Command "${script.replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
buildInvokeCommand(remoteScript) {
|
||||
const escapedScript = remoteScript.replace(/"/g, '\\"').replace(/'/g, "''");
|
||||
if (this.transport === 'ssh') {
|
||||
return `pwsh -NoProfile -NonInteractive -Command "Invoke-Command -HostName '${this.host}' -ScriptBlock { ${escapedScript} }"`;
|
||||
}
|
||||
// WinRM (default)
|
||||
const credentialPart = this.credential
|
||||
? `-Credential (New-Object PSCredential('${this.credential.split(':')[0]}', (ConvertTo-SecureString '${this.credential.split(':')[1]}' -AsPlainText -Force)))`
|
||||
: '';
|
||||
return `pwsh -NoProfile -NonInteractive -Command "Invoke-Command -ComputerName '${this.host}' ${credentialPart} -ScriptBlock { ${escapedScript} }"`;
|
||||
}
|
||||
}
|
||||
exports["default"] = RemotePowershellProvider;
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 6389:
|
||||
|
||||
2
dist/index.js.map
generated
vendored
2
dist/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -107,6 +107,29 @@ class BuildParameters {
|
||||
public unityHubVersionOnMac!: string;
|
||||
public dockerWorkspacePath!: string;
|
||||
|
||||
// Remote PowerShell provider
|
||||
public remotePowershellHost!: string;
|
||||
public remotePowershellCredential!: string;
|
||||
public remotePowershellTransport!: string;
|
||||
|
||||
// GitHub Actions provider
|
||||
public githubActionsRepo!: string;
|
||||
public githubActionsWorkflow!: string;
|
||||
public githubActionsToken!: string;
|
||||
public githubActionsRef!: string;
|
||||
|
||||
// GitLab CI provider
|
||||
public gitlabProjectId!: string;
|
||||
public gitlabTriggerToken!: string;
|
||||
public gitlabApiUrl!: string;
|
||||
public gitlabRef!: string;
|
||||
|
||||
// Ansible provider
|
||||
public ansibleInventory!: string;
|
||||
public ansiblePlaybook!: string;
|
||||
public ansibleExtraVars!: string;
|
||||
public ansibleVaultPassword!: string;
|
||||
|
||||
public static shouldUseRetainedWorkspaceMode(buildParameters: BuildParameters) {
|
||||
return buildParameters.maxRetainedWorkspaces > 0 && Orchestrator.lockedWorkspace !== ``;
|
||||
}
|
||||
@@ -242,6 +265,29 @@ class BuildParameters {
|
||||
cacheUnityInstallationOnMac: Input.cacheUnityInstallationOnMac,
|
||||
unityHubVersionOnMac: Input.unityHubVersionOnMac,
|
||||
dockerWorkspacePath: Input.dockerWorkspacePath,
|
||||
|
||||
// Remote PowerShell provider
|
||||
remotePowershellHost: Input.remotePowershellHost,
|
||||
remotePowershellCredential: Input.remotePowershellCredential,
|
||||
remotePowershellTransport: Input.remotePowershellTransport,
|
||||
|
||||
// GitHub Actions provider
|
||||
githubActionsRepo: Input.githubActionsRepo,
|
||||
githubActionsWorkflow: Input.githubActionsWorkflow,
|
||||
githubActionsToken: Input.githubActionsToken,
|
||||
githubActionsRef: Input.githubActionsRef,
|
||||
|
||||
// GitLab CI provider
|
||||
gitlabProjectId: Input.gitlabProjectId,
|
||||
gitlabTriggerToken: Input.gitlabTriggerToken,
|
||||
gitlabApiUrl: Input.gitlabApiUrl,
|
||||
gitlabRef: Input.gitlabRef,
|
||||
|
||||
// Ansible provider
|
||||
ansibleInventory: Input.ansibleInventory,
|
||||
ansiblePlaybook: Input.ansiblePlaybook,
|
||||
ansibleExtraVars: Input.ansibleExtraVars,
|
||||
ansibleVaultPassword: Input.ansibleVaultPassword,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -282,6 +282,82 @@ class Input {
|
||||
return Input.getInput('skipActivation')?.toLowerCase() ?? 'false';
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// Remote PowerShell provider
|
||||
// ### ### ###
|
||||
|
||||
static get remotePowershellHost(): string {
|
||||
return Input.getInput('remotePowershellHost') ?? '';
|
||||
}
|
||||
|
||||
static get remotePowershellCredential(): string {
|
||||
return Input.getInput('remotePowershellCredential') ?? '';
|
||||
}
|
||||
|
||||
static get remotePowershellTransport(): string {
|
||||
return Input.getInput('remotePowershellTransport') ?? 'wsman';
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// GitHub Actions provider
|
||||
// ### ### ###
|
||||
|
||||
static get githubActionsRepo(): string {
|
||||
return Input.getInput('githubActionsRepo') ?? '';
|
||||
}
|
||||
|
||||
static get githubActionsWorkflow(): string {
|
||||
return Input.getInput('githubActionsWorkflow') ?? '';
|
||||
}
|
||||
|
||||
static get githubActionsToken(): string {
|
||||
return Input.getInput('githubActionsToken') ?? '';
|
||||
}
|
||||
|
||||
static get githubActionsRef(): string {
|
||||
return Input.getInput('githubActionsRef') ?? 'main';
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// GitLab CI provider
|
||||
// ### ### ###
|
||||
|
||||
static get gitlabProjectId(): string {
|
||||
return Input.getInput('gitlabProjectId') ?? '';
|
||||
}
|
||||
|
||||
static get gitlabTriggerToken(): string {
|
||||
return Input.getInput('gitlabTriggerToken') ?? '';
|
||||
}
|
||||
|
||||
static get gitlabApiUrl(): string {
|
||||
return Input.getInput('gitlabApiUrl') ?? 'https://gitlab.com';
|
||||
}
|
||||
|
||||
static get gitlabRef(): string {
|
||||
return Input.getInput('gitlabRef') ?? 'main';
|
||||
}
|
||||
|
||||
// ### ### ###
|
||||
// Ansible provider
|
||||
// ### ### ###
|
||||
|
||||
static get ansibleInventory(): string {
|
||||
return Input.getInput('ansibleInventory') ?? '';
|
||||
}
|
||||
|
||||
static get ansiblePlaybook(): string {
|
||||
return Input.getInput('ansiblePlaybook') ?? '';
|
||||
}
|
||||
|
||||
static get ansibleExtraVars(): string {
|
||||
return Input.getInput('ansibleExtraVars') ?? '';
|
||||
}
|
||||
|
||||
static get ansibleVaultPassword(): string {
|
||||
return Input.getInput('ansibleVaultPassword') ?? '';
|
||||
}
|
||||
|
||||
public static ToEnvVarFormat(input: string) {
|
||||
if (input.toUpperCase() === input) {
|
||||
return input;
|
||||
|
||||
@@ -13,6 +13,10 @@ import OrchestratorEnvironmentVariable from './options/orchestrator-environment-
|
||||
import TestOrchestrator from './providers/test';
|
||||
import LocalOrchestrator from './providers/local';
|
||||
import LocalDockerOrchestrator from './providers/docker';
|
||||
import RemotePowershellProvider from './providers/remote-powershell';
|
||||
import GitHubActionsProvider from './providers/github-actions';
|
||||
import GitLabCIProvider from './providers/gitlab-ci';
|
||||
import AnsibleProvider from './providers/ansible';
|
||||
import loadProvider from './providers/provider-loader';
|
||||
import GitHub from '../github';
|
||||
import SharedWorkspaceLocking from './services/core/shared-workspace-locking';
|
||||
@@ -158,6 +162,18 @@ class Orchestrator {
|
||||
case 'local':
|
||||
Orchestrator.Provider = new LocalOrchestrator();
|
||||
break;
|
||||
case 'remote-powershell':
|
||||
Orchestrator.Provider = new RemotePowershellProvider(Orchestrator.buildParameters);
|
||||
break;
|
||||
case 'github-actions':
|
||||
Orchestrator.Provider = new GitHubActionsProvider(Orchestrator.buildParameters);
|
||||
break;
|
||||
case 'gitlab-ci':
|
||||
Orchestrator.Provider = new GitLabCIProvider(Orchestrator.buildParameters);
|
||||
break;
|
||||
case 'ansible':
|
||||
Orchestrator.Provider = new AnsibleProvider(Orchestrator.buildParameters);
|
||||
break;
|
||||
default:
|
||||
// Try to load provider using the dynamic loader for unknown providers
|
||||
try {
|
||||
|
||||
187
src/model/orchestrator/providers/ansible/index.ts
Normal file
187
src/model/orchestrator/providers/ansible/index.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import BuildParameters from '../../../build-parameters';
|
||||
import { OrchestratorSystem } from '../../services/core/orchestrator-system';
|
||||
import OrchestratorEnvironmentVariable from '../../options/orchestrator-environment-variable';
|
||||
import OrchestratorLogger from '../../services/core/orchestrator-logger';
|
||||
import { ProviderInterface } from '../provider-interface';
|
||||
import OrchestratorSecret from '../../options/orchestrator-secret';
|
||||
import { ProviderResource } from '../provider-resource';
|
||||
import { ProviderWorkflow } from '../provider-workflow';
|
||||
|
||||
/**
|
||||
* Ansible provider — executes Unity builds via Ansible playbooks
|
||||
* against managed inventory.
|
||||
*
|
||||
* Use case: Teams with existing Ansible infrastructure for server
|
||||
* management who want to leverage their inventory for build distribution.
|
||||
*/
|
||||
class AnsibleProvider implements ProviderInterface {
|
||||
private buildParameters: BuildParameters;
|
||||
private inventory: string;
|
||||
private playbook: string;
|
||||
private extraVariables: string;
|
||||
private vaultPassword: string;
|
||||
|
||||
constructor(buildParameters: BuildParameters) {
|
||||
this.buildParameters = buildParameters;
|
||||
this.inventory = buildParameters.ansibleInventory || '';
|
||||
this.playbook = buildParameters.ansiblePlaybook || '';
|
||||
this.extraVariables = buildParameters.ansibleExtraVars || '';
|
||||
this.vaultPassword = buildParameters.ansibleVaultPassword || '';
|
||||
}
|
||||
|
||||
async setupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildGuid: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildParameters: BuildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
): Promise<void> {
|
||||
OrchestratorLogger.log(`[Ansible] Setting up playbook execution`);
|
||||
|
||||
if (!this.inventory) {
|
||||
throw new Error('ansibleInventory is required for the ansible provider');
|
||||
}
|
||||
|
||||
// Verify ansible is available
|
||||
try {
|
||||
const version = await OrchestratorSystem.Run('ansible --version | head -1');
|
||||
OrchestratorLogger.log(`[Ansible] ${version.trim()}`);
|
||||
} catch (error: any) {
|
||||
throw new Error(`Ansible not found on PATH: ${error.message || error}`);
|
||||
}
|
||||
|
||||
// Verify inventory exists
|
||||
try {
|
||||
await OrchestratorSystem.Run(`test -e "${this.inventory}"`);
|
||||
} catch {
|
||||
throw new Error(`Inventory not found: ${this.inventory}`);
|
||||
}
|
||||
}
|
||||
|
||||
async runTaskInWorkflow(
|
||||
buildGuid: string,
|
||||
image: string,
|
||||
commands: string,
|
||||
mountdir: string,
|
||||
workingdir: string,
|
||||
environment: OrchestratorEnvironmentVariable[],
|
||||
secrets: OrchestratorSecret[],
|
||||
): Promise<string> {
|
||||
OrchestratorLogger.log(`[Ansible] Running playbook against inventory ${this.inventory}`);
|
||||
|
||||
if (!this.playbook) {
|
||||
throw new Error(
|
||||
'ansiblePlaybook is required — no default playbook is provided yet. ' +
|
||||
'Provide a playbook that accepts build_guid, build_image, build_commands, mount_dir, and working_dir variables.',
|
||||
);
|
||||
}
|
||||
|
||||
// Build extra-vars JSON
|
||||
// These use snake_case because they are Ansible variable names passed to playbooks
|
||||
const playbookVariables: Record<string, string> = {
|
||||
// eslint-disable-next-line camelcase
|
||||
build_guid: buildGuid,
|
||||
// eslint-disable-next-line camelcase
|
||||
build_image: image,
|
||||
// eslint-disable-next-line camelcase
|
||||
build_commands: commands,
|
||||
// eslint-disable-next-line camelcase
|
||||
mount_dir: mountdir,
|
||||
// eslint-disable-next-line camelcase
|
||||
working_dir: workingdir,
|
||||
};
|
||||
|
||||
for (const element of environment) {
|
||||
playbookVariables[element.name.toLowerCase()] = element.value;
|
||||
}
|
||||
|
||||
// Merge user-provided extra vars
|
||||
if (this.extraVariables) {
|
||||
try {
|
||||
const userVariables = JSON.parse(this.extraVariables);
|
||||
Object.assign(playbookVariables, userVariables);
|
||||
} catch {
|
||||
OrchestratorLogger.logWarning(`[Ansible] Failed to parse ansibleExtraVars as JSON, using as-is`);
|
||||
}
|
||||
}
|
||||
|
||||
const extraVariablesJson = JSON.stringify(playbookVariables).replace(/'/g, "'\\''");
|
||||
|
||||
// Build ansible-playbook command
|
||||
const commandParts = [
|
||||
'ansible-playbook',
|
||||
`-i "${this.inventory}"`,
|
||||
`"${this.playbook}"`,
|
||||
`-e '${extraVariablesJson}'`,
|
||||
'--no-color',
|
||||
];
|
||||
|
||||
if (this.vaultPassword) {
|
||||
commandParts.push(`--vault-password-file "${this.vaultPassword}"`);
|
||||
}
|
||||
|
||||
// Add secret variables as extra environment
|
||||
const environmentPrefix = secrets
|
||||
.map((secret) => `${secret.EnvironmentVariable}='${secret.ParameterValue}'`)
|
||||
.join(' ');
|
||||
|
||||
const fullCommand = environmentPrefix ? `${environmentPrefix} ${commandParts.join(' ')}` : commandParts.join(' ');
|
||||
|
||||
try {
|
||||
const output = await OrchestratorSystem.Run(fullCommand);
|
||||
OrchestratorLogger.log(`[Ansible] Playbook completed successfully`);
|
||||
|
||||
return output;
|
||||
} catch (error: any) {
|
||||
OrchestratorLogger.logWarning(`[Ansible] Playbook failed: ${error.message || error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async cleanupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildParameters: BuildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
): Promise<void> {
|
||||
OrchestratorLogger.log(`[Ansible] Cleanup complete`);
|
||||
}
|
||||
|
||||
async garbageCollect(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
filter: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
previewOnly: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
olderThan: Number,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
fullCache: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
baseDependencies: boolean,
|
||||
): Promise<string> {
|
||||
return '';
|
||||
}
|
||||
|
||||
async listResources(): Promise<ProviderResource[]> {
|
||||
if (!this.inventory) return [];
|
||||
|
||||
const resource = new ProviderResource();
|
||||
resource.Name = this.inventory;
|
||||
|
||||
return [resource];
|
||||
}
|
||||
|
||||
async listWorkflow(): Promise<ProviderWorkflow[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async watchWorkflow(): Promise<string> {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
export default AnsibleProvider;
|
||||
267
src/model/orchestrator/providers/github-actions/index.ts
Normal file
267
src/model/orchestrator/providers/github-actions/index.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
import BuildParameters from '../../../build-parameters';
|
||||
import { OrchestratorSystem } from '../../services/core/orchestrator-system';
|
||||
import OrchestratorEnvironmentVariable from '../../options/orchestrator-environment-variable';
|
||||
import OrchestratorLogger from '../../services/core/orchestrator-logger';
|
||||
import { ProviderInterface } from '../provider-interface';
|
||||
import OrchestratorSecret from '../../options/orchestrator-secret';
|
||||
import { ProviderResource } from '../provider-resource';
|
||||
import { ProviderWorkflow } from '../provider-workflow';
|
||||
|
||||
/**
|
||||
* GitHub Actions provider — triggers builds as workflow_dispatch events
|
||||
* on a target repository via the GitHub API.
|
||||
*
|
||||
* Use case: Distribute builds across orgs, use specialized runner pools,
|
||||
* or trigger builds in repos with Unity licenses.
|
||||
*/
|
||||
class GitHubActionsProvider implements ProviderInterface {
|
||||
private buildParameters: BuildParameters;
|
||||
private repo: string;
|
||||
private workflow: string;
|
||||
private token: string;
|
||||
private ref: string;
|
||||
private runId: number = 0;
|
||||
|
||||
constructor(buildParameters: BuildParameters) {
|
||||
this.buildParameters = buildParameters;
|
||||
this.repo = buildParameters.githubActionsRepo || '';
|
||||
this.workflow = buildParameters.githubActionsWorkflow || '';
|
||||
this.token = buildParameters.githubActionsToken || '';
|
||||
this.ref = buildParameters.githubActionsRef || 'main';
|
||||
}
|
||||
|
||||
async setupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildGuid: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildParameters: BuildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
): Promise<void> {
|
||||
OrchestratorLogger.log(`[GitHubActions] Setting up workflow dispatch to ${this.repo}`);
|
||||
|
||||
if (!this.repo || !this.workflow) {
|
||||
throw new Error('githubActionsRepo and githubActionsWorkflow are required for the github-actions provider');
|
||||
}
|
||||
|
||||
if (!this.token) {
|
||||
throw new Error('githubActionsToken is required (PAT with actions:write scope)');
|
||||
}
|
||||
|
||||
// Verify repository and workflow exist
|
||||
try {
|
||||
const result = await OrchestratorSystem.Run(
|
||||
`GH_TOKEN=${this.token} gh api repos/${this.repo}/actions/workflows/${this.workflow} --jq '.id'`,
|
||||
);
|
||||
OrchestratorLogger.log(`[GitHubActions] Workflow verified: ${this.workflow} (ID: ${result.trim()})`);
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to verify workflow ${this.workflow} in ${this.repo}: ${error.message || error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async runTaskInWorkflow(
|
||||
buildGuid: string,
|
||||
image: string,
|
||||
commands: string,
|
||||
mountdir: string,
|
||||
workingdir: string,
|
||||
environment: OrchestratorEnvironmentVariable[],
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
secrets: OrchestratorSecret[],
|
||||
): Promise<string> {
|
||||
OrchestratorLogger.log(`[GitHubActions] Dispatching workflow ${this.workflow} on ${this.repo}@${this.ref}`);
|
||||
|
||||
// Build inputs payload
|
||||
const inputs: Record<string, string> = {
|
||||
buildGuid,
|
||||
image,
|
||||
commands: Buffer.from(commands).toString('base64'),
|
||||
mountdir,
|
||||
workingdir,
|
||||
};
|
||||
|
||||
// Add environment variables as a JSON input
|
||||
if (environment.length > 0) {
|
||||
inputs.environment = JSON.stringify(environment.map((element) => ({ name: element.name, value: element.value })));
|
||||
}
|
||||
|
||||
// Record the time before dispatch to identify the run
|
||||
const beforeDispatch = new Date().toISOString();
|
||||
|
||||
// Dispatch the workflow
|
||||
const inputsJson = JSON.stringify(inputs).replace(/'/g, "'\\''");
|
||||
try {
|
||||
await OrchestratorSystem.Run(
|
||||
`GH_TOKEN=${this.token} gh api repos/${this.repo}/actions/workflows/${this.workflow}/dispatches -X POST -f ref='${this.ref}' -f "inputs=${inputsJson}"`,
|
||||
);
|
||||
OrchestratorLogger.log(`[GitHubActions] Workflow dispatched`);
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to dispatch workflow: ${error.message || error}`);
|
||||
}
|
||||
|
||||
// Poll for the run to appear
|
||||
OrchestratorLogger.log(`[GitHubActions] Waiting for workflow run to start...`);
|
||||
let attempts = 0;
|
||||
const maxAttempts = 30;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
attempts++;
|
||||
await new Promise((resolve) => setTimeout(resolve, 10_000));
|
||||
|
||||
try {
|
||||
const runsJson = await OrchestratorSystem.Run(
|
||||
`GH_TOKEN=${this.token} gh api "repos/${this.repo}/actions/workflows/${this.workflow}/runs?created=>${beforeDispatch}&per_page=5" --jq '.workflow_runs[0] | {id, status, conclusion}'`,
|
||||
true,
|
||||
);
|
||||
|
||||
const run = JSON.parse(runsJson.trim());
|
||||
if (run.id) {
|
||||
this.runId = run.id;
|
||||
OrchestratorLogger.log(`[GitHubActions] Run started: ${this.runId} (status: ${run.status})`);
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Run not yet available
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.runId) {
|
||||
throw new Error(`Workflow run did not start within ${maxAttempts * 10}s`);
|
||||
}
|
||||
|
||||
// Poll until completion and stream logs
|
||||
let status = 'in_progress';
|
||||
while (status === 'in_progress' || status === 'queued') {
|
||||
await new Promise((resolve) => setTimeout(resolve, 15_000));
|
||||
|
||||
try {
|
||||
const statusJson = await OrchestratorSystem.Run(
|
||||
`GH_TOKEN=${this.token} gh api repos/${this.repo}/actions/runs/${this.runId} --jq '{status, conclusion}'`,
|
||||
true,
|
||||
);
|
||||
|
||||
const result = JSON.parse(statusJson.trim());
|
||||
status = result.status;
|
||||
|
||||
if (status === 'completed') {
|
||||
OrchestratorLogger.log(`[GitHubActions] Run ${this.runId} completed: ${result.conclusion}`);
|
||||
|
||||
if (result.conclusion !== 'success') {
|
||||
throw new Error(`Workflow run failed with conclusion: ${result.conclusion}`);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
OrchestratorLogger.log(`[GitHubActions] Run ${this.runId} status: ${status}`);
|
||||
} catch (error: any) {
|
||||
if (error.message && error.message.includes('conclusion')) {
|
||||
throw error;
|
||||
}
|
||||
OrchestratorLogger.logWarning(`[GitHubActions] Status check error: ${error.message || error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch logs
|
||||
try {
|
||||
const logs = await OrchestratorSystem.Run(
|
||||
`GH_TOKEN=${this.token} gh run view ${this.runId} --repo ${this.repo} --log`,
|
||||
true,
|
||||
);
|
||||
|
||||
return logs;
|
||||
} catch {
|
||||
return `Run ${this.runId} completed successfully (logs unavailable)`;
|
||||
}
|
||||
}
|
||||
|
||||
async cleanupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildParameters: BuildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
): Promise<void> {
|
||||
OrchestratorLogger.log(`[GitHubActions] Cleanup complete (no resources to tear down)`);
|
||||
}
|
||||
|
||||
async garbageCollect(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
filter: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
previewOnly: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
olderThan: Number,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
fullCache: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
baseDependencies: boolean,
|
||||
): Promise<string> {
|
||||
return '';
|
||||
}
|
||||
|
||||
async listResources(): Promise<ProviderResource[]> {
|
||||
if (!this.repo || !this.token) return [];
|
||||
|
||||
try {
|
||||
const runnersJson = await OrchestratorSystem.Run(
|
||||
`GH_TOKEN=${this.token} gh api repos/${this.repo}/actions/runners --jq '.runners[] | .name'`,
|
||||
true,
|
||||
);
|
||||
|
||||
return runnersJson
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.map((name) => {
|
||||
const resource = new ProviderResource();
|
||||
resource.Name = name.trim();
|
||||
|
||||
return resource;
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async listWorkflow(): Promise<ProviderWorkflow[]> {
|
||||
if (!this.repo || !this.token) return [];
|
||||
|
||||
try {
|
||||
const runsJson = await OrchestratorSystem.Run(
|
||||
`GH_TOKEN=${this.token} gh api repos/${this.repo}/actions/runs?per_page=10 --jq '.workflow_runs[] | .name'`,
|
||||
true,
|
||||
);
|
||||
|
||||
return runsJson
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.map((name) => {
|
||||
const workflow = new ProviderWorkflow();
|
||||
workflow.Name = name.trim();
|
||||
|
||||
return workflow;
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async watchWorkflow(): Promise<string> {
|
||||
if (!this.runId) return 'No active run to watch';
|
||||
|
||||
try {
|
||||
return await OrchestratorSystem.Run(
|
||||
`GH_TOKEN=${this.token} gh run watch ${this.runId} --repo ${this.repo}`,
|
||||
true,
|
||||
);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
export default GitHubActionsProvider;
|
||||
211
src/model/orchestrator/providers/gitlab-ci/index.ts
Normal file
211
src/model/orchestrator/providers/gitlab-ci/index.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import BuildParameters from '../../../build-parameters';
|
||||
import { OrchestratorSystem } from '../../services/core/orchestrator-system';
|
||||
import OrchestratorEnvironmentVariable from '../../options/orchestrator-environment-variable';
|
||||
import OrchestratorLogger from '../../services/core/orchestrator-logger';
|
||||
import { ProviderInterface } from '../provider-interface';
|
||||
import OrchestratorSecret from '../../options/orchestrator-secret';
|
||||
import { ProviderResource } from '../provider-resource';
|
||||
import { ProviderWorkflow } from '../provider-workflow';
|
||||
|
||||
/**
|
||||
* GitLab CI provider — triggers builds as GitLab CI pipelines
|
||||
* via the GitLab API.
|
||||
*
|
||||
* Use case: Teams using GitLab CI, hybrid GitHub/GitLab setups,
|
||||
* or GitLab runners with Unity licenses.
|
||||
*/
|
||||
class GitLabCIProvider implements ProviderInterface {
|
||||
private buildParameters: BuildParameters;
|
||||
private projectId: string;
|
||||
private triggerToken: string;
|
||||
private apiUrl: string;
|
||||
private ref: string;
|
||||
private pipelineId: number = 0;
|
||||
|
||||
constructor(buildParameters: BuildParameters) {
|
||||
this.buildParameters = buildParameters;
|
||||
this.projectId = buildParameters.gitlabProjectId || '';
|
||||
this.triggerToken = buildParameters.gitlabTriggerToken || '';
|
||||
this.apiUrl = (buildParameters.gitlabApiUrl || 'https://gitlab.com').replace(/\/+$/, '');
|
||||
this.ref = buildParameters.gitlabRef || 'main';
|
||||
}
|
||||
|
||||
async setupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildGuid: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildParameters: BuildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
): Promise<void> {
|
||||
OrchestratorLogger.log(`[GitLabCI] Setting up pipeline trigger for project ${this.projectId}`);
|
||||
|
||||
if (!this.projectId || !this.triggerToken) {
|
||||
throw new Error('gitlabProjectId and gitlabTriggerToken are required for the gitlab-ci provider');
|
||||
}
|
||||
|
||||
// Verify project access
|
||||
const encodedProject = encodeURIComponent(this.projectId);
|
||||
try {
|
||||
await OrchestratorSystem.Run(
|
||||
`curl -sf -H "PRIVATE-TOKEN: ${this.triggerToken}" "${this.apiUrl}/api/v4/projects/${encodedProject}" -o /dev/null`,
|
||||
);
|
||||
OrchestratorLogger.log(`[GitLabCI] Project access verified`);
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to access GitLab project ${this.projectId}: ${error.message || error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async runTaskInWorkflow(
|
||||
buildGuid: string,
|
||||
image: string,
|
||||
commands: string,
|
||||
mountdir: string,
|
||||
workingdir: string,
|
||||
environment: OrchestratorEnvironmentVariable[],
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
secrets: OrchestratorSecret[],
|
||||
): Promise<string> {
|
||||
OrchestratorLogger.log(`[GitLabCI] Triggering pipeline on project ${this.projectId}@${this.ref}`);
|
||||
|
||||
const encodedProject = encodeURIComponent(this.projectId);
|
||||
|
||||
// Build variables for the pipeline
|
||||
const pipelineVariables: string[] = [
|
||||
`-f "variables[BUILD_GUID]=${buildGuid}"`,
|
||||
`-f "variables[BUILD_IMAGE]=${image}"`,
|
||||
`-f "variables[BUILD_COMMANDS]=${Buffer.from(commands).toString('base64')}"`,
|
||||
`-f "variables[MOUNT_DIR]=${mountdir}"`,
|
||||
`-f "variables[WORKING_DIR]=${workingdir}"`,
|
||||
];
|
||||
|
||||
for (const element of environment) {
|
||||
pipelineVariables.push(`-f "variables[${element.name}]=${element.value}"`);
|
||||
}
|
||||
|
||||
// Trigger pipeline
|
||||
try {
|
||||
const response = await OrchestratorSystem.Run(
|
||||
`curl -sf -X POST "${this.apiUrl}/api/v4/projects/${encodedProject}/trigger/pipeline" -f "token=${
|
||||
this.triggerToken
|
||||
}" -f "ref=${this.ref}" ${pipelineVariables.join(' ')}`,
|
||||
);
|
||||
|
||||
const pipeline = JSON.parse(response);
|
||||
this.pipelineId = pipeline.id;
|
||||
OrchestratorLogger.log(`[GitLabCI] Pipeline triggered: ${this.pipelineId} (status: ${pipeline.status})`);
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to trigger pipeline: ${error.message || error}`);
|
||||
}
|
||||
|
||||
// Poll until completion
|
||||
let status = 'pending';
|
||||
const terminalStatuses = new Set(['success', 'failed', 'canceled', 'skipped']);
|
||||
|
||||
while (!terminalStatuses.has(status)) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 15_000));
|
||||
|
||||
try {
|
||||
const statusResponse = await OrchestratorSystem.Run(
|
||||
`curl -sf -H "PRIVATE-TOKEN: ${this.triggerToken}" "${this.apiUrl}/api/v4/projects/${encodedProject}/pipelines/${this.pipelineId}"`,
|
||||
true,
|
||||
);
|
||||
|
||||
const pipelineStatus = JSON.parse(statusResponse);
|
||||
status = pipelineStatus.status;
|
||||
OrchestratorLogger.log(`[GitLabCI] Pipeline ${this.pipelineId} status: ${status}`);
|
||||
} catch (error: any) {
|
||||
OrchestratorLogger.logWarning(`[GitLabCI] Status check error: ${error.message || error}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (status !== 'success') {
|
||||
throw new Error(`Pipeline ${this.pipelineId} finished with status: ${status}`);
|
||||
}
|
||||
|
||||
// Fetch job logs
|
||||
try {
|
||||
const jobsResponse = await OrchestratorSystem.Run(
|
||||
`curl -sf -H "PRIVATE-TOKEN: ${this.triggerToken}" "${this.apiUrl}/api/v4/projects/${encodedProject}/pipelines/${this.pipelineId}/jobs"`,
|
||||
true,
|
||||
);
|
||||
|
||||
const jobs = JSON.parse(jobsResponse);
|
||||
const logs: string[] = [];
|
||||
|
||||
for (const job of jobs) {
|
||||
try {
|
||||
const jobLog = await OrchestratorSystem.Run(
|
||||
`curl -sf -H "PRIVATE-TOKEN: ${this.triggerToken}" "${this.apiUrl}/api/v4/projects/${encodedProject}/jobs/${job.id}/trace"`,
|
||||
true,
|
||||
);
|
||||
logs.push(`=== Job: ${job.name} (${job.status}) ===\n${jobLog}`);
|
||||
} catch {
|
||||
logs.push(`=== Job: ${job.name} (${job.status}) === (logs unavailable)`);
|
||||
}
|
||||
}
|
||||
|
||||
return logs.join('\n\n');
|
||||
} catch {
|
||||
return `Pipeline ${this.pipelineId} completed successfully (logs unavailable)`;
|
||||
}
|
||||
}
|
||||
|
||||
async cleanupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildParameters: BuildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
): Promise<void> {
|
||||
OrchestratorLogger.log(`[GitLabCI] Cleanup complete`);
|
||||
}
|
||||
|
||||
async garbageCollect(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
filter: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
previewOnly: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
olderThan: Number,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
fullCache: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
baseDependencies: boolean,
|
||||
): Promise<string> {
|
||||
return '';
|
||||
}
|
||||
|
||||
async listResources(): Promise<ProviderResource[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async listWorkflow(): Promise<ProviderWorkflow[]> {
|
||||
if (!this.projectId || !this.triggerToken) return [];
|
||||
|
||||
try {
|
||||
const encodedProject = encodeURIComponent(this.projectId);
|
||||
const response = await OrchestratorSystem.Run(
|
||||
`curl -sf -H "PRIVATE-TOKEN: ${this.triggerToken}" "${this.apiUrl}/api/v4/projects/${encodedProject}/pipelines?per_page=10"`,
|
||||
true,
|
||||
);
|
||||
|
||||
return JSON.parse(response).map((pipeline: any) => {
|
||||
const workflow = new ProviderWorkflow();
|
||||
workflow.Name = `Pipeline #${pipeline.id} (${pipeline.status})`;
|
||||
|
||||
return workflow;
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async watchWorkflow(): Promise<string> {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
export default GitLabCIProvider;
|
||||
160
src/model/orchestrator/providers/remote-powershell/index.ts
Normal file
160
src/model/orchestrator/providers/remote-powershell/index.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import BuildParameters from '../../../build-parameters';
|
||||
import { OrchestratorSystem } from '../../services/core/orchestrator-system';
|
||||
import OrchestratorEnvironmentVariable from '../../options/orchestrator-environment-variable';
|
||||
import OrchestratorLogger from '../../services/core/orchestrator-logger';
|
||||
import { ProviderInterface } from '../provider-interface';
|
||||
import OrchestratorSecret from '../../options/orchestrator-secret';
|
||||
import { ProviderResource } from '../provider-resource';
|
||||
import { ProviderWorkflow } from '../provider-workflow';
|
||||
|
||||
/**
|
||||
* Remote PowerShell provider — executes Unity builds on remote machines
|
||||
* via PowerShell Remoting (WinRM or SSH).
|
||||
*
|
||||
* Use case: Teams with dedicated build machines not part of a CI system.
|
||||
*/
|
||||
class RemotePowershellProvider implements ProviderInterface {
|
||||
private buildParameters: BuildParameters;
|
||||
private host: string;
|
||||
private transport: string;
|
||||
private credential: string;
|
||||
private sessionId: string = '';
|
||||
|
||||
constructor(buildParameters: BuildParameters) {
|
||||
this.buildParameters = buildParameters;
|
||||
this.host = buildParameters.remotePowershellHost || '';
|
||||
this.transport = buildParameters.remotePowershellTransport || 'wsman';
|
||||
this.credential = buildParameters.remotePowershellCredential || '';
|
||||
}
|
||||
|
||||
async setupWorkflow(
|
||||
buildGuid: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildParameters: BuildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
): Promise<void> {
|
||||
OrchestratorLogger.log(`[RemotePowershell] Setting up remote session to ${this.host} via ${this.transport}`);
|
||||
|
||||
if (!this.host) {
|
||||
throw new Error('remotePowershellHost is required for the remote-powershell provider');
|
||||
}
|
||||
|
||||
// Test connectivity
|
||||
const testCommand = this.buildPwshCommand(`Test-WSMan -ComputerName "${this.host}" -ErrorAction Stop`);
|
||||
try {
|
||||
await OrchestratorSystem.Run(testCommand);
|
||||
OrchestratorLogger.log(`[RemotePowershell] Connection test passed`);
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to connect to remote host ${this.host}: ${error.message || error}`);
|
||||
}
|
||||
|
||||
this.sessionId = buildGuid;
|
||||
OrchestratorLogger.log(`[RemotePowershell] Session ${this.sessionId} ready`);
|
||||
}
|
||||
|
||||
async runTaskInWorkflow(
|
||||
buildGuid: string,
|
||||
image: string,
|
||||
commands: string,
|
||||
mountdir: string,
|
||||
workingdir: string,
|
||||
environment: OrchestratorEnvironmentVariable[],
|
||||
secrets: OrchestratorSecret[],
|
||||
): Promise<string> {
|
||||
OrchestratorLogger.log(`[RemotePowershell] Executing task on ${this.host}`);
|
||||
|
||||
// Build environment variable block for remote session
|
||||
const environmentBlock = environment.map((element) => `$env:${element.name} = '${element.value}'`).join('; ');
|
||||
|
||||
const secretBlock = secrets
|
||||
.map((secret) => `$env:${secret.EnvironmentVariable} = '${secret.ParameterValue}'`)
|
||||
.join('; ');
|
||||
|
||||
// Wrap commands for remote execution
|
||||
const remoteScript = [environmentBlock, secretBlock, `Set-Location "${workingdir}"`, commands]
|
||||
.filter(Boolean)
|
||||
.join('; ');
|
||||
|
||||
const invokeCommand = this.buildInvokeCommand(remoteScript);
|
||||
|
||||
try {
|
||||
const output = await OrchestratorSystem.Run(invokeCommand);
|
||||
OrchestratorLogger.log(`[RemotePowershell] Task completed successfully`);
|
||||
|
||||
return output;
|
||||
} catch (error: any) {
|
||||
OrchestratorLogger.logWarning(`[RemotePowershell] Task failed: ${error.message || error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async cleanupWorkflow(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
buildParameters: BuildParameters,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
branchName: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
defaultSecretsArray: { ParameterKey: string; EnvironmentVariable: string; ParameterValue: string }[],
|
||||
): Promise<void> {
|
||||
OrchestratorLogger.log(`[RemotePowershell] Cleaning up session ${this.sessionId}`);
|
||||
|
||||
// Remote sessions are stateless per invocation — no cleanup needed
|
||||
}
|
||||
|
||||
async garbageCollect(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
filter: string,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
previewOnly: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
olderThan: Number,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
fullCache: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
baseDependencies: boolean,
|
||||
): Promise<string> {
|
||||
OrchestratorLogger.log(`[RemotePowershell] Garbage collection not supported for remote PowerShell provider`);
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
async listResources(): Promise<ProviderResource[]> {
|
||||
const resource = new ProviderResource();
|
||||
resource.Name = this.host;
|
||||
|
||||
return [resource];
|
||||
}
|
||||
|
||||
async listWorkflow(): Promise<ProviderWorkflow[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async watchWorkflow(): Promise<string> {
|
||||
return '';
|
||||
}
|
||||
|
||||
private buildPwshCommand(script: string): string {
|
||||
return `pwsh -NoProfile -NonInteractive -Command "${script.replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
|
||||
private buildInvokeCommand(remoteScript: string): string {
|
||||
const escapedScript = remoteScript.replace(/"/g, '\\"').replace(/'/g, "''");
|
||||
|
||||
if (this.transport === 'ssh') {
|
||||
return `pwsh -NoProfile -NonInteractive -Command "Invoke-Command -HostName '${this.host}' -ScriptBlock { ${escapedScript} }"`;
|
||||
}
|
||||
|
||||
// WinRM (default)
|
||||
const credentialPart = this.credential
|
||||
? `-Credential (New-Object PSCredential('${this.credential.split(':')[0]}', (ConvertTo-SecureString '${
|
||||
this.credential.split(':')[1]
|
||||
}' -AsPlainText -Force)))`
|
||||
: '';
|
||||
|
||||
return `pwsh -NoProfile -NonInteractive -Command "Invoke-Command -ComputerName '${this.host}' ${credentialPart} -ScriptBlock { ${escapedScript} }"`;
|
||||
}
|
||||
}
|
||||
export default RemotePowershellProvider;
|
||||
Reference in New Issue
Block a user