mkbrut 0.15.0 → 0.16.0

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mkbrut/add_segment.rb +38 -0
  3. data/lib/mkbrut/add_segment_options.rb +22 -0
  4. data/lib/mkbrut/app.rb +7 -2
  5. data/lib/mkbrut/base.rb +1 -0
  6. data/lib/mkbrut/cli.rb +90 -8
  7. data/lib/mkbrut/ops/insert_code_in_method.rb +19 -7
  8. data/lib/mkbrut/ops/insert_into_file.rb +36 -0
  9. data/lib/mkbrut/ops/prism_parsing_op.rb +14 -2
  10. data/lib/mkbrut/ops.rb +1 -0
  11. data/lib/mkbrut/segments/bare_bones.rb +8 -0
  12. data/lib/mkbrut/segments/demo.rb +8 -0
  13. data/lib/mkbrut/segments/heroku.rb +23 -3
  14. data/lib/mkbrut/segments/sidekiq.rb +136 -1
  15. data/lib/mkbrut/segments.rb +1 -1
  16. data/lib/mkbrut/version.rb +1 -1
  17. data/lib/mkbrut.rb +2 -0
  18. data/templates/Base/.gitignore +3 -0
  19. data/templates/Base/Dockerfile.dx +18 -21
  20. data/templates/Base/Gemfile.erb +1 -1
  21. data/templates/Base/bin/run +107 -67
  22. data/templates/Base/bin/run.run +4 -0
  23. data/templates/Base/bin/setup +32 -1
  24. data/templates/Base/dx/bash_customizations +0 -4
  25. data/templates/Base/package.json.erb +1 -1
  26. data/templates/segments/Heroku/deploy/Dockerfile +23 -21
  27. data/templates/segments/Heroku/deploy/heroku_config.rb +7 -6
  28. data/templates/segments/Sidekiq/app/boot_sidekiq.rb +2 -0
  29. data/templates/segments/Sidekiq/app/config/sidekiq.yml +4 -0
  30. data/templates/segments/Sidekiq/app/src/back_end/jobs/app_job.rb +3 -0
  31. data/templates/segments/Sidekiq/app/src/back_end/jobs/example_job.rb +12 -0
  32. data/templates/segments/Sidekiq/app/src/back_end/segments/sidekiq_segment.rb +56 -0
  33. data/templates/segments/Sidekiq/bin/run.sidekiq +4 -0
  34. data/templates/segments/Sidekiq/specs/back_end/jobs/example_job.spec.rb +5 -0
  35. data/templates/segments/Sidekiq/specs/integration/sidekiq_works.spec.rb +38 -0
  36. metadata +14 -3
  37. data/templates/Base/.env.development.local +0 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 44490857b7335985f6af254391ecc787fccfcb53bc5a42023462d6bf164a86b7
4
- data.tar.gz: c664733ee5228fbe20616d919036c2b4717fad9fb2fdfe78d14da9ffc7118694
3
+ metadata.gz: 01efe075fd366adf916bf216ee4bca597ac46b4e70cad921e716e4cf9354066c
4
+ data.tar.gz: 7790e6f5c3d8f35f79a89ec64d3e4879fa8d80fc085cfb608135ec2dc354239e
5
5
  SHA512:
6
- metadata.gz: f72502451676ded40f36837d6cf214d60ab8a0c4a5d28295cc859ef823ede5235521e04e3ef6497df61770ac69777116b01d6f42903e54160cf88b185ac307c2
7
- data.tar.gz: 6c39188a7be33648d0a9907f665088483cdcbbf8f6c0294f4b4e446770e9d21a815877c1431b4adf3d00bb8e40ce1594de81541063e2421e8928a37768fead1c
6
+ metadata.gz: 9bf39f9ded150a7d98aed61649ad65182110fa52ccbd0ad491d7b40e07e00f63369b42ab0beec97b5cfb349faa8980ff0e8d3f9b8c446aa5bfd0fdf50ed07efb
7
+ data.tar.gz: d7ecb8ad272923a9f4471963d5535ebad77f8cdf69d786ea35e7a466b239e4062725f94053916ca0321de2010d2b77e9a43315e3464149c376faf8059235af24
@@ -0,0 +1,38 @@
1
+ require "fileutils"
2
+ require "erb"
3
+
4
+ module MKBrut
5
+ class AddSegment
6
+ def initialize(current_dir:, add_segment_options:, out:, err:)
7
+ @out = out
8
+ @add_segment_options = add_segment_options
9
+
10
+ @out.puts "Adding #{@add_segment_options.segment_name} to this app"
11
+
12
+ if @add_segment_options.dry_run?
13
+ @out.puts "Dry Run"
14
+ MKBrut::Ops::BaseOp.dry_run = true
15
+ end
16
+
17
+ templates_dir = Pathname(
18
+ Gem::Specification.find_by_name("mkbrut").gem_dir
19
+ ) / "templates"
20
+
21
+ @segment = if @add_segment_options.segment_name == "sidekiq"
22
+ MKBrut::Segments::Sidekiq.new(
23
+ project_root: add_segment_options.project_root,
24
+ templates_dir:
25
+ )
26
+ elsif @add_segment_options.segment_name == "heroku"
27
+ MKBrut::Segments::Heroku.new(
28
+ project_root: add_segment_options.project_root,
29
+ templates_dir:
30
+ )
31
+ end
32
+ end
33
+
34
+ def add!
35
+ @segment.add!
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,22 @@
1
+ class MKBrut::AddSegmentOptions
2
+ attr_reader :segment_name, :project_root
3
+
4
+ def initialize(
5
+ segment_name:,
6
+ project_root:,
7
+ dry_run: nil,
8
+ versions: nil,
9
+ **rest
10
+ )
11
+ if segment_name.nil?
12
+ raise ArgumentError, "segment_name is required"
13
+ end
14
+
15
+ @segment_name = segment_name
16
+ @project_root = project_root
17
+ @dry_run = !!dry_run
18
+ @versions = versions
19
+ end
20
+
21
+ def dry_run? = @dry_run
22
+ end
data/lib/mkbrut/app.rb CHANGED
@@ -47,14 +47,19 @@ module MKBrut
47
47
  case segment
48
48
  when "heroku"
49
49
  @segments << MKBrut::Segments::Heroku.new(
50
- app_options:,
51
- current_dir:,
50
+ project_root: @base.project_root,
51
+ templates_dir:
52
+ )
53
+ when "sidekiq"
54
+ @segments << MKBrut::Segments::Sidekiq.new(
55
+ project_root: @base.project_root,
52
56
  templates_dir:
53
57
  )
54
58
  else
55
59
  raise "Segment #{segment} is not supported"
56
60
  end
57
61
  end
62
+ @segments.sort!
58
63
  end
59
64
 
60
65
  def create!
data/lib/mkbrut/base.rb CHANGED
@@ -8,6 +8,7 @@ class MKBrut::Base
8
8
  def session_secret = SecureRandom.hex(64)
9
9
  end
10
10
 
11
+ attr_reader :project_root
11
12
  def initialize(app_options:, current_dir:, templates_dir:)
12
13
  @project_root = current_dir / app_options.app_name
13
14
  @templates_dir = templates_dir / "Base"
data/lib/mkbrut/cli.rb CHANGED
@@ -11,14 +11,56 @@ module MKBrut
11
11
 
12
12
  def run
13
13
 
14
- app_options = parse_options(@args, MKBrut::Versions.new)
15
- new_app = MKBrut::App.new(
16
- current_dir: Pathname.pwd.expand_path,
17
- app_options:,
18
- out: PrefixedIO.new(@out, "mkbrut"),
19
- err: @err
20
- )
21
- new_app.create!
14
+ command = if @args.first == "add-segment"
15
+ @args.shift
16
+ :add_segment
17
+ elsif @args.first == "new-app" || @args.first == "new"
18
+ @args.shift
19
+ :new_app
20
+ else
21
+ :new_app
22
+ end
23
+
24
+ if command == :new_app
25
+ app_options = parse_options(@args, MKBrut::Versions.new)
26
+ new_app = MKBrut::App.new(
27
+ current_dir: Pathname.pwd.expand_path,
28
+ app_options:,
29
+ out: PrefixedIO.new(@out, "mkbrut"),
30
+ err: @err
31
+ )
32
+ new_app.create!
33
+ elsif command == :add_segment
34
+ add_segment_options = parse_add_segment_options(@args, MKBrut::Versions.new)
35
+ add_segment = MKBrut::AddSegment.new(
36
+ current_dir: add_segment_options.project_root,
37
+ add_segment_options:,
38
+ out: PrefixedIO.new(@out, "mkbrut"),
39
+ err: @err
40
+ )
41
+ add_segment.add!
42
+ @out.puts ""
43
+ @out.puts "Sidekiq has now been set up for your app. The configuration used is"
44
+ @out.puts "a basic one, suitable for getting started, however you know own this"
45
+ @out.puts "configuration. Most of it is in these files:"
46
+ @out.puts ""
47
+ @out.puts " app/config/sidekiq.yml"
48
+ @out.puts " app/src/back_end/segments/sidekiq_segment.rb"
49
+ @out.puts ""
50
+ @out.puts "You are encouraged to verify everything is set up as follows:"
51
+ @out.puts ""
52
+ @out.puts "1. Quit dx/start, and start it back up - this will downloaded and set up ValKey/Redis"
53
+ @out.puts "2. Re-run bin/setup. This will install needed gems and create binstubs"
54
+ @out.puts "3. Run the example integration test:"
55
+ @out.puts ""
56
+ @out.puts " bin/test e2e specs/integration/sidekiq_works.spec.rb"
57
+ @out.puts ""
58
+ @out.puts " This will use the actual Sidekiq server, so if it passes, you should"
59
+ @out.puts " all set and can start creating jobs"
60
+ @out.puts ""
61
+ else
62
+ raise "Unknown command #{command}"
63
+ end
22
64
  0
23
65
  rescue => e
24
66
  @err.puts "Error: #{e.message}"
@@ -103,5 +145,45 @@ module MKBrut
103
145
  MKBrut::AppOptions.new(**options, versions:)
104
146
  end
105
147
 
148
+ def parse_add_segment_options(args, versions)
149
+ options = {}
150
+ @option_parser = OptionParser.new do |opts|
151
+ opts.banner = [
152
+ "Usage: mkbrut add-segment [options] segment-name",
153
+ "",
154
+ " Adds a segment to an existing Brut-powered app",
155
+ "",
156
+ "VERSION",
157
+ "",
158
+ " #{MKBrut::VERSION}",
159
+ "",
160
+ "OPTIONS",
161
+ "",
162
+ ].join("\n")
163
+
164
+ opts.on("-r PROJECT_ROOT_DIR", "--project-root=PROJECT_ROOT_DIR", "Path to the root of the existing Brut app (defaults to current directory)")
165
+ opts.on("--dry-run", "Only show what would happen, don't actually do anything")
166
+ opts.on("--[no-]demo", "Include, or not, additional files that demonstrate Brut's features (default is true for now")
167
+ opts.on("-h", "--help", "Show this help message") do
168
+ @out.puts @option_parser
169
+ @out.puts
170
+ @out.puts "ARGUMENTS"
171
+ @out.puts
172
+ @out.puts " segment-name - name of the segment to add. Known values:"
173
+ @out.puts " sidekiq - add support for Sidekiq"
174
+ @out.puts " heroku - add support for Heroku container-based deployment"
175
+ @out.puts
176
+ @out.puts "ENVIRONMENT VARIABLES"
177
+ @out.puts
178
+ @out.puts " BRUT_CLI_RAISE_ON_ERROR - if set to 'true', any error will raise an exception instead of printing to stderr"
179
+ @out.puts
180
+ exit
181
+ end
182
+ end
183
+
184
+ @option_parser.parse!(args, into: options)
185
+ options[:project_root] = Pathname.new(options[:'project-root'] || ".").expand_path
186
+ MKBrut::AddSegmentOptions.new(segment_name: @args[0], **options, versions:)
187
+ end
106
188
  end
107
189
  end
@@ -1,14 +1,25 @@
1
1
  class MKBrut::Ops::InsertCodeInMethod < MKBrut::Ops::PrismParsingOp
2
- def initialize(file:, class_name:, method_name:, code:, where: :end)
3
- @file = file
4
- @class_name = class_name
5
- @method_name = method_name.to_sym
6
- @code = code
7
- @where = where
2
+ def initialize(file:,
3
+ class_name:,
4
+ method_name:,
5
+ class_method: false,
6
+ code:,
7
+ ignore_if_file_not_found: false,
8
+ where: :end)
9
+ @file = file
10
+ @class_name = class_name
11
+ @method_name = method_name.to_sym
12
+ @class_method = class_method
13
+ @code = code
14
+ @ignore_if_file_not_found = ignore_if_file_not_found
15
+ @where = where
8
16
  end
9
17
 
10
18
  def call
11
- method_node = find_method(class_name: @class_name, method_name: @method_name)
19
+ if !@file.exist? && @ignore_if_file_not_found
20
+ return
21
+ end
22
+ method_node = find_method(class_name: @class_name, method_name: @method_name, class_method: @class_method)
12
23
 
13
24
  insertion_point = if @where == :start
14
25
  insertion_point_for_code_at_start_of_method(method_node: method_node)
@@ -42,6 +53,7 @@ private
42
53
  }.join("\n") + post_indent
43
54
  end
44
55
 
56
+ # XXX: This does not work with non-ASCII strings
45
57
  def insertion_point_for_code_at_end_of_method(method_node:)
46
58
  line_number_of_method_end = method_node.location.end_line - 1
47
59
  length_of_method_end = @source.lines[line_number_of_method_end].length
@@ -0,0 +1,36 @@
1
+ class MKBrut::Ops::InsertIntoFile < MKBrut::Ops::BaseOp
2
+ def initialize(file:, content:, before_line:)
3
+ @file = file
4
+ @content = content
5
+ @before_line = before_line
6
+ end
7
+
8
+ def call
9
+
10
+ contents = File.read(@file)
11
+ locations = []
12
+ new_contents = []
13
+ contents.split("\n").each_with_index do |line, index|
14
+ if line == @before_line
15
+ new_contents << @content
16
+ locations << (index + 1)
17
+ end
18
+ new_contents << line
19
+ end
20
+ if locations.empty?
21
+ raise "Did not find line '#{@before_line}' exactly in #{@file}"
22
+ end
23
+ if locations.size > 1
24
+ raise "Found exact line '#{@before_line}' #{locations.size} times in #{@file}, should be exactly once so we know where to insert"
25
+ end
26
+
27
+ if dry_run?
28
+ puts "Would add to #{@file}, resulting in this content:\n#{new_contents.join("\n")}\n"
29
+ return
30
+ end
31
+
32
+ File.open(@file, "w") do |file|
33
+ file.puts new_contents.join("\n")
34
+ end
35
+ end
36
+ end
@@ -56,10 +56,22 @@ private
56
56
  class_node
57
57
  end
58
58
 
59
- def find_method(class_name:, method_name:)
59
+ def find_method(class_name:, method_name:, class_method: false)
60
60
  class_node = find_class(class_name:)
61
61
  method_node = class_node.body.body.detect { |node|
62
- node.is_a?(Prism::DefNode) && node.name == @method_name
62
+
63
+ is_def = node.is_a?(Prism::DefNode)
64
+ method_name_match = node.name == @method_name
65
+ receiver_match = if class_method
66
+ node.receiver.is_a?(Prism::SelfNode)
67
+ else
68
+ node.receiver.nil?
69
+ end
70
+
71
+ if is_def && method_name_match && !receiver_match
72
+ puts "Found method '#{method_name}' in class '#{class_name}' but it was a #{node.receiver.class} receiver"
73
+ end
74
+ is_def && method_name_match && receiver_match
63
75
  }
64
76
 
65
77
  if !method_node
data/lib/mkbrut/ops.rb CHANGED
@@ -8,6 +8,7 @@ module MKBrut
8
8
  autoload :InsertRoute, "mkbrut/ops/insert_route"
9
9
  autoload :InsertCodeInMethod, "mkbrut/ops/insert_code_in_method"
10
10
  autoload :AppendToFile, "mkbrut/ops/append_to_file"
11
+ autoload :InsertIntoFile, "mkbrut/ops/insert_into_file"
11
12
  autoload :PrismParsingOp, "mkbrut/ops/prism_parsing_op"
12
13
  autoload :AddI18nMessage, "mkbrut/ops/add_i18n_message"
13
14
  autoload :AddCSSImport, "mkbrut/ops/add_css_import"
@@ -9,6 +9,14 @@ class MKBrut::Segments::BareBones < MKBrut::Base
9
9
  @erb_binding = ErbBindingDelegate.new(app_options)
10
10
  end
11
11
 
12
+ def <=>(other)
13
+ if self.class == other.class
14
+ 0
15
+ else
16
+ -1
17
+ end
18
+ end
19
+
12
20
  def add!
13
21
 
14
22
  operations = copy_files(@templates_dir, @project_root) +
@@ -10,6 +10,14 @@ class MKBrut::Segments::Demo < MKBrut::Base
10
10
  @erb_binding = ErbBindingDelegate.new(app_options)
11
11
  end
12
12
 
13
+ def <=>(other)
14
+ if self.class == other.class
15
+ 0
16
+ else
17
+ 1
18
+ end
19
+ end
20
+
13
21
  def add!
14
22
  operations = copy_files(@templates_dir, @project_root) +
15
23
  other_operations(@project_root)
@@ -1,10 +1,9 @@
1
1
  class MKBrut::Segments::Heroku < MKBrut::Base
2
2
  def self.friendly_name = "Heroku-based Deployment"
3
3
 
4
- def initialize(app_options:, current_dir:, templates_dir:)
5
- @project_root = current_dir / app_options.app_name
4
+ def initialize(project_root:, templates_dir:)
5
+ @project_root = project_root
6
6
  @templates_dir = templates_dir / "segments" / "Heroku"
7
- @erb_binding = MKBrut::ErbBindingDelegate.new(app_options)
8
7
  end
9
8
 
10
9
  def add!
@@ -16,8 +15,29 @@ class MKBrut::Segments::Heroku < MKBrut::Base
16
15
  end
17
16
  end
18
17
 
18
+ def <=>(other)
19
+ if self.class == other.class
20
+ 0
21
+ elsif other.class == MKBrut::Segments::Sidekiq
22
+ # If both herkou and sidekiq segments are activated, we want to do heroku first,
23
+ # since Sidekiq will need to modify it.
24
+ -1
25
+ else
26
+ 1
27
+ end
28
+ end
29
+
19
30
  def other_operations
20
31
  [
32
+ MKBrut::Ops::InsertIntoFile.new(
33
+ file: @project_root / "Dockerfile.dx",
34
+ contents: %{
35
+ # Installs the Heroku CLI, per Heroku docs at
36
+ # https://devcenter.heroku.com/articles/heroku-cli#install-with-ubuntu-debian-apt-get
37
+ RUN curl https://cli-assets.heroku.com/install-ubuntu.sh | sh
38
+ },
39
+ before_line: "# Up to now, RUN directives ran as the root user. When using this container"
40
+ ),
21
41
  MKBrut::Ops::AppendToFile.new(
22
42
  file: @project_root / ".gitignore",
23
43
  content: %{
@@ -1,3 +1,138 @@
1
1
  # Adds Sidekiq to a Brut app
2
- class MKBrut::Segments::Sidekiq
2
+ class MKBrut::Segments::Sidekiq < MKBrut::Base
3
+ def self.friendly_name = "Use Sidekiq to Run Background Jobs"
4
+ def initialize(project_root:, templates_dir:)
5
+ @project_root = project_root
6
+ @templates_dir = templates_dir / "segments" / "Sidekiq"
7
+ end
8
+
9
+ def add!
10
+ operations = copy_files(@templates_dir, @project_root) +
11
+ other_operations
12
+
13
+ operations.each do |operation|
14
+ operation.call
15
+ end
16
+ end
17
+
18
+ def <=>(other)
19
+ if self.class == other.class
20
+ 0
21
+ elsif other.class == MKBrut::Segments::Heroku
22
+ # If both herkou and sidekiq segments are activated, we want to do heroku first,
23
+ # since Sidekiq will need to modify it.
24
+ 1
25
+ else
26
+ -1
27
+ end
28
+ end
29
+
30
+ def other_operations
31
+ [
32
+ MKBrut::Ops::AppendToFile.new(
33
+ file: @project_root / "docker-compose.dx.yml",
34
+ content: %{
35
+ redis:
36
+ # Change the value to what you are using in production.
37
+ # If you are using actual Redis, change that here.
38
+ image: valkey/valkey:8.1
39
+ },
40
+ ),
41
+ MKBrut::Ops::AppendToFile.new(
42
+ file: @project_root / "Procfile.development",
43
+ content: "sidekiq: bin/run sidekiq\n"
44
+ ),
45
+ MKBrut::Ops::AppendToFile.new(
46
+ file: @project_root / "Procfile.test",
47
+ content: "sidekiq: bin/run sidekiq\n"
48
+ ),
49
+ MKBrut::Ops::AppendToFile.new(
50
+ file: @project_root / "Gemfile",
51
+ content: "# Sidekiq is used for background jobs\ngem \"sidekiq\"\n"
52
+ ),
53
+ MKBrut::Ops::AppendToFile.new(
54
+ file: @project_root / ".env.development",
55
+ content: %{
56
+ # URL of the Redis/ValKey to use for Sidekiq
57
+ SIDEKIQ_REDIS_URL=redis://redis:6379/1
58
+ # Tells Sidekiq which ENV var to use for the Redis/ValKey URL
59
+ REDIS_PROVIDER=SIDEKIQ_REDIS_URL
60
+ # Username for basic auth into the Sidekiq Admin UI
61
+ SIDEKIQ_BASIC_AUTH_USER=sidekiq
62
+ # Passsword for basic auth into the Sidekiq Admin UI
63
+ SIDEKIQ_BASIC_AUTH_PASSWORD=password
64
+ }
65
+ ),
66
+ MKBrut::Ops::AppendToFile.new(
67
+ file: @project_root / ".env.test",
68
+ content: %{
69
+ SIDEKIQ_REDIS_URL=redis://redis:6379/2
70
+ REDIS_PROVIDER=SIDEKIQ_REDIS_URL
71
+ SIDEKIQ_BASIC_AUTH_USER=sidekiq-test
72
+ SIDEKIQ_BASIC_AUTH_PASSWORD=password
73
+ }
74
+ ),
75
+ MKBrut::Ops::InsertIntoFile.new(
76
+ file: @project_root / "bin" / "test-server",
77
+ before_line: "wait",
78
+ content: "bin/run sidekiq &\n",
79
+ ),
80
+ MKBrut::Ops::InsertIntoFile.new(
81
+ file: @project_root / "bin" / "setup",
82
+ before_line: " log \"Installing rspec binstubs\"",
83
+ content: %{log "Installing sidekiq binstubs"
84
+ system! "bundle binstub sidekiq"
85
+ }
86
+ ),
87
+ MKBrut::Ops::InsertIntoFile.new(
88
+ file: @project_root / "specs" / "spec_helper.rb",
89
+ before_line: "require \"brut/spec_support\"",
90
+ content: "require \"sidekiq/testing\""
91
+ ),
92
+ MKBrut::Ops::InsertIntoFile.new(
93
+ file: @project_root / "config.ru",
94
+ before_line: "bootstrap = Bootstrap.new.bootstrap!",
95
+ content: "require \"sidekiq/web\"\n"
96
+ ),
97
+ MKBrut::Ops::InsertIntoFile.new(
98
+ file: @project_root / "config.ru",
99
+ before_line: " run bootstrap.rack_app",
100
+ content: %{
101
+ map "/sidekiq" do
102
+ use Rack::Auth::Basic, "Sidekiq" do |username, password|
103
+ [username, password] == [ENV.fetch("SIDEKIQ_BASIC_AUTH_USER"), ENV.fetch("SIDEKIQ_BASIC_AUTH_PASSWORD")]
104
+ end
105
+ run Sidekiq::Web.new
106
+ end
107
+ }
108
+ ),
109
+ MKBrut::Ops::InsertCodeInMethod.new(
110
+ file: @project_root / "app" / "src" / "app.rb",
111
+ class_name: "App",
112
+ method_name: "initialize",
113
+ code: "@sidekiq_segment = SidekiqSegment.new",
114
+ ),
115
+ MKBrut::Ops::InsertCodeInMethod.new(
116
+ file: @project_root / "app" / "src" / "app.rb",
117
+ class_name: "App",
118
+ method_name: "boot!",
119
+ code: "@sidekiq_segment.boot!"
120
+ ),
121
+ MKBrut::Ops::InsertCodeInMethod.new(
122
+ file: project_root / "deploy" / "heroku_config.rb",
123
+ class_name: "HerokuConfig",
124
+ method_name: "additional_images",
125
+ class_method: true,
126
+ ignore_if_file_not_found: true,
127
+ code: %{
128
+ {
129
+ "sidekiq" => {
130
+ cmd: "bin/run sidekiq",
131
+ }
132
+ }
133
+ },
134
+ where: :end
135
+ ),
136
+ ]
137
+ end
3
138
  end
@@ -2,7 +2,7 @@ module MKBrut
2
2
  module Segments
3
3
  autoload :BareBones, "mkbrut/segments/bare_bones"
4
4
  autoload :Demo, "mkbrut/segments/demo"
5
- autoload :Sidekiq, "mkbrut/segments/demo"
5
+ autoload :Sidekiq, "mkbrut/segments/sidekiq"
6
6
  autoload :Heroku, "mkbrut/segments/heroku"
7
7
  end
8
8
  end
@@ -1,3 +1,3 @@
1
1
  module MKBrut
2
- VERSION = "0.15.0"
2
+ VERSION = "0.16.0"
3
3
  end
data/lib/mkbrut.rb CHANGED
@@ -3,6 +3,8 @@ module MKBrut
3
3
  autoload :AppId, "mkbrut/app_id"
4
4
  autoload :AppName, "mkbrut/app_name"
5
5
  autoload :AppOptions, "mkbrut/app_options"
6
+ autoload :AddSegment, "mkbrut/add_segment"
7
+ autoload :AddSegmentOptions, "mkbrut/add_segment_options"
6
8
  autoload :Base, "mkbrut/base"
7
9
  autoload :CLI, "mkbrut/cli"
8
10
  autoload :ErbBindingDelegate, "mkbrut/erb_binding_delegate"
@@ -17,6 +17,9 @@
17
17
 
18
18
  # This could contain actual secrets and should not be checked in
19
19
  /.env.development.local
20
+ # This could also contain actual secrets and should not be checked in - it's configuration
21
+ # per developer, not per app
22
+ /dx/bash_customizations.local
20
23
 
21
24
  # These are generated artifacts
22
25
  /deploy/Dockerfile.*
@@ -25,13 +25,18 @@ ENV DEBIAN_FRONTEND=noninteractive
25
25
  # These packages are needed to set up other repos to install other
26
26
  # packages and/or are useful in installing other software
27
27
  #
28
+ # --mount=type=cache,target=/var/cache/apt means that we'll save
29
+ # the downloaded file used to install between builds, so that if they
30
+ # have not changed, builds will be faster.
31
+ #
28
32
  # - ca-certificates - needed by Postgres client
29
33
  # - curl - needed by various installation instructions
30
34
  # - gnupg - needed to install Docker
31
35
  # - lsb-release - needed by Postgres install instructions
32
36
  # - rsync - needed by Brut's build_assets command
33
37
  # - vim - needed when we want to edit files inside container
34
- RUN apt-get -y clean && \
38
+ RUN --mount=type=cache,target=/var/cache/apt \
39
+ apt-get -y clean && \
35
40
  apt-get -y update && \
36
41
  apt-get install --quiet --yes \
37
42
  ca-certificates \
@@ -47,7 +52,8 @@ RUN apt-get -y clean && \
47
52
  #
48
53
  # npx playwright install-deps --dry-run chromium
49
54
  #
50
- RUN apt-get install -y --no-install-recommends libasound2 \
55
+ RUN --mount=type=cache,target=/var/cache/apt \
56
+ apt-get install -y --no-install-recommends libasound2 \
51
57
  libatk-bridge2.0-0 \
52
58
  libatk1.0-0 \
53
59
  libatspi2.0-0 \
@@ -117,10 +123,6 @@ RUN echo "gem: --no-document" >> ~/.gemrc && \
117
123
  gem update --system && \
118
124
  gem install bundler
119
125
 
120
- # Installs the Heroku CLI, per Heroku docs at
121
- # https://devcenter.heroku.com/articles/heroku-cli#install-with-ubuntu-debian-apt-get
122
- RUN curl https://cli-assets.heroku.com/install-ubuntu.sh | sh
123
-
124
126
  # Up to now, RUN directives ran as the root user. When using this container
125
127
  # for development, we do not want to run as the root user. On macOS, this is not
126
128
  # a big deal, but in Linux, the root user inside this container can create files
@@ -178,28 +180,23 @@ COPY --chown=appuser:${user_gid} dx/bash_customizations.local /home/appuser/.bas
178
180
  # as belonging to.
179
181
  USER appuser
180
182
 
181
- # Install NodeJS, per https://nodejs.org/en/download
182
- #
183
- # Yes, this is seting up nvm just to install one version of node we will ever user in
184
- # here and yes, this sucks. But it's better to use the vendor's official
185
- # recommendation.
186
- RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash && \
187
- \. "$HOME/.nvm/nvm.sh" && \
188
- nvm install 22 && \
189
- node -v && nvm current && npm -v
190
-
191
-
183
+ # Installs NodeJS from the official image. This is simpler
184
+ # than the canonical instructions which use NVM. Inside the container, there
185
+ # is not value to use NVM.
186
+ COPY --from=node:22-slim /usr/local /usr/local
192
187
 
193
188
  # This arg should have the version of playwright present in the app's Gemfile.lock.
194
189
  # This is because the playright NodeJS library and the Ruby gem must have the same
195
190
  # version or things won't work right. The value is blank because it really has to be
196
191
  # detected during `dx/build`.
197
192
  ARG PLAYWRIGHT_VERSION
193
+
198
194
  # Now install the version of Playwright we detected as well as Chromium. NOTE, we
199
195
  # must install Chromium, not chrome, because there is no version of Chrome for an
200
196
  # ARM-based Debian linux, which is what this Dockerfile will build on an Apple
201
- # Silicon Mac.
202
- RUN \. "$HOME/.nvm/nvm.sh" && \
203
- nvm use default && \
204
- npm install -g playwright@$PLAYWRIGHT_VERSION && \
197
+ # Silicon Mac. Note that we set NPM_CONFIG_PREFIX so that the global
198
+ # install will work for this user.
199
+ ENV NPM_CONFIG_PREFIX=/home/appuser/.npm-global
200
+ ENV PATH=/home/appuser/.npm-global/bin:$PATH
201
+ RUN npm install -g playwright@$PLAYWRIGHT_VERSION && \
205
202
  npx playwright install chromium
@@ -40,7 +40,7 @@ gem "pg"
40
40
  # We lock the version because it must be the same as the verison
41
41
  # of playwright in package.json. This ensures that we can reliably
42
42
  # install the right browsers and that everything will work right.
43
- gem "playwright-ruby-client", "1.52.0", groups: [ :test ]
43
+ gem "playwright-ruby-client", "1.55.0", groups: [ :test ]
44
44
 
45
45
  # Sinatra needs a webserver and puma is the best
46
46
  gem "puma"
@@ -1,86 +1,126 @@
1
- #!/bin/sh
1
+ #!/usr/bin/env bash
2
2
 
3
3
  set -e
4
- SCRIPT_DIR=$( cd -- "$( dirname -- "${0}" )" > /dev/null 2>&1 && pwd )
5
-
6
- usage() {
7
- echo "Usage: $0"
8
- echo
9
- echo " Run the app in the given RACK_ENV."
10
- echo " You likely want to use bin/dev instead of this command."
11
- echo
12
- echo "ENVIRONMENT VARIABLES"
13
- echo
14
- echo " PORT - The port to run the app on. Default is 6502"
15
- echo " RACK_ENV - The Rack environment to use. Default is development"
16
- echo
17
- }
18
-
19
- for arg in "$@"; do
20
- if [ "${arg}" = "-h" ] || [ "${arg}" = "--help" ] || [ "${arg}" = "help" ]; then
21
- usage
22
- exit 0
23
- fi
24
- done
25
4
 
5
+ SCRIPT_DIR=$( cd -- "$( dirname -- "${0}" )" > /dev/null 2>&1 && pwd )
26
6
  PORT="${PORT:-6502}"
27
7
  RACK_ENV="${RACK_ENV:-development}"
28
8
 
29
9
  export PORT
30
10
  export RACK_ENV
31
11
 
32
- set -e
12
+ declare -A CMD_MAP
13
+ declare -A PIDFILE_MAP
14
+ declare -A CREATES_PIDFILE_MAP
33
15
 
34
- # Everything here is trying its best to prevent more than
35
- # one server from running, as this is extremely confusing.
36
- # Note that the puma invocation at the bottom
37
- # of this script uses the pidfile concept.
38
- if [ -f tmp/pidfile ]; then
39
- echo "[ $0 ] pidfile found"
40
- pid=$(cat tmp/pidfile)
41
-
42
- # First, try to gracefully stop the server with kill
43
- if ps -p "${pid}" > /dev/null; then
44
- echo "[ $0 ] Attempting to kill PID '${pid}'"
45
- kill "${pid}"
46
- else
47
- echo "[ $0 ] PID '${pid}' no longer running"
16
+ kill_previous() {
17
+ local pidfile=$1
18
+
19
+ if [[ -z "$pidfile" ]]; then
20
+ echo "[ $0 ] Error: no pidfile path given"
21
+ return 0
48
22
  fi
49
23
 
50
- # Now, wait 5 seconds to see if it stopped
51
- if timeout 5 tail --pid="${pid}" -f /dev/null; then
52
- echo "[ $0 ] PID '${pid}' stopped. Restarting server"
53
- else
54
- # if it has not stopeed, use kill -9 which should work.
55
- # But, like all things computer, it's not guaranteed.
56
- echo "[ $0 ] PID '${pid}' has not stopped. Trying kill -9"
57
- kill -9 "${pid}"
58
- if timeout 1 tail --pid="${pid}" -f /dev/null; then
59
- echo "[ $0 ] PID '${pid}' killed. Restarting server"
24
+ if [ -f "${pidfile}" ]; then
25
+ echo "[ $0 ] pidfile found"
26
+ pid=$(cat "${pidfile}")
27
+
28
+ if [[ -z "$pid" || ! "$pid" =~ ^[0-9]+$ ]]; then
29
+ echo "[ $0 ] Invalid or empty PID in $pidfile: '$pid'; removing"
30
+ rm -f "$pidfile"
31
+ return 0
32
+ fi
33
+
34
+ if ps -p "${pid}" > /dev/null; then
35
+ echo "[ $0 ] Attempting to kill PID '${pid}'"
36
+ kill "${pid}"
37
+ else
38
+ echo "[ $0 ] PID '${pid}' no longer running"
39
+ fi
40
+ if timeout 5 tail --pid="${pid}" -f /dev/null; then
41
+ echo "[ $0 ] PID '${pid}' stopped. Restarting server"
60
42
  else
61
- echo "[ $0 ] PID '${pid}' still running. Something seriously wrong"
62
- echo "[ $0 ] You may need to stop all Docker containers and restart them"
43
+ echo "[ $0 ] PID '${pid}' has not stopped. Trying kill -9"
44
+ kill -9 "${pid}"
45
+ if timeout 1 tail --pid="${pid}" -f /dev/null; then
46
+ echo "[ $0 ] PID '${pid}' killed. Restarting server"
47
+ else
48
+ echo "[ $0 ] PID '${pid}' still running. Something seriously wrong"
49
+ fi
63
50
  exit 1
64
51
  fi
52
+
53
+ else
54
+ echo "[ $0 ] No pidfile-Starting up"
65
55
  fi
56
+ }
66
57
 
67
- else
68
- echo "[ $0 ] No pidfile-Starting up"
58
+ dotenv_run() {
59
+ local env_files="${SCRIPT_DIR}/../.env.${RACK_ENV}.local,${SCRIPT_DIR}/../.env.${RACK_ENV}"
60
+ dotenv -f "$env_files" --ignore -- "$@"
61
+ }
62
+
63
+
64
+ for f in "$SCRIPT_DIR"/run.*; do
65
+ if [[ -f "$f" ]]; then
66
+ echo "[ $0 ] Sourcing $f"
67
+ . "$f"
68
+ fi
69
+ done
70
+
71
+ case $# in
72
+ 0) name="run" ;;
73
+ 1) name="$1" ;;
74
+ *) echo "Error: too many arguments ($#)." >&2; usage; exit 2 ;;
75
+ esac
76
+
77
+ if [[ ! -v CMD_MAP[$name] ]]; then
78
+ echo "[ $0 ] Error: unknown service '$name'." >&2
79
+ echo -n "[ $0 ] Known services: " >&2
80
+ printf "%s " "${!CMD_MAP[@]}" >&2
81
+ echo >&2
82
+ exit 1
69
83
  fi
70
84
 
71
- # Run puma in a UNIX environment provided by dotenv.
72
- #
73
- # A few things to note:
74
- #
75
- # * dotenv is OK if -f is given non-existent files. That's why this works
76
- # in production where there are no .env files.
77
- # * the `--` marks the end of dotenv's options and the start of the command
78
- # to run. That's why the flags given to puma are not interpreted by dotenv
79
- # as flags for itself.
80
- dotenv \
81
- -f "${SCRIPT_DIR}/../.env.${RACK_ENV}.local,${SCRIPT_DIR}/../.env.${RACK_ENV}" \
82
- --ignore \
83
- -- \
84
- bin/puma \
85
- -C puma.config.rb \
86
- --pid tmp/pidfile
85
+ cmd="${CMD_MAP[$name]}"
86
+ creates_pidfile="${CREATES_PIDFILE_MAP[$name]:-false}"
87
+ pidfile="${PIDFILE_MAP[$name]:-}"
88
+
89
+ echo "[ $0 ]: Command for $name:"
90
+ echo
91
+ echo " $cmd"
92
+ echo
93
+
94
+ kill_previous "$pidfile"
95
+
96
+ if [[ "$creates_pidfile" == true ]]; then
97
+ echo "[ $0 ] Running '$cmd', which creates its own pidfile"
98
+ eval "dotenv_run $cmd"
99
+ else
100
+ # Command wont' create a pidfile, so we will
101
+ if [[ -z "${pidfile:-}" ]]; then
102
+ echo "Error: no pidfile path configured for '$name' but creates_pidfile=false." >&2
103
+ exit 1
104
+ fi
105
+ echo "[ $0 ] Running '$cmd', which will not create a pidfile"
106
+ echo "[ $0 ] We will do that and save it to ${pidfile}"
107
+
108
+ echo "[ $0 ] Ensuring $(dirname "$pidfile") exists"
109
+ mkdir -p "$(dirname "$pidfile")"
110
+
111
+ eval "dotenv_run $cmd &"
112
+ child_pid=$!
113
+
114
+ set +e
115
+ echo "$child_pid" > "$pidfile"
116
+
117
+ trap 'kill -TERM "$child_pid" 2>/dev/null' TERM INT
118
+
119
+ wait "$child_pid"
120
+
121
+ echo "[ $0 ] TERM or INT received, removing ${pidfile} and exiting"
122
+
123
+ status=$?
124
+ rm -f "$pidfile"
125
+ exit "$status"
126
+ fi
@@ -0,0 +1,4 @@
1
+ PIDFILE="${SCRIPT_DIR}/../tmp/pidfile"
2
+ CMD_MAP[run]="bin/puma --port ${PORT} -C puma.config.rb --environment ${RACK_ENV} --pid ${PIDFILE}"
3
+ PIDFILE_MAP[run]="${PIDFILE}"
4
+ CREATES_PIDFILE_MAP[run]="true"
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
-
4
3
  require "fileutils"
5
4
  require "open3"
6
5
  require "optparse"
7
6
  require "pathname"
7
+ require "bundler"
8
+ require "json"
8
9
 
9
10
  # This is intended to run inside the Workspace (i.e. Docker container) to
10
11
  # set up the Foundation, thus enabling development. This script should:
@@ -21,6 +22,7 @@ require "pathname"
21
22
  # Take care to only add code and not change what is here.
22
23
 
23
24
  def setup(update_gems:,update_node:)
25
+ ensure_versions_are_consistent!
24
26
  if update_gems
25
27
  log "Updating gems"
26
28
  system! "bundle update"
@@ -82,6 +84,35 @@ def setup(update_gems:,update_node:)
82
84
  help
83
85
  end
84
86
 
87
+ def ensure_versions_are_consistent!
88
+ gemfile = Bundler::Definition.build(ROOT_DIR / "Gemfile", ROOT_DIR / "Gemfile.lock", nil)
89
+ package_json = JSON.parse(File.read(ROOT_DIR / "package.json"))
90
+
91
+ playwright_version = nil
92
+ gemfile.dependencies.each do |dep|
93
+ if dep.name == "playwright-ruby-client"
94
+ if !dep.specific?
95
+ puts "You must specify an exact/specific version of 'playwright-ruby-client' in your Gemfile"
96
+ exit 1
97
+ end
98
+ playwright_version = dep.requirement.requirements.first[1]
99
+ end
100
+ end
101
+
102
+ if playwright_version
103
+ playwright_version_in_package_json = package_json.dig("devDependencies","playwright")
104
+ if !playwright_version_in_package_json
105
+ puts "You must have 'playwright' in the devDependencies section of your package.json"
106
+ exit 1
107
+ end
108
+ if playwright_version != playwright_version_in_package_json
109
+ puts "The version of 'playwright' in package.json (#{playwright_version_in_package_json}) does not match the version of 'playwright-ruby-client' in the Gemfile (#{playwright_version})"
110
+ puts "They must be exactly the same, or your end-to-end tests may behave strangely"
111
+ exit 1
112
+ end
113
+ end
114
+ end
115
+
85
116
  def setup_dot_env_local
86
117
 
87
118
  dot_env = ROOT_DIR / ".env.development"
@@ -6,7 +6,3 @@ if [ ! -n "$PROJECT_ROOT" ]; then
6
6
  fi
7
7
  export GEM_HOME=${PROJECT_ROOT}/local-gems/gem-home
8
8
  export PATH=${PATH}:${GEM_HOME}/bin
9
- # This sets up the Node version so we don't have to do it before every. single.
10
- # shell. invocation.
11
- . ~/.nvm/nvm.sh
12
- nvm use default
@@ -11,7 +11,7 @@
11
11
  "esbuild": "^0.20.2",
12
12
  "jsdom": "^25.0.1",
13
13
  "mocha": "^10.7.3",
14
- "playwright": "1.50.1",
14
+ "playwright": "1.55.0",
15
15
  "typescript": "^5.8.3",
16
16
  "typescript-language-server": "^4.3.4",
17
17
  "vscode-langservers-extracted": "^4.10.0"
@@ -37,7 +37,8 @@ WORKDIR /brut-app
37
37
  # - lsb-release is used to generically access information for this OS's version
38
38
  # - wget allows us to copy/paste commands from vendors about how to install
39
39
  # software even though it does the same thing as curl
40
- RUN apt-get update --quiet --yes && \
40
+ RUN --mount=type=cache,target=/var/cache/apt \
41
+ apt-get update --quiet --yes && \
41
42
  apt-get install --no-install-recommends --quiet --yes \
42
43
  ca-certificates \
43
44
  curl \
@@ -54,16 +55,17 @@ RUN apt-get update --quiet --yes && \
54
55
  #
55
56
  # Incancation is based on: https://www.postgresql.org/download/linux/debian/
56
57
  RUN /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y && \
57
- apt-get update && \
58
- apt-get --yes --quiet install postgresql-client-16 && \
58
+ apt-get update --quiet --yes && \
59
+ apt-get --yes --quiet --no-install-recommends install postgresql-client-16 && \
59
60
  rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
60
61
 
61
62
  # Set basic env vars for production
62
63
  ENV RACK_ENV="production" \
63
64
  BUNDLE_DEPLOYMENT="1" \
64
65
  BUNDLE_PATH="/usr/local/bundle" \
65
- BUNDLE_WITHOUT="development" \
66
- APP_GIT_SHA1="${app_git_sha1}"
66
+ BUNDLE_WITHOUT="development"
67
+
68
+ RUN rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
67
69
 
68
70
  # This makes a new image that we'll throw away after building
69
71
  # needed artifacts.
@@ -74,35 +76,34 @@ FROM base AS build
74
76
  # - libpq-dev is needed by postgres
75
77
  # - pkg-config is, I guess, not considered "essential" (as in build-essential),
76
78
  # but still needed to install downstream stuff
77
- RUN apt-get update --quiet --yes && \
79
+ RUN --mount=type=cache,target=/var/cache/apt \
80
+ apt-get update --quiet --yes && \
78
81
  apt-get install --no-install-recommends --quiet --yes \
79
82
  build-essential \
80
83
  git \
81
84
  libpq-dev \
82
85
  pkg-config
83
86
 
84
- # Install NodeJS, per https://nodejs.org/en/download
85
- RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash && \
86
- \. "$HOME/.nvm/nvm.sh" && \
87
- nvm install 22 && \
88
- node -v && nvm current && npm -v
87
+ # Copy NodeJS from the official image. This should be faster than
88
+ # installing it via e.g. nvm
89
+ COPY --from=node:22-slim /usr/local /usr/local
89
90
 
90
- # Copy the app into the file, excluding the contents of .dockerignore
91
- COPY . .
91
+ # Copy Gemfile only as it may not change between deploys
92
+ COPY Gemfile Gemfile.lock .
92
93
 
93
94
  # Install RubyGems from app's Gemfile
94
- RUN bundle install --verbose && \
95
+ RUN --mount=type=cache,target=${BUNDLE_PATH}/cache \
96
+ bundle install --verbose && \
95
97
  rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git
96
98
 
99
+ COPY package.json package-lock.json ./
97
100
  # Install Node Modules from package.json
98
- RUN \. "$HOME/.nvm/nvm.sh" && \
99
- nvm use default && \
100
- npm clean-install --no-audit --no-fund --verbose
101
+ RUN npm clean-install --no-audit --no-fund --verbose
102
+
103
+ COPY . .
101
104
 
102
105
  # Build all assets
103
- RUN \. "$HOME/.nvm/nvm.sh" && \
104
- nvm use default && \
105
- bin/build-assets && \
106
+ RUN bin/build-assets && \
106
107
  rm -rf node_modules
107
108
 
108
109
  # We are now switching back to building the image that will be deployed.
@@ -118,7 +119,8 @@ RUN groupadd --system --gid 1000 brut && \
118
119
  chown -R brut:brut logs tmp
119
120
  USER 1000:1000
120
121
 
122
+ ENV APP_GIT_SHA1="${app_git_sha1}"
123
+
121
124
  # This is used to execute other commands. When the app is run in production,
122
125
  # this script is used to run it.
123
126
  ENTRYPOINT ["/brut-app/deploy/docker-entrypoint"]
124
-
@@ -8,19 +8,20 @@ class HerokuConfig
8
8
  # The format of this Hash is:
9
9
  #
10
10
  # {
11
- # image name» => {
12
- # cmd: command line for Dockerfile RUN directive»",
11
+ # *image name* => {
12
+ # cmd: *command line for Dockerfile RUN directive*,
13
13
  # }
14
14
  # }
15
15
  #
16
- # For example, if you have the Sidekiq segment installed, `bin/run-sidekiq`
16
+ # For example, if you have the Sidekiq segment installed, `bin/run sidekiq`
17
17
  # runs Sidekiq, so you would return this hash:
18
18
  #
19
19
  # {
20
20
  # "sidekiq" => {
21
- # cmd: "bin/run-sidekiq",
21
+ # cmd: "bin/run sidekiq",
22
22
  # }
23
23
  # }
24
- def self.additional_images = {}
24
+ #
25
+ def self.additional_images
26
+ end
25
27
  end
26
-
@@ -0,0 +1,2 @@
1
+ require_relative "bootstrap"
2
+ bootstrap = Bootstrap.new.bootstrap!
@@ -0,0 +1,4 @@
1
+ :concurrency: <%= ENV.fetch("SIDEKIQ_CONCURRENCY") { 5 } %>
2
+ :timeout: <%= ENV.fetch("SIDEKIQ_TIMEOUT_SECONDS") { 25 } %>
3
+ :queues:
4
+ - default
@@ -0,0 +1,3 @@
1
+ class AppJob
2
+ include Sidekiq::Job
3
+ end
@@ -0,0 +1,12 @@
1
+ # This is entirely to allow the integration test to run
2
+ # that verifies you have Sidekqi working and the segment was
3
+ # installed correctly. Once you have created a real job for
4
+ # your app and done whatever testing you need, please delete
5
+ # this job and the integration test.
6
+ class ExampleJob < AppJob
7
+ def perform(path_to_file, contents)
8
+ File.open(path_to_file, "w") do |f|
9
+ f.puts(contents)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,56 @@
1
+ require "sidekiq"
2
+ # Encapsulates all Sidekiq configuraiton beyond what can be done in
3
+ # app/config/sidekiq.yml. You can edit this as needed.
4
+ class SidekiqSegment
5
+
6
+ def initialize
7
+ Brut.container.store(
8
+ "flush_spans_in_sidekiq?",
9
+ "Boolean",
10
+ "True if sidekiq jobs should flush all OTel spans after the job completes"
11
+ ) do |project_env|
12
+ if ENV["FLUSH_SPANS_IN_SIDEKIQ"] == "true"
13
+ true
14
+ elsif ENV["FLUSH_SPANS_IN_SIDEKIQ"] == "false"
15
+ false
16
+ else
17
+ project_env.development?
18
+ end
19
+ end
20
+ end
21
+
22
+ def boot!
23
+ Sidekiq.configure_server do |config|
24
+ config.redis = {
25
+ # Per https://devcenter.heroku.com/articles/connecting-heroku-redis#connecting-in-ruby
26
+ ssl_params: {
27
+ verify_mode: OpenSSL::SSL::VERIFY_NONE,
28
+ },
29
+ }
30
+ config.logger = SemanticLogger["Sidekiq:server"]
31
+ if Brut.container.flush_spans_in_sidekiq?
32
+ SemanticLogger[self.class].info("Sidekiq jobs will flush spans")
33
+ config.server_middleware do |chain|
34
+ if defined? OpenTelemetry::Instrumentation::Sidekiq::Middlewares::Server::TracerMiddleware
35
+ chain.insert_before OpenTelemetry::Instrumentation::Sidekiq::Middlewares::Server::TracerMiddleware,
36
+ Brut::BackEnd::Sidekiq::Middlewares::Server::FlushSpans
37
+ else
38
+ SemanticLogger["Sidekiq:server"].warn("OpenTelemetry::Instrumentation::Sidekiq::Middlewares::Server::TracerMiddleware not defined")
39
+ end
40
+ end
41
+ else
42
+ SemanticLogger[self.class].info("Sidekiq jobs will not flush spans")
43
+ end
44
+ end
45
+
46
+ Sidekiq.configure_client do |config|
47
+ config.redis = {
48
+ # Per https://devcenter.heroku.com/articles/connecting-heroku-redis#connecting-in-ruby
49
+ ssl_params: {
50
+ verify_mode: OpenSSL::SSL::VERIFY_NONE,
51
+ },
52
+ }
53
+ config.logger = SemanticLogger["Sidekiq:client"]
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,4 @@
1
+ PIDFILE="${SCRIPT_DIR}/../tmp/pidfile.sidekiq"
2
+ CMD_MAP[sidekiq]="bin/sidekiq --config ${SCRIPT_DIR}/../app/config/sidekiq.yml --require ${SCRIPT_DIR}/../app/boot_sidekiq.rb --verbose"
3
+ PIDFILE_MAP[sidekiq]="${PIDFILE}"
4
+ CREATES_PIDFILE_MAP[sidekiq]="false"
@@ -0,0 +1,5 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe ExampleJob do
4
+ implementation_is_trivial
5
+ end
@@ -0,0 +1,38 @@
1
+ require "spec_helper"
2
+ require "timeout"
3
+
4
+ # This test will ensure that Sidekiq segment was installed
5
+ # correctly and your app can use Sidekiq. Once you have
6
+ # created a real job for your app, delete ExampleJob and this test.
7
+ RSpec.describe "Sidekiq is working", e2e: true do
8
+ it "processes jobs using the real Sidekiq server" do
9
+ # ExampleJob will write contents to a file. This
10
+ # test will delete that fail, queue the job, then
11
+ # wait up to 5 seconds for the job to run and create
12
+ # the file with the contents. This should be more than
13
+ # enough time.
14
+ file = Brut.container.tmp_dir / "sidekiq_test.txt"
15
+ if file.exist?
16
+ file.delete
17
+ end
18
+
19
+ # If the file still exists at this point, the test
20
+ # will pass even if Sidekiq is not working.
21
+ confidence_check { expect(file.exist?).to eq(false) }
22
+
23
+ ExampleJob.perform_async(file.to_s, "test content for file")
24
+ queue_name = ExampleJob.sidekiq_options["queue"]
25
+ expect {
26
+ Timeout.timeout(5) do
27
+ loop do
28
+ if file.exist?
29
+ break
30
+ end
31
+ sleep 0.1
32
+ end
33
+ end
34
+ }.not_to raise_error
35
+ content = File.read(file)
36
+ expect(content).to eq("test content for file\n")
37
+ end
38
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mkbrut
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dave Copeland
@@ -34,6 +34,8 @@ extra_rdoc_files: []
34
34
  files:
35
35
  - exe/mkbrut
36
36
  - lib/mkbrut.rb
37
+ - lib/mkbrut/add_segment.rb
38
+ - lib/mkbrut/add_segment_options.rb
37
39
  - lib/mkbrut/app.rb
38
40
  - lib/mkbrut/app_id.rb
39
41
  - lib/mkbrut/app_name.rb
@@ -51,6 +53,7 @@ files:
51
53
  - lib/mkbrut/ops/base_op.rb
52
54
  - lib/mkbrut/ops/copy_file.rb
53
55
  - lib/mkbrut/ops/insert_code_in_method.rb
56
+ - lib/mkbrut/ops/insert_into_file.rb
54
57
  - lib/mkbrut/ops/insert_route.rb
55
58
  - lib/mkbrut/ops/mkdir.rb
56
59
  - lib/mkbrut/ops/prism_parsing_op.rb
@@ -68,7 +71,6 @@ files:
68
71
  - lib/mkbrut/versions.rb
69
72
  - templates/Base/.dockerignore
70
73
  - templates/Base/.env.development.erb
71
- - templates/Base/.env.development.local
72
74
  - templates/Base/.env.test.erb
73
75
  - templates/Base/.gitignore
74
76
  - templates/Base/.projections.json
@@ -119,6 +121,7 @@ files:
119
121
  - templates/Base/bin/dev
120
122
  - templates/Base/bin/release
121
123
  - templates/Base/bin/run
124
+ - templates/Base/bin/run.run
122
125
  - templates/Base/bin/scaffold
123
126
  - templates/Base/bin/setup
124
127
  - templates/Base/bin/startup-message
@@ -173,6 +176,14 @@ files:
173
176
  - templates/segments/Heroku/deploy/Dockerfile
174
177
  - templates/segments/Heroku/deploy/docker-entrypoint
175
178
  - templates/segments/Heroku/deploy/heroku_config.rb
179
+ - templates/segments/Sidekiq/app/boot_sidekiq.rb
180
+ - templates/segments/Sidekiq/app/config/sidekiq.yml
181
+ - templates/segments/Sidekiq/app/src/back_end/jobs/app_job.rb
182
+ - templates/segments/Sidekiq/app/src/back_end/jobs/example_job.rb
183
+ - templates/segments/Sidekiq/app/src/back_end/segments/sidekiq_segment.rb
184
+ - templates/segments/Sidekiq/bin/run.sidekiq
185
+ - templates/segments/Sidekiq/specs/back_end/jobs/example_job.spec.rb
186
+ - templates/segments/Sidekiq/specs/integration/sidekiq_works.spec.rb
176
187
  homepage: https://brutrb.com
177
188
  licenses: []
178
189
  metadata: {}
@@ -190,7 +201,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
190
201
  - !ruby/object:Gem::Version
191
202
  version: '0'
192
203
  requirements: []
193
- rubygems_version: 3.7.1
204
+ rubygems_version: 3.7.2
194
205
  specification_version: 4
195
206
  summary: Create a new Brut App
196
207
  test_files: []
@@ -1,2 +0,0 @@
1
- # Place developer-specific overrides of .env.development in here,
2
- # e.g. API keys needed for local dev. DO NOT CHECK INTO VERSION CONTROL