jets 2.1.1 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.codebuild/README.md +10 -60
  3. data/.codebuild/docs/bin/build.sh +6 -0
  4. data/.codebuild/docs/bin/cli_docs.sh +5 -0
  5. data/.codebuild/docs/bin/git_commit.sh +27 -0
  6. data/.codebuild/docs/bin/git_setup.sh +19 -0
  7. data/.codebuild/docs/bin/subnav.sh +14 -0
  8. data/.codebuild/docs/buildspec.yml +8 -0
  9. data/.codebuild/docs/project.rb +10 -0
  10. data/.gitignore +5 -5
  11. data/CHANGELOG.md +8 -0
  12. data/jets.gemspec +5 -5
  13. data/lib/jets/aws_services/stack_status.rb +5 -1
  14. data/lib/jets/cfn/builders/api_gateway_builder.rb +10 -19
  15. data/lib/jets/cfn/builders/api_resources_builder.rb +46 -0
  16. data/lib/jets/cfn/builders/parent_builder.rb +15 -0
  17. data/lib/jets/cfn/built_template.rb +15 -0
  18. data/lib/jets/commands/clean/log.rb +18 -1
  19. data/lib/jets/commands/delete.rb +14 -1
  20. data/lib/jets/commands/deploy.rb +43 -12
  21. data/lib/jets/commands/help/build.md +1 -1
  22. data/lib/jets/commands/help/{destroy.md → degenerate.md} +1 -1
  23. data/lib/jets/commands/help/upgrade.md +2 -0
  24. data/lib/jets/commands/main.rb +2 -1
  25. data/lib/jets/commands/templates/skeleton/Gemfile.tt +1 -0
  26. data/lib/jets/naming.rb +4 -0
  27. data/lib/jets/resource/api_gateway/resource.rb +7 -3
  28. data/lib/jets/resource/api_gateway/rest_api/change_detection.rb +0 -32
  29. data/lib/jets/resource/api_gateway/rest_api/routes/change.rb +9 -1
  30. data/lib/jets/resource/api_gateway/rest_api/routes/change/base.rb +26 -5
  31. data/lib/jets/resource/api_gateway/rest_api/routes/change/media_types.rb +36 -0
  32. data/lib/jets/resource/api_gateway/rest_api/routes/change/page.rb +93 -0
  33. data/lib/jets/resource/api_gateway/rest_api/routes/change/to.rb +0 -4
  34. data/lib/jets/resource/api_gateway/rest_api/routes/change/variable.rb +0 -4
  35. data/lib/jets/resource/child_stack/api_resource.rb +60 -0
  36. data/lib/jets/resource/child_stack/api_resource/page.rb +20 -0
  37. data/lib/jets/resource/child_stack/app_class.rb +14 -3
  38. data/lib/jets/router.rb +57 -40
  39. data/lib/jets/router/method_creator/code.rb +3 -1
  40. data/lib/jets/router/method_creator/edit.rb +6 -1
  41. data/lib/jets/router/method_creator/index.rb +9 -3
  42. data/lib/jets/router/method_creator/new.rb +6 -1
  43. data/lib/jets/router/method_creator/show.rb +6 -1
  44. data/lib/jets/router/route.rb +1 -0
  45. data/lib/jets/version.rb +1 -1
  46. metadata +22 -17
  47. data/.codebuild/bin/jets +0 -3
  48. data/.codebuild/buildspec-base.yml +0 -14
  49. data/.codebuild/integration.sh +0 -72
  50. data/.codebuild/jets.postman_collection.json +0 -323
  51. data/.codebuild/scripts/install-docker.sh +0 -12
  52. data/.codebuild/scripts/install-dynamodb-local.sh +0 -22
  53. data/.codebuild/scripts/install-java.sh +0 -22
  54. data/.codebuild/scripts/install-node.sh +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 82e56d77408579a757373a115134668315a2c47fdbcd297f8eae827951274f6a
4
- data.tar.gz: 0152fbf8fe291033319761e987667026a77958b67398ba237d9152cc70a00615
3
+ metadata.gz: bdc43f68b08d9c92593eeb3ccfc2477a1080af2cd4430e32f4766c2756c70b23
4
+ data.tar.gz: f842a52a1c9a448545f3a7ade22e1c14c8ce2a4d556169c574b4f5a58675c450
5
5
  SHA512:
6
- metadata.gz: f638230df0a341ee7b4e0a73355a89a650a78178d7ebb46326c4215d40ea4d33d6648a3d0909a98789e416b4c03c59bae8b5c8e5a94e254c45de78915e5214c4
7
- data.tar.gz: 5a9e606725ae25d18c3ce4b1ea901780d7062245035c20a9d5139566b0e92ab998d9c3e6d2bb14081a5e77c04bfa5bb871ed95db51ac48467c42f186292b82ab
6
+ metadata.gz: e4efe902131423d3aa95b2e9e42e9b7cdefc78871d0c6eedbc1bbc98ee5cb2771ce7e9ba6c908c00f018960ddbba6aa1e3d5ea745b31c3cb1593770f20bb1ebc
7
+ data.tar.gz: 363c2292150bf101d9ee5196de2bf82465d59cbb255d3c541c4fc3e44e08421b83bafe52c797a6ef07a7bdae63a727cc476000eadc914397243d1a587ad9824d
data/.codebuild/README.md CHANGED
@@ -1,68 +1,18 @@
1
- # Code Build Commands
1
+ ## Update Project
2
2
 
3
- ## The code definitions are stored in the codebuild s3 bucket.
3
+ To update the CodeBuild project:
4
4
 
5
- aws s3 cp .codebuild/definitions/jets_base.json s3://$S3_BUCKET/codebuild/definitions/jets_base.json
6
- aws s3 cp .codebuild/definitions/jets_main.json s3://$S3_BUCKET/codebuild/definitions/jets_main.json
5
+ Main services:
7
6
 
8
- ## JetsBase
7
+ export AWS_PROFILE=bolt-oss
8
+ cb deploy jets --type docs # mainly cli docs and subnav update
9
9
 
10
- The JetsBase codebuild project projects builds the Docker base image and pushes it to Docker hub. Here are the commands to manage the codebuild project.
10
+ ## Start a Deploy
11
11
 
12
- aws codebuild create-project --cli-input-json file://.codebuild/definitions/jets_base.json
13
- aws codebuild update-project --cli-input-json file://.codebuild/definitions/jets_base.json
12
+ To start a CodeBuild build which kicks off a deploy:
14
13
 
15
- aws codebuild start-build --project-name JetsBase --source-version codebuild
14
+ cb start jets --type docs
16
15
 
17
- aws codebuild batch-get-projects --names JetsBase
18
- aws codebuild list-builds-for-project --project-name JetsBase
16
+ To specify a branch:
19
17
 
20
- BUILD_ID=$(aws codebuild list-builds-for-project --project-name JetsBase | jq -r '.ids[0]')
21
- aws codebuild batch-get-builds --ids $BUILD_ID
22
-
23
- STREAM=$(aws codebuild batch-get-builds --ids $BUILD_ID | jq -r '.builds[0].logs.streamName')
24
- cw tail -f /aws/codebuild/JetsBase $STREAM
25
-
26
- If you want to manually build the Docker base image. Run:
27
-
28
- docker build -t tongueroo/jets:base -f Dockerfile.base .
29
- docker push tongueroo/jets:base
30
-
31
- ## JetsMain
32
-
33
- aws codebuild create-project --cli-input-json file://.codebuild/definitions/jets_main.json
34
- aws codebuild update-project --cli-input-json file://.codebuild/definitions/jets_main.json
35
-
36
- aws codebuild start-build --project-name JetsMain --source-version codebuild
37
-
38
- aws codebuild batch-get-projects --names JetsMain
39
- aws codebuild list-builds-for-project --project-name JetsMain
40
-
41
- BUILD_ID=$(aws codebuild list-builds-for-project --project-name JetsMain | jq -r '.ids[0]')
42
- aws codebuild batch-get-builds --ids $BUILD_ID
43
-
44
- STREAM=$(aws codebuild batch-get-builds --ids $BUILD_ID | jq -r '.builds[0].logs.streamName')
45
- cw tail -f /aws/codebuild/JetsMain $STREAM
46
-
47
- ## Run CodeBuild Locally
48
-
49
- time docker run -it -v /var/run/docker.sock:/var/run/docker.sock \
50
- -e "IMAGE_NAME=tongueroo/jets:base" \
51
- -e "ARTIFACTS=/tmp/artifacts" \
52
- -e "SOURCE=/home/ec2-user/environment/jets" \
53
- -e "DB_USER=$DB_USER" \
54
- -e "DB_PASS=$DB_PASS" \
55
- -e "DB_HOST=$DB_HOST" \
56
- amazon/aws-codebuild-local
57
-
58
- ## Run ingreration.sh
59
-
60
- Can run the integration.sh test locally by running:
61
-
62
- export DB_NAME=demo
63
- export DB_USER=dbuser
64
- export DB_PASS=dbpass
65
- export DB_HOST=rdshost
66
- .codebuild/integration.sh
67
-
68
- Note, you'll need to use a real RDS db instance. Make sure DATABASE_URL is not set, this is working with the DB_* vars.
18
+ cb start jets --type docs --branch codebuild
@@ -0,0 +1,6 @@
1
+ #!/bin/bash -eux
2
+
3
+ .codebuild/docs/bin/git_setup.sh
4
+ .codebuild/docs/bin/cli_docs.sh
5
+ .codebuild/docs/bin/subnav.sh
6
+ .codebuild/docs/bin/git_commit.sh
@@ -0,0 +1,5 @@
1
+ #!/bin/bash -eux
2
+
3
+ # Generate docs
4
+ bundle
5
+ rake docs
@@ -0,0 +1,27 @@
1
+ #!/bin/bash -eux
2
+
3
+ out=$(git status docs)
4
+ if [[ "$out" = *"nothing to commit"* ]]; then
5
+ exit
6
+ fi
7
+
8
+ COMMIT_MESSAGE="docs updated by codebuild"
9
+
10
+ # If the last commit already updated the docs, then exit.
11
+ # Preventable measure to avoid infinite loop.
12
+ if git log -1 --pretty=oneline | grep "$COMMIT_MESSAGE" ; then
13
+ exit
14
+ fi
15
+
16
+ # If reach here, we have some changes on docs that we should commit.
17
+ git add docs
18
+ git commit -m "$COMMIT_MESSAGE"
19
+
20
+ # SSH_KEY_S3_PATH set as codebuild environment variable
21
+ aws s3 cp $SSH_KEY_S3_PATH ~/.ssh/id_rsa
22
+ chmod 400 ~/.ssh/id_rsa
23
+ sed -i -- 's|https://github.com/|git@github.com:|g' .git/config
24
+
25
+ # https://makandracards.com/makandra/12107-git-show-current-branch-name-only
26
+ current_branch=$(git rev-parse --abbrev-ref HEAD)
27
+ git push origin "$current_branch"
@@ -0,0 +1,19 @@
1
+ #!/bin/bash -eux
2
+
3
+ # git push:
4
+ # CODEBUILD_WEBHOOK_HEAD_REF=refs/heads/codebuild
5
+ # CODEBUILD_WEBHOOK_TRIGGER=branch/codebuild
6
+ # CODEBUILD_SOURCE_VERSION=f0eb542 # resolved sha
7
+ # cb start:
8
+ # CODEBUILD_SOURCE_VERSION=codebuild
9
+
10
+ set +u # cb start will not have CODEBUILD_WEBHOOK_TRIGGER set
11
+ if [ -n "$CODEBUILD_WEBHOOK_TRIGGER" ]; then # git push
12
+ BRANCH=$(echo $CODEBUILD_WEBHOOK_TRIGGER | sed "s/.*\///")
13
+ elif [ -n "$CODEBUILD_SOURCE_VERSION" ]; then # cb start
14
+ BRANCH=$CODEBUILD_SOURCE_VERSION # contains the actual branch
15
+ else
16
+ BRANCH=UNKNOWN-BRANCH
17
+ fi
18
+ git checkout $BRANCH
19
+ set -u
@@ -0,0 +1,14 @@
1
+ #!/bin/bash -eux
2
+
3
+ cd docs
4
+ # Unsure why but if cli_docs.sh runs bundle first, then bundle wont use the right bundler file
5
+ export BUNDLE_GEMFILE=$(pwd)/Gemfile
6
+ bundle
7
+ bundle exec jekyll serve --detach
8
+
9
+ # Error: front_matter_parser-0.2.1/lib/front_matter_parser/syntax_parser/multi_line_comment.rb:19:in `match': invalid byte sequence in US-ASCII (ArgumentError)
10
+ # Workaround: https://stackoverflow.com/questions/17031651/invalid-byte-sequence-in-us-ascii-argument-error-when-i-run-rake-dbseed-in-ra
11
+ export RUBYOPT="-KU -E utf-8:utf-8" # fixes front_matter_parser error within the `jekyll-sort reorder` call
12
+
13
+ gem install jekyll-sort
14
+ jekyll-sort reorder # Updates nav_order front matter of pages that are in the nav
@@ -0,0 +1,8 @@
1
+ version: 0.2
2
+
3
+ phases:
4
+ build:
5
+ commands:
6
+ - git config --global user.email "tongueroo@gmail.com"
7
+ - git config --global user.name "Tung Nguyen"
8
+ - .codebuild/docs/bin/build.sh
@@ -0,0 +1,10 @@
1
+ github_url("https://github.com/tongueroo/jets.git")
2
+ linux_image("aws/codebuild/ruby:2.5.3-1.7.0")
3
+ triggers(
4
+ webhook: true,
5
+ filter_groups: [[{type: "HEAD_REF", pattern: "master"}, {type: "EVENT", pattern: "PUSH"}]]
6
+ )
7
+
8
+ environment_variables(
9
+ SSH_KEY_S3_PATH: "ssm:/codebuild/jets/ssh_key_s3_path"
10
+ )
data/.gitignore CHANGED
@@ -1,12 +1,13 @@
1
1
  *.gem
2
2
  *.rbc
3
3
  .bundle
4
+ .byebug_history
4
5
  .config
5
6
  .yardoc
6
- InstalledFiles
7
7
  _yardoc
8
8
  coverage
9
9
  doc/
10
+ InstalledFiles
10
11
  lib/bundler/man
11
12
  pkg
12
13
  rdoc
@@ -15,10 +16,9 @@ test/tmp
15
16
  test/version_tmp
16
17
  tmp
17
18
 
18
- spec/fixtures/project/handlers
19
19
  .codebuild/definitions
20
- demo*
21
20
  /html
21
+ /demo
22
+ Gemfile.lock
22
23
  spec/fixtures/apps/franky/dynamodb/migrate
23
- Gemfile.lock
24
- .byebug_history
24
+ spec/fixtures/project/handlers
data/CHANGELOG.md CHANGED
@@ -3,6 +3,14 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  This project *loosely tries* to adhere to [Semantic Versioning](http://semver.org/).
5
5
 
6
+ ## [2.1.2]
7
+ - improve start .env.development example for postgres
8
+ - #351 Update Turbine documentation
9
+ - #355 improve jets routes command to also validate routes
10
+ - #356 improvements to named route helpers
11
+ - #357 paginate api gateway resources to support more routes
12
+ - #358 rate limit backoff
13
+
6
14
  ## [2.1.1]
7
15
  - #347 Add documentation about torch vs. warm
8
16
  - #348 Provide another minimal privileges example
data/jets.gemspec CHANGED
@@ -7,11 +7,11 @@ require "jets/rdoc"
7
7
  Gem::Specification.new do |spec|
8
8
  spec.name = "jets"
9
9
  spec.version = Jets::VERSION
10
- spec.authors = ["Tung Nguyen"]
11
- spec.email = ["tongueroo@gmail.com"]
12
- spec.summary = "Ruby Serverless Framework on AWS Lambda"
13
- spec.description = "Jets is a framework that allows you to create serverless applications with a beautiful language: Ruby. It includes everything required to build an application and deploy it to AWS Lambda. Jets makes serverless accessible to everyone."
14
- spec.homepage = "http://rubyonjets.com"
10
+ spec.author = "Tung Nguyen"
11
+ spec.email = "tongueroo@gmail.com"
12
+ spec.summary = "Ruby Serverless Framework"
13
+ spec.description = "Jets is a framework that allows you to create serverless applications with a beautiful language: Ruby. It includes everything required to build and deploy an application. Jets leverages the power of Ruby to make serverless joyful for everyone."
14
+ spec.homepage = "https://rubyonjets.com"
15
15
  spec.license = "MIT"
16
16
 
17
17
  spec.required_ruby_version = '~> 2.5'
@@ -1,13 +1,17 @@
1
1
  module Jets::AwsServices
2
2
  module StackStatus
3
+ # Only cache if it is true because the initial deploy checks live status until a stack exists.
4
+ @@stack_exists_cache = [] # helps with CloudFormation rate limit
3
5
  def stack_exists?(stack_name)
4
6
  return false if ENV['TEST']
7
+ return true if @@stack_exists_cache.include?(stack_name)
5
8
 
6
9
  exist = nil
7
10
  begin
8
11
  # When the stack does not exist an exception is raised. Example:
9
12
  # Aws::CloudFormation::Errors::ValidationError: Stack with id blah does not exist
10
- resp = cfn.describe_stacks(stack_name: stack_name)
13
+ cfn.describe_stacks(stack_name: stack_name)
14
+ @@stack_exists_cache << stack_name
11
15
  exist = true
12
16
  rescue Aws::CloudFormation::Errors::ValidationError => e
13
17
  if e.message =~ /does not exist/
@@ -12,9 +12,9 @@ module Jets::Cfn::Builders
12
12
  def compose
13
13
  return unless @options[:templates] || @options[:stack_type] != :minimal
14
14
 
15
- add_gateway_rest_api
16
- add_custom_domain
17
- add_gateway_routes
15
+ add_gateway_routes # "child template": build before add_gateway_rest_api. RestApi logical id and change detection is dependent on it.
16
+ add_gateway_rest_api # changes parent template
17
+ add_custom_domain # changes parent template
18
18
  end
19
19
 
20
20
  # template_path is an interface method
@@ -57,23 +57,14 @@ module Jets::Cfn::Builders
57
57
  end
58
58
 
59
59
  # Adds route related Resources and Outputs
60
+ # Delegates to ApiResourcesBuilder
61
+ PAGE_LIMIT = Integer(ENV['JETS_AWS_OUTPUTS_LIMIT'] || 60) # Allow override for testing
60
62
  def add_gateway_routes
61
- # The routes required a Gateway Resource to contain them.
62
- # TODO: Support more routes. Right now outputing all routes in 1 template will hit the 60 routes limit.
63
- # Will have to either output them as a joined string or break this up to multiple templates.
64
- # http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cloudformation-limits.html
65
- # Outputs: Maximum number of outputs that you can declare in your AWS CloudFormation template. 60 outputs
66
- # Output name: Maximum size of an output name. 255 characters.
67
- #
68
- # Note we must use .all_paths, not .routes here because we need to
69
- # build the parent ApiGateway::Resource nodes also
70
- Jets::Router.all_paths.each do |path|
71
- homepage = path == ''
72
- next if homepage # handled by RootResourceId output already
73
-
74
- resource = Jets::Resource::ApiGateway::Resource.new(path, internal: true)
75
- add_resource(resource)
76
- add_outputs(resource.outputs)
63
+ # Reject homepage. Otherwise we have 60 - 1 resources on the first page.
64
+ # There's a next call in ApiResources.add_gateway_resources to skip the homepage.
65
+ all_paths = Jets::Router.all_paths.reject { |p| p == '' }
66
+ all_paths.each_slice(PAGE_LIMIT).each_with_index do |paths, i|
67
+ ApiResourcesBuilder.new(@options, paths, i+1).build
77
68
  end
78
69
  end
79
70
  end
@@ -0,0 +1,46 @@
1
+ module Jets::Cfn::Builders
2
+ class ApiResourcesBuilder
3
+ include Interface
4
+ include Jets::AwsServices
5
+
6
+ def initialize(options={}, paths=[], page)
7
+ @options, @paths, @page = options, paths, page
8
+ @template = ActiveSupport::HashWithIndifferentAccess.new(Resources: {})
9
+ end
10
+
11
+ # compose is an interface method
12
+ def compose
13
+ return unless @options[:templates] || @options[:stack_type] != :minimal
14
+
15
+ add_rest_api_parameter
16
+ add_gateway_routes
17
+ end
18
+
19
+ # template_path is an interface method
20
+ def template_path
21
+ Jets::Naming.api_resources_template_path(@page)
22
+ end
23
+
24
+ def add_rest_api_parameter
25
+ add_parameter("RestApi", Description: "RestApi")
26
+ end
27
+
28
+ def add_gateway_routes
29
+ @paths.each do |path|
30
+ homepage = path == ''
31
+ next if homepage # handled by RootResourceId output already
32
+
33
+ resource = Jets::Resource::ApiGateway::Resource.new(path)
34
+ add_resource(resource)
35
+ add_outputs(resource.outputs)
36
+
37
+ parent_path = resource.parent_path_parameter
38
+ add_parameter(parent_path) unless part_of_template?(parent_path)
39
+ end
40
+ end
41
+
42
+ def part_of_template?(parent_path)
43
+ @template["Resources"].key?(parent_path)
44
+ end
45
+ end
46
+ end
@@ -57,6 +57,7 @@ module Jets::Cfn::Builders
57
57
 
58
58
  if full? and !Jets::Router.routes.empty?
59
59
  add_api_gateway
60
+ add_api_resources
60
61
  add_api_deployment
61
62
  end
62
63
  end
@@ -80,6 +81,20 @@ module Jets::Cfn::Builders
80
81
  add_child_resources(resource)
81
82
  end
82
83
 
84
+ def add_api_resources
85
+ expression = "#{Jets::Naming.template_path_prefix}-api-resources-*"
86
+ # IE: path: #{Jets.build_root}/templates/demo-dev-2-api-resources-1.yml"
87
+ Dir.glob(expression).sort.each do |path|
88
+ next unless File.file?(path)
89
+
90
+ regexp = Regexp.new("#{Jets.config.project_namespace}-api-resources-(\\d+).yml") # tricky to escape \d pattern
91
+ md = path.match(regexp)
92
+ page = md[1]
93
+ resource = Jets::Resource::ChildStack::ApiResource.new(@options[:s3_bucket], page: page)
94
+ add_child_resources(resource)
95
+ end
96
+ end
97
+
83
98
  def add_api_deployment
84
99
  resource = Jets::Resource::ChildStack::ApiDeployment.new(@options[:s3_bucket])
85
100
  add_child_resources(resource)
@@ -0,0 +1,15 @@
1
+ module Jets::Cfn
2
+ # Caches the built template to reduce filesystem IO calls.
3
+ class BuiltTemplate
4
+ class << self
5
+ @@cache = {}
6
+ def get(path)
7
+ if @@cache[path]
8
+ @@cache[path] # using cache
9
+ else
10
+ @@cache[path] = YAML.load_file(path) # setting and using cache
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -19,12 +19,29 @@ class Jets::Commands::Clean
19
19
  say "Removing CloudWatch logs for #{prefix_guess}..."
20
20
  log_groups.each do |g|
21
21
  next if keep_log_group?(g.log_group_name)
22
- logs.delete_log_group(log_group_name: g.log_group_name) unless @options[:noop]
22
+ delete_log_group(g.log_group_name) unless @options[:noop]
23
23
  say "Removed log group: #{g.log_group_name}"
24
24
  end
25
25
  say "Removed CloudWatch logs for #{prefix_guess}"
26
26
  end
27
27
 
28
+ def delete_log_group(log_group_name)
29
+ retries = 0
30
+ logs.delete_log_group(log_group_name: log_group_name)
31
+ rescue Aws::CloudWatchLogs::Errors::ThrottlingException => e
32
+ retries += 1
33
+ seconds = 2 ** retries
34
+
35
+ puts "WARN: delete_log_group #{e.class} #{e.message}".color(:yellow)
36
+ puts "Backing off and will retry in #{seconds} seconds."
37
+ sleep(seconds)
38
+ if seconds > 90 # 2 ** 6 is 64 so will give up after 6 retries
39
+ puts "Giving up after #{retries} retries"
40
+ else
41
+ retry
42
+ end
43
+ end
44
+
28
45
  def clean_deploys
29
46
  groups = deploy_log_groups.sort_by do |g|
30
47
  g.log_group_name