Merge remote-tracking branch 'origin/feature/ci-platform-providers' into release/lts-2.0.0

# Conflicts:
#	action.yml
#	dist/index.js.map
#	src/model/build-parameters.ts
#	src/model/input.ts
#	src/model/orchestrator/orchestrator.ts
This commit is contained in:
frostebite
2026-03-05 21:07:52 +00:00
15 changed files with 3341 additions and 1 deletions
Generated Vendored
+893
View File
@@ -376,6 +376,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) {
@@ -1827,6 +1846,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;
@@ -2639,6 +2715,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));
@@ -2757,6 +2837,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 {
@@ -2906,6 +2998,196 @@ Orchestrator.validateAwsTemplates = false;
exports["default"] = Orchestrator;
/***/ }),
/***/ 72073:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
const core = __importStar(__nccwpck_require__(42186));
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 ansible-playbook binary exists (may be separate from ansible)
try {
await orchestrator_system_1.OrchestratorSystem.Run('command -v ansible-playbook || which ansible-playbook || where ansible-playbook');
orchestrator_logger_1.default.log(`[Ansible] ansible-playbook binary verified`);
}
catch (error) {
core.error('ansible-playbook not found. Install Ansible or ensure it is in PATH.');
throw new Error(`ansible-playbook 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:
@@ -5064,6 +5346,455 @@ 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 __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
const core = __importStar(__nccwpck_require__(42186));
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);
const MAX_POLLING_DURATION_MS = 14400000; // 4 hours
/**
* 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 (with maximum duration guard)
let status = 'in_progress';
const pollingStartTime = Date.now();
const runUrl = `https://github.com/${this.repo}/actions/runs/${this.runId}`;
while (status === 'in_progress' || status === 'queued') {
const elapsedMs = Date.now() - pollingStartTime;
if (elapsedMs >= MAX_POLLING_DURATION_MS) {
const hours = Math.round(MAX_POLLING_DURATION_MS / 3600000);
const message = `GitHub Actions workflow did not complete within ${hours} hours. Run URL: ${runUrl}`;
core.error(message);
throw new Error(message);
}
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;
}
if (error.message && error.message.includes('did not complete within')) {
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 __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
const core = __importStar(__nccwpck_require__(42186));
const orchestrator_system_1 = __nccwpck_require__(9744);
const orchestrator_logger_1 = __importDefault(__nccwpck_require__(32549));
const provider_workflow_1 = __nccwpck_require__(60511);
const MAX_POLLING_DURATION_MS = 14400000; // 4 hours
/**
* 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 (with maximum duration guard)
let status = 'pending';
const terminalStatuses = new Set(['success', 'failed', 'canceled', 'skipped']);
const pollingStartTime = Date.now();
const pipelineUrl = `${this.apiUrl}/${this.projectId}/-/pipelines/${this.pipelineId}`;
while (!terminalStatuses.has(status)) {
const elapsedMs = Date.now() - pollingStartTime;
if (elapsedMs >= MAX_POLLING_DURATION_MS) {
const hours = Math.round(MAX_POLLING_DURATION_MS / 3600000);
const message = `GitLab CI pipeline did not complete within ${hours} hours. Pipeline URL: ${pipelineUrl}`;
core.error(message);
throw new Error(message);
}
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:
@@ -7284,6 +8015,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:
@@ -7405,6 +8150,154 @@ 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)
// Split on the FIRST colon only — passwords may contain colons
let credentialPart = '';
if (this.credential) {
const colonIndex = this.credential.indexOf(':');
if (colonIndex === -1) {
throw new Error('remotePowershellCredential must be in "username:password" format (no colon found)');
}
const user = this.credential.substring(0, colonIndex);
const pass = this.credential.substring(colonIndex + 1);
credentialPart = `-Credential (New-Object PSCredential('${user}', (ConvertTo-SecureString '${pass}' -AsPlainText -Force)))`;
}
return `pwsh -NoProfile -NonInteractive -Command "Invoke-Command -ComputerName '${this.host}' ${credentialPart} -ScriptBlock { ${escapedScript} }"`;
}
}
exports["default"] = RemotePowershellProvider;
/***/ }),
/***/ 6389:
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long