/
hatchet
executable file
·203 lines (175 loc) · 5.67 KB
/
hatchet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
#!/usr/bin/env ruby
unless File.respond_to? :realpath
class File #:nodoc:
def self.realpath path
return realpath(File.readlink(path)) if symlink?(path)
path
end
end
end
$: << File.expand_path(File.dirname(File.realpath(__FILE__)) + '/../lib')
require 'hatchet'
require 'thor'
require 'threaded'
require 'date'
require 'yaml'
require 'pathname'
class HatchetCLI < Thor
desc "init", "bootstraps a project with minimal files required to add hatchet tests"
define_method("init") do
Hatchet::InitProject.new.call
end
desc "ci:install_heroku", "installs the `heroku` cli"
define_method("ci:install_heroku") do
if `which heroku` && $?.success?
puts "The `heroku` command is already installed"
return
else
puts "installing `heroku` command"
end
script = File.expand_path File.join(__dir__, "../etc/setup_heroku.sh")
cmd(script)
end
desc "ci:setup", "sets up project to run on a linux CI environment"
define_method("ci:setup") do
script = File.expand_path File.join(__dir__, "../etc/ci_setup.rb")
cmd(script)
end
desc "install", "installs repos defined in 'hatchet.json'"
def install
warn_dot_ignore!
lock_hash = load_lockfile
puts "Installing repos for hatchet"
missing_commit = false
dirs.map do |directory, git_repo|
Threaded.later do
commit = lock_hash[directory]
directory = File.expand_path(directory)
if !(Dir[directory] && Dir[directory].empty?)
puts "== pulling '#{git_repo}' into '#{directory}'\n"
pull(directory, git_repo)
else
puts "== cloning '#{git_repo}' into '#{directory}'\n"
clone(directory, git_repo)
end
if commit
checkout_commit(directory, commit)
else
missing_commit = true
end
end
end.map(&:join)
self.lock if missing_commit
end
desc "locks to specific git commits", "updates hatchet.lock"
def lock
lock_hash = {}
lockfile_hash = load_lockfile(create_if_does_not_exist: true)
dirs.map do |directory, git_repo|
Threaded.later do
puts "== locking #{directory}"
unless Dir.exist?(directory)
raise "Bad git repo #{git_repo.inspect}" if bad_repo?(git_repo)
clone(directory, git_repo, quiet: false)
end
if lockfile_hash[directory] == "master"
lock_hash[directory] = "master"
elsif lockfile_hash[directory] == "main"
lock_hash[directory] = "main"
else
commit = commit_at_directory(directory)
lock_hash[directory] = commit
end
end
end.map(&:join)
lock_hash = lock_hash.sort
File.open('hatchet.lock', 'w') {|f| f << lock_hash.to_yaml }
puts "Done!"
end
desc "list", "lists all repos and their destination listed in hatchet.json"
def list
repos.each do |repo, directory|
puts "#{repo}: #{directory}"
end
end
desc "destroy", "Deletes application(s)"
option :all, type: :boolean, desc: "Delete ALL hatchet apps"
option :older_than, type: :numeric, desc: "Delete all hatchet apps older than N minutes"
def destroy
api_key = ENV['HEROKU_API_KEY'] || `heroku auth:token`.chomp
platform_api = PlatformAPI.connect_oauth(api_key, cache: Moneta.new(:Null))
api_rate_limit = ApiRateLimit.new(platform_api)
reaper = Hatchet::Reaper.new(api_rate_limit: api_rate_limit)
case
when options[:all]
puts "Destroying ALL apps"
reaper.destroy_all
puts "Done"
when options[:older_than]
minutes = options[:older_than].to_i
puts "Destroying apps older than #{minutes}m"
reaper.destroy_older_apps(
minutes: minutes,
force_refresh: true,
on_conflict: :refresh_api_and_continue
)
puts "Done"
else
raise "No flags given run `hatchet help destroy` for options"
end
end
private
def load_lockfile(create_if_does_not_exist: false)
return YAML.safe_load(File.read('hatchet.lock')).to_h
rescue Errno::ENOENT
if create_if_does_not_exist
FileUtils.touch('hatchet.lock')
{}
else
raise "No such file found `hatchet.lock` please run `$ bundle exec hatchet lock`"
end
end
def bad_repo?(url)
`git ls-remote --exit-code -h "#{url}"`
$? != 0
end
def warn_dot_ignore!
return false unless File.exist?('.gitignore')
gitignore = File.open('.gitignore').read
repo_path = config.repo_directory_path.gsub(/^\.\//, '') # remove ./ from front of dir
return if gitignore.include?(repo_path)
puts "WARNING: add #{File.join(repo_path, '*')} to your .gitignore file \n\n"
end
def config
@config ||= Hatchet::Config.new
end
def repos
config.repos
end
def dirs
config.dirs
end
def checkout_commit(directory, commit)
cmd("cd #{directory} && git fetch origin #{commit} && git checkout #{commit} && git checkout - && git reset --hard #{commit}")
end
def commit_at_directory(directory)
cmd("cd #{directory} && git log -n1 --pretty=format:%H").strip
end
def pull(path, git_repo, commit: false)
cmd("cd #{path} && git pull --rebase #{git_repo} --quiet")
end
def clone(path, git_repo, quiet: true)
path = File.join(path, '..') # up one dir to prevent repos/codetriage/codetriage/#...
FileUtils.mkdir_p(path)
cmd("cd #{path} && git clone #{git_repo} --quiet", quiet: quiet)
end
def cmd(command, let_fail: false, stdin: nil, quiet: true)
command = "printf '#{stdin}' | #{command}" if stdin
puts "Running: #{command}" unless quiet
result = `#{command}`
return result if let_fail
raise "Command #{command} failed:\n#{result}" unless $?.success?
return result
end
end
HatchetCLI.start(ARGV)