capistrano-node-dotenv 1.0.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/release.yml +25 -0
- data/.gitignore +10 -0
- data/.rubocop.yml +88 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +25 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +235 -0
- data/Rakefile +25 -0
- data/capistrano-node-dotenv.gemspec +28 -0
- data/lib/capistrano/node/dotenv/helpers.rb +177 -0
- data/lib/capistrano/node/dotenv/parser.rb +102 -0
- data/lib/capistrano/node/dotenv/paths.rb +19 -0
- data/lib/capistrano/node/dotenv/tasks/backup.rake +27 -0
- data/lib/capistrano/node/dotenv/tasks/check.rake +12 -0
- data/lib/capistrano/node/dotenv/tasks/check_config_present.rake +12 -0
- data/lib/capistrano/node/dotenv/tasks/check_dotenv_file_exists.rake +12 -0
- data/lib/capistrano/node/dotenv/tasks/check_git_tracking.rake +12 -0
- data/lib/capistrano/node/dotenv/tasks/create_local.rake +37 -0
- data/lib/capistrano/node/dotenv/tasks/dotenv_symlink.rake +10 -0
- data/lib/capistrano/node/dotenv/tasks/get.rake +55 -0
- data/lib/capistrano/node/dotenv/tasks/get_stage.rake +18 -0
- data/lib/capistrano/node/dotenv/tasks/load.rake +10 -0
- data/lib/capistrano/node/dotenv/tasks/rollback.rake +23 -0
- data/lib/capistrano/node/dotenv/tasks/setup.rake +19 -0
- data/lib/capistrano/node/dotenv/tasks/update.rake +41 -0
- data/lib/capistrano/node/dotenv/version.rb +9 -0
- data/lib/capistrano/node/dotenv.rb +21 -0
- data/scripts/bump_version.rb +172 -0
- metadata +159 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Capistrano
|
|
4
|
+
module Node
|
|
5
|
+
module Dotenv
|
|
6
|
+
module Parser
|
|
7
|
+
def parse_dotenv(content, stage_name)
|
|
8
|
+
global_env = {}
|
|
9
|
+
stage_env = {}
|
|
10
|
+
current_section = :stage
|
|
11
|
+
markers_found = false
|
|
12
|
+
|
|
13
|
+
content.each_line do |line|
|
|
14
|
+
new_section, is_marker = resolve_section_marker(line.strip, stage_name)
|
|
15
|
+
if is_marker
|
|
16
|
+
current_section = new_section
|
|
17
|
+
markers_found = true
|
|
18
|
+
next
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
collect_dotenv_pair(line.strip, global_env, stage_env, current_section, markers_found)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
build_dotenv_result(global_env, stage_env, stage_name)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def resolve_section_marker(stripped, stage_name)
|
|
28
|
+
return [:global, true] if stripped.match?(/\A#\s*Begin\s+global\s+envs\b/i)
|
|
29
|
+
return [nil, true] if stripped.match?(/\A#\s*End\s+global\s+envs\b/i)
|
|
30
|
+
return [nil, true] if stripped.match?(/\A#\s*End\s+.+?\s+envs\b/i)
|
|
31
|
+
|
|
32
|
+
stage_begin = stripped.match(/\A#\s*Begin\s+(.+?)\s+envs\b/i)
|
|
33
|
+
if stage_begin
|
|
34
|
+
section = stage_begin[1].strip.casecmp(stage_name.to_s).zero? ? :stage : nil
|
|
35
|
+
return [section, true]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
[nil, false]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def collect_dotenv_pair(stripped, global_env, stage_env, current_section, markers_found)
|
|
42
|
+
parsed_line = parse_dotenv_line(stripped)
|
|
43
|
+
return unless parsed_line
|
|
44
|
+
|
|
45
|
+
key, value = parsed_line
|
|
46
|
+
if markers_found
|
|
47
|
+
global_env[key] = value if current_section == :global
|
|
48
|
+
stage_env[key] = value if current_section == :stage
|
|
49
|
+
else
|
|
50
|
+
stage_env[key] = value
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def build_dotenv_result(global_env, stage_env, stage_name)
|
|
55
|
+
global_env.sort.to_h.merge(stage_name.to_s => stage_env.sort.to_h)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def parse_dotenv_line(line)
|
|
59
|
+
return nil if line.empty? || line.start_with?('#')
|
|
60
|
+
|
|
61
|
+
stripped = line.sub(/\Aexport\s+/, '')
|
|
62
|
+
key, raw_value = stripped.split('=', 2)
|
|
63
|
+
return nil if key.nil? || raw_value.nil?
|
|
64
|
+
|
|
65
|
+
parsed_key = key.strip
|
|
66
|
+
value = raw_value.strip
|
|
67
|
+
|
|
68
|
+
if value.start_with?('"') && value.end_with?('"')
|
|
69
|
+
value = value[1..-2]
|
|
70
|
+
value = value.gsub('\\n', "\n").gsub('\\"', '"').gsub('\\\\', '\\')
|
|
71
|
+
elsif value.start_with?("'") && value.end_with?("'")
|
|
72
|
+
value = value[1..-2]
|
|
73
|
+
else
|
|
74
|
+
value = value.sub(/\s+#.*\z/, '').strip
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
[parsed_key, value]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def generate_dotenv_content(global_env_hash, env_hash)
|
|
81
|
+
date = Time.now.strftime('%Y-%m-%d %H:%M:%S')
|
|
82
|
+
global_start = "# Begin global envs #{date}\n"
|
|
83
|
+
global_end = "# End global envs #{date}\n"
|
|
84
|
+
stage_start = "# Begin #{fetch(:dotenv_env) || fetch(:stage)} envs #{date}\n"
|
|
85
|
+
stage_end = "# End #{fetch(:dotenv_env) || fetch(:stage)} envs #{date}\n"
|
|
86
|
+
|
|
87
|
+
content = global_start
|
|
88
|
+
global_env_hash.each do |key, value|
|
|
89
|
+
content += "#{key}=#{value}\n"
|
|
90
|
+
end
|
|
91
|
+
content += global_end
|
|
92
|
+
content += stage_start
|
|
93
|
+
env_hash.each do |key, value|
|
|
94
|
+
content += "#{key}=#{value}\n"
|
|
95
|
+
end
|
|
96
|
+
content += stage_end
|
|
97
|
+
content
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
|
|
5
|
+
module Capistrano
|
|
6
|
+
module Node
|
|
7
|
+
module Dotenv
|
|
8
|
+
module Paths
|
|
9
|
+
def dotenv_local_path
|
|
10
|
+
Pathname.new fetch(:dotenv_local_path)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def dotenv_remote_path
|
|
14
|
+
shared_path.join fetch(:dotenv_remote_path)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :dotenv do
|
|
4
|
+
include Capistrano::Node::Dotenv::Paths
|
|
5
|
+
include Capistrano::Node::Dotenv::Helpers
|
|
6
|
+
desc 'Backup remote .env file'
|
|
7
|
+
task :backup do
|
|
8
|
+
on release_roles :all do
|
|
9
|
+
unless remote_file_exists?
|
|
10
|
+
puts 'No remote .env file to backup.'
|
|
11
|
+
next
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
begin
|
|
15
|
+
puts 'Creating backup of remote .env file...'
|
|
16
|
+
create_remote_backup
|
|
17
|
+
puts 'Backup created successfully.'
|
|
18
|
+
|
|
19
|
+
puts 'Cleaning up remote backups...'
|
|
20
|
+
cleanup_remote_backups
|
|
21
|
+
puts 'Remote backups cleaned up successfully.'
|
|
22
|
+
rescue StandardError => e
|
|
23
|
+
puts "Error during backup process: #{e.message}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :dotenv do
|
|
4
|
+
include Capistrano::Node::Dotenv::Paths
|
|
5
|
+
include Capistrano::Node::Dotenv::Helpers
|
|
6
|
+
desc 'dotenv `application.yml` file checks'
|
|
7
|
+
task :check do
|
|
8
|
+
invoke 'dotenv:check_dotenv_file_exists'
|
|
9
|
+
invoke 'dotenv:check_git_tracking'
|
|
10
|
+
invoke 'dotenv:check_config_present'
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :dotenv do
|
|
4
|
+
include Capistrano::Node::Dotenv::Paths
|
|
5
|
+
include Capistrano::Node::Dotenv::Helpers
|
|
6
|
+
task :check_config_present do
|
|
7
|
+
next unless local_dotenv(dotenv_env).empty?
|
|
8
|
+
|
|
9
|
+
check_config_present_error
|
|
10
|
+
exit 1
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :dotenv do
|
|
4
|
+
include Capistrano::Node::Dotenv::Paths
|
|
5
|
+
include Capistrano::Node::Dotenv::Helpers
|
|
6
|
+
task :check_dotenv_file_exists do
|
|
7
|
+
next if File.exist?(dotenv_local_path)
|
|
8
|
+
|
|
9
|
+
check_dotenv_file_exists_error
|
|
10
|
+
exit 1
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :dotenv do
|
|
4
|
+
include Capistrano::Node::Dotenv::Paths
|
|
5
|
+
include Capistrano::Node::Dotenv::Helpers
|
|
6
|
+
task :check_git_tracking do
|
|
7
|
+
next unless system("git ls-files #{fetch(:dotenv_local_path)} --error-unmatch >/dev/null 2>&1")
|
|
8
|
+
|
|
9
|
+
check_git_tracking_error
|
|
10
|
+
exit 1
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :dotenv do
|
|
4
|
+
include Capistrano::Node::Dotenv::Paths
|
|
5
|
+
include Capistrano::Node::Dotenv::Helpers
|
|
6
|
+
include Capistrano::Node::Dotenv::Parser
|
|
7
|
+
|
|
8
|
+
task :create_local do
|
|
9
|
+
run_locally do
|
|
10
|
+
puts "found #{stages.count} stages"
|
|
11
|
+
yamls_combined = {}
|
|
12
|
+
empty_stages = {}
|
|
13
|
+
stages.each do |f|
|
|
14
|
+
stage = File.basename(f, '.rb')
|
|
15
|
+
puts "download #{stage} .env"
|
|
16
|
+
begin
|
|
17
|
+
res = capture "cap #{stage} dotenv:get_stage"
|
|
18
|
+
yamls_combined.merge!(parse_dotenv(res, stage))
|
|
19
|
+
rescue StandardError
|
|
20
|
+
yamls_combined.merge!(stage => { 'KEY' => 'VALUE' }) # add empty stage to combined yml to avoid losing data from other stages
|
|
21
|
+
empty_stages.merge!(stage => { "#{stage.upcase}_KEY" => 'VALUE' })
|
|
22
|
+
puts "could not get #{stage} .env"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if empty_stages.count == stages.count
|
|
27
|
+
puts "no .env files found for any stage. creating empty local env file with all stages: #{empty_stages.keys.join(', ')}"
|
|
28
|
+
yamls_combined.merge!({ 'GLOBAL' => 'VALUE' })
|
|
29
|
+
yamls_combined.merge!(empty_stages)
|
|
30
|
+
puts yamls_combined.sort.to_h.to_yaml
|
|
31
|
+
end
|
|
32
|
+
write_combined_yaml(yamls_combined.sort.to_h)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
desc 'Initializes local env file (config/application.yml)'
|
|
36
|
+
task init: :create_local
|
|
37
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :dotenv do
|
|
4
|
+
include Capistrano::Node::Dotenv::Paths
|
|
5
|
+
include Capistrano::Node::Dotenv::Helpers
|
|
6
|
+
task :dotenv_symlink do
|
|
7
|
+
set :linked_files, fetch(:linked_files, []).push(fetch(:dotenv_remote_path))
|
|
8
|
+
end
|
|
9
|
+
after 'deploy:started', 'dotenv:dotenv_symlink'
|
|
10
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :dotenv do
|
|
4
|
+
include Capistrano::Node::Dotenv::Paths
|
|
5
|
+
include Capistrano::Node::Dotenv::Helpers
|
|
6
|
+
desc 'Compares the remote .env file with the local one and allows to update the local file with remote values'
|
|
7
|
+
task :get do
|
|
8
|
+
if File.exist?(dotenv_local_path)
|
|
9
|
+
local_yml = local_dotenv(dotenv_env)
|
|
10
|
+
local_global_env, local_stage_env, local_rest = configs(local_yml, dotenv_env)
|
|
11
|
+
on release_roles :all do
|
|
12
|
+
unless remote_file_exists?
|
|
13
|
+
puts 'Remote .env file does not exist. Nothing to get.'
|
|
14
|
+
exit
|
|
15
|
+
end
|
|
16
|
+
remote = parse_dotenv(capture("cat #{dotenv_remote_path}"), dotenv_env)
|
|
17
|
+
remote_global_env, remote_stage_env, _remote_rest = configs(remote, dotenv_env)
|
|
18
|
+
|
|
19
|
+
differences_global = compare_hashes(remote_global_env, local_global_env || {})
|
|
20
|
+
differences_stage = compare_hashes(remote_stage_env, local_stage_env || {})
|
|
21
|
+
print_changes(differences_global,
|
|
22
|
+
'Remote env file has extra/different global entries compared to local.')
|
|
23
|
+
if differences_stage
|
|
24
|
+
print_changes(differences_stage,
|
|
25
|
+
"Remote env file has extra/different entries in #{dotenv_env} section compared to local.")
|
|
26
|
+
end
|
|
27
|
+
puts 'No Differences found between remote and local env file' unless differences_global || differences_stage
|
|
28
|
+
|
|
29
|
+
# puts 'No Differences found between remote and local application.yml' unless differences_global || differences_stage
|
|
30
|
+
|
|
31
|
+
# # ask to overwrite local yml if differences found
|
|
32
|
+
stage_overwrite_local = ask_to_overwrite("Overwrite local env file #{dotenv_env} section") if differences_stage
|
|
33
|
+
global_overwrite_local = ask_to_overwrite('Overwrite local env file globals') if differences_global
|
|
34
|
+
puts 'Nothing written to local env file' unless stage_overwrite_local || global_overwrite_local
|
|
35
|
+
exit unless stage_overwrite_local || global_overwrite_local
|
|
36
|
+
# stage_overwrite = ask_to_overwrite("Overwrite local application.yml #{figaro_yml_env} section") if differences_stage
|
|
37
|
+
# global_overwrite = ask_to_overwrite('Overwrite local application.yml globals') if differences_global
|
|
38
|
+
# puts 'Nothing written to local application.yml' unless stage_overwrite || global_overwrite
|
|
39
|
+
# exit unless stage_overwrite || global_overwrite
|
|
40
|
+
# compose new yml
|
|
41
|
+
|
|
42
|
+
composed_yml = {}
|
|
43
|
+
composed_yml.merge!(local_rest) # local yml is always included to avoid losing any data
|
|
44
|
+
composed_yml.merge!(local_global_env) unless global_overwrite_local
|
|
45
|
+
composed_yml.merge!(remote_global_env) if global_overwrite_local
|
|
46
|
+
composed_yml[dotenv_env.to_s] = stage_overwrite_local ? remote_stage_env : local_stage_env
|
|
47
|
+
puts composed_yml.to_yaml
|
|
48
|
+
# # write to new file
|
|
49
|
+
write_combined_yaml(composed_yml.sort.to_h)
|
|
50
|
+
end
|
|
51
|
+
else
|
|
52
|
+
invoke 'dotenv:create_local'
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :dotenv do
|
|
4
|
+
include Capistrano::Node::Dotenv::Paths
|
|
5
|
+
include Capistrano::Node::Dotenv::Helpers
|
|
6
|
+
|
|
7
|
+
rake_roles = fetch(:rake_roles, :app)
|
|
8
|
+
task :get_stage do
|
|
9
|
+
on roles(rake_roles) do
|
|
10
|
+
raise 'The file does not exist.' unless remote_file_exists?
|
|
11
|
+
|
|
12
|
+
puts capture "cat #{dotenv_remote_path}"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
desc 'shows the content of the remote .env file for the current stage'
|
|
17
|
+
task show: :get_stage
|
|
18
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :load do
|
|
4
|
+
task :defaults do
|
|
5
|
+
set :dotenv_local_path, 'config/application.yml'
|
|
6
|
+
set :dotenv_remote_path, '.env'
|
|
7
|
+
set :dotenv_env, -> { fetch(:node_env) || fetch(:stage) }
|
|
8
|
+
set :dotenv_remote_backup, false
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :dotenv do
|
|
4
|
+
include Capistrano::Node::Dotenv::Paths
|
|
5
|
+
include Capistrano::Node::Dotenv::Helpers
|
|
6
|
+
desc 'Rolls back the remote .env file to the previous version if a backup exists'
|
|
7
|
+
task :rollback do
|
|
8
|
+
on release_roles :all do
|
|
9
|
+
unless remote_backup_exists?
|
|
10
|
+
puts 'No backup of remote .env file to rollback.'
|
|
11
|
+
next
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
begin
|
|
15
|
+
puts 'Rolling back remote .env file...'
|
|
16
|
+
rollback_remote_backup
|
|
17
|
+
puts 'Rollback completed successfully.'
|
|
18
|
+
rescue StandardError => e
|
|
19
|
+
puts "Error during rollback process: #{e.message}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :dotenv do
|
|
4
|
+
include Capistrano::Node::Dotenv::Paths
|
|
5
|
+
include Capistrano::Node::Dotenv::Helpers
|
|
6
|
+
include Capistrano::Node::Dotenv::Parser
|
|
7
|
+
desc 'Uploads the local env file to the server and saves it in the shared folder. This task is used to set up the remote .env file for the first time or to update it with local values. It will overwrite the remote .env file if it already exists.'
|
|
8
|
+
task :setup do
|
|
9
|
+
content = local_dotenv(dotenv_env)
|
|
10
|
+
|
|
11
|
+
on release_roles :all do
|
|
12
|
+
global_yml, stage_yml, _other_stages_yml = configs(content, dotenv_env)
|
|
13
|
+
dotenv_string = generate_dotenv_content(global_yml, stage_yml)
|
|
14
|
+
|
|
15
|
+
execute :mkdir, '-pv', File.dirname(dotenv_remote_path)
|
|
16
|
+
upload! StringIO.new(dotenv_string), dotenv_remote_path
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :dotenv do
|
|
4
|
+
include Capistrano::Node::Dotenv::Paths
|
|
5
|
+
include Capistrano::Node::Dotenv::Helpers
|
|
6
|
+
include Capistrano::Node::Dotenv::Parser
|
|
7
|
+
desc 'Compares the remote .env file with the local one and allows to update the remote file with local values'
|
|
8
|
+
task :update do
|
|
9
|
+
local = local_dotenv(dotenv_env)
|
|
10
|
+
local_global_env, local_stage_env, _local_rest = configs(local, dotenv_env)
|
|
11
|
+
on release_roles :all do
|
|
12
|
+
remote = parse_dotenv(capture("cat #{dotenv_remote_path}"), fetch(:dotenv_env))
|
|
13
|
+
remote_global_env, remote_stage_env, _remote_rest = configs(remote, dotenv_env)
|
|
14
|
+
# Compare hashes and handle nil results with empty hashes
|
|
15
|
+
differences_global = compare_hashes(local_global_env, remote_global_env)
|
|
16
|
+
differences_stage = compare_hashes(local_stage_env, remote_stage_env)
|
|
17
|
+
|
|
18
|
+
print_changes(differences_global, 'Local application.yml has extra/different global entries compared to remote.')
|
|
19
|
+
if differences_stage
|
|
20
|
+
print_changes(differences_stage, "Local application.yml has extra/different entries in #{dotenv_env} section compared to remote.")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
puts 'No Differences found between remote and local application.yml' unless differences_global || differences_stage
|
|
24
|
+
|
|
25
|
+
stage_overwrite_remote = ask_to_overwrite("Overwrite remote #{dotenv_env} section") if differences_stage
|
|
26
|
+
global_overwrite_remote = ask_to_overwrite('Overwrite remote globals') if differences_global
|
|
27
|
+
|
|
28
|
+
puts 'Nothing written to remote application.yml' unless stage_overwrite_remote || global_overwrite_remote
|
|
29
|
+
exit unless stage_overwrite_remote || global_overwrite_remote
|
|
30
|
+
global_yaml = global_overwrite_remote ? local_global_env : remote_global_env
|
|
31
|
+
stage_yaml = stage_overwrite_remote ? local_stage_env : remote_stage_env
|
|
32
|
+
# write to new file
|
|
33
|
+
dotenv_string = generate_dotenv_content(global_yaml, stage_yaml)
|
|
34
|
+
|
|
35
|
+
invoke 'dotenv:backup'
|
|
36
|
+
# execute :mkdir, '-pv', File.dirname(dotenv_remote_path)
|
|
37
|
+
upload! StringIO.new(dotenv_string), dotenv_remote_path
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
# before 'dotenv:update', 'dotenv:backup'
|
|
41
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Capistrano
|
|
4
|
+
module Node
|
|
5
|
+
module Dotenv
|
|
6
|
+
require 'capistrano/node/dotenv/paths'
|
|
7
|
+
require 'capistrano/node/dotenv/helpers'
|
|
8
|
+
require 'capistrano/node/dotenv/parser'
|
|
9
|
+
|
|
10
|
+
def self.path
|
|
11
|
+
Dir.pwd
|
|
12
|
+
end
|
|
13
|
+
task_files = Dir.glob("#{File.expand_path(__dir__)}/dotenv/tasks/**/*.rake")
|
|
14
|
+
task_files.each do |file|
|
|
15
|
+
load file
|
|
16
|
+
rescue StandardError => e
|
|
17
|
+
puts "Failed to load #{file}: #{e.message}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Version management script for Capistrano Node Dotenv Gem
|
|
5
|
+
# Usage: ruby scripts/bump_version.rb [patch|minor|major]
|
|
6
|
+
|
|
7
|
+
require 'fileutils'
|
|
8
|
+
require_relative '../lib/capistrano/node/dotenv/version'
|
|
9
|
+
|
|
10
|
+
class VersionBumper
|
|
11
|
+
VALID_TYPES = %w[patch minor major].freeze
|
|
12
|
+
VERSION_FILE = 'lib/capistrano/node/dotenv/version.rb'
|
|
13
|
+
CHANGELOG_FILE = 'CHANGELOG.md'
|
|
14
|
+
|
|
15
|
+
def initialize(bump_type)
|
|
16
|
+
@bump_type = bump_type.to_s.downcase
|
|
17
|
+
@current_version = Capistrano::Node::Dotenv::VERSION
|
|
18
|
+
validate_bump_type!
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def bump!
|
|
22
|
+
puts "Current version: #{@current_version}"
|
|
23
|
+
|
|
24
|
+
new_version = calculate_new_version
|
|
25
|
+
puts "New version: #{new_version}"
|
|
26
|
+
|
|
27
|
+
# Update version file
|
|
28
|
+
update_version_file(new_version)
|
|
29
|
+
|
|
30
|
+
# Update changelog
|
|
31
|
+
update_changelog(new_version)
|
|
32
|
+
|
|
33
|
+
# Create git commit
|
|
34
|
+
create_git_commit(new_version) if git_available?
|
|
35
|
+
|
|
36
|
+
puts 'Version bumped successfully!'
|
|
37
|
+
puts ''
|
|
38
|
+
puts '📋 Next steps:'
|
|
39
|
+
puts '1. Review the changes (git diff)'
|
|
40
|
+
puts '2. Fill in CHANGELOG.md with actual changes'
|
|
41
|
+
puts '3. Commit and tag:'
|
|
42
|
+
puts " git add #{VERSION_FILE} #{CHANGELOG_FILE}"
|
|
43
|
+
puts " git commit -m 'Release version #{new_version}'"
|
|
44
|
+
puts " git tag -a v#{new_version} -m 'Release version #{new_version}'"
|
|
45
|
+
puts '4. Push to GitHub:'
|
|
46
|
+
puts " git push origin main && git push origin v#{new_version}"
|
|
47
|
+
puts '5. Create GitHub Release:'
|
|
48
|
+
puts " gh release create v#{new_version} --title 'v#{new_version}' \\"
|
|
49
|
+
puts " --notes-file <(sed -n '/## \\[#{new_version}\\]/,/## \\[/p' CHANGELOG.md | sed '$d')"
|
|
50
|
+
puts ''
|
|
51
|
+
puts '→ GitHub Actions will automatically publish to RubyGems.org! 🚀'
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def validate_bump_type!
|
|
57
|
+
return if VALID_TYPES.include?(@bump_type)
|
|
58
|
+
|
|
59
|
+
puts "Error: Invalid bump type '#{@bump_type}'"
|
|
60
|
+
puts "Valid types: #{VALID_TYPES.join(', ')}"
|
|
61
|
+
exit 1
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def calculate_new_version
|
|
65
|
+
parts = @current_version.split('.').map(&:to_i)
|
|
66
|
+
|
|
67
|
+
case @bump_type
|
|
68
|
+
when 'patch'
|
|
69
|
+
parts[2] += 1
|
|
70
|
+
when 'minor'
|
|
71
|
+
parts[1] += 1
|
|
72
|
+
parts[2] = 0
|
|
73
|
+
when 'major'
|
|
74
|
+
parts[0] += 1
|
|
75
|
+
parts[1] = 0
|
|
76
|
+
parts[2] = 0
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
parts.join('.')
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def update_version_file(new_version)
|
|
83
|
+
puts "Updating #{VERSION_FILE}..."
|
|
84
|
+
|
|
85
|
+
content = File.read(VERSION_FILE)
|
|
86
|
+
updated_content = content.gsub(
|
|
87
|
+
/VERSION = ['"][^'"]*['"]/,
|
|
88
|
+
"VERSION = '#{new_version}'"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
File.write(VERSION_FILE, updated_content)
|
|
92
|
+
puts '✓ Version file updated'
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def update_changelog(new_version)
|
|
96
|
+
return unless File.exist?(CHANGELOG_FILE)
|
|
97
|
+
|
|
98
|
+
puts "Updating #{CHANGELOG_FILE}..."
|
|
99
|
+
|
|
100
|
+
# Read existing changelog
|
|
101
|
+
content = File.read(CHANGELOG_FILE)
|
|
102
|
+
|
|
103
|
+
# Find the [Unreleased] section and replace it
|
|
104
|
+
lines = content.lines
|
|
105
|
+
|
|
106
|
+
today = Time.now.strftime('%Y-%m-%d')
|
|
107
|
+
|
|
108
|
+
# Replace [Unreleased] header with new version
|
|
109
|
+
unreleased_index = lines.find_index { |line| line.start_with?('## [Unreleased]') }
|
|
110
|
+
|
|
111
|
+
if unreleased_index
|
|
112
|
+
# Create new version entry
|
|
113
|
+
new_entry = [
|
|
114
|
+
"## [Unreleased]\n",
|
|
115
|
+
"\n",
|
|
116
|
+
"## [#{new_version}] - #{today}\n"
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
# Replace [Unreleased] line
|
|
120
|
+
lines[unreleased_index] = new_entry.join
|
|
121
|
+
|
|
122
|
+
# Update comparison links at the bottom
|
|
123
|
+
# Find and update the [Unreleased] link
|
|
124
|
+
unreleased_link_index = lines.rindex { |line| line.start_with?('[Unreleased]:') }
|
|
125
|
+
if unreleased_link_index
|
|
126
|
+
# Update [Unreleased] to compare from new version to HEAD
|
|
127
|
+
lines[unreleased_link_index] = "[Unreleased]: https://github.com/zauberware/capistrano-node-dotenv/compare/v#{new_version}...HEAD\n"
|
|
128
|
+
# Add new version link
|
|
129
|
+
lines.insert(unreleased_link_index + 1, "[#{new_version}]: https://github.com/zauberware/capistrano-node-dotenv/releases/tag/v#{new_version}\n")
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Write back to file
|
|
134
|
+
File.write(CHANGELOG_FILE, lines.join)
|
|
135
|
+
puts '✓ Changelog updated (please fill in the release notes under the new version)'
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def git_available?
|
|
139
|
+
system('git --version > /dev/null 2>&1')
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def create_git_commit(new_version)
|
|
143
|
+
puts 'Creating git commit...'
|
|
144
|
+
|
|
145
|
+
# Add modified files
|
|
146
|
+
system("git add #{VERSION_FILE}")
|
|
147
|
+
system("git add #{CHANGELOG_FILE}") if File.exist?(CHANGELOG_FILE)
|
|
148
|
+
|
|
149
|
+
# Create commit
|
|
150
|
+
commit_message = "Bump version to #{new_version}"
|
|
151
|
+
if system("git commit -m '#{commit_message}'")
|
|
152
|
+
puts "✓ Git commit created: #{commit_message}"
|
|
153
|
+
else
|
|
154
|
+
puts 'âš Failed to create git commit (you may need to commit manually)'
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Main execution
|
|
160
|
+
if ARGV.empty?
|
|
161
|
+
puts "Usage: #{$PROGRAM_NAME} [patch|minor|major]"
|
|
162
|
+
puts ''
|
|
163
|
+
puts 'Examples:'
|
|
164
|
+
puts " #{$PROGRAM_NAME} patch # 1.0.0 -> 1.0.1"
|
|
165
|
+
puts " #{$PROGRAM_NAME} minor # 1.0.0 -> 1.1.0"
|
|
166
|
+
puts " #{$PROGRAM_NAME} major # 1.0.0 -> 2.0.0"
|
|
167
|
+
exit 1
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
bump_type = ARGV[0]
|
|
171
|
+
bumper = VersionBumper.new(bump_type)
|
|
172
|
+
bumper.bump!
|