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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +42 -0
- data/.gitignore +4 -0
- data/CLAUDE.md +55 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +179 -0
- data/README.md +107 -0
- data/Rakefile +8 -0
- data/bard-new.gemspec +25 -0
- data/features/new.feature +12 -0
- data/features/provision.feature +10 -0
- data/features/step_definitions/bard_new_steps.rb +64 -0
- data/features/support/bard-coverage +16 -0
- data/features/support/env.rb +22 -0
- data/features/support/new_server.rb +136 -0
- data/features/support/provision_server.rb +282 -0
- data/lib/bard/new/cli/new.rb +102 -0
- data/lib/bard/new/cli/provision.rb +32 -0
- data/lib/bard/new/provision/app.rb +9 -0
- data/lib/bard/new/provision/apt.rb +15 -0
- data/lib/bard/new/provision/authorizedkeys.rb +23 -0
- data/lib/bard/new/provision/base.rb +17 -0
- data/lib/bard/new/provision/data.rb +23 -0
- data/lib/bard/new/provision/deploy.rb +9 -0
- data/lib/bard/new/provision/http.rb +15 -0
- data/lib/bard/new/provision/logrotation.rb +27 -0
- data/lib/bard/new/provision/masterkey.rb +17 -0
- data/lib/bard/new/provision/mysql.rb +21 -0
- data/lib/bard/new/provision/nginx.rb +31 -0
- data/lib/bard/new/provision/repo.rb +71 -0
- data/lib/bard/new/provision/rvm.rb +20 -0
- data/lib/bard/new/provision/ssh.rb +79 -0
- data/lib/bard/new/provision/swapfile.rb +21 -0
- data/lib/bard/new/provision/user.rb +43 -0
- data/lib/bard/new/rails_template.rb +213 -0
- data/lib/bard/new/version.rb +5 -0
- data/lib/bard/plugins/new.rb +2 -0
- data/spec/acceptance/docker/Dockerfile.new +68 -0
- data/spec/acceptance/docker/Dockerfile.provision +41 -0
- data/spec/acceptance/docker/entrypoint-new.sh +3 -0
- data/spec/acceptance/docker/test_key +27 -0
- data/spec/acceptance/docker/test_key.pub +1 -0
- data/spec/bard/new/cli/new_spec.rb +85 -0
- data/spec/bard/new/cli/provision_spec.rb +40 -0
- data/spec/bard/new/provision/app_spec.rb +33 -0
- data/spec/bard/new/provision/apt_spec.rb +39 -0
- data/spec/bard/new/provision/authorizedkeys_spec.rb +40 -0
- data/spec/bard/new/provision/base_spec.rb +34 -0
- data/spec/bard/new/provision/data_spec.rb +54 -0
- data/spec/bard/new/provision/deploy_spec.rb +33 -0
- data/spec/bard/new/provision/http_spec.rb +57 -0
- data/spec/bard/new/provision/logrotation_spec.rb +34 -0
- data/spec/bard/new/provision/masterkey_spec.rb +62 -0
- data/spec/bard/new/provision/mysql_spec.rb +55 -0
- data/spec/bard/new/provision/nginx_spec.rb +81 -0
- data/spec/bard/new/provision/repo_spec.rb +208 -0
- data/spec/bard/new/provision/rvm_spec.rb +49 -0
- data/spec/bard/new/provision/ssh_spec.rb +242 -0
- data/spec/bard/new/provision/swapfile_spec.rb +33 -0
- data/spec/bard/new/provision/user_spec.rb +103 -0
- data/spec/spec_helper.rb +19 -0
- 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,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"]
|