mirror of
https://github.com/game-ci/unity-builder.git
synced 2026-02-03 15:39:07 +08:00
lint fix
This commit is contained in:
81
dist/index.js
generated
vendored
81
dist/index.js
generated
vendored
@@ -1828,7 +1828,7 @@ class AwsClientFactory {
|
||||
};
|
||||
}
|
||||
// Return undefined to let AWS SDK use default credential chain
|
||||
return undefined;
|
||||
return;
|
||||
}
|
||||
static getCloudFormation() {
|
||||
if (!this.cloudFormation) {
|
||||
@@ -3939,23 +3939,18 @@ class Kubernetes {
|
||||
if (process.env['cloudRunnerTests'] === 'true') {
|
||||
try {
|
||||
cloud_runner_logger_1.default.log('Cleaning up old images in k3d node before pulling new image...');
|
||||
const { CloudRunnerSystem } = await Promise.resolve().then(() => __importStar(__nccwpck_require__(4197)));
|
||||
const { CloudRunnerSystem: CloudRunnerSystemModule } = await Promise.resolve().then(() => __importStar(__nccwpck_require__(4197)));
|
||||
// Aggressive cleanup: remove stopped containers and non-Unity images
|
||||
// IMPORTANT: Preserve Unity images (unityci/editor) to avoid re-pulling the 3.9GB image
|
||||
const K3D_NODE_CONTAINERS = ['k3d-unity-builder-agent-0', 'k3d-unity-builder-server-0'];
|
||||
const cleanupCommands = [];
|
||||
for (const NODE of K3D_NODE_CONTAINERS) {
|
||||
// Remove all stopped containers (this frees runtime space but keeps images)
|
||||
cleanupCommands.push(`docker exec ${NODE} sh -c "crictl rm --all 2>/dev/null || true" || true`);
|
||||
// Remove non-Unity images only (preserve unityci/editor images to avoid re-pulling 3.9GB)
|
||||
// This is safe because we explicitly exclude Unity images from deletion
|
||||
cleanupCommands.push(`docker exec ${NODE} sh -c "for img in \$(crictl images -q 2>/dev/null); do repo=\$(crictl inspecti \$img --format '{{.repo}}' 2>/dev/null || echo ''); if echo \"\$repo\" | grep -qvE 'unityci/editor|unity'; then crictl rmi \$img 2>/dev/null || true; fi; done" || true`);
|
||||
// Clean up unused layers (prune should preserve referenced images)
|
||||
cleanupCommands.push(`docker exec ${NODE} sh -c "crictl rmi --prune 2>/dev/null || true" || true`);
|
||||
cleanupCommands.push(`docker exec ${NODE} sh -c "crictl rm --all 2>/dev/null || true" || true`, `docker exec ${NODE} sh -c "for img in $(crictl images -q 2>/dev/null); do repo=$(crictl inspecti $img --format '{{.repo}}' 2>/dev/null || echo ''); if echo "$repo" | grep -qvE 'unityci/editor|unity'; then crictl rmi $img 2>/dev/null || true; fi; done" || true`, `docker exec ${NODE} sh -c "crictl rmi --prune 2>/dev/null || true" || true`);
|
||||
}
|
||||
for (const cmd of cleanupCommands) {
|
||||
try {
|
||||
await CloudRunnerSystem.Run(cmd, true, true);
|
||||
await CloudRunnerSystemModule.Run(cmd, true, true);
|
||||
}
|
||||
catch (cmdError) {
|
||||
// Ignore individual command failures - cleanup is best effort
|
||||
@@ -3975,14 +3970,14 @@ class Kubernetes {
|
||||
// If not cached, try to ensure it's available to avoid disk pressure during pull
|
||||
if (process.env['cloudRunnerTests'] === 'true' && image.includes('unityci/editor')) {
|
||||
try {
|
||||
const { CloudRunnerSystem } = await Promise.resolve().then(() => __importStar(__nccwpck_require__(4197)));
|
||||
const { CloudRunnerSystem: CloudRunnerSystemModule2 } = await Promise.resolve().then(() => __importStar(__nccwpck_require__(4197)));
|
||||
// Check if image is cached on agent node (where pods run)
|
||||
const agentImageCheck = await CloudRunnerSystem.Run(`docker exec k3d-unity-builder-agent-0 sh -c "crictl images | grep -q unityci/editor && echo 'cached' || echo 'not_cached'" || echo 'not_cached'`, true, true);
|
||||
const agentImageCheck = await CloudRunnerSystemModule2.Run(`docker exec k3d-unity-builder-agent-0 sh -c "crictl images | grep -q unityci/editor && echo 'cached' || echo 'not_cached'" || echo 'not_cached'`, true, true);
|
||||
if (agentImageCheck.includes('not_cached')) {
|
||||
// Check if image is on server node
|
||||
const serverImageCheck = await CloudRunnerSystem.Run(`docker exec k3d-unity-builder-server-0 sh -c "crictl images | grep -q unityci/editor && echo 'cached' || echo 'not_cached'" || echo 'not_cached'`, true, true);
|
||||
const serverImageCheck = await CloudRunnerSystemModule2.Run(`docker exec k3d-unity-builder-server-0 sh -c "crictl images | grep -q unityci/editor && echo 'cached' || echo 'not_cached'" || echo 'not_cached'`, true, true);
|
||||
// Check available disk space on agent node
|
||||
const diskInfo = await CloudRunnerSystem.Run('docker exec k3d-unity-builder-agent-0 sh -c "df -h /var/lib/rancher/k3s 2>/dev/null | tail -1 || df -h / 2>/dev/null | tail -1 || echo unknown" || echo unknown', true, true);
|
||||
const diskInfo = await CloudRunnerSystemModule2.Run('docker exec k3d-unity-builder-agent-0 sh -c "df -h /var/lib/rancher/k3s 2>/dev/null | tail -1 || df -h / 2>/dev/null | tail -1 || echo unknown" || echo unknown', true, true);
|
||||
cloud_runner_logger_1.default.logWarning(`Unity image not cached on agent node (where pods run). Server node: ${serverImageCheck.includes('cached') ? 'has image' : 'no image'}. Disk info: ${diskInfo.trim()}. Pod will attempt to pull image (3.9GB) which may fail due to disk pressure.`);
|
||||
// If image is on server but not agent, log a warning
|
||||
// NOTE: We don't attempt to pull here because:
|
||||
@@ -3996,9 +3991,9 @@ class Kubernetes {
|
||||
else {
|
||||
// Image not on either node - check if we have enough space to pull
|
||||
// Extract available space from disk info
|
||||
const availableSpaceMatch = diskInfo.match(/(\d+(?:\.\d+)?)\s*([GMK]?i?B)/i);
|
||||
const availableSpaceMatch = diskInfo.match(/(\d+(?:\.\d+)?)\s*([gkm]?i?b)/i);
|
||||
if (availableSpaceMatch) {
|
||||
const availableValue = parseFloat(availableSpaceMatch[1]);
|
||||
const availableValue = Number.parseFloat(availableSpaceMatch[1]);
|
||||
const availableUnit = availableSpaceMatch[2].toUpperCase();
|
||||
let availableGB = availableValue;
|
||||
if (availableUnit.includes('M')) {
|
||||
@@ -4247,8 +4242,8 @@ class KubernetesJobSpecFactory {
|
||||
const memoryMB = Number.parseInt(buildParameters.containerMemory);
|
||||
const cpuMB = Number.parseInt(buildParameters.containerCpu);
|
||||
return {
|
||||
memory: !isNaN(memoryMB) && memoryMB > 0 ? `${memoryMB / 1024}G` : '750M',
|
||||
cpu: !isNaN(cpuMB) && cpuMB > 0 ? `${cpuMB / 1024}` : '1',
|
||||
memory: !Number.isNaN(memoryMB) && memoryMB > 0 ? `${memoryMB / 1024}G` : '750M',
|
||||
cpu: !Number.isNaN(cpuMB) && cpuMB > 0 ? `${cpuMB / 1024}` : '1',
|
||||
};
|
||||
})(),
|
||||
},
|
||||
@@ -4784,14 +4779,14 @@ class KubernetesStorage {
|
||||
if (pvcEvents.length > 0) {
|
||||
cloud_runner_logger_1.default.log(`PVC Events: ${JSON.stringify(pvcEvents, undefined, 2)}`);
|
||||
// Check if event indicates WaitForFirstConsumer
|
||||
const waitForConsumerEvent = pvcEvents.find((e) => e.reason === 'WaitForFirstConsumer' || e.message?.includes('waiting for first consumer'));
|
||||
const waitForConsumerEvent = pvcEvents.find((event) => event.reason === 'WaitForFirstConsumer' || event.message?.includes('waiting for first consumer'));
|
||||
if (waitForConsumerEvent) {
|
||||
cloud_runner_logger_1.default.log(`PVC is waiting for first consumer. This is normal for WaitForFirstConsumer storage classes. Proceeding without waiting.`);
|
||||
return true; // Exit wait loop - PVC will bind when pod is created
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (eventError) {
|
||||
catch {
|
||||
// Ignore event fetch errors
|
||||
}
|
||||
}
|
||||
@@ -4824,7 +4819,7 @@ class KubernetesStorage {
|
||||
count: x.count || 0,
|
||||
}));
|
||||
}
|
||||
catch (eventError) {
|
||||
catch {
|
||||
// Ignore event fetch errors
|
||||
}
|
||||
// Check if storage class exists
|
||||
@@ -4841,12 +4836,10 @@ class KubernetesStorage {
|
||||
storageClassInfo = `StorageClass "${storageClassName}" exists. Provisioner: ${sc.body.provisioner || 'unknown'}`;
|
||||
}
|
||||
catch (scError) {
|
||||
if (scError.statusCode === 404) {
|
||||
storageClassInfo = `StorageClass "${storageClassName}" does NOT exist! This is likely why the PVC is stuck in Pending.`;
|
||||
}
|
||||
else {
|
||||
storageClassInfo = `Failed to check StorageClass "${storageClassName}": ${scError.message || scError}`;
|
||||
}
|
||||
storageClassInfo =
|
||||
scError.statusCode === 404
|
||||
? `StorageClass "${storageClassName}" does NOT exist! This is likely why the PVC is stuck in Pending.`
|
||||
: `Failed to check StorageClass "${storageClassName}": ${scError.message || scError}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4949,8 +4942,8 @@ class KubernetesTaskRunner {
|
||||
// split output chunk and handle per line
|
||||
for (const chunk of outputChunk.split(`\n`)) {
|
||||
// Skip empty chunks and kubectl error messages (case-insensitive)
|
||||
const lowerChunk = chunk.toLowerCase();
|
||||
if (chunk.trim() && !lowerChunk.includes('unable to retrieve container logs')) {
|
||||
const lowerCaseChunk = chunk.toLowerCase();
|
||||
if (chunk.trim() && !lowerCaseChunk.includes('unable to retrieve container logs')) {
|
||||
({ shouldReadLogs, shouldCleanup, output } = follow_log_stream_service_1.FollowLogStreamService.handleIteration(chunk, shouldReadLogs, shouldCleanup, output));
|
||||
}
|
||||
}
|
||||
@@ -5122,13 +5115,13 @@ class KubernetesTaskRunner {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
cloud_runner_logger_1.default.log(`Trying fallback method: ${attempt.substring(0, 80)}...`);
|
||||
cloud_runner_logger_1.default.log(`Trying fallback method: ${attempt.slice(0, 80)}...`);
|
||||
const result = await cloud_runner_system_1.CloudRunnerSystem.Run(attempt, true, true);
|
||||
if (result && result.trim()) {
|
||||
// Prefer content that has "Collected Logs" over content that doesn't
|
||||
if (!logFileContent || !logFileContent.includes('Collected Logs')) {
|
||||
logFileContent = result;
|
||||
cloud_runner_logger_1.default.log(`Successfully read logs using fallback method (${logFileContent.length} chars): ${attempt.substring(0, 50)}...`);
|
||||
cloud_runner_logger_1.default.log(`Successfully read logs using fallback method (${logFileContent.length} chars): ${attempt.slice(0, 50)}...`);
|
||||
// If this content has "Collected Logs", we're done
|
||||
if (logFileContent.includes('Collected Logs')) {
|
||||
cloud_runner_logger_1.default.log('Fallback method successfully captured "Collected Logs".');
|
||||
@@ -5140,11 +5133,11 @@ class KubernetesTaskRunner {
|
||||
}
|
||||
}
|
||||
else {
|
||||
cloud_runner_logger_1.default.log(`Fallback method returned empty result: ${attempt.substring(0, 50)}...`);
|
||||
cloud_runner_logger_1.default.log(`Fallback method returned empty result: ${attempt.slice(0, 50)}...`);
|
||||
}
|
||||
}
|
||||
catch (attemptError) {
|
||||
cloud_runner_logger_1.default.log(`Fallback method failed: ${attempt.substring(0, 50)}... Error: ${attemptError?.message || attemptError}`);
|
||||
cloud_runner_logger_1.default.log(`Fallback method failed: ${attempt.slice(0, 50)}... Error: ${attemptError?.message || attemptError}`);
|
||||
// Continue to next attempt
|
||||
}
|
||||
}
|
||||
@@ -5269,7 +5262,7 @@ class KubernetesTaskRunner {
|
||||
message += `\nRecent Events:\n${JSON.stringify(podEvents.slice(-5), undefined, 2)}`;
|
||||
}
|
||||
}
|
||||
catch (eventError) {
|
||||
catch {
|
||||
// Ignore event fetch errors
|
||||
}
|
||||
cloud_runner_logger_1.default.logWarning(message);
|
||||
@@ -5365,12 +5358,9 @@ class KubernetesTaskRunner {
|
||||
message += `\n\nScheduling Issue: ${unschedulable.reason || 'Unknown'} - ${unschedulable.message || 'No message'}`;
|
||||
}
|
||||
// Check if pod is assigned to a node
|
||||
if (podStatusDetails?.hostIP) {
|
||||
message += `\n\nPod assigned to node: ${podStatusDetails.hostIP}`;
|
||||
}
|
||||
else {
|
||||
message += `\n\nPod not yet assigned to a node (scheduling pending)`;
|
||||
}
|
||||
message += podStatusDetails?.hostIP
|
||||
? `\n\nPod assigned to node: ${podStatusDetails.hostIP}`
|
||||
: `\n\nPod not yet assigned to a node (scheduling pending)`;
|
||||
}
|
||||
// Check node resources if pod is assigned
|
||||
if (podStatusDetails?.hostIP) {
|
||||
@@ -5380,7 +5370,6 @@ class KubernetesTaskRunner {
|
||||
const assignedNode = nodes.body.items.find((n) => n.status?.addresses?.some((a) => a.address === hostIP));
|
||||
if (assignedNode?.status && assignedNode.metadata?.name) {
|
||||
const allocatable = assignedNode.status.allocatable || {};
|
||||
const capacity = assignedNode.status.capacity || {};
|
||||
message += `\n\nNode Resources (${assignedNode.metadata.name}):\n Allocatable CPU: ${allocatable.cpu || 'unknown'}\n Allocatable Memory: ${allocatable.memory || 'unknown'}\n Allocatable Ephemeral Storage: ${allocatable['ephemeral-storage'] || 'unknown'}`;
|
||||
// Check for taints that might prevent scheduling
|
||||
if (assignedNode.spec?.taints && assignedNode.spec.taints.length > 0) {
|
||||
@@ -5391,12 +5380,12 @@ class KubernetesTaskRunner {
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (nodeError) {
|
||||
catch {
|
||||
// Ignore node check errors
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (podStatusError) {
|
||||
catch {
|
||||
// Ignore pod status fetch errors
|
||||
}
|
||||
}
|
||||
@@ -5466,7 +5455,7 @@ class KubernetesTaskRunner {
|
||||
}
|
||||
cloud_runner_logger_1.default.logWarning(message);
|
||||
}
|
||||
catch (statusError) {
|
||||
catch {
|
||||
message = `Pod ${podName} timed out and could not retrieve final status: ${waitError?.message || waitError}`;
|
||||
cloud_runner_logger_1.default.logWarning(message);
|
||||
}
|
||||
@@ -6507,7 +6496,7 @@ class Caching {
|
||||
// This will fail if the archive is corrupted
|
||||
await cloud_runner_system_1.CloudRunnerSystem.Run(`tar -tf ${cacheSelection}.tar${compressionSuffix} > /dev/null 2>&1 || (echo "Tar file validation failed" && exit 1)`);
|
||||
}
|
||||
catch (validationError) {
|
||||
catch {
|
||||
const message = `Cache archive ${cacheSelection}.tar${compressionSuffix} appears to be corrupted or incomplete. Skipping cache extraction.`;
|
||||
cloud_runner_logger_1.default.logWarning(message);
|
||||
remote_client_logger_1.RemoteClientLogger.logWarning(message);
|
||||
@@ -6854,8 +6843,8 @@ class RemoteClient {
|
||||
await cloud_runner_system_1.CloudRunnerSystem.Run(`git config --global filter.lfs.smudge "git-lfs smudge --skip -- %f"`);
|
||||
await cloud_runner_system_1.CloudRunnerSystem.Run(`git config --global filter.lfs.process "git-lfs filter-process --skip"`);
|
||||
try {
|
||||
const depthArg = cloud_runner_options_1.default.cloneDepth !== '0' ? `--depth ${cloud_runner_options_1.default.cloneDepth}` : '';
|
||||
await cloud_runner_system_1.CloudRunnerSystem.Run(`git clone ${depthArg} ${cloud_runner_folders_1.CloudRunnerFolders.targetBuildRepoUrl} ${node_path_1.default.basename(cloud_runner_folders_1.CloudRunnerFolders.repoPathAbsolute)}`.trim());
|
||||
const depthArgument = cloud_runner_options_1.default.cloneDepth !== '0' ? `--depth ${cloud_runner_options_1.default.cloneDepth}` : '';
|
||||
await cloud_runner_system_1.CloudRunnerSystem.Run(`git clone ${depthArgument} ${cloud_runner_folders_1.CloudRunnerFolders.targetBuildRepoUrl} ${node_path_1.default.basename(cloud_runner_folders_1.CloudRunnerFolders.repoPathAbsolute)}`.trim());
|
||||
}
|
||||
catch (error) {
|
||||
throw error;
|
||||
|
||||
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
@@ -28,6 +28,7 @@ class CloudRunner {
|
||||
private static cloudRunnerEnvironmentVariables: CloudRunnerEnvironmentVariable[];
|
||||
static lockedWorkspace: string = ``;
|
||||
public static readonly retainedWorkspacePrefix: string = `retained-workspace`;
|
||||
|
||||
// When true, validates AWS CloudFormation templates even when using local-docker execution
|
||||
// This is set by AWS_FORCE_PROVIDER=aws-local mode
|
||||
public static validateAwsTemplates: boolean = false;
|
||||
@@ -132,6 +133,7 @@ class CloudRunner {
|
||||
break;
|
||||
case 'aws':
|
||||
CloudRunner.Provider = new AwsBuildPlatform(CloudRunner.buildParameters);
|
||||
|
||||
// Validate that AWS provider is actually being used when expected
|
||||
if (isLocalStack && forceAwsProvider) {
|
||||
CloudRunnerLogger.log('✓ AWS provider initialized with LocalStack - AWS functionality will be validated');
|
||||
|
||||
@@ -27,6 +27,7 @@ function getStackWaitTime(): number {
|
||||
if (!Number.isNaN(overrideValue) && overrideValue > 0) {
|
||||
return overrideValue;
|
||||
}
|
||||
|
||||
return DEFAULT_STACK_WAIT_TIME_SECONDS;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export class AwsClientFactory {
|
||||
}
|
||||
|
||||
// Return undefined to let AWS SDK use default credential chain
|
||||
return undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
static getCloudFormation(): CloudFormation {
|
||||
|
||||
@@ -25,6 +25,7 @@ function getStackWaitTime(): number {
|
||||
if (!Number.isNaN(overrideValue) && overrideValue > 0) {
|
||||
return overrideValue;
|
||||
}
|
||||
|
||||
return DEFAULT_STACK_WAIT_TIME_SECONDS;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ function getStackWaitTime(): number {
|
||||
if (!Number.isNaN(overrideValue) && overrideValue > 0) {
|
||||
return overrideValue;
|
||||
}
|
||||
|
||||
return DEFAULT_STACK_WAIT_TIME_SECONDS;
|
||||
}
|
||||
|
||||
|
||||
@@ -165,7 +165,9 @@ class Kubernetes implements ProviderInterface {
|
||||
if (process.env['cloudRunnerTests'] === 'true') {
|
||||
try {
|
||||
CloudRunnerLogger.log('Cleaning up old images in k3d node before pulling new image...');
|
||||
const { CloudRunnerSystem } = await import('../../services/core/cloud-runner-system');
|
||||
const { CloudRunnerSystem: CloudRunnerSystemModule } = await import(
|
||||
'../../services/core/cloud-runner-system'
|
||||
);
|
||||
|
||||
// Aggressive cleanup: remove stopped containers and non-Unity images
|
||||
// IMPORTANT: Preserve Unity images (unityci/editor) to avoid re-pulling the 3.9GB image
|
||||
@@ -174,19 +176,16 @@ class Kubernetes implements ProviderInterface {
|
||||
|
||||
for (const NODE of K3D_NODE_CONTAINERS) {
|
||||
// Remove all stopped containers (this frees runtime space but keeps images)
|
||||
cleanupCommands.push(`docker exec ${NODE} sh -c "crictl rm --all 2>/dev/null || true" || true`);
|
||||
// Remove non-Unity images only (preserve unityci/editor images to avoid re-pulling 3.9GB)
|
||||
// This is safe because we explicitly exclude Unity images from deletion
|
||||
cleanupCommands.push(
|
||||
`docker exec ${NODE} sh -c "for img in \$(crictl images -q 2>/dev/null); do repo=\$(crictl inspecti \$img --format '{{.repo}}' 2>/dev/null || echo ''); if echo \"\$repo\" | grep -qvE 'unityci/editor|unity'; then crictl rmi \$img 2>/dev/null || true; fi; done" || true`,
|
||||
`docker exec ${NODE} sh -c "crictl rm --all 2>/dev/null || true" || true`,
|
||||
`docker exec ${NODE} sh -c "for img in $(crictl images -q 2>/dev/null); do repo=$(crictl inspecti $img --format '{{.repo}}' 2>/dev/null || echo ''); if echo "$repo" | grep -qvE 'unityci/editor|unity'; then crictl rmi $img 2>/dev/null || true; fi; done" || true`,
|
||||
`docker exec ${NODE} sh -c "crictl rmi --prune 2>/dev/null || true" || true`,
|
||||
);
|
||||
// Clean up unused layers (prune should preserve referenced images)
|
||||
cleanupCommands.push(`docker exec ${NODE} sh -c "crictl rmi --prune 2>/dev/null || true" || true`);
|
||||
}
|
||||
|
||||
for (const cmd of cleanupCommands) {
|
||||
try {
|
||||
await CloudRunnerSystem.Run(cmd, true, true);
|
||||
await CloudRunnerSystemModule.Run(cmd, true, true);
|
||||
} catch (cmdError) {
|
||||
// Ignore individual command failures - cleanup is best effort
|
||||
CloudRunnerLogger.log(`Cleanup command failed (non-fatal): ${cmdError}`);
|
||||
@@ -195,6 +194,7 @@ class Kubernetes implements ProviderInterface {
|
||||
CloudRunnerLogger.log('Cleanup completed (containers and non-Unity images removed, Unity images preserved)');
|
||||
} catch (cleanupError) {
|
||||
CloudRunnerLogger.logWarning(`Failed to cleanup images before job creation: ${cleanupError}`);
|
||||
|
||||
// Continue anyway - image might already be cached
|
||||
}
|
||||
}
|
||||
@@ -205,10 +205,12 @@ class Kubernetes implements ProviderInterface {
|
||||
// If not cached, try to ensure it's available to avoid disk pressure during pull
|
||||
if (process.env['cloudRunnerTests'] === 'true' && image.includes('unityci/editor')) {
|
||||
try {
|
||||
const { CloudRunnerSystem } = await import('../../services/core/cloud-runner-system');
|
||||
const { CloudRunnerSystem: CloudRunnerSystemModule2 } = await import(
|
||||
'../../services/core/cloud-runner-system'
|
||||
);
|
||||
|
||||
// Check if image is cached on agent node (where pods run)
|
||||
const agentImageCheck = await CloudRunnerSystem.Run(
|
||||
const agentImageCheck = await CloudRunnerSystemModule2.Run(
|
||||
`docker exec k3d-unity-builder-agent-0 sh -c "crictl images | grep -q unityci/editor && echo 'cached' || echo 'not_cached'" || echo 'not_cached'`,
|
||||
true,
|
||||
true,
|
||||
@@ -216,14 +218,14 @@ class Kubernetes implements ProviderInterface {
|
||||
|
||||
if (agentImageCheck.includes('not_cached')) {
|
||||
// Check if image is on server node
|
||||
const serverImageCheck = await CloudRunnerSystem.Run(
|
||||
const serverImageCheck = await CloudRunnerSystemModule2.Run(
|
||||
`docker exec k3d-unity-builder-server-0 sh -c "crictl images | grep -q unityci/editor && echo 'cached' || echo 'not_cached'" || echo 'not_cached'`,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
// Check available disk space on agent node
|
||||
const diskInfo = await CloudRunnerSystem.Run(
|
||||
const diskInfo = await CloudRunnerSystemModule2.Run(
|
||||
'docker exec k3d-unity-builder-agent-0 sh -c "df -h /var/lib/rancher/k3s 2>/dev/null | tail -1 || df -h / 2>/dev/null | tail -1 || echo unknown" || echo unknown',
|
||||
true,
|
||||
true,
|
||||
@@ -248,9 +250,9 @@ class Kubernetes implements ProviderInterface {
|
||||
} else {
|
||||
// Image not on either node - check if we have enough space to pull
|
||||
// Extract available space from disk info
|
||||
const availableSpaceMatch = diskInfo.match(/(\d+(?:\.\d+)?)\s*([GMK]?i?B)/i);
|
||||
const availableSpaceMatch = diskInfo.match(/(\d+(?:\.\d+)?)\s*([gkm]?i?b)/i);
|
||||
if (availableSpaceMatch) {
|
||||
const availableValue = parseFloat(availableSpaceMatch[1]);
|
||||
const availableValue = Number.parseFloat(availableSpaceMatch[1]);
|
||||
const availableUnit = availableSpaceMatch[2].toUpperCase();
|
||||
let availableGB = availableValue;
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ class KubernetesJobSpecFactory {
|
||||
buildGuid,
|
||||
},
|
||||
};
|
||||
|
||||
// Reduce TTL for tests to free up resources faster (default 9999s = ~2.8 hours)
|
||||
// For CI/test environments, use shorter TTL (300s = 5 minutes) to prevent disk pressure
|
||||
const jobTTL = process.env['cloudRunnerTests'] === 'true' ? 300 : 9999;
|
||||
@@ -116,9 +117,10 @@ class KubernetesJobSpecFactory {
|
||||
// For main build containers, use the configured resources
|
||||
const memoryMB = Number.parseInt(buildParameters.containerMemory);
|
||||
const cpuMB = Number.parseInt(buildParameters.containerCpu);
|
||||
|
||||
return {
|
||||
memory: !isNaN(memoryMB) && memoryMB > 0 ? `${memoryMB / 1024}G` : '750M',
|
||||
cpu: !isNaN(cpuMB) && cpuMB > 0 ? `${cpuMB / 1024}` : '1',
|
||||
memory: !Number.isNaN(memoryMB) && memoryMB > 0 ? `${memoryMB / 1024}G` : '750M',
|
||||
cpu: !Number.isNaN(cpuMB) && cpuMB > 0 ? `${cpuMB / 1024}` : '1',
|
||||
};
|
||||
})(),
|
||||
},
|
||||
@@ -163,6 +165,7 @@ class KubernetesJobSpecFactory {
|
||||
},
|
||||
],
|
||||
restartPolicy: 'Never',
|
||||
|
||||
// Add tolerations for CI/test environments to allow scheduling even with disk pressure
|
||||
// This is acceptable for CI where we aggressively clean up disk space
|
||||
tolerations: [
|
||||
@@ -196,6 +199,7 @@ class KubernetesJobSpecFactory {
|
||||
// Only set ephemeral-storage request for production builds
|
||||
job.spec.template.spec.containers[0].resources.requests[`ephemeral-storage`] = '2Gi';
|
||||
}
|
||||
|
||||
// For tests, don't set ephemeral-storage request - let Kubernetes use available space
|
||||
|
||||
return job;
|
||||
|
||||
@@ -89,6 +89,7 @@ class KubernetesStorage {
|
||||
|
||||
if (shouldSkipWait) {
|
||||
CloudRunnerLogger.log(`Skipping PVC wait - will bind when pod is created`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,16 +124,18 @@ class KubernetesStorage {
|
||||
|
||||
// Check if event indicates WaitForFirstConsumer
|
||||
const waitForConsumerEvent = pvcEvents.find(
|
||||
(e) => e.reason === 'WaitForFirstConsumer' || e.message?.includes('waiting for first consumer'),
|
||||
(event) =>
|
||||
event.reason === 'WaitForFirstConsumer' || event.message?.includes('waiting for first consumer'),
|
||||
);
|
||||
if (waitForConsumerEvent) {
|
||||
CloudRunnerLogger.log(
|
||||
`PVC is waiting for first consumer. This is normal for WaitForFirstConsumer storage classes. Proceeding without waiting.`,
|
||||
);
|
||||
|
||||
return true; // Exit wait loop - PVC will bind when pod is created
|
||||
}
|
||||
}
|
||||
} catch (eventError) {
|
||||
} catch {
|
||||
// Ignore event fetch errors
|
||||
}
|
||||
}
|
||||
@@ -169,7 +172,7 @@ class KubernetesStorage {
|
||||
type: x.type || '',
|
||||
count: x.count || 0,
|
||||
}));
|
||||
} catch (eventError) {
|
||||
} catch {
|
||||
// Ignore event fetch errors
|
||||
}
|
||||
|
||||
@@ -189,11 +192,10 @@ class KubernetesStorage {
|
||||
sc.body.provisioner || 'unknown'
|
||||
}`;
|
||||
} catch (scError: any) {
|
||||
if (scError.statusCode === 404) {
|
||||
storageClassInfo = `StorageClass "${storageClassName}" does NOT exist! This is likely why the PVC is stuck in Pending.`;
|
||||
} else {
|
||||
storageClassInfo = `Failed to check StorageClass "${storageClassName}": ${scError.message || scError}`;
|
||||
}
|
||||
storageClassInfo =
|
||||
scError.statusCode === 404
|
||||
? `StorageClass "${storageClassName}" does NOT exist! This is likely why the PVC is stuck in Pending.`
|
||||
: `Failed to check StorageClass "${storageClassName}": ${scError.message || scError}`;
|
||||
}
|
||||
}
|
||||
} catch (scCheckError) {
|
||||
|
||||
@@ -38,6 +38,7 @@ class KubernetesTaskRunner {
|
||||
const lowerChunk = outputChunk.toLowerCase();
|
||||
if (lowerChunk.includes('unable to retrieve container logs')) {
|
||||
CloudRunnerLogger.log(`Filtered kubectl error: ${outputChunk.trim()}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -46,8 +47,8 @@ class KubernetesTaskRunner {
|
||||
// split output chunk and handle per line
|
||||
for (const chunk of outputChunk.split(`\n`)) {
|
||||
// Skip empty chunks and kubectl error messages (case-insensitive)
|
||||
const lowerChunk = chunk.toLowerCase();
|
||||
if (chunk.trim() && !lowerChunk.includes('unable to retrieve container logs')) {
|
||||
const lowerCaseChunk = chunk.toLowerCase();
|
||||
if (chunk.trim() && !lowerCaseChunk.includes('unable to retrieve container logs')) {
|
||||
({ shouldReadLogs, shouldCleanup, output } = FollowLogStreamService.handleIteration(
|
||||
chunk,
|
||||
shouldReadLogs,
|
||||
@@ -66,6 +67,7 @@ class KubernetesTaskRunner {
|
||||
true,
|
||||
callback,
|
||||
);
|
||||
|
||||
// Reset failure count on success
|
||||
kubectlLogsFailedCount = 0;
|
||||
} catch (error: any) {
|
||||
@@ -105,6 +107,7 @@ class KubernetesTaskRunner {
|
||||
// Pod is terminated, try to create a temporary pod to read from the PVC
|
||||
// First, check if we can still access the pod's filesystem
|
||||
CloudRunnerLogger.log(`Pod is terminated, attempting to read log file via temporary pod...`);
|
||||
|
||||
// For terminated pods, we might not be able to exec, so we'll skip this fallback
|
||||
// and rely on the log file being written to the PVC (if mounted)
|
||||
CloudRunnerLogger.logWarning(`Cannot read log file from terminated pod via exec`);
|
||||
@@ -112,6 +115,7 @@ class KubernetesTaskRunner {
|
||||
|
||||
if (logFileContent && logFileContent.trim()) {
|
||||
CloudRunnerLogger.log(`Successfully read log file from pod (${logFileContent.length} chars)`);
|
||||
|
||||
// Process the log file content line by line
|
||||
for (const line of logFileContent.split(`\n`)) {
|
||||
const lowerLine = line.toLowerCase();
|
||||
@@ -132,12 +136,14 @@ class KubernetesTaskRunner {
|
||||
}
|
||||
} else {
|
||||
CloudRunnerLogger.logWarning(`Log file read returned empty content, continuing with available logs`);
|
||||
|
||||
// If we can't read the log file, break out of the loop to return whatever logs we have
|
||||
// This prevents infinite retries when kubectl logs consistently fails
|
||||
break;
|
||||
}
|
||||
} catch (execError: any) {
|
||||
CloudRunnerLogger.logWarning(`Failed to read log file from pod: ${execError}`);
|
||||
|
||||
// If we've exhausted all options, break to return whatever logs we have
|
||||
break;
|
||||
}
|
||||
@@ -220,6 +226,7 @@ class KubernetesTaskRunner {
|
||||
|
||||
if (needsFallback) {
|
||||
CloudRunnerLogger.log('Output is empty, attempting aggressive log collection fallback...');
|
||||
|
||||
// Give the pod a moment to finish writing logs before we try to read them
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
}
|
||||
@@ -251,11 +258,14 @@ class KubernetesTaskRunner {
|
||||
const attempts = [
|
||||
// For terminated pods, try --previous first
|
||||
`kubectl exec ${podName} -c ${containerName} -n ${namespace} --previous -- cat /home/job-log.txt 2>/dev/null || echo ""`,
|
||||
|
||||
// Try current container
|
||||
`kubectl exec ${podName} -c ${containerName} -n ${namespace} -- cat /home/job-log.txt 2>/dev/null || echo ""`,
|
||||
|
||||
// Try reading from PVC (/data) in case log was copied there
|
||||
`kubectl exec ${podName} -c ${containerName} -n ${namespace} --previous -- cat /data/job-log.txt 2>/dev/null || echo ""`,
|
||||
`kubectl exec ${podName} -c ${containerName} -n ${namespace} -- cat /data/job-log.txt 2>/dev/null || echo ""`,
|
||||
|
||||
// Try kubectl logs as fallback (might capture stdout even if exec fails)
|
||||
`kubectl logs ${podName} -c ${containerName} -n ${namespace} --previous 2>/dev/null || echo ""`,
|
||||
`kubectl logs ${podName} -c ${containerName} -n ${namespace} 2>/dev/null || echo ""`,
|
||||
@@ -268,18 +278,19 @@ class KubernetesTaskRunner {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
CloudRunnerLogger.log(`Trying fallback method: ${attempt.substring(0, 80)}...`);
|
||||
CloudRunnerLogger.log(`Trying fallback method: ${attempt.slice(0, 80)}...`);
|
||||
const result = await CloudRunnerSystem.Run(attempt, true, true);
|
||||
if (result && result.trim()) {
|
||||
// Prefer content that has "Collected Logs" over content that doesn't
|
||||
if (!logFileContent || !logFileContent.includes('Collected Logs')) {
|
||||
logFileContent = result;
|
||||
CloudRunnerLogger.log(
|
||||
`Successfully read logs using fallback method (${logFileContent.length} chars): ${attempt.substring(
|
||||
`Successfully read logs using fallback method (${logFileContent.length} chars): ${attempt.slice(
|
||||
0,
|
||||
50,
|
||||
)}...`,
|
||||
);
|
||||
|
||||
// If this content has "Collected Logs", we're done
|
||||
if (logFileContent.includes('Collected Logs')) {
|
||||
CloudRunnerLogger.log('Fallback method successfully captured "Collected Logs".');
|
||||
@@ -289,14 +300,13 @@ class KubernetesTaskRunner {
|
||||
CloudRunnerLogger.log(`Skipping this result - already have content with "Collected Logs".`);
|
||||
}
|
||||
} else {
|
||||
CloudRunnerLogger.log(`Fallback method returned empty result: ${attempt.substring(0, 50)}...`);
|
||||
CloudRunnerLogger.log(`Fallback method returned empty result: ${attempt.slice(0, 50)}...`);
|
||||
}
|
||||
} catch (attemptError: any) {
|
||||
CloudRunnerLogger.log(
|
||||
`Fallback method failed: ${attempt.substring(0, 50)}... Error: ${
|
||||
attemptError?.message || attemptError
|
||||
}`,
|
||||
`Fallback method failed: ${attempt.slice(0, 50)}... Error: ${attemptError?.message || attemptError}`,
|
||||
);
|
||||
|
||||
// Continue to next attempt
|
||||
}
|
||||
}
|
||||
@@ -311,12 +321,15 @@ class KubernetesTaskRunner {
|
||||
CloudRunnerLogger.log(
|
||||
`Read log file from pod as fallback (${logFileContent.length} chars) to capture missing messages`,
|
||||
);
|
||||
|
||||
// Get the lines we already have in output to avoid duplicates
|
||||
const existingLines = new Set(output.split('\n').map((line) => line.trim()));
|
||||
|
||||
// Process the log file content line by line and add missing lines
|
||||
for (const line of logFileContent.split(`\n`)) {
|
||||
const trimmedLine = line.trim();
|
||||
const lowerLine = trimmedLine.toLowerCase();
|
||||
|
||||
// Skip empty lines, kubectl errors, and lines we already have
|
||||
if (
|
||||
trimmedLine &&
|
||||
@@ -338,6 +351,7 @@ class KubernetesTaskRunner {
|
||||
CloudRunnerLogger.logWarning(
|
||||
`Could not read log file from pod as fallback: ${logFileError?.message || logFileError}`,
|
||||
);
|
||||
|
||||
// Continue with existing output - this is a best-effort fallback
|
||||
}
|
||||
}
|
||||
@@ -348,6 +362,7 @@ class KubernetesTaskRunner {
|
||||
CloudRunnerLogger.logWarning(
|
||||
'Could not retrieve "Collected Logs" from pod after all attempts. Pod may have been killed before logs were written.',
|
||||
);
|
||||
|
||||
// Add a minimal message so BuildResults is not completely empty
|
||||
// This helps with debugging and prevents test failures due to empty results
|
||||
if (output.trim().length === 0) {
|
||||
@@ -362,10 +377,12 @@ class KubernetesTaskRunner {
|
||||
CloudRunnerLogger.logWarning(
|
||||
`Error checking pod status for log file fallback: ${fallbackError?.message || fallbackError}`,
|
||||
);
|
||||
|
||||
// If output is empty and we hit an error, still add a message so BuildResults isn't empty
|
||||
if (needsFallback && output.trim().length === 0) {
|
||||
output = `Error retrieving logs: ${fallbackError?.message || fallbackError}\n`;
|
||||
}
|
||||
|
||||
// Continue with existing output - this is a best-effort fallback
|
||||
}
|
||||
|
||||
@@ -460,14 +477,16 @@ class KubernetesTaskRunner {
|
||||
if (podEvents.length > 0) {
|
||||
message += `\nRecent Events:\n${JSON.stringify(podEvents.slice(-5), undefined, 2)}`;
|
||||
}
|
||||
} catch (eventError) {
|
||||
} catch {
|
||||
// Ignore event fetch errors
|
||||
}
|
||||
|
||||
CloudRunnerLogger.logWarning(message);
|
||||
|
||||
// For permanent failures, mark as incomplete and store the error message
|
||||
// We'll throw an error after the wait loop exits
|
||||
waitComplete = false;
|
||||
|
||||
return true; // Return true to exit wait loop
|
||||
}
|
||||
|
||||
@@ -498,6 +517,7 @@ class KubernetesTaskRunner {
|
||||
message = `Pod ${podName} cannot be scheduled:\n${schedulingMessage}`;
|
||||
CloudRunnerLogger.logWarning(message);
|
||||
waitComplete = false;
|
||||
|
||||
return true; // Exit wait loop to throw error
|
||||
}
|
||||
|
||||
@@ -513,6 +533,7 @@ class KubernetesTaskRunner {
|
||||
message = `Pod ${podName} failed to pull image. Check image availability and credentials.`;
|
||||
CloudRunnerLogger.logWarning(message);
|
||||
waitComplete = false;
|
||||
|
||||
return true; // Exit wait loop to throw error
|
||||
}
|
||||
|
||||
@@ -522,6 +543,7 @@ class KubernetesTaskRunner {
|
||||
CloudRunnerLogger.log(
|
||||
`Pod ${podName} is pulling image (check ${consecutivePendingCount}). This may take several minutes for large images.`,
|
||||
);
|
||||
|
||||
// Don't increment consecutivePendingCount if we're actively pulling
|
||||
consecutivePendingCount = Math.max(4, consecutivePendingCount - 1);
|
||||
}
|
||||
@@ -542,6 +564,7 @@ class KubernetesTaskRunner {
|
||||
|
||||
if (consecutivePendingCount >= maxPendingChecks) {
|
||||
message = `Pod ${podName} stuck in Pending state for too long (${consecutivePendingCount} checks). This indicates a scheduling problem.`;
|
||||
|
||||
// Get events for context
|
||||
try {
|
||||
const events = await kubeClient.listNamespacedEvent(namespace);
|
||||
@@ -595,11 +618,9 @@ class KubernetesTaskRunner {
|
||||
}
|
||||
|
||||
// Check if pod is assigned to a node
|
||||
if (podStatusDetails?.hostIP) {
|
||||
message += `\n\nPod assigned to node: ${podStatusDetails.hostIP}`;
|
||||
} else {
|
||||
message += `\n\nPod not yet assigned to a node (scheduling pending)`;
|
||||
}
|
||||
message += podStatusDetails?.hostIP
|
||||
? `\n\nPod assigned to node: ${podStatusDetails.hostIP}`
|
||||
: `\n\nPod not yet assigned to a node (scheduling pending)`;
|
||||
}
|
||||
|
||||
// Check node resources if pod is assigned
|
||||
@@ -612,7 +633,6 @@ class KubernetesTaskRunner {
|
||||
);
|
||||
if (assignedNode?.status && assignedNode.metadata?.name) {
|
||||
const allocatable = assignedNode.status.allocatable || {};
|
||||
const capacity = assignedNode.status.capacity || {};
|
||||
message += `\n\nNode Resources (${assignedNode.metadata.name}):\n Allocatable CPU: ${
|
||||
allocatable.cpu || 'unknown'
|
||||
}\n Allocatable Memory: ${allocatable.memory || 'unknown'}\n Allocatable Ephemeral Storage: ${
|
||||
@@ -627,11 +647,11 @@ class KubernetesTaskRunner {
|
||||
message += `\n Node Taints: ${taints}`;
|
||||
}
|
||||
}
|
||||
} catch (nodeError) {
|
||||
} catch {
|
||||
// Ignore node check errors
|
||||
}
|
||||
}
|
||||
} catch (podStatusError) {
|
||||
} catch {
|
||||
// Ignore pod status fetch errors
|
||||
}
|
||||
} catch {
|
||||
@@ -639,6 +659,7 @@ class KubernetesTaskRunner {
|
||||
}
|
||||
CloudRunnerLogger.logWarning(message);
|
||||
waitComplete = false;
|
||||
|
||||
return true; // Exit wait loop to throw error
|
||||
}
|
||||
|
||||
@@ -706,7 +727,7 @@ class KubernetesTaskRunner {
|
||||
}
|
||||
|
||||
CloudRunnerLogger.logWarning(message);
|
||||
} catch (statusError) {
|
||||
} catch {
|
||||
message = `Pod ${podName} timed out and could not retrieve final status: ${waitError?.message || waitError}`;
|
||||
CloudRunnerLogger.logWarning(message);
|
||||
}
|
||||
@@ -725,6 +746,7 @@ class KubernetesTaskRunner {
|
||||
CloudRunnerLogger.logWarning(
|
||||
`Pod ${podName} completed with phase ${finalPhase} before reaching Running state. Will attempt to retrieve logs.`,
|
||||
);
|
||||
|
||||
return true; // Allow workflow to continue and try to get logs
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -110,10 +110,12 @@ export class Caching {
|
||||
await CloudRunnerSystem.Run(
|
||||
`find ${cacheParent} -name "*.tar*" -type f -mmin +360 -delete 2>/dev/null || true`,
|
||||
);
|
||||
|
||||
// Try with sudo if available
|
||||
await CloudRunnerSystem.Run(
|
||||
`sudo find ${cacheParent} -name "*.tar*" -type f -mmin +360 -delete 2>/dev/null || true`,
|
||||
);
|
||||
|
||||
// As last resort, try to remove files one by one
|
||||
await CloudRunnerSystem.Run(
|
||||
`find ${cacheParent} -name "*.tar*" -type f -mmin +360 -exec rm -f {} + 2>/dev/null || true`,
|
||||
@@ -125,6 +127,7 @@ export class Caching {
|
||||
// If disk is still very high (>95%), be even more aggressive
|
||||
if (diskUsagePercent > 95) {
|
||||
CloudRunnerLogger.log(`Disk usage is very high (${diskUsagePercent}%), performing aggressive cleanup...`);
|
||||
|
||||
// Remove files older than 1 hour
|
||||
await CloudRunnerSystem.Run(
|
||||
`find ${cacheParent} -name "*.tar*" -type f -mmin +60 -delete 2>/dev/null || true`,
|
||||
@@ -156,6 +159,7 @@ export class Caching {
|
||||
const message = `Cannot create cache archive: disk is still at ${diskUsageAfterCleanup}% after cleanup. Tar operation would hang. Skipping cache push; please free up disk space manually if this persists.`;
|
||||
CloudRunnerLogger.logWarning(message);
|
||||
RemoteClientLogger.log(message);
|
||||
|
||||
// Restore working directory before early return
|
||||
process.chdir(`${startPath}`);
|
||||
|
||||
@@ -187,6 +191,7 @@ export class Caching {
|
||||
try {
|
||||
// Check if timeout command is available
|
||||
await CloudRunnerSystem.Run(`which timeout > /dev/null 2>&1`, true, true);
|
||||
|
||||
// Use timeout if available (600 seconds = 10 minutes)
|
||||
tarCommandToRun = `timeout 600 ${tarCommand}`;
|
||||
} catch {
|
||||
@@ -224,6 +229,7 @@ export class Caching {
|
||||
await CloudRunnerSystem.Run(
|
||||
`sudo find ${cacheParent} -name "*.tar*" -type f -mmin +60 -delete 2>/dev/null || true`,
|
||||
);
|
||||
|
||||
// As last resort, try to remove files one by one
|
||||
await CloudRunnerSystem.Run(
|
||||
`find ${cacheParent} -name "*.tar*" -type f -mmin +60 -exec rm -f {} + 2>/dev/null || true`,
|
||||
@@ -239,6 +245,7 @@ export class Caching {
|
||||
await CloudRunnerSystem.Run(
|
||||
`chmod -R u+w ${cacheRoot} 2>/dev/null || chown -R $(whoami) ${cacheRoot} 2>/dev/null || true`,
|
||||
);
|
||||
|
||||
// Remove cache entries older than 30 minutes
|
||||
await CloudRunnerSystem.Run(
|
||||
`find ${cacheRoot} -name "*.tar*" -type f -mmin +30 -delete 2>/dev/null || true`,
|
||||
@@ -365,8 +372,10 @@ export class Caching {
|
||||
const message = `Disk is at ${diskUsagePercent}% - skipping cache extraction to prevent hang. Cache may be incomplete or corrupted.`;
|
||||
CloudRunnerLogger.logWarning(message);
|
||||
RemoteClientLogger.logWarning(message);
|
||||
|
||||
// Continue without cache - build will proceed without cached Library
|
||||
process.chdir(startPath);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -377,12 +386,14 @@ export class Caching {
|
||||
await CloudRunnerSystem.Run(
|
||||
`tar -tf ${cacheSelection}.tar${compressionSuffix} > /dev/null 2>&1 || (echo "Tar file validation failed" && exit 1)`,
|
||||
);
|
||||
} catch (validationError) {
|
||||
} catch {
|
||||
const message = `Cache archive ${cacheSelection}.tar${compressionSuffix} appears to be corrupted or incomplete. Skipping cache extraction.`;
|
||||
CloudRunnerLogger.logWarning(message);
|
||||
RemoteClientLogger.logWarning(message);
|
||||
|
||||
// Continue without cache - build will proceed without cached Library
|
||||
process.chdir(startPath);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -394,6 +405,7 @@ export class Caching {
|
||||
// Extract with timeout to prevent infinite hangs
|
||||
try {
|
||||
let tarExtractCommand = `tar -xf ${cacheSelection}.tar${compressionSuffix} -C ${fullResultsFolder}`;
|
||||
|
||||
// Add timeout if available (600 seconds = 10 minutes)
|
||||
try {
|
||||
await CloudRunnerSystem.Run(`which timeout > /dev/null 2>&1`, true, true);
|
||||
@@ -405,6 +417,7 @@ export class Caching {
|
||||
await CloudRunnerSystem.Run(tarExtractCommand);
|
||||
} catch (extractError: any) {
|
||||
const errorMessage = extractError?.message || extractError?.toString() || '';
|
||||
|
||||
// Check for common tar errors that indicate corruption or disk issues
|
||||
if (
|
||||
errorMessage.includes('Unexpected EOF') ||
|
||||
@@ -416,10 +429,13 @@ export class Caching {
|
||||
const message = `Cache extraction failed (likely due to corrupted archive or disk space): ${errorMessage}. Continuing without cache.`;
|
||||
CloudRunnerLogger.logWarning(message);
|
||||
RemoteClientLogger.logWarning(message);
|
||||
|
||||
// Continue without cache - build will proceed without cached Library
|
||||
process.chdir(startPath);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Re-throw other errors
|
||||
throw extractError;
|
||||
}
|
||||
|
||||
@@ -306,9 +306,11 @@ export class RemoteClient {
|
||||
await CloudRunnerSystem.Run(`git config --global filter.lfs.smudge "git-lfs smudge --skip -- %f"`);
|
||||
await CloudRunnerSystem.Run(`git config --global filter.lfs.process "git-lfs filter-process --skip"`);
|
||||
try {
|
||||
const depthArg = CloudRunnerOptions.cloneDepth !== '0' ? `--depth ${CloudRunnerOptions.cloneDepth}` : '';
|
||||
const depthArgument = CloudRunnerOptions.cloneDepth !== '0' ? `--depth ${CloudRunnerOptions.cloneDepth}` : '';
|
||||
await CloudRunnerSystem.Run(
|
||||
`git clone ${depthArg} ${CloudRunnerFolders.targetBuildRepoUrl} ${path.basename(CloudRunnerFolders.repoPathAbsolute)}`.trim(),
|
||||
`git clone ${depthArgument} ${CloudRunnerFolders.targetBuildRepoUrl} ${path.basename(
|
||||
CloudRunnerFolders.repoPathAbsolute,
|
||||
)}`.trim(),
|
||||
);
|
||||
} catch (error: any) {
|
||||
throw error;
|
||||
|
||||
@@ -52,9 +52,10 @@ describe('Cloud Runner pre-built S3 steps', () => {
|
||||
projectPath: 'test-project',
|
||||
unityVersion: UnityVersioning.determineUnityVersion('test-project', UnityVersioning.read('test-project')),
|
||||
targetPlatform: 'StandaloneLinux64',
|
||||
cacheKey: cacheKey,
|
||||
buildGuid: buildGuid,
|
||||
cacheKey,
|
||||
buildGuid,
|
||||
cloudRunnerDebug: true,
|
||||
|
||||
// Use customJob to run a minimal job that sets up test data and then runs S3 hooks
|
||||
customJob: `
|
||||
- name: setup-test-data
|
||||
@@ -139,7 +140,7 @@ describe('Cloud Runner pre-built S3 steps', () => {
|
||||
s3Endpoint = s3Endpoint.replace('host.docker.internal', 'localhost');
|
||||
CloudRunnerLogger.log(`Converted endpoint from host.docker.internal to localhost: ${s3Endpoint}`);
|
||||
}
|
||||
const endpointArgs = s3Endpoint ? `--endpoint-url ${s3Endpoint}` : '';
|
||||
const endpointArguments = s3Endpoint ? `--endpoint-url ${s3Endpoint}` : '';
|
||||
|
||||
// Configure AWS credentials if available (needed for LocalStack)
|
||||
// LocalStack accepts any credentials, but they must be provided
|
||||
@@ -179,7 +180,7 @@ describe('Cloud Runner pre-built S3 steps', () => {
|
||||
|
||||
try {
|
||||
const results = await CloudRunnerSystem.RunAndReadLines(
|
||||
`aws ${endpointArgs} s3 ls s3://${CloudRunner.buildParameters.awsStackName}/cloud-runner-cache/`,
|
||||
`aws ${endpointArguments} s3 ls s3://${CloudRunner.buildParameters.awsStackName}/cloud-runner-cache/`,
|
||||
);
|
||||
CloudRunnerLogger.log(`S3 verification successful: ${results.join(`,`)}`);
|
||||
} catch (s3Error: any) {
|
||||
@@ -188,6 +189,7 @@ describe('Cloud Runner pre-built S3 steps', () => {
|
||||
CloudRunnerLogger.log(
|
||||
`S3 verification failed (this is expected if upload failed during build): ${s3Error?.message || s3Error}`,
|
||||
);
|
||||
|
||||
// Check if the error is due to missing credentials or connection issues
|
||||
const errorMessage = (s3Error?.message || s3Error?.toString() || '').toLowerCase();
|
||||
if (errorMessage.includes('invalidaccesskeyid') || errorMessage.includes('could not connect')) {
|
||||
|
||||
@@ -106,23 +106,29 @@ describe('Cloud Runner Caching', () => {
|
||||
if (fs.existsSync(cachePath)) {
|
||||
try {
|
||||
CloudRunnerLogger.log(`Cleaning up cache directory: ${cachePath}`);
|
||||
|
||||
// Try to change ownership first (if running as root or with sudo)
|
||||
// Then try multiple cleanup methods to handle permission issues
|
||||
await CloudRunnerSystem.Run(
|
||||
`chmod -R u+w ${cachePath} 2>/dev/null || chown -R $(whoami) ${cachePath} 2>/dev/null || true`,
|
||||
);
|
||||
|
||||
// Try regular rm first
|
||||
await CloudRunnerSystem.Run(`rm -rf ${cachePath}/* 2>/dev/null || true`);
|
||||
|
||||
// If that fails, try with sudo if available
|
||||
await CloudRunnerSystem.Run(`sudo rm -rf ${cachePath}/* 2>/dev/null || true`);
|
||||
|
||||
// As last resort, try to remove files one by one, ignoring permission errors
|
||||
await CloudRunnerSystem.Run(
|
||||
`find ${cachePath} -type f -exec rm -f {} + 2>/dev/null || find ${cachePath} -type f -delete 2>/dev/null || true`,
|
||||
);
|
||||
|
||||
// Remove empty directories
|
||||
await CloudRunnerSystem.Run(`find ${cachePath} -type d -empty -delete 2>/dev/null || true`);
|
||||
} catch (error: any) {
|
||||
CloudRunnerLogger.log(`Failed to cleanup cache: ${error.message}`);
|
||||
|
||||
// Don't throw - cleanup failures shouldn't fail the test suite
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ describe('Cloud Runner Retain Workspace', () => {
|
||||
CloudRunnerLogger.log('Cleanup between builds completed (containers removed, images preserved)');
|
||||
} catch (cleanupError) {
|
||||
CloudRunnerLogger.logWarning(`Failed to cleanup between builds: ${cleanupError}`);
|
||||
|
||||
// Continue anyway
|
||||
}
|
||||
}
|
||||
@@ -111,22 +112,28 @@ describe('Cloud Runner Retain Workspace', () => {
|
||||
const workspaceCachePath = `./cloud-runner-cache/${path.basename(
|
||||
CloudRunnerFolders.uniqueCloudRunnerJobFolderAbsolute,
|
||||
)}`;
|
||||
|
||||
// Try to fix permissions first to avoid permission denied errors
|
||||
await CloudRunnerSystem.Run(
|
||||
`chmod -R u+w ${workspaceCachePath} 2>/dev/null || chown -R $(whoami) ${workspaceCachePath} 2>/dev/null || true`,
|
||||
);
|
||||
|
||||
// Try regular rm first
|
||||
await CloudRunnerSystem.Run(`rm -rf ${workspaceCachePath} 2>/dev/null || true`);
|
||||
|
||||
// If that fails, try with sudo if available
|
||||
await CloudRunnerSystem.Run(`sudo rm -rf ${workspaceCachePath} 2>/dev/null || true`);
|
||||
|
||||
// As last resort, try to remove files one by one, ignoring permission errors
|
||||
await CloudRunnerSystem.Run(
|
||||
`find ${workspaceCachePath} -type f -exec rm -f {} + 2>/dev/null || find ${workspaceCachePath} -type f -delete 2>/dev/null || true`,
|
||||
);
|
||||
|
||||
// Remove empty directories
|
||||
await CloudRunnerSystem.Run(`find ${workspaceCachePath} -type d -empty -delete 2>/dev/null || true`);
|
||||
} catch (error: any) {
|
||||
CloudRunnerLogger.log(`Failed to cleanup workspace: ${error.message}`);
|
||||
|
||||
// Don't throw - cleanup failures shouldn't fail the test suite
|
||||
}
|
||||
}
|
||||
@@ -136,23 +143,29 @@ describe('Cloud Runner Retain Workspace', () => {
|
||||
if (fs.existsSync(cachePath)) {
|
||||
try {
|
||||
CloudRunnerLogger.log(`Cleaning up cache directory: ${cachePath}`);
|
||||
|
||||
// Try to change ownership first (if running as root or with sudo)
|
||||
// Then try multiple cleanup methods to handle permission issues
|
||||
await CloudRunnerSystem.Run(
|
||||
`chmod -R u+w ${cachePath} 2>/dev/null || chown -R $(whoami) ${cachePath} 2>/dev/null || true`,
|
||||
);
|
||||
|
||||
// Try regular rm first
|
||||
await CloudRunnerSystem.Run(`rm -rf ${cachePath}/* 2>/dev/null || true`);
|
||||
|
||||
// If that fails, try with sudo if available
|
||||
await CloudRunnerSystem.Run(`sudo rm -rf ${cachePath}/* 2>/dev/null || true`);
|
||||
|
||||
// As last resort, try to remove files one by one, ignoring permission errors
|
||||
await CloudRunnerSystem.Run(
|
||||
`find ${cachePath} -type f -exec rm -f {} + 2>/dev/null || find ${cachePath} -type f -delete 2>/dev/null || true`,
|
||||
);
|
||||
|
||||
// Remove empty directories
|
||||
await CloudRunnerSystem.Run(`find ${cachePath} -type d -empty -delete 2>/dev/null || true`);
|
||||
} catch (error: any) {
|
||||
CloudRunnerLogger.log(`Failed to cleanup cache: ${error.message}`);
|
||||
|
||||
// Don't throw - cleanup failures shouldn't fail the test suite
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ describe('Cloud Runner Kubernetes', () => {
|
||||
throw new Error(
|
||||
`Test failed: Pod was evicted due to resource constraints (ephemeral-storage). ` +
|
||||
`This indicates the test environment doesn't have enough disk space. ` +
|
||||
`Results: ${results.substring(0, 500)}`,
|
||||
`Results: ${results.slice(0, 500)}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ describe('Cloud Runner Kubernetes', () => {
|
||||
throw new Error(
|
||||
`Build did not complete successfully: ${incompleteLogsMessage}\n` +
|
||||
`This indicates the pod was evicted or killed before post-build completed.\n` +
|
||||
`Build results:\n${results.substring(0, 500)}`,
|
||||
`Build results:\n${results.slice(0, 500)}`,
|
||||
);
|
||||
} else {
|
||||
// Normal case - logs are complete
|
||||
|
||||
Reference in New Issue
Block a user