bard 2.2.0 → 3.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 +4 -4
- data/README.md +7 -487
- data/Rakefile +1 -4
- data/bard.gemspec +2 -11
- data/lib/bard/config.rb +22 -20
- data/lib/bard/plugins/ping/target_methods.rb +0 -9
- data/lib/bard/plugins/ssh/connection.rb +2 -35
- data/lib/bard/target.rb +0 -15
- data/lib/bard/version.rb +1 -1
- data/lib/bard.rb +2 -1
- data/spec/bard/capability_spec.rb +0 -8
- data/spec/bard/config_spec.rb +30 -67
- data/spec/bard/target_spec.rb +1 -36
- metadata +4 -225
- data/ARCHITECTURE.md +0 -957
- data/CUSTOM_STRATEGIES.md +0 -701
- data/MIGRATION_GUIDE.md +0 -513
- data/PLUGINS.md +0 -99
- data/bin/bard +0 -4
- data/features/ci.feature +0 -63
- data/features/data.feature +0 -13
- data/features/deploy.feature +0 -14
- data/features/deploy_git_workflow.feature +0 -89
- data/features/run.feature +0 -14
- data/features/step_definitions/bard_steps.rb +0 -136
- data/features/support/bard-coverage +0 -16
- data/features/support/env.rb +0 -22
- data/features/support/test_server.rb +0 -216
- data/lib/bard/cli.rb +0 -44
- data/lib/bard/command.rb +0 -31
- data/lib/bard/copy.rb +0 -45
- data/lib/bard/plugins/data.rb +0 -56
- data/lib/bard/plugins/deploy/ci/github_actions.rb +0 -202
- data/lib/bard/plugins/deploy/ci/jenkins.rb +0 -176
- data/lib/bard/plugins/deploy/ci/local.rb +0 -57
- data/lib/bard/plugins/deploy/ci/runner.rb +0 -112
- data/lib/bard/plugins/deploy/ci/state.rb +0 -40
- data/lib/bard/plugins/deploy/ci.rb +0 -38
- data/lib/bard/plugins/deploy/ssh_strategy.rb +0 -26
- data/lib/bard/plugins/deploy/strategy.rb +0 -60
- data/lib/bard/plugins/deploy.rb +0 -260
- data/lib/bard/plugins/git.rb +0 -45
- data/lib/bard/plugins/github.rb +0 -152
- data/lib/bard/plugins/github_pages/strategy.rb +0 -170
- data/lib/bard/plugins/github_pages.rb +0 -35
- data/lib/bard/plugins/hurt.rb +0 -13
- data/lib/bard/plugins/install/.github/dependabot.yml +0 -18
- data/lib/bard/plugins/install/.github/workflows/cache-ci.yml +0 -15
- data/lib/bard/plugins/install/.github/workflows/ci.yml +0 -74
- data/lib/bard/plugins/install/apt_dependencies.rb +0 -33
- data/lib/bard/plugins/install/ci +0 -5
- data/lib/bard/plugins/install/setup +0 -13
- data/lib/bard/plugins/install/specified_bundler.rb +0 -15
- data/lib/bard/plugins/install/specified_ruby.rb +0 -59
- data/lib/bard/plugins/install.rb +0 -9
- data/lib/bard/plugins/open.rb +0 -20
- data/lib/bard/plugins/ping/check.rb +0 -66
- data/lib/bard/plugins/ping.rb +0 -10
- data/lib/bard/plugins/run.rb +0 -19
- data/lib/bard/plugins/setup.rb +0 -75
- data/lib/bard/plugins/ssh/copy.rb +0 -95
- data/lib/bard/plugins/ssh.rb +0 -10
- data/lib/bard/plugins/vim.rb +0 -6
- data/lib/bard/retryable.rb +0 -25
- data/lib/bard/secrets.rb +0 -10
- data/spec/acceptance/.gitignore +0 -4
- data/spec/acceptance/docker/Dockerfile +0 -36
- data/spec/acceptance/docker/test_key +0 -27
- data/spec/acceptance/docker/test_key.pub +0 -1
- data/spec/bard/ci/github_actions_spec.rb +0 -138
- data/spec/bard/ci/jenkins_spec.rb +0 -139
- data/spec/bard/ci/runner_spec.rb +0 -61
- data/spec/bard/ci_spec.rb +0 -10
- data/spec/bard/cli/ci_spec.rb +0 -170
- data/spec/bard/cli/data_spec.rb +0 -58
- data/spec/bard/cli/deploy_spec.rb +0 -243
- data/spec/bard/cli/hurt_spec.rb +0 -17
- data/spec/bard/cli/install_spec.rb +0 -19
- data/spec/bard/cli/master_key_spec.rb +0 -38
- data/spec/bard/cli/open_spec.rb +0 -51
- data/spec/bard/cli/ping_spec.rb +0 -32
- data/spec/bard/cli/run_spec.rb +0 -57
- data/spec/bard/cli/setup_spec.rb +0 -99
- data/spec/bard/cli/ssh_spec.rb +0 -40
- data/spec/bard/cli/stage_spec.rb +0 -127
- data/spec/bard/cli/vim_spec.rb +0 -28
- data/spec/bard/command_spec.rb +0 -30
- data/spec/bard/copy_spec.rb +0 -69
- data/spec/bard/deploy_strategy/github_pages_spec.rb +0 -58
- data/spec/bard/deploy_strategy/ssh_spec.rb +0 -135
- data/spec/bard/deploy_strategy_spec.rb +0 -107
- data/spec/bard/dynamic_dsl_spec.rb +0 -46
- data/spec/bard/git_spec.rb +0 -101
- data/spec/bard/github_spec.rb +0 -45
- data/spec/bard/ping_spec.rb +0 -60
- data/spec/bard/ssh_copy_spec.rb +0 -44
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
require "bard/command"
|
|
2
|
-
|
|
3
|
-
module Bard
|
|
4
|
-
class DeployStrategy
|
|
5
|
-
@strategies = {}
|
|
6
|
-
|
|
7
|
-
class << self
|
|
8
|
-
attr_reader :strategies
|
|
9
|
-
|
|
10
|
-
def inherited(subclass)
|
|
11
|
-
super
|
|
12
|
-
# Extract strategy name from class name
|
|
13
|
-
# e.g., Bard::DeployStrategy::SSH -> :ssh
|
|
14
|
-
name = extract_strategy_name(subclass)
|
|
15
|
-
strategies[name] = subclass
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def [](name)
|
|
19
|
-
strategies[name.to_sym]
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
private
|
|
23
|
-
|
|
24
|
-
def extract_strategy_name(klass)
|
|
25
|
-
# Get the class name without module prefix
|
|
26
|
-
class_name = klass.name.split('::').last
|
|
27
|
-
# Convert from CamelCase to snake_case
|
|
28
|
-
class_name
|
|
29
|
-
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
30
|
-
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
31
|
-
.downcase
|
|
32
|
-
.to_sym
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
attr_reader :target
|
|
37
|
-
|
|
38
|
-
def initialize(target)
|
|
39
|
-
@target = target
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def deploy(clone: nil, branch: "master", force: false)
|
|
43
|
-
raise NotImplementedError, "Subclasses must implement #deploy"
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Helper methods for strategies
|
|
47
|
-
def run!(command)
|
|
48
|
-
Command.run!(command)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def run(command)
|
|
52
|
-
Command.run(command)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def system!(command)
|
|
56
|
-
result = Kernel.system(command)
|
|
57
|
-
raise "Command failed: #{command}" unless result
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
data/lib/bard/plugins/deploy.rb
DELETED
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
require "bard/plugins/ping"
|
|
2
|
-
require "bard/plugins/git"
|
|
3
|
-
require "bard/command"
|
|
4
|
-
require "bard/plugins/deploy/strategy"
|
|
5
|
-
require "bard/plugins/deploy/ssh_strategy"
|
|
6
|
-
require "bard/plugins/deploy/ci"
|
|
7
|
-
require "bard/plugins/deploy/ci/jenkins"
|
|
8
|
-
require "bard/plugins/deploy/ci/local"
|
|
9
|
-
require "bard/plugins/deploy/ci/github_actions"
|
|
10
|
-
require "tmpdir"
|
|
11
|
-
|
|
12
|
-
class Bard::CLI
|
|
13
|
-
option :"skip-ci", type: :boolean
|
|
14
|
-
option :"local-ci", type: :boolean
|
|
15
|
-
option :ci, type: :string
|
|
16
|
-
option :clone, type: :boolean
|
|
17
|
-
option :target, type: :string, default: "production"
|
|
18
|
-
desc "deploy [BRANCH]", "deploys branch to target (default: current branch to production)"
|
|
19
|
-
def deploy(branch = nil)
|
|
20
|
-
branch ||= Bard::Git.current_branch
|
|
21
|
-
|
|
22
|
-
if branch == "master"
|
|
23
|
-
if !Bard::Git.up_to_date_with_remote?(branch)
|
|
24
|
-
run! "git push origin #{branch}:#{branch}"
|
|
25
|
-
end
|
|
26
|
-
invoke :ci, [branch], options.slice("local-ci", "ci") unless options["skip-ci"] || config.ci == false
|
|
27
|
-
|
|
28
|
-
else
|
|
29
|
-
run! "git fetch origin"
|
|
30
|
-
if Bard::Git.current_branch != "master" && !Bard::Git.in_linked_worktree?
|
|
31
|
-
run! "git fetch origin master:master"
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
unless Bard::Git.fast_forward_merge?("origin/master", branch)
|
|
35
|
-
puts "The master branch has advanced. Attempting rebase..."
|
|
36
|
-
if branch == Bard::Git.current_branch
|
|
37
|
-
run! "git rebase origin/master"
|
|
38
|
-
else
|
|
39
|
-
tmpdir = Dir.mktmpdir("bard-rebase")
|
|
40
|
-
begin
|
|
41
|
-
run! "git worktree add --detach #{tmpdir} #{branch}"
|
|
42
|
-
success = Dir.chdir(tmpdir) { system("git rebase origin/master") }
|
|
43
|
-
rebased_sha = Dir.chdir(tmpdir) { `git rev-parse HEAD`.strip } if success
|
|
44
|
-
run! "git worktree remove #{tmpdir} --force"
|
|
45
|
-
unless success
|
|
46
|
-
puts red("!!! ") + "Rebase failed due to conflicts."
|
|
47
|
-
puts "Please rebase #{branch} manually:"
|
|
48
|
-
puts " git checkout #{branch}"
|
|
49
|
-
puts " git rebase origin/master"
|
|
50
|
-
exit 1
|
|
51
|
-
end
|
|
52
|
-
run! "git branch -f #{branch} #{rebased_sha}"
|
|
53
|
-
ensure
|
|
54
|
-
FileUtils.rm_rf(tmpdir) if Dir.exist?(tmpdir)
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
run! "git push -f origin #{branch}:#{branch}"
|
|
60
|
-
|
|
61
|
-
invoke :ci, [branch], options.slice("local-ci", "ci") unless options["skip-ci"] || config.ci == false
|
|
62
|
-
|
|
63
|
-
run! "git push origin #{branch}:master"
|
|
64
|
-
if Bard::Git.current_branch == "master"
|
|
65
|
-
run! "git pull --ff-only origin master"
|
|
66
|
-
elsif Bard::Git.in_linked_worktree?
|
|
67
|
-
run! "git fetch origin master"
|
|
68
|
-
else
|
|
69
|
-
run! "git fetch origin master:master"
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
if `git remote` =~ /\bgithub\b/
|
|
74
|
-
run! "git push github"
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
to = options[:target].to_sym
|
|
78
|
-
|
|
79
|
-
target = config[to]
|
|
80
|
-
strategy = target.deploy_strategy_instance
|
|
81
|
-
if options[:clone]
|
|
82
|
-
strategy.deploy(clone: project_name)
|
|
83
|
-
else
|
|
84
|
-
strategy.deploy
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
puts green("Deploy Succeeded")
|
|
88
|
-
|
|
89
|
-
if branch != "master"
|
|
90
|
-
puts "Deleting branch: #{branch}"
|
|
91
|
-
run! "git push --delete origin #{branch}"
|
|
92
|
-
|
|
93
|
-
if branch == Bard::Git.current_branch && Bard::Git.in_linked_worktree?
|
|
94
|
-
worktree_path = Dir.pwd
|
|
95
|
-
main_checkout = File.dirname(File.expand_path(`git rev-parse --git-common-dir`.chomp))
|
|
96
|
-
Dir.chdir(main_checkout)
|
|
97
|
-
run! "git worktree remove #{worktree_path}"
|
|
98
|
-
run! "git branch -D #{branch}"
|
|
99
|
-
puts "Worktree removed. Run: cd #{main_checkout}"
|
|
100
|
-
else
|
|
101
|
-
if branch == Bard::Git.current_branch
|
|
102
|
-
run! "git checkout master"
|
|
103
|
-
end
|
|
104
|
-
run! "git branch -D #{branch}"
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
ping to
|
|
109
|
-
rescue Bard::Command::Error => e
|
|
110
|
-
puts red("!!! ") + "Running command failed: #{yellow(e.message)}"
|
|
111
|
-
exit 1
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
desc "stage [branch=HEAD]", "pushes current branch, and stages it"
|
|
115
|
-
def stage(branch = Bard::Git.current_branch)
|
|
116
|
-
if config[:production] == config[:staging]
|
|
117
|
-
raise Thor::Error.new("`bard stage` is disabled until a production target is defined. Until then, please use `bard deploy` to deploy to the staging target.")
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
run! "git push -u origin #{branch}", verbose: true
|
|
121
|
-
|
|
122
|
-
target = config[:staging]
|
|
123
|
-
strategy = target.deploy_strategy_instance
|
|
124
|
-
|
|
125
|
-
if staging_provisioned?(target)
|
|
126
|
-
strategy.deploy(branch: branch, force: true)
|
|
127
|
-
puts green("Stage Succeeded")
|
|
128
|
-
|
|
129
|
-
else # clone from scratch
|
|
130
|
-
puts yellow("Staging site for #{project_name} not found — provisioning it from scratch…")
|
|
131
|
-
strategy.deploy(clone: project_name, branch: branch)
|
|
132
|
-
puts green("Stage Succeeded")
|
|
133
|
-
|
|
134
|
-
puts "#{project_name} was rebuilt from scratch; its database and files are empty."
|
|
135
|
-
if $stdin.tty? && !no?("Restore #{project_name}'s data from production now? (bard data --from production --to staging) [Y/n]")
|
|
136
|
-
invoke :data, [], from: "production", to: "staging"
|
|
137
|
-
else
|
|
138
|
-
puts "Run #{yellow("bard data --from production --to staging")} to restore its data."
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
ping :staging
|
|
143
|
-
rescue Bard::Command::Error => e
|
|
144
|
-
puts red("!!! ") + "Running command failed: #{yellow(e.message)}"
|
|
145
|
-
exit 1
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
no_commands do
|
|
149
|
-
def staging_provisioned?(target)
|
|
150
|
-
!!target.run("test -e #{target.path}/.git", home: true)
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
option :"local-ci", type: :boolean
|
|
155
|
-
option :ci, type: :string
|
|
156
|
-
option :status, type: :boolean
|
|
157
|
-
option :resume, type: :boolean
|
|
158
|
-
desc "ci [branch=HEAD]", "runs ci against BRANCH"
|
|
159
|
-
def ci(branch = Bard::Git.current_branch)
|
|
160
|
-
runner_name = if options["local-ci"]
|
|
161
|
-
:local
|
|
162
|
-
elsif options["ci"]
|
|
163
|
-
options["ci"].to_sym
|
|
164
|
-
else
|
|
165
|
-
config.ci
|
|
166
|
-
end
|
|
167
|
-
ci = Bard::CI.new(project_name, branch, runner_name: runner_name)
|
|
168
|
-
unless ci.exists?
|
|
169
|
-
puts red("No CI found for #{project_name}!")
|
|
170
|
-
puts "Re-run with --skip-ci to bypass CI, if you absolutely must, and know what you're doing."
|
|
171
|
-
exit 1
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
return puts ci.status if options["status"]
|
|
175
|
-
|
|
176
|
-
if options["resume"]
|
|
177
|
-
puts "Continuous integration: resuming build..."
|
|
178
|
-
success = ci.resume do |elapsed_time, last_time|
|
|
179
|
-
if last_time
|
|
180
|
-
percentage = (elapsed_time.to_f / last_time.to_f * 100).to_i
|
|
181
|
-
output = " Estimated completion: #{percentage}%"
|
|
182
|
-
else
|
|
183
|
-
output = " No estimated completion time. Elapsed time: #{elapsed_time} sec"
|
|
184
|
-
end
|
|
185
|
-
print "\x08" * output.length
|
|
186
|
-
print output
|
|
187
|
-
$stdout.flush
|
|
188
|
-
end
|
|
189
|
-
else
|
|
190
|
-
puts "Continuous integration: starting build on #{branch}..."
|
|
191
|
-
|
|
192
|
-
success = ci.run do |elapsed_time, last_time|
|
|
193
|
-
if last_time
|
|
194
|
-
percentage = (elapsed_time.to_f / last_time.to_f * 100).to_i
|
|
195
|
-
output = " Estimated completion: #{percentage}%"
|
|
196
|
-
else
|
|
197
|
-
output = " No estimated completion time. Elapsed time: #{elapsed_time} sec"
|
|
198
|
-
end
|
|
199
|
-
print "\x08" * output.length
|
|
200
|
-
print output
|
|
201
|
-
$stdout.flush
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
if success
|
|
206
|
-
puts
|
|
207
|
-
puts "Continuous integration: success!"
|
|
208
|
-
else
|
|
209
|
-
puts
|
|
210
|
-
puts ci.console
|
|
211
|
-
puts red("Automated tests failed!")
|
|
212
|
-
exit 1
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
option :from, default: "production"
|
|
217
|
-
option :to, default: "local"
|
|
218
|
-
desc "master_key --from=production --to=local", "copy master key from from to to"
|
|
219
|
-
def master_key
|
|
220
|
-
from = config[options[:from]]
|
|
221
|
-
to = config[options[:to]]
|
|
222
|
-
Bard::Copy.file "config/master.key", from: from, to: to
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
require "bard/config"
|
|
227
|
-
|
|
228
|
-
class Bard::Config
|
|
229
|
-
def ci(system = nil)
|
|
230
|
-
if system.nil?
|
|
231
|
-
@ci_system
|
|
232
|
-
else
|
|
233
|
-
@ci_system = system
|
|
234
|
-
end
|
|
235
|
-
end
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
require "bard/target"
|
|
239
|
-
|
|
240
|
-
class Bard::Target
|
|
241
|
-
def deploy_strategy
|
|
242
|
-
@deploy_strategy
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
def deploy_strategy_instance
|
|
246
|
-
strategy = @deploy_strategy
|
|
247
|
-
strategy ||= :ssh if has_capability?(:ssh)
|
|
248
|
-
raise "No deployment strategy configured for target #{key}" unless strategy
|
|
249
|
-
|
|
250
|
-
strategy_class = Bard::DeployStrategy[strategy]
|
|
251
|
-
raise "Unknown deployment strategy: #{strategy}" unless strategy_class
|
|
252
|
-
|
|
253
|
-
strategy_class.new(self)
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
def strategy_options(strategy_name)
|
|
257
|
-
@strategy_options_hash ||= {}
|
|
258
|
-
@strategy_options_hash[strategy_name] || {}
|
|
259
|
-
end
|
|
260
|
-
end
|
data/lib/bard/plugins/git.rb
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
module Bard
|
|
2
|
-
module Git
|
|
3
|
-
module_function
|
|
4
|
-
|
|
5
|
-
def current_branch
|
|
6
|
-
ref = `git symbolic-ref HEAD 2>&1`.chomp
|
|
7
|
-
return false if ref =~ /^fatal:/
|
|
8
|
-
ref.sub(/refs\/heads\//, '') # refs/heads/master ... we want "master"
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def fast_forward_merge?(root, branch)
|
|
12
|
-
root_head = sha_of(root)
|
|
13
|
-
branch_head = sha_of(branch)
|
|
14
|
-
common_ancestor = `git merge-base #{root_head} #{branch_head}`.chomp
|
|
15
|
-
common_ancestor == root_head
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def up_to_date_with_remote? branch
|
|
19
|
-
sha_of(branch) == sha_of("origin/#{branch}")
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def sha_of ref
|
|
23
|
-
sha = `git rev-parse #{ref} 2>/dev/null`.chomp
|
|
24
|
-
return sha if $?.success?
|
|
25
|
-
nil # Branch doesn't exist
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def in_linked_worktree?
|
|
29
|
-
git_dir = `git rev-parse --git-dir 2>/dev/null`.chomp
|
|
30
|
-
common_dir = `git rev-parse --git-common-dir 2>/dev/null`.chomp
|
|
31
|
-
return false if git_dir.empty? || common_dir.empty?
|
|
32
|
-
File.expand_path(git_dir) != File.expand_path(common_dir)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def github_pages_url
|
|
36
|
-
remote = `git remote get-url origin`.chomp
|
|
37
|
-
match = remote.match(%r{github\.com[:/]+([^/]+?)/(.+?)(?:\.git)?/?\z})
|
|
38
|
-
raise "Could not derive a github.io URL: origin remote #{remote.inspect} is not a github.com repository" unless match
|
|
39
|
-
owner, repo = match[1], match[2]
|
|
40
|
-
host = "#{owner.downcase}.github.io"
|
|
41
|
-
repo.casecmp?(host) ? "https://#{host}/" : "https://#{host}/#{repo}/"
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
data/lib/bard/plugins/github.rb
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
require "net/http"
|
|
2
|
-
require "json"
|
|
3
|
-
require "base64"
|
|
4
|
-
require "rbnacl"
|
|
5
|
-
require "bard/retryable"
|
|
6
|
-
require "bard/secrets"
|
|
7
|
-
|
|
8
|
-
module Bard
|
|
9
|
-
class Github < Struct.new(:project_name)
|
|
10
|
-
include Retryable
|
|
11
|
-
|
|
12
|
-
def initialize(project_name, api_key: nil)
|
|
13
|
-
super(project_name)
|
|
14
|
-
@api_key = api_key
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def get path, params={}
|
|
18
|
-
request(path) do |uri|
|
|
19
|
-
uri.query = URI.encode_www_form(params)
|
|
20
|
-
Net::HTTP::Get.new(uri)
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def post path, params={}
|
|
25
|
-
request(path) do |uri|
|
|
26
|
-
Net::HTTP::Post.new(uri).tap do |r|
|
|
27
|
-
r.body = JSON.dump(params)
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def put path, params={}
|
|
33
|
-
request(path) do |uri|
|
|
34
|
-
Net::HTTP::Put.new(uri).tap do |r|
|
|
35
|
-
r.body = JSON.dump(params)
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def patch path, params={}
|
|
41
|
-
request(path) do |uri|
|
|
42
|
-
Net::HTTP::Patch.new(uri).tap do |r|
|
|
43
|
-
r.body = JSON.dump(params)
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def delete path, params={}
|
|
49
|
-
request(path) do |uri|
|
|
50
|
-
Net::HTTP::Delete.new(uri).tap do |r|
|
|
51
|
-
r.body = JSON.dump(params)
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def read_file path, branch: "master"
|
|
57
|
-
metadata = get("contents/#{path}", ref: branch)
|
|
58
|
-
Base64.decode64(metadata["content"])
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def add_deploy_key title:, key:
|
|
62
|
-
post("keys", title:, key:)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def add_master_key master_key
|
|
66
|
-
response = get("actions/secrets/public-key")
|
|
67
|
-
public_key, public_key_id = response.values_at("key", "key_id")
|
|
68
|
-
|
|
69
|
-
def encrypt_secret(encoded_public_key, secret)
|
|
70
|
-
decoded_key = Base64.decode64(encoded_public_key)
|
|
71
|
-
public_key = RbNaCl::PublicKey.new(decoded_key)
|
|
72
|
-
box = RbNaCl::Boxes::Sealed.from_public_key(public_key)
|
|
73
|
-
encrypted_secret = box.encrypt(secret)
|
|
74
|
-
Base64.strict_encode64(encrypted_secret)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
encrypted_master_key = encrypt_secret(public_key, master_key)
|
|
78
|
-
|
|
79
|
-
put("actions/secrets/RAILS_MASTER_KEY", {
|
|
80
|
-
encrypted_value: encrypted_master_key,
|
|
81
|
-
key_id: public_key_id,
|
|
82
|
-
})
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def add_master_branch_protection
|
|
86
|
-
put("branches/master/protection", {
|
|
87
|
-
required_status_checks: {
|
|
88
|
-
strict: false,
|
|
89
|
-
contexts: [],
|
|
90
|
-
},
|
|
91
|
-
enforce_admins: nil,
|
|
92
|
-
required_pull_request_reviews: nil,
|
|
93
|
-
restrictions: nil,
|
|
94
|
-
})
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def create_repo
|
|
98
|
-
post("https://api.github.com/orgs/botandrosedesign/repos", {
|
|
99
|
-
name: project_name,
|
|
100
|
-
private: true,
|
|
101
|
-
})
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def delete_repo
|
|
105
|
-
delete(nil)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
private
|
|
109
|
-
|
|
110
|
-
def api_key
|
|
111
|
-
@api_key ||= Bard::Secrets.fetch("github-apikey")
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def request path, &block
|
|
115
|
-
uri = if path =~ /^http/
|
|
116
|
-
URI(path)
|
|
117
|
-
else
|
|
118
|
-
base = "https://api.github.com/repos/botandrosedesign/#{project_name}"
|
|
119
|
-
if path
|
|
120
|
-
URI(File.join(base, path))
|
|
121
|
-
else
|
|
122
|
-
URI(base)
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
retry_with_backoff do
|
|
127
|
-
req = nil
|
|
128
|
-
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
|
129
|
-
req = block.call(uri)
|
|
130
|
-
req["Accept"] = "application/vnd.github+json"
|
|
131
|
-
req["Authorization"] = "Bearer #{api_key}"
|
|
132
|
-
req["X-GitHub-Api-Version"] = "2022-11-28"
|
|
133
|
-
http.request(req)
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
case response
|
|
137
|
-
when Net::HTTPRedirection then
|
|
138
|
-
Net::HTTP.get(URI(response["Location"]))
|
|
139
|
-
when Net::HTTPSuccess then
|
|
140
|
-
if response["Content-Type"].to_s.include?("/json")
|
|
141
|
-
JSON.load(response.body)
|
|
142
|
-
else
|
|
143
|
-
response.body
|
|
144
|
-
end
|
|
145
|
-
else
|
|
146
|
-
raise [req.method, req.uri, req.to_hash, response, response.body].inspect
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
require "bard/plugins/deploy/strategy"
|
|
2
|
-
require "bard/plugins/git"
|
|
3
|
-
require "fileutils"
|
|
4
|
-
require "socket"
|
|
5
|
-
require "uri"
|
|
6
|
-
|
|
7
|
-
module Bard
|
|
8
|
-
class DeployStrategy
|
|
9
|
-
class GithubPages < DeployStrategy
|
|
10
|
-
def initialize(target, url = nil, **options)
|
|
11
|
-
super(target)
|
|
12
|
-
@url = url || target.github_pages
|
|
13
|
-
@options = options
|
|
14
|
-
|
|
15
|
-
target.url(url) if url
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def deploy
|
|
19
|
-
@sha = Git.sha_of(Git.current_branch)
|
|
20
|
-
@build_dir = "tmp/github-build-#{@sha}"
|
|
21
|
-
@branch = "gh-pages"
|
|
22
|
-
@domain = extract_domain
|
|
23
|
-
@port = pick_free_port
|
|
24
|
-
|
|
25
|
-
puts "Starting deployment to GitHub Pages..."
|
|
26
|
-
|
|
27
|
-
build_site
|
|
28
|
-
tree_sha = create_tree_from_build
|
|
29
|
-
new_commit = create_commit(tree_sha)
|
|
30
|
-
commit_and_push(new_commit)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
private
|
|
34
|
-
|
|
35
|
-
def extract_domain
|
|
36
|
-
return nil unless @url
|
|
37
|
-
domain = @url
|
|
38
|
-
domain = URI.parse(domain).hostname if domain =~ /^http/
|
|
39
|
-
domain
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def pick_free_port
|
|
43
|
-
server = TCPServer.new("127.0.0.1", 0)
|
|
44
|
-
port = server.addr[1]
|
|
45
|
-
server.close
|
|
46
|
-
port
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def build_site
|
|
50
|
-
# No custom domain => the site serves under /<repo>/, so make the mirror relocatable:
|
|
51
|
-
# -k rewrites absolute links to relative, and the sed strips the localhost prefix wget
|
|
52
|
-
# bakes into any links it couldn't crawl.
|
|
53
|
-
convert_links = @domain ? "" : "-k"
|
|
54
|
-
relocate_step = @domain ? "" : %(find . -type f \\( -name '*.html' -o -name '*.css' \\) -exec sed -i "s#http://localhost:$PORT/#/#g" {} +)
|
|
55
|
-
cname_step = @domain ? "echo #{@domain} > CNAME" : ""
|
|
56
|
-
system "rm -rf #{@build_dir.sub(@sha, "*")}"
|
|
57
|
-
run! <<~SH
|
|
58
|
-
set -e
|
|
59
|
-
PORT=#{@port}
|
|
60
|
-
|
|
61
|
-
OUTPUT=$(bundle exec rake assets:clean assets:precompile 2>&1) || echo "$OUTPUT"
|
|
62
|
-
|
|
63
|
-
BUILD=#{@build_dir}
|
|
64
|
-
rm -rf $BUILD
|
|
65
|
-
mkdir -p $BUILD
|
|
66
|
-
cp -R public/assets $BUILD/
|
|
67
|
-
|
|
68
|
-
# Start rails in the foreground, backgrounded — NOT as a daemon —
|
|
69
|
-
# so a port-bind failure surfaces instead of being swallowed by fork.
|
|
70
|
-
rm -f tmp/pids/server.pid
|
|
71
|
-
RAILS_ENV=production bundle exec rails s -p $PORT -P tmp/pids/server.pid >tmp/pids/server.log 2>&1 &
|
|
72
|
-
RAILS_PID=$!
|
|
73
|
-
|
|
74
|
-
echo waiting...
|
|
75
|
-
for i in $(seq 1 30); do
|
|
76
|
-
if ! kill -0 $RAILS_PID 2>/dev/null; then
|
|
77
|
-
echo "Rails server failed to start on port $PORT:"
|
|
78
|
-
cat tmp/pids/server.log
|
|
79
|
-
exit 1
|
|
80
|
-
fi
|
|
81
|
-
curl -sf http://localhost:$PORT >/dev/null 2>&1 && break
|
|
82
|
-
sleep 1
|
|
83
|
-
done
|
|
84
|
-
|
|
85
|
-
cd $BUILD
|
|
86
|
-
echo copying...
|
|
87
|
-
wget -nv -r -l inf #{convert_links} --no-remove-listing -FEnH --reject-regex "(\\.*)\\?(.*)" http://localhost:$PORT/ 2>&1
|
|
88
|
-
|
|
89
|
-
#{relocate_step}
|
|
90
|
-
#{cname_step}
|
|
91
|
-
SH
|
|
92
|
-
ensure
|
|
93
|
-
# cleanup
|
|
94
|
-
run! <<~SH
|
|
95
|
-
PID=$(cat tmp/pids/server.pid 2>/dev/null)
|
|
96
|
-
if [ -n "$PID" ]; then
|
|
97
|
-
kill $PID 2>/dev/null
|
|
98
|
-
for i in 1 2 3 4 5; do
|
|
99
|
-
kill -0 $PID 2>/dev/null || break
|
|
100
|
-
sleep 1
|
|
101
|
-
done
|
|
102
|
-
kill -9 $PID 2>/dev/null || true
|
|
103
|
-
fi
|
|
104
|
-
rm -rf public/assets
|
|
105
|
-
SH
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def create_tree_from_build
|
|
109
|
-
# Create temporary index
|
|
110
|
-
git_index_file = ".git/tmp-index"
|
|
111
|
-
git = "GIT_INDEX_FILE=#{git_index_file} git"
|
|
112
|
-
run! "#{git} read-tree --empty"
|
|
113
|
-
|
|
114
|
-
# Add build files to temporary index
|
|
115
|
-
Dir.chdir(@build_dir) do
|
|
116
|
-
Dir.glob('**/*', File::FNM_DOTMATCH).each do |file|
|
|
117
|
-
next if file == '.' || file == '..'
|
|
118
|
-
if File.file?(file)
|
|
119
|
-
run! "#{git} update-index --add --cacheinfo 100644 $(#{git} hash-object -w #{file}) #{file}"
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# Create tree object from index
|
|
125
|
-
tree_sha = `#{git} write-tree`.chomp
|
|
126
|
-
|
|
127
|
-
# Clean up temporary index
|
|
128
|
-
FileUtils.rm_f(git_index_file)
|
|
129
|
-
|
|
130
|
-
tree_sha
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def create_commit(tree_sha)
|
|
134
|
-
# Get parent commit if branch exists
|
|
135
|
-
parent = get_parent_commit
|
|
136
|
-
|
|
137
|
-
# Create commit object
|
|
138
|
-
message = "'Deploying to #{@branch} from @ #{@sha} 🚀'"
|
|
139
|
-
args = ['commit-tree', tree_sha]
|
|
140
|
-
args += ['-p', parent] if parent
|
|
141
|
-
args += ['-m', message]
|
|
142
|
-
|
|
143
|
-
commit_sha = `git #{args.join(' ')}`.chomp
|
|
144
|
-
|
|
145
|
-
commit_sha
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def get_parent_commit
|
|
149
|
-
Git.sha_of("#{@branch}^{commit}")
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def commit_and_push(commit_sha)
|
|
153
|
-
if branch_exists?
|
|
154
|
-
run! "git update-ref refs/heads/#{@branch} #{commit_sha}"
|
|
155
|
-
else
|
|
156
|
-
run! "git branch #{@branch} #{commit_sha}"
|
|
157
|
-
end
|
|
158
|
-
run! "git push -f origin #{@branch}:refs/heads/#{@branch}"
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def branch_exists?
|
|
162
|
-
system("git show-ref --verify --quiet refs/heads/#{@branch}")
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
def cleanup
|
|
166
|
-
# Cleanup method for tests
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
end
|