Skip to content

Instantly share code, notes, and snippets.

@nathan-appere
Forked from pcreux/clone-heroku-app
Created July 5, 2023 08:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nathan-appere/53c5e1d7c9b4a6b4e673a21d66ebad68 to your computer and use it in GitHub Desktop.
Save nathan-appere/53c5e1d7c9b4a6b4e673a21d66ebad68 to your computer and use it in GitHub Desktop.
Script to clone a heroku app (including buildpacks, users, add-ons, environment variables, lab features)
#!/usr/bin/env ruby
# Copy a heroku app (buildpacks, add-ons, labs, config, users).
# This script is idempotent so it can run against an existing app.
#
# Usage:
# $> clone-heroku-app source-app target-app
require 'json'
HEROKU_TEAM = 'my-team'
PIPELINE = 'my-pipeline'
PIPELINE_STAGE = 'production'
STACK = 'heroku-18'
SOURCE_APP = ARGV[0] or raise "Usage: clone-heroku-app source-app target-app"
TARGET_APP = ARGV[1] or raise "Usage: clone-heroku-app source-app target-app"
def run(cmd)
system(cmd) || exit(-1)
end
def run?(cmd)
puts "$> #{cmd}"
puts "Press any key to run. CTRL+C to abort."
STDIN.gets
run(cmd)
end
# Create app
unless `heroku apps -t #{HEROKU_TEAM}`[TARGET_APP]
run?("heroku create #{TARGET_APP} --remote #{TARGET_APP} -s #{STACK} --team=#{HEROKU_TEAM}")
end
puts "=> #{TARGET_APP} created"
# Add it to the pipeline
unless `heroku pipelines:info #{PIPELINE}`[TARGET_APP]
run?("heroku pipelines:add #{PIPELINE} -a #{TARGET_APP} -s #{PIPELINE_STAGE}")
end
puts "=> #{TARGET_APP} added to pipeline #{PIPELINE}"
# Add buildpacks
source_buildpacks = `heroku buildpacks -a #{SOURCE_APP}`.split("\n").map { |l| l.split(' ').last }[1..-1]
current_buildpacks = `heroku buildpacks -a #{TARGET_APP}`.split("\n").map { |l| l.split(' ').last }[1..-1]
buildpacks_to_add = source_buildpacks - current_buildpacks
if buildpacks_to_add.any?
puts "The following buildpacks will be added:"
buildpacks_to_add.each do |buildpack|
puts " * #{buildpack}"
end
end
buildpacks_to_add.each do |buildpack|
run?("heroku buildpacks:add #{buildpack} -a #{TARGET_APP}")
end
if buildpacks_to_add.any?
puts "Make sure that the buildpacks order is correct..."
puts "#{SOURCE_APP}:"
run "heroku buildpacks -a #{SOURCE_APP}"
puts "---"
puts "#{TARGET_APP}:"
run "heroku buildpacks -a #{TARGET_APP}"
puts ""
puts ""
end
puts "=> Buildpacks synced"
# Attach postgresql add-ons
unless `heroku addons -a #{TARGET_APP}`["heroku-postgresql"]
pg_info = `heroku pg:info -a #{SOURCE_APP}`.split("\n")
main_database_addon_name = pg_info.find { |l| l["Add-on"] }.split(" ").last
run? "heroku addons:attach #{main_database_addon_name} -a #{TARGET_APP}"
end
puts "=> Addons attached"
source_app_config = JSON.parse `heroku config --json -a #{SOURCE_APP}`
target_app_config = JSON.parse `heroku config --json -a #{TARGET_APP}`
target_config = source_app_config.reject { |k, _v|
%w(DATABASE_URL HEROKU_ REDIS_PROVIDER).any? { |prefix|
k.start_with?(prefix)
}
}
changed_config_keys = (target_config.to_a - target_app_config.to_a).to_h.keys
changes = changed_config_keys.map { |key|
[ key, [ target_app_config[key], target_config[key] ] ]
}.to_h
if changes.any?
puts "The following environment variables will be updated:"
pp changes
values = changes.map { |var, changes|
_before, after = changes
[ var, "'#{after}'" ].join("=")
}.join(" ")
run? "heroku config:set #{values} -a #{TARGET_APP}"
end
puts "=> Environment variables set"
def lab_features(app)
JSON.parse(`heroku labs --json -a #{app}`)
.fetch("app")
.select { |feature| feature.fetch("enabled") }
.map { |feature| feature.fetch("name") }
end
missing_lab_features = lab_features(SOURCE_APP) - lab_features(TARGET_APP)
if missing_lab_features.any?
puts "The following lab features are going to be added: #{missing_lab_features.join(", ")}"
missing_lab_features.each do |feature|
run?("heroku labs:enable #{feature} -a #{TARGET_APP}")
end
end
puts "=> Lab features enabled"
User = Struct.new(:email, :permissions)
def get_users(app)
JSON.parse(`heroku access --json -a #{app}`).map do |access_data|
User.new(
access_data.fetch("user").fetch("email"),
access_data.fetch("permissions").map do |permission_data|
permission_data.fetch("name")
end
)
end
end
source_users = get_users(SOURCE_APP)
target_users = get_users(TARGET_APP)
source_users.each do |web_user|
unless target_users.map(&:email).include? source_user.email
run?("heroku access:add #{source_user.email} --permissions #{web_user.permissions.join(',')} -a #{TARGET_APP}")
end
end
puts "=> User access configured"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment