diff --git a/src/Components/ServerlessFramework.php b/src/Components/ServerlessFramework.php index f6adbe0..c597c28 100644 --- a/src/Components/ServerlessFramework.php +++ b/src/Components/ServerlessFramework.php @@ -43,11 +43,13 @@ public function deploy(int $deploymentId, string $environment, array $awsCredent $oslsPackage = ($input->hasOption('osls4') && $input->getOption('osls4')) ? 'osls@4' : 'osls@3'; $newLogs = ''; + // The whole output ($newLogs is emptied when pushed to Bref Cloud) + $output = ''; try { $process = $this->serverlessExec($oslsPackage, 'deploy', $environment, $awsCredentials, $options); - async(function () use ($process, &$newLogs) { + async(function () use ($process, &$newLogs, &$output) { while (($chunk = $process->getStdout()->read()) !== null) { if (empty($chunk)) continue; foreach (self::IGNORED_LOGS as $ignoredLog) { @@ -55,9 +57,10 @@ public function deploy(int $deploymentId, string $environment, array $awsCredent } IO::verbose($chunk); $newLogs .= $chunk; + $output .= $chunk; } }); - async(function () use ($process, &$newLogs) { + async(function () use ($process, &$newLogs, &$output) { while (($chunk = $process->getStderr()->read()) !== null) { if (empty($chunk)) continue; foreach (self::IGNORED_LOGS as $ignoredLog) { @@ -65,6 +68,7 @@ public function deploy(int $deploymentId, string $environment, array $awsCredent } IO::verbose($chunk); $newLogs .= $chunk; + $output .= $chunk; } }); // Send logs to Bref Cloud every x seconds @@ -88,8 +92,11 @@ public function deploy(int $deploymentId, string $environment, array $awsCredent $newLogs .= "Error while running 'serverless deploy', deployment failed\n"; IO::writeln("Error while running 'serverless deploy', deployment failed"); - // Bref Cloud finds the error in the logs - $brefCloud->markDeploymentFinished($deploymentId, false, null, $newLogs); + // Bref Cloud finds the error in the logs. + // The stack is only known after a successful deployment: if this one created it, it is sent + // so that removing the environment deletes it. + [$stackName, $region] = $this->findCreatedStack($output) ?? [null, null]; + $brefCloud->markDeploymentFinished($deploymentId, false, null, $newLogs, $region, $stackName); return; } @@ -116,6 +123,31 @@ public function deploy(int $deploymentId, string $environment, array $awsCredent } } + /** + * The stack that the deployment created, if it did. + * + * With `--verbose`, osls logs the events of the stack. The first one is the stack's own (nested stacks come after): + * `CREATE_IN_PROGRESS` when osls creates it, `UPDATE_IN_PROGRESS` when it already existed (e.g. deployed some + * other way before), and there is none when the deployment failed before CloudFormation. + * + * @return array{string, string}|null The stack name and its region. + */ + public function findCreatedStack(string $output): ?array + { + if (! preg_match('/^\s*(\w+) - AWS::CloudFormation::Stack - (\S+)\s*$/m', $output, $firstStackEvent)) { + return null; + } + if ($firstStackEvent[1] !== 'CREATE_IN_PROGRESS') { + return null; + } + // "Deploying to stage ()" + if (! preg_match('/^\s*Deploying .+ to stage .+ \(([a-z0-9-]+)\)\s*$/m', $output, $deploying)) { + return null; + } + + return [$firstStackEvent[2], $deploying[1]]; + } + /** * @param array{ accessKeyId: string, secretAccessKey: string, sessionToken: string } $awsCredentials * @return array diff --git a/tests/Components/ServerlessFrameworkTest.php b/tests/Components/ServerlessFrameworkTest.php new file mode 100644 index 0000000..a146fdc --- /dev/null +++ b/tests/Components/ServerlessFrameworkTest.php @@ -0,0 +1,94 @@ +assertSame($expected, (new ServerlessFramework)->findCreatedStack($output)); + } + + /** + * @return array + */ + public static function failedDeployments(): array + { + return [ + 'first deployment, failed in CloudFormation' => [ + <<<'OUTPUT' + Deploying night-deploy-hints to stage after (eu-west-3) + + Excluding development dependencies for service package + CREATE_IN_PROGRESS - AWS::CloudFormation::Stack - night-deploy-hints-after + CREATE_IN_PROGRESS - AWS::S3::Bucket - ServerlessDeploymentBucket + CREATE_IN_PROGRESS - AWS::S3::Bucket - ServerlessDeploymentBucket + CREATE_COMPLETE - AWS::S3::Bucket - ServerlessDeploymentBucket + CREATE_IN_PROGRESS - AWS::S3::BucketPolicy - ServerlessDeploymentBucketPolicy + CREATE_COMPLETE - AWS::S3::BucketPolicy - ServerlessDeploymentBucketPolicy + CREATE_COMPLETE - AWS::CloudFormation::Stack - night-deploy-hints-after + Uploading CloudFormation file to S3 + Uploading State file to S3 + Uploading service night-deploy-hints.zip file to S3 (1.18 MB) + UPDATE_IN_PROGRESS - AWS::CloudFormation::Stack - night-deploy-hints-after + CREATE_IN_PROGRESS - AWS::Lambda::Function - HelloLambdaFunction + CREATE_FAILED - AWS::Lambda::Function - HelloLambdaFunction + UPDATE_ROLLBACK_IN_PROGRESS - AWS::CloudFormation::Stack - night-deploy-hints-after + UPDATE_ROLLBACK_COMPLETE - AWS::CloudFormation::Stack - night-deploy-hints-after + + × Stack night-deploy-hints-after failed to deploy (469s) + Environment: darwin, node 26.0.0, framework 3.61.0 + Credentials: Local, environment variables + Docs: github.com/oss-serverless/serverless + + Error: + CREATE_FAILED: HelloLambdaFunction (AWS::Lambda::Function) + Resource handler returned message: "Function code combined with layers exceeds the maximum allowed size of 262144000 bytes." + + OUTPUT, + ['night-deploy-hints-after', 'eu-west-3'], + ], + // e.g. deployed with the Serverless Framework before moving to Bref Cloud + 'the stack already existed, a nested stack is created' => [ + <<<'OUTPUT' + Deploying night-deploy-hints to stage after (eu-west-3) + + Excluding development dependencies for service package + Uploading CloudFormation file to S3 + Uploading State file to S3 + Uploading service night-deploy-hints.zip file to S3 (1.18 MB) + UPDATE_IN_PROGRESS - AWS::CloudFormation::Stack - night-deploy-hints-after + CREATE_IN_PROGRESS - AWS::CloudFormation::Stack - PermissionsNestedStack + CREATE_IN_PROGRESS - AWS::Lambda::Function - HelloLambdaFunction + CREATE_FAILED - AWS::Lambda::Function - HelloLambdaFunction + UPDATE_ROLLBACK_IN_PROGRESS - AWS::CloudFormation::Stack - night-deploy-hints-after + UPDATE_ROLLBACK_COMPLETE - AWS::CloudFormation::Stack - night-deploy-hints-after + + × Stack night-deploy-hints-after failed to deploy (95s) + + OUTPUT, + null, + ], + 'failed before CloudFormation' => [ + <<<'OUTPUT' + Environment: darwin, node 26.0.0, framework 3.61.0 + Docs: github.com/oss-serverless/serverless + + Error: + Cannot resolve serverless.yml: Variables resolution errored with: + - Cannot resolve variable at "provider.environment.API_KEY": Value not found at "env" source + + OUTPUT, + null, + ], + ]; + } +}