Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 36 additions & 4 deletions src/Components/ServerlessFramework.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,28 +43,32 @@ 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) {
if (str_contains($chunk, $ignoredLog)) continue 2;
}
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) {
if (str_contains($chunk, $ignoredLog)) continue 2;
}
IO::verbose($chunk);
$newLogs .= $chunk;
$output .= $chunk;
}
});
// Send logs to Bref Cloud every x seconds
Expand All @@ -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;
}

Expand All @@ -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 <service> to stage <stage> (<region>)"
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<string, string>
Expand Down
94 changes: 94 additions & 0 deletions tests/Components/ServerlessFrameworkTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php declare(strict_types=1);

namespace Bref\Cli\Test\Components;

use Bref\Cli\Components\ServerlessFramework;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

class ServerlessFrameworkTest extends TestCase
{
/**
* @param array{string, string}|null $expected
*/
#[DataProvider('failedDeployments')]
public function test_finds_the_stack_that_a_failed_deployment_created(string $output, ?array $expected): void
{
$this->assertSame($expected, (new ServerlessFramework)->findCreatedStack($output));
}

/**
* @return array<string, array{string, array{string, string}|null}>
*/
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,
],
];
}
}
Loading