import AwsBuildPlatform from './providers/aws'; import { BuildParameters, Input } from '..'; import Kubernetes from './providers/k8s'; import OrchestratorLogger from './services/core/orchestrator-logger'; import { OrchestratorStepParameters } from './options/orchestrator-step-parameters'; import { WorkflowCompositionRoot } from './workflows/workflow-composition-root'; import { OrchestratorError } from './error/orchestrator-error'; import { TaskParameterSerializer } from './services/core/task-parameter-serializer'; import * as core from '@actions/core'; import OrchestratorSecret from './options/orchestrator-secret'; import { ProviderInterface } from './providers/provider-interface'; import OrchestratorEnvironmentVariable from './options/orchestrator-environment-variable'; import TestOrchestrator from './providers/test'; import LocalOrchestrator from './providers/local'; import LocalDockerOrchestrator from './providers/docker'; import loadProvider from './providers/provider-loader'; import GitHub from '../github'; import SharedWorkspaceLocking from './services/core/shared-workspace-locking'; import { FollowLogStreamService } from './services/core/follow-log-stream-service'; import OrchestratorResult from './services/core/orchestrator-result'; import OrchestratorOptions from './options/orchestrator-options'; import ResourceTracking from './services/core/resource-tracking'; class Orchestrator { public static Provider: ProviderInterface; public static buildParameters: BuildParameters; private static defaultSecrets: OrchestratorSecret[]; private static orchestratorEnvironmentVariables: OrchestratorEnvironmentVariable[]; 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; public static get isOrchestratorEnvironment() { return process.env[`GITHUB_ACTIONS`] !== `true`; } public static get isOrchestratorAsyncEnvironment() { return process.env[`ASYNC_WORKFLOW`] === `true`; } public static async setup(buildParameters: BuildParameters) { OrchestratorLogger.setup(); OrchestratorLogger.log(`Setting up orchestrator`); Orchestrator.buildParameters = buildParameters; ResourceTracking.logAllocationSummary('setup'); await ResourceTracking.logDiskUsageSnapshot('setup'); if (Orchestrator.buildParameters.githubCheckId === ``) { Orchestrator.buildParameters.githubCheckId = await GitHub.createGitHubCheck( Orchestrator.buildParameters.buildGuid, ); } await Orchestrator.setupSelectedBuildPlatform(); Orchestrator.defaultSecrets = TaskParameterSerializer.readDefaultSecrets(); Orchestrator.orchestratorEnvironmentVariables = TaskParameterSerializer.createOrchestratorEnvironmentVariables(buildParameters); if (GitHub.githubInputEnabled) { const buildParameterPropertyNames = Object.getOwnPropertyNames(buildParameters); for (const element of Orchestrator.orchestratorEnvironmentVariables) { // OrchestratorLogger.log(`Orchestrator output ${Input.ToEnvVarFormat(element.name)} = ${element.value}`); core.setOutput(Input.ToEnvVarFormat(element.name), element.value); } for (const element of buildParameterPropertyNames) { // OrchestratorLogger.log(`Orchestrator output ${Input.ToEnvVarFormat(element)} = ${buildParameters[element]}`); core.setOutput(Input.ToEnvVarFormat(element), buildParameters[element]); } core.setOutput( Input.ToEnvVarFormat(`buildArtifact`), `build-${Orchestrator.buildParameters.buildGuid}.tar${ Orchestrator.buildParameters.useCompressionStrategy ? '.lz4' : '' }`, ); } FollowLogStreamService.Reset(); } private static async setupSelectedBuildPlatform() { OrchestratorLogger.log(`Orchestrator platform selected ${Orchestrator.buildParameters.providerStrategy}`); // Detect LocalStack endpoints and handle AWS provider appropriately // AWS_FORCE_PROVIDER options: // - 'aws': Force AWS provider (requires LocalStack Pro with ECS support) // - 'aws-local': Validate AWS templates/config but execute via local-docker (for CI without ECS) // - unset/other: Auto-fallback to local-docker when LocalStack detected const awsForceProvider = process.env.AWS_FORCE_PROVIDER || ''; const forceAwsProvider = awsForceProvider === 'aws' || awsForceProvider === 'true'; const useAwsLocalMode = awsForceProvider === 'aws-local'; const endpointsToCheck = [ process.env.AWS_ENDPOINT, process.env.AWS_S3_ENDPOINT, process.env.AWS_CLOUD_FORMATION_ENDPOINT, process.env.AWS_ECS_ENDPOINT, process.env.AWS_KINESIS_ENDPOINT, process.env.AWS_CLOUD_WATCH_LOGS_ENDPOINT, OrchestratorOptions.awsEndpoint, OrchestratorOptions.awsS3Endpoint, OrchestratorOptions.awsCloudFormationEndpoint, OrchestratorOptions.awsEcsEndpoint, OrchestratorOptions.awsKinesisEndpoint, OrchestratorOptions.awsCloudWatchLogsEndpoint, ] .filter((x) => typeof x === 'string') .join(' '); const isLocalStack = /localstack|localhost|127\.0\.0\.1/i.test(endpointsToCheck); let provider = Orchestrator.buildParameters.providerStrategy; let validateAwsTemplates = false; if (provider === 'aws' && isLocalStack) { if (useAwsLocalMode) { // aws-local mode: Validate AWS templates but execute via local-docker // This provides confidence in AWS CloudFormation without requiring LocalStack Pro OrchestratorLogger.log('AWS_FORCE_PROVIDER=aws-local: Validating AWS templates, executing via local-docker'); validateAwsTemplates = true; provider = 'local-docker'; } else if (forceAwsProvider) { // Force full AWS provider (requires LocalStack Pro with ECS support) OrchestratorLogger.log( 'LocalStack endpoints detected but AWS_FORCE_PROVIDER=aws; using full AWS provider (requires ECS support)', ); } else { // Auto-fallback to local-docker OrchestratorLogger.log('LocalStack endpoints detected; routing provider to local-docker for this run'); OrchestratorLogger.log( 'Note: Set AWS_FORCE_PROVIDER=aws-local to validate AWS templates with local-docker execution', ); provider = 'local-docker'; } } // Store whether we should validate AWS templates (used by aws-local mode) Orchestrator.validateAwsTemplates = validateAwsTemplates; // Check for CLI provider executable if (Orchestrator.buildParameters.providerExecutable) { const { default: CliProvider } = await import('./providers/cli'); Orchestrator.Provider = new CliProvider( Orchestrator.buildParameters.providerExecutable, Orchestrator.buildParameters, ); OrchestratorLogger.log(`Using CLI provider executable: ${Orchestrator.buildParameters.providerExecutable}`); return; } switch (provider) { case 'k8s': Orchestrator.Provider = new Kubernetes(Orchestrator.buildParameters); break; case 'aws': Orchestrator.Provider = new AwsBuildPlatform(Orchestrator.buildParameters); // Validate that AWS provider is actually being used when expected if (isLocalStack && forceAwsProvider) { OrchestratorLogger.log('✓ AWS provider initialized with LocalStack - AWS functionality will be validated'); } else if (isLocalStack && !forceAwsProvider) { OrchestratorLogger.log( '⚠ WARNING: AWS provider was requested but LocalStack detected without AWS_FORCE_PROVIDER', ); OrchestratorLogger.log('⚠ This may cause AWS functionality tests to fail validation'); } break; case 'test': Orchestrator.Provider = new TestOrchestrator(); break; case 'local-docker': Orchestrator.Provider = new LocalDockerOrchestrator(); break; case 'local-system': Orchestrator.Provider = new LocalOrchestrator(); break; case 'local': Orchestrator.Provider = new LocalOrchestrator(); break; default: // Try to load provider using the dynamic loader for unknown providers try { Orchestrator.Provider = await loadProvider(provider, Orchestrator.buildParameters); } catch (error: any) { OrchestratorLogger.log(`Failed to load provider '${provider}' using dynamic loader: ${error.message}`); OrchestratorLogger.log('Falling back to local provider...'); Orchestrator.Provider = new LocalOrchestrator(); } break; } // Final validation: Ensure provider matches expectations const finalProviderName = Orchestrator.Provider.constructor.name; if (Orchestrator.buildParameters.providerStrategy === 'aws' && finalProviderName !== 'AWSBuildEnvironment') { OrchestratorLogger.log(`⚠ WARNING: Expected AWS provider but got ${finalProviderName}`); OrchestratorLogger.log('⚠ AWS functionality tests may not be validating AWS services correctly'); } } static async run(buildParameters: BuildParameters, baseImage: string) { if (baseImage.includes(`undefined`)) { throw new Error(`baseImage is undefined`); } await Orchestrator.setup(buildParameters); // When aws-local mode is enabled, validate AWS CloudFormation templates // This ensures AWS templates are correct even when executing via local-docker if (Orchestrator.validateAwsTemplates) { await Orchestrator.validateAwsCloudFormationTemplates(); } await Orchestrator.Provider.setupWorkflow( Orchestrator.buildParameters.buildGuid, Orchestrator.buildParameters, Orchestrator.buildParameters.branch, Orchestrator.defaultSecrets, ); try { if (buildParameters.maxRetainedWorkspaces > 0) { Orchestrator.lockedWorkspace = SharedWorkspaceLocking.NewWorkspaceName(); const result = await SharedWorkspaceLocking.GetLockedWorkspace( Orchestrator.lockedWorkspace, Orchestrator.buildParameters.buildGuid, Orchestrator.buildParameters, ); if (result) { OrchestratorLogger.logLine(`Using retained workspace ${Orchestrator.lockedWorkspace}`); Orchestrator.orchestratorEnvironmentVariables = [ ...Orchestrator.orchestratorEnvironmentVariables, { name: `LOCKED_WORKSPACE`, value: Orchestrator.lockedWorkspace }, ]; } else { OrchestratorLogger.log(`Max retained workspaces reached ${buildParameters.maxRetainedWorkspaces}`); buildParameters.maxRetainedWorkspaces = 0; Orchestrator.lockedWorkspace = ``; } } await Orchestrator.updateStatusWithBuildParameters(); const output = await new WorkflowCompositionRoot().run( new OrchestratorStepParameters( baseImage, Orchestrator.orchestratorEnvironmentVariables, Orchestrator.defaultSecrets, ), ); await Orchestrator.Provider.cleanupWorkflow( Orchestrator.buildParameters, Orchestrator.buildParameters.branch, Orchestrator.defaultSecrets, ); if (!Orchestrator.buildParameters.isCliMode) core.endGroup(); if (buildParameters.asyncWorkflow && this.isOrchestratorEnvironment && this.isOrchestratorAsyncEnvironment) { await GitHub.updateGitHubCheck(Orchestrator.buildParameters.buildGuid, `success`, `success`, `completed`); } if (BuildParameters.shouldUseRetainedWorkspaceMode(buildParameters)) { const workspace = Orchestrator.lockedWorkspace || ``; await SharedWorkspaceLocking.ReleaseWorkspace( workspace, Orchestrator.buildParameters.buildGuid, Orchestrator.buildParameters, ); const isLocked = await SharedWorkspaceLocking.IsWorkspaceLocked(workspace, Orchestrator.buildParameters); if (isLocked) { throw new Error( `still locked after releasing ${await SharedWorkspaceLocking.GetAllLocksForWorkspace( workspace, buildParameters, )}`, ); } Orchestrator.lockedWorkspace = ``; } await GitHub.triggerWorkflowOnComplete(Orchestrator.buildParameters.finalHooks); if (buildParameters.constantGarbageCollection) { Orchestrator.Provider.garbageCollect(``, true, buildParameters.garbageMaxAge, true, true); } return new OrchestratorResult(buildParameters, output, true, true, false); } catch (error: any) { OrchestratorLogger.log(JSON.stringify(error, undefined, 4)); await GitHub.updateGitHubCheck( Orchestrator.buildParameters.buildGuid, `Failed - Error ${error?.message || error}`, `failure`, `completed`, ); if (!Orchestrator.buildParameters.isCliMode) core.endGroup(); await OrchestratorError.handleException(error, Orchestrator.buildParameters, Orchestrator.defaultSecrets); throw error; } } private static async updateStatusWithBuildParameters() { const content = { ...Orchestrator.buildParameters }; content.gitPrivateToken = ``; content.unitySerial = ``; content.unityEmail = ``; content.unityPassword = ``; const jsonContent = JSON.stringify(content, undefined, 4); await GitHub.updateGitHubCheck(jsonContent, Orchestrator.buildParameters.buildGuid); } /** * Validates AWS CloudFormation templates without deploying them. * Used by aws-local mode to ensure AWS templates are correct when executing via local-docker. * This provides confidence that AWS ECS deployments would work with the generated templates. */ private static async validateAwsCloudFormationTemplates() { OrchestratorLogger.log('=== AWS CloudFormation Template Validation (aws-local mode) ==='); try { // Import AWS template formations const { BaseStackFormation } = await import('./providers/aws/cloud-formations/base-stack-formation'); const { TaskDefinitionFormation } = await import('./providers/aws/cloud-formations/task-definition-formation'); // Validate base stack template const baseTemplate = BaseStackFormation.formation; OrchestratorLogger.log(`✓ Base stack template generated (${baseTemplate.length} chars)`); // Check for required resources in base stack const requiredBaseResources = ['AWS::EC2::VPC', 'AWS::ECS::Cluster', 'AWS::S3::Bucket', 'AWS::IAM::Role']; for (const resource of requiredBaseResources) { if (baseTemplate.includes(resource)) { OrchestratorLogger.log(` ✓ Contains ${resource}`); } else { throw new Error(`Base stack template missing required resource: ${resource}`); } } // Validate task definition template const taskTemplate = TaskDefinitionFormation.formation; OrchestratorLogger.log(`✓ Task definition template generated (${taskTemplate.length} chars)`); // Check for required resources in task definition const requiredTaskResources = ['AWS::ECS::TaskDefinition', 'AWS::Logs::LogGroup']; for (const resource of requiredTaskResources) { if (taskTemplate.includes(resource)) { OrchestratorLogger.log(` ✓ Contains ${resource}`); } else { throw new Error(`Task definition template missing required resource: ${resource}`); } } // Validate YAML syntax by checking for common patterns if (!baseTemplate.includes('AWSTemplateFormatVersion')) { throw new Error('Base stack template missing AWSTemplateFormatVersion'); } if (!taskTemplate.includes('AWSTemplateFormatVersion')) { throw new Error('Task definition template missing AWSTemplateFormatVersion'); } OrchestratorLogger.log('=== AWS CloudFormation templates validated successfully ==='); OrchestratorLogger.log('Note: Actual execution will use local-docker provider'); } catch (error: any) { OrchestratorLogger.log(`AWS CloudFormation template validation failed: ${error.message}`); throw error; } } } export default Orchestrator;