bard-new 0.1.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 (62) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +42 -0
  3. data/.gitignore +4 -0
  4. data/CLAUDE.md +55 -0
  5. data/Gemfile +10 -0
  6. data/Gemfile.lock +179 -0
  7. data/README.md +107 -0
  8. data/Rakefile +8 -0
  9. data/bard-new.gemspec +25 -0
  10. data/features/new.feature +12 -0
  11. data/features/provision.feature +10 -0
  12. data/features/step_definitions/bard_new_steps.rb +64 -0
  13. data/features/support/bard-coverage +16 -0
  14. data/features/support/env.rb +22 -0
  15. data/features/support/new_server.rb +136 -0
  16. data/features/support/provision_server.rb +282 -0
  17. data/lib/bard/new/cli/new.rb +102 -0
  18. data/lib/bard/new/cli/provision.rb +32 -0
  19. data/lib/bard/new/provision/app.rb +9 -0
  20. data/lib/bard/new/provision/apt.rb +15 -0
  21. data/lib/bard/new/provision/authorizedkeys.rb +23 -0
  22. data/lib/bard/new/provision/base.rb +17 -0
  23. data/lib/bard/new/provision/data.rb +23 -0
  24. data/lib/bard/new/provision/deploy.rb +9 -0
  25. data/lib/bard/new/provision/http.rb +15 -0
  26. data/lib/bard/new/provision/logrotation.rb +27 -0
  27. data/lib/bard/new/provision/masterkey.rb +17 -0
  28. data/lib/bard/new/provision/mysql.rb +21 -0
  29. data/lib/bard/new/provision/nginx.rb +31 -0
  30. data/lib/bard/new/provision/repo.rb +71 -0
  31. data/lib/bard/new/provision/rvm.rb +20 -0
  32. data/lib/bard/new/provision/ssh.rb +79 -0
  33. data/lib/bard/new/provision/swapfile.rb +21 -0
  34. data/lib/bard/new/provision/user.rb +43 -0
  35. data/lib/bard/new/rails_template.rb +213 -0
  36. data/lib/bard/new/version.rb +5 -0
  37. data/lib/bard/plugins/new.rb +2 -0
  38. data/spec/acceptance/docker/Dockerfile.new +68 -0
  39. data/spec/acceptance/docker/Dockerfile.provision +41 -0
  40. data/spec/acceptance/docker/entrypoint-new.sh +3 -0
  41. data/spec/acceptance/docker/test_key +27 -0
  42. data/spec/acceptance/docker/test_key.pub +1 -0
  43. data/spec/bard/new/cli/new_spec.rb +85 -0
  44. data/spec/bard/new/cli/provision_spec.rb +40 -0
  45. data/spec/bard/new/provision/app_spec.rb +33 -0
  46. data/spec/bard/new/provision/apt_spec.rb +39 -0
  47. data/spec/bard/new/provision/authorizedkeys_spec.rb +40 -0
  48. data/spec/bard/new/provision/base_spec.rb +34 -0
  49. data/spec/bard/new/provision/data_spec.rb +54 -0
  50. data/spec/bard/new/provision/deploy_spec.rb +33 -0
  51. data/spec/bard/new/provision/http_spec.rb +57 -0
  52. data/spec/bard/new/provision/logrotation_spec.rb +34 -0
  53. data/spec/bard/new/provision/masterkey_spec.rb +62 -0
  54. data/spec/bard/new/provision/mysql_spec.rb +55 -0
  55. data/spec/bard/new/provision/nginx_spec.rb +81 -0
  56. data/spec/bard/new/provision/repo_spec.rb +208 -0
  57. data/spec/bard/new/provision/rvm_spec.rb +49 -0
  58. data/spec/bard/new/provision/ssh_spec.rb +242 -0
  59. data/spec/bard/new/provision/swapfile_spec.rb +33 -0
  60. data/spec/bard/new/provision/user_spec.rb +103 -0
  61. data/spec/spec_helper.rb +19 -0
  62. metadata +214 -0
@@ -0,0 +1,27 @@
1
+ # install log rotation if missing
2
+
3
+ class Bard::Provision::LogRotation < Bard::Provision
4
+ def call
5
+ print "Log Rotation:"
6
+
7
+ provision_server.run! <<~SH, quiet: true
8
+ file=/etc/logrotate.d/#{config.project_name}
9
+ if [ ! -f $file ]; then
10
+ sudo tee $file > /dev/null <<EOF
11
+ $(pwd)/log/*.log {
12
+ weekly
13
+ size 100M
14
+ missingok
15
+ rotate 52
16
+ delaycompress
17
+ notifempty
18
+ copytruncate
19
+ create 664 www www
20
+ }
21
+ EOF
22
+ fi
23
+ SH
24
+
25
+ puts " ✓"
26
+ end
27
+ end
@@ -0,0 +1,17 @@
1
+ require "bard/plugins/ssh/copy"
2
+
3
+ # copy master key if missing
4
+
5
+ class Bard::Provision::MasterKey < Bard::Provision
6
+ def call
7
+ print "Master Key:"
8
+ if File.exist?("config/master.key")
9
+ if !provision_server.run "[ -f config/master.key ]", quiet: true
10
+ print " Uploading config/master.key,"
11
+ Bard::Copy.file "config/master.key", from: config[:local], to: provision_server
12
+ end
13
+ end
14
+
15
+ puts " ✓"
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ # install mysql
2
+
3
+ class Bard::Provision::MySQL < Bard::Provision
4
+ def call
5
+ print "MySQL:"
6
+ if !mysql_responding?
7
+ print " Installing,"
8
+ provision_server.run! [
9
+ "sudo DEBIAN_FRONTEND=noninteractive apt-get install -y mysql-server",
10
+ %{sudo mysql -uroot -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '' PASSWORD EXPIRE NEVER; FLUSH PRIVILEGES;"},
11
+ %{mysql -uroot -e "UPDATE mysql.user SET password_lifetime = NULL WHERE user = 'root' AND host = 'localhost';"},
12
+ ].join("; "), home: true
13
+ end
14
+
15
+ puts " ✓"
16
+ end
17
+
18
+ def mysql_responding?
19
+ provision_server.run "sudo systemctl is-active --quiet mysql", home: true, quiet: true
20
+ end
21
+ end
@@ -0,0 +1,31 @@
1
+ # install nginx
2
+
3
+ class Bard::Provision::Nginx < Bard::Provision
4
+ def call
5
+ print "Nginx:"
6
+ if !http_responding?
7
+ print " Installing nginx,"
8
+ provision_server.run! [
9
+ %(grep -qxF "RAILS_ENV=production" /etc/environment || echo "RAILS_ENV=production" | sudo tee -a /etc/environment),
10
+ %(grep -qxF "EDITOR=vim" /etc/environment || echo "EDITOR=vim" | sudo tee -a /etc/environment),
11
+ "sudo apt-get install -y nginx",
12
+ "sudo rm -f /etc/nginx/sites-enabled/default",
13
+ ].join("; "), home: true
14
+ end
15
+
16
+ if !app_configured?
17
+ print " Creating nginx config for app,"
18
+ provision_server.run! "bard setup"
19
+ end
20
+
21
+ puts " ✓"
22
+ end
23
+
24
+ def http_responding?
25
+ provision_server.run "nc -zv localhost 80 2>/dev/null", home: true, quiet: true
26
+ end
27
+
28
+ def app_configured?
29
+ provision_server.run "[ -f /etc/nginx/sites-enabled/#{config.project_name} ]", quiet: true
30
+ end
31
+ end
@@ -0,0 +1,71 @@
1
+ require "bard/plugins/github"
2
+
3
+ # generate and install ssh public key into deploy keys
4
+ # add repo to known hosts
5
+ # clone repo
6
+
7
+ class Bard::Provision::Repo < Bard::Provision
8
+ def call
9
+ print "Repo:"
10
+ if !already_cloned?
11
+ if !can_clone_project?
12
+ if !ssh_keypair?
13
+ print " Generating keypair in ~/.ssh,"
14
+ provision_server.run! "ssh-keygen -t rsa -b 2048 -f ~/.ssh/id_rsa -q -N \"\"", home: true
15
+ end
16
+ print " Add public key to GitHub repo deploy keys,"
17
+ title = "#{target.ssh_uri.user}@#{target.ssh_uri.host}"
18
+ key = provision_server.run "cat ~/.ssh/id_rsa.pub", home: true
19
+ Bard::Github.new(config.project_name).add_deploy_key title:, key:
20
+ end
21
+ print " Cloning repo,"
22
+ provision_server.run! "git clone git@github.com:botandrosedesign/#{project_name}", home: true
23
+ else
24
+ if !on_latest_master?
25
+ print " Updating to latest master,"
26
+ update_to_latest_master!
27
+ end
28
+ end
29
+
30
+ puts " ✓"
31
+ end
32
+
33
+ private
34
+
35
+ def ssh_keypair?
36
+ provision_server.run "[ -f ~/.ssh/id_rsa.pub ]", home: true, quiet: true
37
+ end
38
+
39
+ def already_cloned?
40
+ provision_server.run "[ -d ~/#{project_name}/.git ]", home: true, quiet: true
41
+ end
42
+
43
+ def can_clone_project?
44
+ github_url = "git@github.com:botandrosedesign/#{project_name}"
45
+ provision_server.run [
46
+ "needle=$(ssh-keyscan -t ed25519 github.com 2>/dev/null | cut -d \" \" -f 2-3)",
47
+ "grep -q \"$needle\" ~/.ssh/known_hosts || ssh-keyscan -H github.com >> ~/.ssh/known_hosts 2>/dev/null",
48
+ "git ls-remote #{github_url}",
49
+ ].join("; "), home: true, quiet: true
50
+ end
51
+
52
+ def project_name
53
+ config.project_name
54
+ end
55
+
56
+ def on_latest_master?
57
+ provision_server.run [
58
+ "cd ~/#{project_name}",
59
+ "git fetch origin",
60
+ "[ $(git rev-parse HEAD) = $(git rev-parse origin/master) ]"
61
+ ].join(" && "), home: true, quiet: true
62
+ end
63
+
64
+ def update_to_latest_master!
65
+ provision_server.run! [
66
+ "cd ~/#{project_name}",
67
+ "git checkout master",
68
+ "git reset --hard origin/master"
69
+ ].join(" && "), home: true
70
+ end
71
+ end
@@ -0,0 +1,20 @@
1
+ # install rvm if missing
2
+
3
+ class Bard::Provision::RVM < Bard::Provision
4
+ def call
5
+ print "RVM:"
6
+ if !provision_server.run "[ -d ~/.rvm ]", quiet: true
7
+ print " Installing RVM,"
8
+ provision_server.run! [
9
+ %(sed -i "1i[[ -s \\"$HOME/.rvm/scripts/rvm\\" ]] && source \\"$HOME/.rvm/scripts/rvm\\" # Load RVM into a shell session *as a function*" ~/.bashrc),
10
+ "gpg --keyserver keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB",
11
+ "curl -sSL https://get.rvm.io | bash -s stable",
12
+ ].join("; ")
13
+ version = File.read(".ruby-version").chomp
14
+ print " Installing Ruby #{version},"
15
+ provision_server.run! "rvm install #{version}"
16
+ end
17
+
18
+ puts " ✓"
19
+ end
20
+ end
@@ -0,0 +1,79 @@
1
+ # move ssh port
2
+ # add to known hosts
3
+
4
+ class Bard::Provision::SSH < Bard::Provision
5
+ def call
6
+ print "SSH:"
7
+
8
+ if password_auth_enabled?
9
+ print " Disabling password authentication,"
10
+ disable_password_auth!
11
+ end
12
+
13
+ if !ssh_available?(provision_server.ssh_uri, port: target_port)
14
+ if !ssh_available?(provision_server.ssh_uri)
15
+ raise "can't find SSH on port #{target_port} or #{provision_server.ssh_uri.port || 22}"
16
+ end
17
+ if !ssh_known_host?(provision_server.ssh_uri)
18
+ print " Adding known host,"
19
+ add_ssh_known_host!(provision_server.ssh_uri)
20
+ end
21
+ print " Reconfiguring port to #{target_port},"
22
+ provision_server.run! %(echo "Port #{target_port}" | sudo tee /etc/ssh/sshd_config.d/port_#{target_port}.conf && sudo service ssh restart), home: true
23
+ 5.times do
24
+ sleep 1
25
+ break if ssh_available?(provision_server.ssh_uri, port: target_port)
26
+ end
27
+ if !ssh_available?(provision_server.ssh_uri, port: target_port)
28
+ raise "reconfigured SSH to port #{target_port} but it's not responding — check firewall and sshd_config Include directive"
29
+ end
30
+ end
31
+
32
+ if !ssh_known_host?(provision_server.ssh_uri)
33
+ print " Adding known host,"
34
+ add_ssh_known_host!(provision_server.ssh_uri)
35
+ end
36
+
37
+ # provision with new target port from now on
38
+ ssh_url.gsub!(/:\d+$/, "")
39
+ ssh_url << ":#{target_port}"
40
+ puts " ✓"
41
+ end
42
+
43
+ private
44
+
45
+ def target_port
46
+ target.ssh_uri.port || 22
47
+ end
48
+
49
+ def ssh_available? ssh_uri, port: nil
50
+ port ||= ssh_uri.port || 22
51
+ system "nc -zv #{ssh_uri.host} #{port} 2>/dev/null"
52
+ end
53
+
54
+ def ssh_known_host? ssh_uri
55
+ port ||= ssh_uri.port || 22
56
+ system "grep -q \"$(ssh-keyscan -t ed25519 -p#{port} #{ssh_uri.host} 2>/dev/null | cut -d ' ' -f 2-3)\" ~/.ssh/known_hosts"
57
+ end
58
+
59
+ def add_ssh_known_host! ssh_uri
60
+ port ||= ssh_uri.port || 22
61
+ system "ssh-keyscan -p#{port} -H #{ssh_uri.host} >> ~/.ssh/known_hosts 2>/dev/null"
62
+ end
63
+
64
+ def password_auth_enabled?
65
+ result = provision_server.run!(
66
+ %q{grep -E '^\s*PasswordAuthentication\s+yes' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf 2>/dev/null || true},
67
+ home: true,
68
+ capture: true
69
+ )
70
+ !!(result && !result.strip.empty?)
71
+ end
72
+
73
+ def disable_password_auth!
74
+ provision_server.run!(
75
+ %q{echo "PasswordAuthentication no" | sudo tee /etc/ssh/sshd_config.d/disable_password_auth.conf && sudo service ssh restart},
76
+ home: true
77
+ )
78
+ end
79
+ end
@@ -0,0 +1,21 @@
1
+ # setup swapfile
2
+
3
+ class Bard::Provision::Swapfile < Bard::Provision
4
+ def call
5
+ print "Swapfile:"
6
+
7
+ provision_server.run! <<~SH, home: true
8
+ if [ ! -f /swapfile ]; then
9
+ sudo fallocate -l $(grep MemTotal /proc/meminfo | awk '{print $2}')K /swapfile
10
+ fi
11
+ sudo chmod 600 /swapfile
12
+ sudo swapon --show | grep -q '/swapfile' || sudo mkswap /swapfile
13
+ sudo swapon --show | grep -q '/swapfile' || sudo swapon /swapfile
14
+ grep -q '/swapfile none swap sw 0 0' /etc/fstab || echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
15
+ SH
16
+
17
+ provision_server.run! "sudo swapon --show | grep -q /swapfile", home: true
18
+
19
+ puts " ✓"
20
+ end
21
+ end
@@ -0,0 +1,43 @@
1
+ # rename user
2
+
3
+ class Bard::Provision::User < Bard::Provision
4
+ def call
5
+ print "User:"
6
+
7
+ if !ssh_with_user?(provision_server.ssh_uri, user: new_user)
8
+ if !ssh_with_user?(provision_server.ssh_uri)
9
+ raise "can't ssh in with user #{new_user} or #{old_user}"
10
+ end
11
+ print " Adding user #{new_user},"
12
+ provision_server.run! [
13
+ "sudo useradd -m -s /bin/bash #{new_user}",
14
+ "sudo usermod -aG sudo #{new_user}",
15
+ "echo \"#{new_user} ALL=(ALL) NOPASSWD:ALL\" | sudo tee -a /etc/sudoers",
16
+ "sudo mkdir -p ~#{new_user}/.ssh",
17
+ "sudo cp ~/.ssh/authorized_keys ~#{new_user}/.ssh/authorized_keys",
18
+ "sudo chown -R #{new_user}:#{new_user} ~#{new_user}/.ssh",
19
+ "sudo chmod +rx ~#{new_user}", # so nginx can read it
20
+ ].join("; "), home: true
21
+ end
22
+
23
+ # provision with new user from now on
24
+ ssh_url.gsub!("#{old_user}@", "#{new_user}@")
25
+ puts " ✓"
26
+ end
27
+
28
+ private
29
+
30
+ def new_user
31
+ target.ssh_uri.user
32
+ end
33
+
34
+ def old_user
35
+ provision_server.ssh_uri.user
36
+ end
37
+
38
+ def ssh_with_user? ssh_uri, user: ssh_uri.user
39
+ ssh_opts = "-o ConnectTimeout=2 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes"
40
+ ssh_opts += " -i #{provision_server.ssh_key}" if provision_server.respond_to?(:ssh_key) && provision_server.ssh_key
41
+ system "ssh #{ssh_opts} -p#{ssh_uri.port || 22} #{user}@#{ssh_uri.host} : >/dev/null 2>&1"
42
+ end
43
+ end
@@ -0,0 +1,213 @@
1
+ ruby_version, project_name = (`rvm current name`.chomp).split("@")
2
+ rails_version = Gem.loaded_specs["railties"].version
3
+
4
+ file ".ruby-version", ruby_version
5
+ file ".ruby-gemset", project_name
6
+ file ".gitignore", <<~GITIGNORE
7
+ # See https://help.github.com/articles/ignoring-files for more about ignoring files.
8
+ #
9
+ # If you find yourself ignoring temporary files generated by your text editor
10
+ # or operating system, you probably want to add a global ignore instead:
11
+ # git config --global core.excludesfile '~/.gitignore_global'
12
+
13
+ # Ignore bundler config.
14
+ /.bundle
15
+
16
+ # Ignore all logfiles and tempfiles.
17
+ /log/*
18
+ /tmp/*
19
+ !/log/.keep
20
+ !/tmp/.keep
21
+
22
+ # Ignore master key for decrypting credentials and more.
23
+ /config/master.key
24
+
25
+ # ignore coverage reports
26
+ /coverage
27
+
28
+ # Ignore database dumps
29
+ /db/data.*
30
+
31
+ # Ignore storage (uploaded files in development and any SQLite databases).
32
+ /storage/*
33
+
34
+ # Ignore Syncthing
35
+ .stfolder/
36
+
37
+ # Thank Apple!
38
+ .DS_Store
39
+ GITIGNORE
40
+
41
+ file "Gemfile", <<~RUBY
42
+ source "https://rubygems.org"
43
+
44
+ gem "bootsnap", require: false
45
+ gem "rails", "~> #{rails_version}"
46
+ gem "solid_cache"
47
+ gem "solid_queue"
48
+ gem "solid_cable"
49
+ gem "bard", github: "botandrose/bard", branch: "v2.0"
50
+ gem "bard-rails"
51
+ gem "sqlite3"
52
+ gem "image_processing"
53
+ gem "puma"
54
+ gem "exception_notification"
55
+
56
+ # css
57
+ gem "sprockets-rails"
58
+ gem "dartsass-sprockets"
59
+ gem "bard-sass"
60
+
61
+ # js
62
+ gem "importmap-rails"
63
+ gem "turbo-rails"
64
+ gem "stimulus-rails"
65
+
66
+ group :development do
67
+ gem "web-console"
68
+ end
69
+
70
+ group :development, :test do
71
+ gem "debug", require: "debug/prelude"
72
+ gem "parallel_tests", "~>3.9.0" # 3.10 pegs CPU
73
+ gem "brakeman", require: false
74
+ gem "rubocop-rails-omakase", require: false
75
+ end
76
+
77
+ group :test do
78
+ gem "cucumber-rails", require: false
79
+ gem "cuprite-downloads"
80
+ gem "capybara-screenshot"
81
+ gem "database_cleaner"
82
+ gem "chop"
83
+ gem "email_spec"
84
+ gem "timecop"
85
+ gem "rspec-rails"
86
+ end
87
+
88
+ group :production do
89
+ gem "foreman-export-systemd_user"
90
+ end
91
+ RUBY
92
+
93
+ file "config/initializers/exception_notification.rb", <<~RUBY
94
+ require "exception_notification/rails"
95
+
96
+ ExceptionNotification.configure do |config|
97
+ config.ignored_exceptions = []
98
+
99
+ # Adds a condition to decide when an exception must be ignored or not.
100
+ # The ignore_if method can be invoked multiple times to add extra conditions.
101
+ config.ignore_if do |exception, options|
102
+ not Rails.env.production?
103
+ end
104
+
105
+ config.ignore_if do |exception, options|
106
+ %w[
107
+ ActiveRecord::RecordNotFound
108
+ AbstractController::ActionNotFound
109
+ ActionController::RoutingError
110
+ ActionController::InvalidAuthenticityToken
111
+ ActionView::MissingTemplate
112
+ ActionController::BadRequest
113
+ ActionDispatch::Http::Parameters::ParseError
114
+ ActionDispatch::Http::MimeNegotiation::InvalidType
115
+ ].include?(exception.class.to_s)
116
+ end
117
+
118
+ config.add_notifier :email, {
119
+ email_prefix: "[\#{File.basename(Dir.pwd)}] ",
120
+ exception_recipients: "micah@botandrose.com",
121
+ smtp_settings: Rails.application.credentials.exception_notification_smtp_settings,
122
+ }
123
+ end
124
+
125
+ if defined?(Rake::Application)
126
+ Rake::Application.prepend Module.new {
127
+ def display_error_message error
128
+ ExceptionNotifier.notify_exception(error)
129
+ super
130
+ end
131
+
132
+ def invoke_task task_name
133
+ super
134
+ rescue RuntimeError => exception
135
+ if exception.message.starts_with?("Don't know how to build task")
136
+ ExceptionNotifier.notify_exception(exception)
137
+ end
138
+ raise exception
139
+ end
140
+ }
141
+ end
142
+
143
+ ActionController::Live.prepend Module.new {
144
+ def log_error exception
145
+ ExceptionNotifier.notify_exception exception, env: request.env
146
+ super
147
+ end
148
+ }
149
+ RUBY
150
+
151
+ file "app/assets/config/manifest.js", <<~RUBY
152
+ //= link_tree ../images
153
+ //= link_directory ../stylesheets .css
154
+ //= link_tree ../../javascript .js
155
+ RUBY
156
+
157
+ run "rm -f app/assets/stylesheets/application.css"
158
+
159
+ file "app/assets/stylesheets/application.sass", <<~SASS
160
+ body
161
+ border: 10px solid red
162
+ SASS
163
+
164
+ gsub_file "app/views/layouts/application.html.erb", " <%# Includes all stylesheet files in app/assets/stylesheets %>\n", ''
165
+ gsub_file "app/views/layouts/application.html.erb", 'stylesheet_link_tag :app,', 'stylesheet_link_tag :application,'
166
+
167
+ file "app/views/static/index.html.slim", <<~SLIM
168
+ h1 #{project_name}
169
+ SLIM
170
+
171
+ insert_into_file "config/database.yml", <<~YAML, after: "database: storage/test.sqlite3"
172
+
173
+ staging:
174
+ <<: *default
175
+ database: storage/staging.sqlite3
176
+ YAML
177
+
178
+ insert_into_file "config/database.yml", <<-YAML, after: "# database: path/to/persistent/storage/production.sqlite3"
179
+
180
+ cable:
181
+ <<: *default
182
+ # database: path/to/persistent/storage/production_cable.sqlite3
183
+ migrations_paths: db/cable_migrate
184
+ queue:
185
+ <<: *default
186
+ # database: path/to/persistent/storage/production_queue.sqlite3
187
+ migrations_paths: db/queue_migrate
188
+ YAML
189
+
190
+ gsub_file "config/database.yml", "path/to/persistent/", ""
191
+
192
+ gsub_file "config/environments/production.rb", / (config\.logger.+STDOUT.*)$/, ' # \1'
193
+
194
+ file "Procfile", "web: bundle exec puma -p 3000\n"
195
+
196
+ append_to_file "Rakefile", <<~'RUBY'
197
+
198
+ task bootstrap: :environment do
199
+ system "bin/rails db:prepare"
200
+ if Rails.env.production?
201
+ system "bin/rails assets:precompile"
202
+ app = File.basename(Dir.pwd)
203
+ system "bundle exec foreman export systemd-user --app #{app}"
204
+ system "systemctl --user restart #{app}.target"
205
+ end
206
+ end
207
+ RUBY
208
+
209
+ after_bundle do
210
+ run "bundle exec bard install"
211
+ run "bin/setup"
212
+ run "bundle exec bard setup"
213
+ end
@@ -0,0 +1,5 @@
1
+ module Bard
2
+ module New
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,2 @@
1
+ require "bard/new/cli/new"
2
+ require "bard/new/cli/provision"
@@ -0,0 +1,68 @@
1
+ FROM ubuntu:24.04
2
+
3
+ ENV DEBIAN_FRONTEND=noninteractive
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && \
7
+ apt-get install -y \
8
+ openssh-server \
9
+ nginx \
10
+ git \
11
+ sudo \
12
+ curl \
13
+ build-essential \
14
+ libsqlite3-dev \
15
+ libsodium-dev \
16
+ gawk \
17
+ tzdata \
18
+ && rm -rf /var/lib/apt/lists/*
19
+
20
+ # Remove default nginx site
21
+ RUN rm -f /etc/nginx/sites-enabled/default
22
+
23
+ # Create deploy user with passwordless sudo
24
+ RUN useradd -m -s /bin/bash deploy && \
25
+ echo 'deploy:password' | chpasswd && \
26
+ echo 'deploy ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
27
+
28
+ # Setup SSH
29
+ RUN mkdir -p /var/run/sshd && \
30
+ mkdir -p /home/deploy/.ssh && \
31
+ chmod 700 /home/deploy/.ssh
32
+
33
+ # Copy SSH authorized key
34
+ COPY spec/acceptance/docker/test_key.pub /home/deploy/.ssh/authorized_keys
35
+ RUN chown -R deploy:deploy /home/deploy/.ssh && \
36
+ chmod 600 /home/deploy/.ssh/authorized_keys
37
+
38
+ # Allow SSH login with keys
39
+ RUN sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config && \
40
+ sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
41
+
42
+ # Install RVM and Ruby as deploy user
43
+ USER deploy
44
+ RUN curl -sSL https://rvm.io/mpapis.asc | gpg --import - && \
45
+ curl -sSL https://rvm.io/pkuczynski.asc | gpg --import - && \
46
+ curl -sSL https://get.rvm.io | bash -s stable
47
+
48
+ RUN bash -lc "rvm install ruby-4.0.2 && rvm use ruby-4.0.2 --default"
49
+
50
+ # Install bard and bard-new gems from source
51
+ RUN bash -lc "git clone --depth 1 --branch v2.0 https://github.com/botandrose/bard.git /home/deploy/bard-src && cd /home/deploy/bard-src && gem build bard.gemspec && gem install bard-*.gem"
52
+ COPY --chown=deploy:deploy . /home/deploy/bard-new-src/
53
+ RUN bash -lc "cd /home/deploy/bard-new-src && gem build bard-new.gemspec && gem install bard-new-*.gem"
54
+
55
+ # Configure git
56
+ RUN git config --global user.email "test@example.com" && \
57
+ git config --global user.name "Test User" && \
58
+ git config --global init.defaultBranch master
59
+
60
+ USER root
61
+
62
+ # Start both sshd and nginx
63
+ COPY spec/acceptance/docker/entrypoint-new.sh /entrypoint.sh
64
+ RUN chmod +x /entrypoint.sh
65
+
66
+ EXPOSE 22
67
+
68
+ CMD ["/entrypoint.sh"]
@@ -0,0 +1,41 @@
1
+ FROM ubuntu:24.04
2
+
3
+ ENV DEBIAN_FRONTEND=noninteractive
4
+
5
+ # Minimal packages: systemd + SSH + git
6
+ RUN apt-get update && \
7
+ apt-get install -y \
8
+ systemd systemd-sysv \
9
+ openssh-server \
10
+ git \
11
+ sudo \
12
+ && rm -rf /var/lib/apt/lists/*
13
+
14
+ # Enable SSH via systemd
15
+ RUN systemctl enable ssh
16
+
17
+ # Root SSH setup (password auth left enabled so SSH step can disable it)
18
+ RUN mkdir -p /root/.ssh
19
+ COPY test_key.pub /root/.ssh/authorized_keys
20
+ RUN chmod 700 /root/.ssh && \
21
+ chmod 600 /root/.ssh/authorized_keys
22
+
23
+ RUN sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config
24
+
25
+ # Prevent interactive prompts during apt-get install via SSH
26
+ RUN echo 'DEBIAN_FRONTEND=noninteractive' >> /etc/environment
27
+
28
+ # Systemd cleanup for container use, then re-enable SSH
29
+ RUN (cd /lib/systemd/system/sysinit.target.wants/; \
30
+ for i in *; do [ $i != systemd-tmpfiles-setup.service ] && rm -f $i; done); \
31
+ rm -f /lib/systemd/system/multi-user.target.wants/*; \
32
+ rm -f /etc/systemd/system/*.wants/*; \
33
+ rm -f /lib/systemd/system/local-fs.target.wants/*; \
34
+ rm -f /lib/systemd/system/sockets.target.wants/*udev*; \
35
+ rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \
36
+ rm -f /lib/systemd/system/basic.target.wants/*
37
+ RUN systemctl enable ssh
38
+
39
+ EXPOSE 22
40
+
41
+ CMD ["/sbin/init"]
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+ nginx
3
+ exec /usr/sbin/sshd -D