git-deploy-ng 0.8.0 → 0.9.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 +4 -4
- data/AGENTS.md +46 -0
- data/CHANGELOG.md +37 -0
- data/CONTRIBUTING.md +41 -3
- data/README.md +335 -0
- data/ROADMAP.md +15 -3
- data/bin/update-changelog +47 -0
- data/lib/git_deploy/changelog.rb +125 -0
- data/lib/git_deploy/configuration.rb +20 -5
- data/lib/git_deploy/generator.rb +39 -2
- data/lib/git_deploy/remote_path.rb +28 -0
- data/lib/git_deploy/ssh_methods.rb +15 -0
- data/lib/git_deploy/templates/generic/after_push.sh +15 -0
- data/lib/git_deploy/templates/generic/before_restart.sh +3 -0
- data/lib/git_deploy/templates/generic/restart.sh +2 -0
- data/lib/git_deploy/templates/php-composer/after_push.sh +15 -0
- data/lib/git_deploy/templates/php-composer/before_restart.sh +12 -0
- data/lib/git_deploy/templates/php-composer/restart.sh +13 -0
- data/lib/git_deploy/templates/rails-puma/after_push.sh +17 -0
- data/lib/git_deploy/templates/rails-puma/before_restart.rb +38 -0
- data/lib/git_deploy/templates/rails-puma/restart.sh +9 -0
- data/lib/git_deploy.rb +43 -5
- data/spec/changelog_spec.rb +110 -0
- data/spec/cli_spec.rb +9 -1
- data/spec/commands_spec.rb +22 -0
- data/spec/configuration_spec.rb +10 -0
- data/spec/documentation_spec.rb +15 -0
- data/spec/generator_spec.rb +75 -0
- data/spec/hooks_spec.rb +15 -0
- data/spec/remote_path_spec.rb +45 -0
- data/spec/setup_spec.rb +48 -1
- data/spec/workflows_spec.rb +57 -0
- metadata +25 -6
- data/README.markdown +0 -172
- /data/lib/git_deploy/templates/{after_push.sh → rails-passenger/after_push.sh} +0 -0
- /data/lib/git_deploy/templates/{before_restart.rb → rails-passenger/before_restart.rb} +0 -0
- /data/lib/git_deploy/templates/{restart.sh → rails-passenger/restart.sh} +0 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
require 'date'
|
|
2
|
+
|
|
3
|
+
class GitDeploy
|
|
4
|
+
module Changelog
|
|
5
|
+
HEADER = <<~HEADER
|
|
6
|
+
# Changelog
|
|
7
|
+
|
|
8
|
+
All notable changes to this project will be documented in this file.
|
|
9
|
+
|
|
10
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
11
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
12
|
+
|
|
13
|
+
HEADER
|
|
14
|
+
|
|
15
|
+
UNRELEASED = '## [Unreleased]'
|
|
16
|
+
|
|
17
|
+
CATEGORY_FOR = {
|
|
18
|
+
'feat' => 'Added',
|
|
19
|
+
'fix' => 'Fixed',
|
|
20
|
+
'docs' => 'Changed',
|
|
21
|
+
'perf' => 'Changed'
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
CATEGORY_ORDER = ['Added', 'Changed', 'Fixed', 'Removed', 'Deprecated', 'Security'].freeze
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
def gemspec_version(path = 'git-deploy-ng.gemspec')
|
|
29
|
+
line = File.readlines(path).find { |l| l =~ /gem\.version\s*=/ }
|
|
30
|
+
line[/['"]([^'"]+)['"]/, 1]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def categorize(subject)
|
|
34
|
+
type = subject[/\A(\w+)(?:\([^)]+\))?!?:/, 1]
|
|
35
|
+
return nil unless type
|
|
36
|
+
|
|
37
|
+
CATEGORY_FOR[type]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def format_entry(subject)
|
|
41
|
+
body = subject.sub(/\A\w+(?:\([^)]+\))?!?:\s*/, '')
|
|
42
|
+
breaking = subject.include?('!:') || subject.match?(/\A\w+!:/)
|
|
43
|
+
entry = body.empty? ? subject : body[0].upcase + body[1..]
|
|
44
|
+
breaking ? "**BREAKING:** #{entry}" : entry
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def group_commits(subjects)
|
|
48
|
+
groups = Hash.new { |h, k| h[k] = [] }
|
|
49
|
+
subjects.each do |subject|
|
|
50
|
+
next if subject.start_with?('Merge ')
|
|
51
|
+
category = categorize(subject) || next
|
|
52
|
+
groups[category] << format_entry(subject)
|
|
53
|
+
end
|
|
54
|
+
groups.each_value(&:uniq!)
|
|
55
|
+
groups
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def render_section(version, date, groups)
|
|
59
|
+
lines = ["## [#{version}] - #{date}", '']
|
|
60
|
+
CATEGORY_ORDER.each do |category|
|
|
61
|
+
entries = groups[category]
|
|
62
|
+
next if entries.nil? || entries.empty?
|
|
63
|
+
|
|
64
|
+
lines << "### #{category}"
|
|
65
|
+
entries.sort.each { |entry| lines << "- #{entry}" }
|
|
66
|
+
lines << ''
|
|
67
|
+
end
|
|
68
|
+
lines.join("\n").rstrip + "\n"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def extract_section(content, version)
|
|
72
|
+
pattern = /^## \[#{Regexp.escape(version)}\][^\n]*\n(.*?)(?=^## \[|\z)/m
|
|
73
|
+
match = content.match(pattern)
|
|
74
|
+
return nil unless match
|
|
75
|
+
|
|
76
|
+
body = match[1].strip
|
|
77
|
+
body.empty? ? nil : body
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def verify_version!(content, version)
|
|
81
|
+
section = extract_section(content, version)
|
|
82
|
+
abort "Error: CHANGELOG.md has no entry for version #{version}" if section.nil?
|
|
83
|
+
section
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def update_content(content, version, date:, groups:)
|
|
87
|
+
section = render_section(version, date, groups)
|
|
88
|
+
if content.nil? || content.dup.force_encoding(Encoding::UTF_8).strip.empty?
|
|
89
|
+
return HEADER + "\n#{UNRELEASED}\n\n" + section + "\n"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
content = content.dup.force_encoding(Encoding::UTF_8)
|
|
93
|
+
|
|
94
|
+
unless content.include?(UNRELEASED)
|
|
95
|
+
abort "Error: CHANGELOG.md is missing the #{UNRELEASED} heading"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if content.match?(/^## \[#{Regexp.escape(version)}\]/m)
|
|
99
|
+
abort "Error: CHANGELOG.md already has a section for version #{version}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
content.sub("#{UNRELEASED}\n", "#{UNRELEASED}\n\n#{section}\n")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def git_log_subjects(since_ref, until_ref: 'HEAD')
|
|
106
|
+
range = since_ref ? "#{since_ref}..#{until_ref}" : until_ref
|
|
107
|
+
`git log #{range} --pretty=format:%s --no-merges`.
|
|
108
|
+
force_encoding(Encoding::UTF_8).
|
|
109
|
+
split("\n", -1).
|
|
110
|
+
reject(&:empty?)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def latest_tag
|
|
114
|
+
tag = `git describe --tags --abbrev=0 2>/dev/null`.strip
|
|
115
|
+
tag.empty? ? nil : tag
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def update_file(path, version, date: Date.today.iso8601, since_ref: latest_tag)
|
|
119
|
+
subjects = git_log_subjects(since_ref)
|
|
120
|
+
groups = group_commits(subjects)
|
|
121
|
+
content = File.exist?(path) ? File.read(path) : nil
|
|
122
|
+
File.write(path, update_content(content, version, date: date, groups: groups))
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -12,14 +12,21 @@ class GitDeploy
|
|
|
12
12
|
|
|
13
13
|
def deploy_to
|
|
14
14
|
@deploy_to ||= begin
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
end
|
|
15
|
+
remote_url
|
|
16
|
+
normalize_deploy_path(
|
|
17
|
+
GitDeploy::RemotePath.deploy_path(remote_url_string) { remote_home }
|
|
18
|
+
)
|
|
20
19
|
end
|
|
21
20
|
end
|
|
22
21
|
|
|
22
|
+
def remote_url_string(remote = options[:remote])
|
|
23
|
+
remote_urls(remote).first
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def normalize_deploy_path(path)
|
|
27
|
+
path.start_with?('/~/') ? path[1..-1] : path
|
|
28
|
+
end
|
|
29
|
+
|
|
23
30
|
def remote_user
|
|
24
31
|
@user ||= begin
|
|
25
32
|
user = remote_url.user
|
|
@@ -56,6 +63,7 @@ class GitDeploy
|
|
|
56
63
|
@remote_url[remote] ||= begin
|
|
57
64
|
url = remote_urls(remote).first
|
|
58
65
|
if url.nil?
|
|
66
|
+
require_remote! if remote.nil? || remote.to_s.strip.empty?
|
|
59
67
|
abort "Error: Remote url not found for remote #{remote.inspect}"
|
|
60
68
|
elsif url =~ /(^|@)github\.com\b/
|
|
61
69
|
abort "Error: Remote url for #{remote.inspect} points to GitHub. Can't deploy there!"
|
|
@@ -91,5 +99,12 @@ class GitDeploy
|
|
|
91
99
|
def tracked_for(branch)
|
|
92
100
|
git_config['config branch.%s.merge' % normalize_branch(branch)]
|
|
93
101
|
end
|
|
102
|
+
|
|
103
|
+
def require_remote!
|
|
104
|
+
remote = options[:remote]
|
|
105
|
+
if remote.nil? || remote.to_s.strip.empty?
|
|
106
|
+
abort "Error: Specify a remote with -r (e.g. git deploy setup -r production)"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
94
109
|
end
|
|
95
110
|
end
|
data/lib/git_deploy/generator.rb
CHANGED
|
@@ -3,10 +3,24 @@ require 'thor/group'
|
|
|
3
3
|
class GitDeploy::Generator < Thor::Group
|
|
4
4
|
include Thor::Actions
|
|
5
5
|
|
|
6
|
+
TEMPLATES = %w[rails-passenger rails-puma php-composer generic].freeze
|
|
7
|
+
|
|
8
|
+
class_option :template, :type => :string, :default => 'rails-passenger'
|
|
9
|
+
|
|
6
10
|
def self.source_root
|
|
7
11
|
File.expand_path('../templates', __FILE__)
|
|
8
12
|
end
|
|
9
13
|
|
|
14
|
+
def verify_deploy_target
|
|
15
|
+
if File.exist?('deploy') && !File.directory?('deploy')
|
|
16
|
+
abort "Error: './deploy' exists but is a file. Remove or rename it before running init."
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
if File.directory?('deploy') && !Dir.empty?('deploy')
|
|
20
|
+
say "Warning: deploy/ already exists and is not empty. Existing files will not be overwritten.", :yellow
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
10
24
|
def copy_main_hook
|
|
11
25
|
copy_hook 'after_push.sh', 'deploy/after_push'
|
|
12
26
|
end
|
|
@@ -16,13 +30,36 @@ class GitDeploy::Generator < Thor::Group
|
|
|
16
30
|
end
|
|
17
31
|
|
|
18
32
|
def copy_restart_callbacks
|
|
19
|
-
|
|
33
|
+
source = Dir[File.join(template_dir, 'before_restart.*')].first
|
|
34
|
+
abort "Error: No before_restart script in template #{options[:template].inspect}" unless source
|
|
35
|
+
copy_hook File.basename(source), 'deploy/before_restart'
|
|
20
36
|
end
|
|
21
37
|
|
|
22
38
|
private
|
|
23
39
|
|
|
40
|
+
def template_dir
|
|
41
|
+
@template_dir ||= begin
|
|
42
|
+
name = options[:template] || 'rails-passenger'
|
|
43
|
+
unless TEMPLATES.include?(name)
|
|
44
|
+
abort "Error: Unknown template #{name.inspect}. Choose from: #{TEMPLATES.join(', ')}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
root = File.expand_path(self.class.source_root)
|
|
48
|
+
dir = File.expand_path(name, root)
|
|
49
|
+
unless dir.start_with?("#{root}#{File::SEPARATOR}") && File.directory?(dir)
|
|
50
|
+
abort "Error: Unknown template #{name.inspect}. Choose from: #{TEMPLATES.join(', ')}"
|
|
51
|
+
end
|
|
52
|
+
dir
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
24
56
|
def copy_hook(template, destination)
|
|
25
|
-
|
|
57
|
+
return if File.exist?(destination)
|
|
58
|
+
|
|
59
|
+
source = File.join(template_dir, template)
|
|
60
|
+
abort "Error: Missing #{template} in template #{options[:template].inspect}" unless File.file?(source)
|
|
61
|
+
|
|
62
|
+
copy_file source, destination
|
|
26
63
|
chmod destination, 0744 unless File.executable? destination
|
|
27
64
|
end
|
|
28
65
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require 'uri'
|
|
2
|
+
|
|
3
|
+
class GitDeploy
|
|
4
|
+
module RemotePath
|
|
5
|
+
SCP_PATH = %r{\A(?:[^@]+@)?[^:]+:(.+)\z}
|
|
6
|
+
|
|
7
|
+
def self.raw_path(url)
|
|
8
|
+
if url.nil? || url.to_s.strip.empty?
|
|
9
|
+
abort "Error: No deploy remote URL configured. Specify a remote with -r."
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
if url.match?(%r{\A[\w-]+://})
|
|
13
|
+
URI.parse(url).path
|
|
14
|
+
else
|
|
15
|
+
match = url.match(SCP_PATH)
|
|
16
|
+
match ? match[1] : url
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.deploy_path(url, home: nil)
|
|
21
|
+
path = raw_path(url)
|
|
22
|
+
return path if path.start_with?('/', '~')
|
|
23
|
+
|
|
24
|
+
home = yield if home.nil? && block_given?
|
|
25
|
+
File.join(home, path)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
class GitDeploy
|
|
2
2
|
module SSHMethods
|
|
3
|
+
require 'fileutils'
|
|
3
4
|
private
|
|
4
5
|
|
|
5
6
|
def sudo_cmd
|
|
@@ -91,6 +92,20 @@ class GitDeploy
|
|
|
91
92
|
channels.each { |c| c.wait }
|
|
92
93
|
end
|
|
93
94
|
|
|
95
|
+
def scp_download(files)
|
|
96
|
+
channels = []
|
|
97
|
+
files.each do |remote, local|
|
|
98
|
+
puts "FILE: [#{options[:remote]}] #{remote} -> [local] #{local}"
|
|
99
|
+
FileUtils.mkdir_p(File.dirname(local)) unless local.end_with?('/') || File.dirname(local) == '.'
|
|
100
|
+
channels << ssh_connection.scp.download(remote, local) unless options.noop?
|
|
101
|
+
end
|
|
102
|
+
channels.each { |c| c.wait }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def remote_home
|
|
106
|
+
@remote_home ||= run("echo $HOME").strip
|
|
107
|
+
end
|
|
108
|
+
|
|
94
109
|
def ssh_connection
|
|
95
110
|
@ssh ||= begin
|
|
96
111
|
ssh = Net::SSH.start(host, remote_user, :port => remote_port || 22)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -e
|
|
3
|
+
oldrev=$1
|
|
4
|
+
newrev=$2
|
|
5
|
+
|
|
6
|
+
run() {
|
|
7
|
+
[ -x $1 ] && $1 $oldrev $newrev
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
echo files changed: $(git diff $oldrev $newrev --diff-filter=ACDMR --name-only | wc -l)
|
|
11
|
+
|
|
12
|
+
umask 002
|
|
13
|
+
|
|
14
|
+
run deploy/before_restart
|
|
15
|
+
run deploy/restart && run deploy/after_restart
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -e
|
|
3
|
+
oldrev=$1
|
|
4
|
+
newrev=$2
|
|
5
|
+
|
|
6
|
+
run() {
|
|
7
|
+
[ -x $1 ] && $1 $oldrev $newrev
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
echo files changed: $(git diff $oldrev $newrev --diff-filter=ACDMR --name-only | wc -l)
|
|
11
|
+
|
|
12
|
+
umask 002
|
|
13
|
+
|
|
14
|
+
run deploy/before_restart
|
|
15
|
+
run deploy/restart && run deploy/after_restart
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Reload PHP-FPM — adjust the service name for your server
|
|
3
|
+
if command -v systemctl >/dev/null 2>&1; then
|
|
4
|
+
for service in php-fpm php8.3-fpm php8.2-fpm; do
|
|
5
|
+
if sudo systemctl reload "$service" 2>/dev/null; then
|
|
6
|
+
echo "reloaded $service"
|
|
7
|
+
exit 0
|
|
8
|
+
fi
|
|
9
|
+
done
|
|
10
|
+
echo "Error: could not reload PHP-FPM (tried php-fpm, php8.3-fpm, php8.2-fpm)" >&2
|
|
11
|
+
exit 1
|
|
12
|
+
fi
|
|
13
|
+
echo "restarted PHP app"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -e
|
|
3
|
+
oldrev=$1
|
|
4
|
+
newrev=$2
|
|
5
|
+
|
|
6
|
+
run() {
|
|
7
|
+
[ -x $1 ] && $1 $oldrev $newrev
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
echo files changed: $(git diff $oldrev $newrev --diff-filter=ACDMR --name-only | wc -l)
|
|
11
|
+
|
|
12
|
+
umask 002
|
|
13
|
+
|
|
14
|
+
git submodule sync && git submodule update --init --recursive
|
|
15
|
+
|
|
16
|
+
run deploy/before_restart
|
|
17
|
+
run deploy/restart && run deploy/after_restart
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
oldrev, newrev = ARGV
|
|
3
|
+
|
|
4
|
+
def run(cmd)
|
|
5
|
+
exit($?.exitstatus) unless system "umask 002 && #{cmd}"
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
RAILS_ENV = ENV['RAILS_ENV'] || 'production'
|
|
9
|
+
use_bundler = File.file? 'Gemfile'
|
|
10
|
+
rake_cmd = use_bundler ? 'bundle exec rake' : 'rake'
|
|
11
|
+
|
|
12
|
+
if use_bundler
|
|
13
|
+
bundler_args = ['--deployment']
|
|
14
|
+
BUNDLE_WITHOUT = ENV['BUNDLE_WITHOUT'] || 'development:test'
|
|
15
|
+
bundler_args << '--without' << BUNDLE_WITHOUT unless BUNDLE_WITHOUT.empty?
|
|
16
|
+
|
|
17
|
+
run "bundle install #{bundler_args.join(' ')}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
if File.file? 'Rakefile'
|
|
21
|
+
tasks = []
|
|
22
|
+
|
|
23
|
+
if File.exist?('db/migrate')
|
|
24
|
+
num_migrations = `git diff #{oldrev} #{newrev} --diff-filter=A --name-only -z -- db/migrate`.split("\0").size
|
|
25
|
+
else
|
|
26
|
+
num_migrations = 0
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
tasks << "db:migrate" if num_migrations > 0
|
|
30
|
+
|
|
31
|
+
asset_paths = %w[app/assets app/javascript config/importmap.rb package.json yarn.lock]
|
|
32
|
+
changed_assets = `git diff #{oldrev} #{newrev} --name-only -z -- #{asset_paths.join(' ')}`.split("\0")
|
|
33
|
+
tasks << "assets:precompile" if changed_assets.size > 0
|
|
34
|
+
|
|
35
|
+
run "#{rake_cmd} #{tasks.join(' ')} RAILS_ENV=#{RAILS_ENV}" if tasks.any?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
run "git clean -x -f -- public/assets tmp/cache/assets" if Dir.exist?('public/assets')
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
mkdir -p tmp/pids
|
|
3
|
+
if [ -f tmp/pids/puma.pid ]; then
|
|
4
|
+
bundle exec pumactl -P tmp/pids/puma.pid restart
|
|
5
|
+
else
|
|
6
|
+
mkdir -p tmp
|
|
7
|
+
touch tmp/restart.txt
|
|
8
|
+
echo "restarting Puma app (tmp/restart.txt fallback — configure puma.pid for pumactl restart)"
|
|
9
|
+
fi
|
data/lib/git_deploy.rb
CHANGED
|
@@ -6,23 +6,34 @@ class GitDeploy < Thor
|
|
|
6
6
|
LOCAL_DIR = File.expand_path('..', __FILE__)
|
|
7
7
|
|
|
8
8
|
require 'git_deploy/configuration'
|
|
9
|
+
require 'git_deploy/remote_path'
|
|
9
10
|
require 'git_deploy/ssh_methods'
|
|
10
11
|
include Configuration
|
|
11
12
|
include SSHMethods
|
|
12
13
|
|
|
13
|
-
class_option :remote, :aliases => '-r', :type => :string
|
|
14
|
+
class_option :remote, :aliases => '-r', :type => :string
|
|
14
15
|
class_option :noop, :aliases => '-n', :type => :boolean, :default => false
|
|
15
16
|
|
|
16
17
|
desc "init", "Generates deployment customization scripts for your app"
|
|
18
|
+
method_option :template, :type => :string, :default => 'rails-passenger',
|
|
19
|
+
:desc => 'Template stack (rails-passenger, rails-puma, php-composer, generic)'
|
|
17
20
|
def init
|
|
18
21
|
require 'git_deploy/generator'
|
|
19
|
-
Generator
|
|
22
|
+
Generator.start(['--template', options[:template]])
|
|
20
23
|
end
|
|
21
24
|
|
|
22
25
|
desc "setup", "Create the remote git repository and install push hooks for it"
|
|
23
26
|
method_option :shared, :aliases => '-g', :type => :boolean, :default => false
|
|
24
27
|
method_option :sudo, :aliases => '-s', :type => :boolean, :default => false
|
|
28
|
+
method_option :force, :aliases => '-f', :type => :boolean, :default => false
|
|
25
29
|
def setup
|
|
30
|
+
require_remote!
|
|
31
|
+
|
|
32
|
+
remote_hook = "#{deploy_to}/.git/hooks/post-receive"
|
|
33
|
+
if run_test("[ -f #{remote_hook} ]") && !options[:force]
|
|
34
|
+
abort "Error: Remote already has a post-receive hook. Use --force to overwrite."
|
|
35
|
+
end
|
|
36
|
+
|
|
26
37
|
sudo = options.sudo? ? "#{sudo_cmd} " : ''
|
|
27
38
|
|
|
28
39
|
unless run_test("test -x #{deploy_to}")
|
|
@@ -35,17 +46,27 @@ class GitDeploy < Thor
|
|
|
35
46
|
run [] do |cmd|
|
|
36
47
|
cmd << "chmod g+ws #{deploy_to}" if options.shared?
|
|
37
48
|
cmd << "cd #{deploy_to}"
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
unless run_test("[ -d #{deploy_to}/.git ]")
|
|
50
|
+
cmd << "git init #{options.shared? ? '--shared' : ''}"
|
|
51
|
+
cmd << "sed -i'' -e 's/master/#{branch}/' .git/HEAD" unless branch == 'master'
|
|
52
|
+
end
|
|
40
53
|
cmd << "git config --bool receive.denyNonFastForwards false" if options.shared?
|
|
41
54
|
cmd << "git config receive.denyCurrentBranch ignore"
|
|
42
55
|
end
|
|
43
56
|
|
|
44
|
-
invoke :hooks
|
|
57
|
+
invoke :hooks, [], force: options[:force]
|
|
45
58
|
end
|
|
46
59
|
|
|
47
60
|
desc "hooks", "Installs git hooks to the remote repository"
|
|
61
|
+
method_option :force, :aliases => '-f', :type => :boolean, :default => false
|
|
48
62
|
def hooks
|
|
63
|
+
require_remote!
|
|
64
|
+
|
|
65
|
+
remote_hook = "#{deploy_to}/.git/hooks/post-receive"
|
|
66
|
+
if run_test("[ -f #{remote_hook} ]") && !options[:force]
|
|
67
|
+
abort "Error: Remote already has a post-receive hook. Use --force to overwrite."
|
|
68
|
+
end
|
|
69
|
+
|
|
49
70
|
hooks_dir = File.join(LOCAL_DIR, 'hooks')
|
|
50
71
|
remote_dir = "#{deploy_to}/.git/hooks"
|
|
51
72
|
|
|
@@ -55,11 +76,13 @@ class GitDeploy < Thor
|
|
|
55
76
|
|
|
56
77
|
desc "restart", "Restarts the application on the server"
|
|
57
78
|
def restart
|
|
79
|
+
require_remote!
|
|
58
80
|
run "cd #{deploy_to} && deploy/restart 2>&1 | tee -a log/deploy.log"
|
|
59
81
|
end
|
|
60
82
|
|
|
61
83
|
desc "rerun", "Runs the `deploy/after_push' callback as if a new revision was pushed via git"
|
|
62
84
|
def rerun
|
|
85
|
+
require_remote!
|
|
63
86
|
run <<-BASH, :echo => false
|
|
64
87
|
bash -e -c '
|
|
65
88
|
cd '#{deploy_to}'
|
|
@@ -71,6 +94,7 @@ class GitDeploy < Thor
|
|
|
71
94
|
|
|
72
95
|
desc "rollback", "Rolls back the checkout to before the last push"
|
|
73
96
|
def rollback
|
|
97
|
+
require_remote!
|
|
74
98
|
run <<-BASH, :echo => false
|
|
75
99
|
bash -e -c '
|
|
76
100
|
cd '#{deploy_to}'
|
|
@@ -87,12 +111,14 @@ class GitDeploy < Thor
|
|
|
87
111
|
method_option :tail, :aliases => '-t', :type => :boolean, :default => false
|
|
88
112
|
method_option :lines, :aliases => '-l', :type => :numeric, :default => 20
|
|
89
113
|
def log(n = nil)
|
|
114
|
+
require_remote!
|
|
90
115
|
tail_args = options.tail? ? '-f' : "-n#{n || options.lines}"
|
|
91
116
|
run "tail #{tail_args} #{deploy_to}/log/deploy.log"
|
|
92
117
|
end
|
|
93
118
|
|
|
94
119
|
desc "upload <files>", "Copy local files to the remote app"
|
|
95
120
|
def upload(*files)
|
|
121
|
+
require_remote!
|
|
96
122
|
files = files.map { |f| Dir[f.strip] }.flatten
|
|
97
123
|
abort "Error: Specify at least one file to upload" if files.empty?
|
|
98
124
|
|
|
@@ -101,4 +127,16 @@ class GitDeploy < Thor
|
|
|
101
127
|
all
|
|
102
128
|
}
|
|
103
129
|
end
|
|
130
|
+
|
|
131
|
+
desc "download <files>", "Copy remote files from the app to local"
|
|
132
|
+
def download(*files)
|
|
133
|
+
require_remote!
|
|
134
|
+
abort "Error: Specify at least one file to download" if files.empty?
|
|
135
|
+
|
|
136
|
+
scp_download files.inject({}) { |all, file|
|
|
137
|
+
file = file.strip
|
|
138
|
+
all[File.join(deploy_to, file)] = file
|
|
139
|
+
all
|
|
140
|
+
}
|
|
141
|
+
end
|
|
104
142
|
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'git_deploy/changelog'
|
|
3
|
+
|
|
4
|
+
describe GitDeploy::Changelog do
|
|
5
|
+
describe '.categorize' do
|
|
6
|
+
it 'maps conventional commit types to Keep a Changelog headings' do
|
|
7
|
+
expect(described_class.categorize('feat: add download')).to eq('Added')
|
|
8
|
+
expect(described_class.categorize('fix: path resolution')).to eq('Fixed')
|
|
9
|
+
expect(described_class.categorize('docs: refresh README')).to eq('Changed')
|
|
10
|
+
expect(described_class.categorize('chore: deps')).to be_nil
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe '.format_entry' do
|
|
15
|
+
it 'capitalizes the subject and marks breaking changes' do
|
|
16
|
+
expect(described_class.format_entry('feat: add download')).
|
|
17
|
+
to eq('Add download')
|
|
18
|
+
expect(described_class.format_entry('feat!: require remote')).
|
|
19
|
+
to eq('**BREAKING:** Require remote')
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe '.group_commits' do
|
|
24
|
+
it 'groups and deduplicates entries by category' do
|
|
25
|
+
groups = described_class.group_commits([
|
|
26
|
+
'feat: add download',
|
|
27
|
+
'fix: path resolution',
|
|
28
|
+
'Merge pull request #12',
|
|
29
|
+
'chore: ignore tmp'
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
expect(groups['Added']).to eq(['Add download'])
|
|
33
|
+
expect(groups['Fixed']).to eq(['Path resolution'])
|
|
34
|
+
expect(groups.key?('Changed')).to be false
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe '.render_section' do
|
|
39
|
+
it 'renders a Keep a Changelog version section' do
|
|
40
|
+
section = described_class.render_section('0.9.0', '2026-07-04', {
|
|
41
|
+
'Added' => ['Download command'],
|
|
42
|
+
'Fixed' => ['Remote path resolution']
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
expect(section).to include('## [0.9.0] - 2026-07-04')
|
|
46
|
+
expect(section).to include('### Added')
|
|
47
|
+
expect(section).to include('- Download command')
|
|
48
|
+
expect(section).to include('### Fixed')
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
describe '.update_content' do
|
|
53
|
+
let(:base) do
|
|
54
|
+
described_class::HEADER + "\n#{described_class::UNRELEASED}\n\n"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'inserts a new version section after Unreleased' do
|
|
58
|
+
updated = described_class.update_content(base, '0.9.0',
|
|
59
|
+
date: '2026-07-04',
|
|
60
|
+
groups: { 'Added' => ['Download command'] })
|
|
61
|
+
|
|
62
|
+
expect(updated).to include("## [Unreleased]\n\n## [0.9.0]")
|
|
63
|
+
expect(updated).to include('- Download command')
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'aborts when the version section already exists' do
|
|
67
|
+
content = base + "## [0.9.0] - 2026-07-04\n\n"
|
|
68
|
+
expect {
|
|
69
|
+
described_class.update_content(content, '0.9.0',
|
|
70
|
+
date: '2026-07-04', groups: { 'Added' => ['X'] })
|
|
71
|
+
}.to output(/already has a section/).to_stderr.and raise_error(SystemExit)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
describe '.extract_section' do
|
|
76
|
+
let(:content) do
|
|
77
|
+
<<~CHANGELOG
|
|
78
|
+
## [Unreleased]
|
|
79
|
+
|
|
80
|
+
## [0.9.0] - 2026-07-04
|
|
81
|
+
|
|
82
|
+
### Added
|
|
83
|
+
- Download command
|
|
84
|
+
|
|
85
|
+
## [0.8.0] - 2026-06-10
|
|
86
|
+
CHANGELOG
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'returns the body for a version section' do
|
|
90
|
+
body = described_class.extract_section(content, '0.9.0')
|
|
91
|
+
expect(body).to include('### Added')
|
|
92
|
+
expect(body).to include('- Download command')
|
|
93
|
+
expect(body).not_to include('0.8.0')
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
describe '.verify_version!' do
|
|
98
|
+
it 'aborts when the version is missing' do
|
|
99
|
+
expect {
|
|
100
|
+
described_class.verify_version!("## [Unreleased]\n", '0.9.0')
|
|
101
|
+
}.to output(/no entry for version 0.9.0/).to_stderr.and raise_error(SystemExit)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
describe '.gemspec_version' do
|
|
106
|
+
it 'reads the version from git-deploy-ng.gemspec' do
|
|
107
|
+
expect(described_class.gemspec_version).to eq('0.9.0')
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
data/spec/cli_spec.rb
CHANGED
|
@@ -4,7 +4,15 @@ describe GitDeploy do
|
|
|
4
4
|
it 'exposes the upstream CLI commands' do
|
|
5
5
|
commands = described_class.all_commands.keys
|
|
6
6
|
expect(commands).to include(
|
|
7
|
-
'init', 'setup', 'hooks', 'restart', 'rerun', 'rollback', 'log', 'upload'
|
|
7
|
+
'init', 'setup', 'hooks', 'restart', 'rerun', 'rollback', 'log', 'upload', 'download'
|
|
8
8
|
)
|
|
9
9
|
end
|
|
10
|
+
|
|
11
|
+
it 'allows init without a remote name' do
|
|
12
|
+
Dir.mktmpdir do |dir|
|
|
13
|
+
Dir.chdir(dir) do
|
|
14
|
+
expect { described_class.start(%w[init]) }.not_to raise_error
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
10
18
|
end
|