bub_bot 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +45 -0
- data/Rakefile +6 -0
- data/bin/bub_bot +5 -0
- data/braindump.md +28 -0
- data/bub_bot.gemspec +37 -0
- data/example_config.yml +56 -0
- data/lib/bub_bot/cli.rb +42 -0
- data/lib/bub_bot/configuration.rb +34 -0
- data/lib/bub_bot/deploy_manager.rb +131 -0
- data/lib/bub_bot/redis_connection.rb +16 -0
- data/lib/bub_bot/repo.rb +115 -0
- data/lib/bub_bot/server_manager.rb +76 -0
- data/lib/bub_bot/slack/client.rb +15 -0
- data/lib/bub_bot/slack/command.rb +100 -0
- data/lib/bub_bot/slack/command_parser.rb +37 -0
- data/lib/bub_bot/slack/commands/claim.rb +89 -0
- data/lib/bub_bot/slack/commands/deploy.rb +123 -0
- data/lib/bub_bot/slack/commands/echo.rb +5 -0
- data/lib/bub_bot/slack/commands/list.rb +26 -0
- data/lib/bub_bot/slack/commands/release.rb +26 -0
- data/lib/bub_bot/slack/response.rb +21 -0
- data/lib/bub_bot/version.rb +3 -0
- data/lib/bub_bot/web_server.rb +92 -0
- data/lib/bub_bot.rb +54 -0
- metadata +257 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: fcd0e50d99abae7189edd2f1f6ae9c7b12631784
|
4
|
+
data.tar.gz: ab061acc31daff26688c897240d3808e1ae389fc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 91b56fd6e73b9c1fb7793a289c72a1bccd49eae9e884be248626040198c6933aa81cbff293ebda8ae3dfa553110619f4a3a6424b8e5282684a6da1c02c5ea8e4
|
7
|
+
data.tar.gz: a352763574e9182999139871426a202cbc28dab42d83d3ecc94243f48216ec86081327ee131dda544148bd6029ab4081278f204299a889b54b24fd88f3e63a39
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
10
|
+
orientation.
|
11
|
+
|
12
|
+
## Our Standards
|
13
|
+
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
15
|
+
include:
|
16
|
+
|
17
|
+
* Using welcoming and inclusive language
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
19
|
+
* Gracefully accepting constructive criticism
|
20
|
+
* Focusing on what is best for the community
|
21
|
+
* Showing empathy towards other community members
|
22
|
+
|
23
|
+
Examples of unacceptable behavior by participants include:
|
24
|
+
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
26
|
+
advances
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
28
|
+
* Public or private harassment
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
30
|
+
address, without explicit permission
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
32
|
+
professional setting
|
33
|
+
|
34
|
+
## Our Responsibilities
|
35
|
+
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
38
|
+
response to any instances of unacceptable behavior.
|
39
|
+
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
44
|
+
threatening, offensive, or harmful.
|
45
|
+
|
46
|
+
## Scope
|
47
|
+
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
49
|
+
when an individual is representing the project or its community. Examples of
|
50
|
+
representing a project or community include using an official project e-mail
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
53
|
+
further defined and clarified by project maintainers.
|
54
|
+
|
55
|
+
## Enforcement
|
56
|
+
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
58
|
+
reported by contacting the project team at kevin@kevinkuchta.com. All
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
63
|
+
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
66
|
+
members of the project's leadership.
|
67
|
+
|
68
|
+
## Attribution
|
69
|
+
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
71
|
+
available at [http://contributor-covenant.org/version/1/4][version]
|
72
|
+
|
73
|
+
[homepage]: http://contributor-covenant.org
|
74
|
+
[version]: http://contributor-covenant.org/version/1/4/
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Kevin Kuchta
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# Dev instructions
|
2
|
+
|
3
|
+
These are just notes to myself. //TODO: rewrite this section so it's useful to people who are not me.
|
4
|
+
|
5
|
+
Use ec2tunnel alias to set up a tunnel (it's already rigged up to slack on the ktest team)
|
6
|
+
`redis-server --save "" --appendonly no`
|
7
|
+
`bundle exec bin/bub_bot test_config.yml`
|
8
|
+
|
9
|
+
Slack setup:
|
10
|
+
- Creat an app
|
11
|
+
- Add a bot user
|
12
|
+
- Add an event subscription
|
13
|
+
- Use the *bot event* for messages.channel
|
14
|
+
- Add an incoming webhook
|
15
|
+
<!--- Add event subscription-->
|
16
|
+
<!--- Enter hostname (verification should happen automatically now)-->
|
17
|
+
<!--- Add messages.channel permission-->
|
18
|
+
- More notes on this in test_config.yml. Before I ship all this I'll need to test out the slack setup instructions.
|
19
|
+
|
20
|
+
# Questions
|
21
|
+
- What's the syntax for deploying different branches to different targets?
|
22
|
+
`bub deploy burrito, kk_web_change to web, kk_core_change to core` maybe? Ideally something simpler.
|
23
|
+
- We'll want a shorthand for deploying the same branch to both (`bub deploy burrito kk_small_fixes to both`). Or maybe not.
|
24
|
+
- If you leave one off, does it deploy develop, or maybe it doesn't deploy to that branch at all? eg `bub deploy kk_small_fixes to web`
|
25
|
+
|
26
|
+
# Deploy
|
27
|
+
- Deploy implicitly takes the server
|
28
|
+
- `bub deploy cannoli kk_some_change` deploys that branch to both servers on cannoli
|
29
|
+
- if that branch is found in both core and web, deploy it
|
30
|
+
- if it's found in only one, deploy it there and deploy develop in the other
|
31
|
+
- `bub deploy cannoli core kk_foo web kk_bar`
|
32
|
+
- deploy branches to servers
|
33
|
+
- try to accept 'kk_foo to core', 'kk_foo on core', 'core kk_foo'
|
34
|
+
- ignore commas and 'and'
|
35
|
+
- `bub deploy cannoli core kk_foo`
|
36
|
+
- if only one target specified, I guess deploy default branch to the other
|
37
|
+
- I suppose we want to always deploy to all systems so it's not in a surprising state
|
38
|
+
- Maybe support `none` or `skip` as a branch if we want to explicitly skip deploying to it.
|
39
|
+
- `bub deploy core kk_foo`
|
40
|
+
- Takes and deploys to the first available server
|
41
|
+
- We *could* have this deploy to any server you already have claimed instead, I guess
|
42
|
+
- Seems like that could be surprising (in a bad way)
|
43
|
+
- Maybe only do that if you have only one server claimed?
|
44
|
+
- I dunno, still seems kinda sketch
|
45
|
+
|
data/Rakefile
ADDED
data/bin/bub_bot
ADDED
data/braindump.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
This is a running log of whatever I was thinking when I stopped work for the day.
|
2
|
+
|
3
|
+
Brain dump for the next time I pick this up:
|
4
|
+
- Ok, `take` works. There's a nice litle parsing system for flexible command parsing.
|
5
|
+
- We've got a custom error class that, if thrown, prints the error message to slack too.
|
6
|
+
- Next up: rubocop + some cleanup, then start in on the deploy command.
|
7
|
+
- Deploy should be largely configured by the config mechanisms
|
8
|
+
- Although there should be helpers for common deploy strategies (eg git pushing and/or running a script)
|
9
|
+
- Ok, I described the deploy command in the readme. I also put together an example config in test_config.yml for deploys. Now all you have to do is actually implement it.
|
10
|
+
|
11
|
+
|
12
|
+
- Ok, I've written most of the deploy command's parsing. It probably works, but it'll need testing. Also needs more error handling. See the TODOs in the command file.
|
13
|
+
- Next up is implementing the git logic.
|
14
|
+
- Old bub implemented deploy by just cloning, then pushing, then deleting it all.
|
15
|
+
- New bub needs to keep a persistent git repo around, if only so we can get a list of branch names to check commands against.
|
16
|
+
- Might need a repo manager class to keep track of the state of those things.
|
17
|
+
|
18
|
+
I wrote a bunch of git integration stuff, but it turns out you can get a list of remote branches _without_ cloning the rep! So most of my work today isn't needed yet (although it might be for deploying).
|
19
|
+
- If I want to, I can do the old clone->deploy->delete cycle for deploys.
|
20
|
+
- Alternately, I can try to keep the repo around between deploys to speed things up.
|
21
|
+
|
22
|
+
So I ended up using most of that git stuff anyway. This is because... git deploy now works! Woo! We're even reusing repos! Todo next:
|
23
|
+
- Clean up the deploy command (it needs to support more things, like "take latest" and "claim whatever we're deploying to").
|
24
|
+
- Make sure we're only deploying to one repo at a time. We _could_ use nora's solution of just cloning a separate copy of the repo for each deploy, but I'd prefer to be able to reuse repos.
|
25
|
+
- Actually, maybe this isn't much of an issue? We only need to keep a lock on a repo while we're getting set up for a deploy. Once git push starts, we don't care that much about what happens to the filesystem.
|
26
|
+
- Implement script-based deploys (for web)
|
27
|
+
- Implement before/after deploy hooks (eg for turning envs on and off when not in use)
|
28
|
+
- Wait, maybe this should be handled separately.
|
data/bub_bot.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'bub_bot/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "bub_bot"
|
8
|
+
spec.version = BubBot::VERSION
|
9
|
+
spec.authors = ["Kevin Kuchta"]
|
10
|
+
spec.email = ["kevin@kevinkuchta.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{A little server reservation and deploy bot for heroku and aptible}
|
13
|
+
spec.homepage = "https://github.com/kkuchta/bub_bot"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
17
|
+
f.match(%r{^(test|spec|features)/})
|
18
|
+
end
|
19
|
+
spec.bindir = "bin"
|
20
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
spec.add_dependency "rack", "~> 1.0"
|
24
|
+
spec.add_dependency "thin", "~> 1.0"
|
25
|
+
spec.add_dependency "git"
|
26
|
+
spec.add_dependency "activesupport"
|
27
|
+
spec.add_dependency "faraday"
|
28
|
+
spec.add_dependency 'actionview'
|
29
|
+
spec.add_dependency "redis"
|
30
|
+
spec.add_dependency 'slack-ruby-client'
|
31
|
+
|
32
|
+
spec.add_development_dependency "bundler", "~> 1.13"
|
33
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
34
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
35
|
+
spec.add_development_dependency "rspec-mocks", "~> 3.5"
|
36
|
+
spec.add_development_dependency "pry-byebug"
|
37
|
+
end
|
data/example_config.yml
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# Ok, slack's apis + integrations are a big mess. This was much simpler a few
|
2
|
+
# years ago. Some learnings:
|
3
|
+
#
|
4
|
+
# - Distributing as a slack app isn't going to work. That seems built around
|
5
|
+
# externally-hosted servers. You get the web-api credentials during this
|
6
|
+
# oauth-like flow that happens when a team clicks the "add app" button.
|
7
|
+
# - Wait! No! In the app config, under "Add features + functionality"
|
8
|
+
# > Permissions > Bot User OAuth Access Token, there's a bot user oauth token
|
9
|
+
# that seems to work for accessing the web api.
|
10
|
+
#
|
11
|
+
|
12
|
+
redis_host: localhost:6379
|
13
|
+
|
14
|
+
bot_name: bub
|
15
|
+
|
16
|
+
# Rack options
|
17
|
+
Port: 65222
|
18
|
+
|
19
|
+
# Bot oauth token
|
20
|
+
bot_oauth_token: abcd-123456
|
21
|
+
|
22
|
+
slack_channel: general
|
23
|
+
|
24
|
+
# H'ok, so:
|
25
|
+
# deploy_targets lists each codebase. Deploy defines how to deploy. Any string
|
26
|
+
# in here will get run through erb. Some parameters that will be provided to erb
|
27
|
+
# are:
|
28
|
+
# - server
|
29
|
+
# - branch
|
30
|
+
#
|
31
|
+
# deploy types are:
|
32
|
+
# - script (just some local script to run; should be in git repo?)
|
33
|
+
# - git (a git remote to push to)
|
34
|
+
# - http (some web callback to hit)
|
35
|
+
|
36
|
+
deploy_targets:
|
37
|
+
core:
|
38
|
+
git: 'git@github.com:MyCo/foo.git'
|
39
|
+
deploy:
|
40
|
+
git: 'git@beta.aptible.com:myco-<%=server %>/foo-rails.git'
|
41
|
+
#default_branch: develop - TODO not yet supported
|
42
|
+
web:
|
43
|
+
git: 'git@github.com:MyCo/web.git'
|
44
|
+
|
45
|
+
# Script params: repo_dir, branch, server to deploy to
|
46
|
+
# (repo should already be on the right branch)
|
47
|
+
deploy:
|
48
|
+
script: 'deploy_web.sh'
|
49
|
+
#default_branch: develop - TODO not yet supported
|
50
|
+
|
51
|
+
servers:
|
52
|
+
#- cannoli
|
53
|
+
#- hotdog
|
54
|
+
- burrito
|
55
|
+
- gyro
|
56
|
+
|
data/lib/bub_bot/cli.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
class BubBot::CLI
|
4
|
+
DEFAULT_CONFIG_FILENAME = 'bub_bot.yml'
|
5
|
+
def initialize(args)
|
6
|
+
@args = args
|
7
|
+
|
8
|
+
@filename = args[0] || DEFAULT_CONFIG_FILENAME
|
9
|
+
end
|
10
|
+
|
11
|
+
def start
|
12
|
+
print_usage && return unless check_usage
|
13
|
+
|
14
|
+
configure_from_file(@filename)
|
15
|
+
|
16
|
+
BubBot.start
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def configure_from_file(filename)
|
22
|
+
file_data = YAML.load_file(filename)
|
23
|
+
|
24
|
+
BubBot.configure do |config|
|
25
|
+
BubBot::Configuration::OPTIONS.each do |option_name|
|
26
|
+
config.public_send((option_name.to_s + '=').to_sym, file_data[option_name.to_s])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def check_usage
|
32
|
+
@args.count <= 1
|
33
|
+
end
|
34
|
+
|
35
|
+
def print_usage
|
36
|
+
puts <<USAGE
|
37
|
+
Usage: bub_bot [config_filename]
|
38
|
+
|
39
|
+
Default to #{DEFAULT_CONFIG_FILENAME} if config_filename is not provided
|
40
|
+
USAGE
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# TODO: merge with some defaults
|
2
|
+
class BubBot::Configuration
|
3
|
+
# Any of these options will be passed on to rack, rather than handled by us.
|
4
|
+
RACK_OPTIONS = %i(
|
5
|
+
Port
|
6
|
+
)
|
7
|
+
|
8
|
+
# These options will be handled by us.
|
9
|
+
BUB_BOT_OPTIONS = %i(
|
10
|
+
servers
|
11
|
+
bot_oauth_token
|
12
|
+
bot_name
|
13
|
+
deploy_targets
|
14
|
+
slack_channel
|
15
|
+
redis_host
|
16
|
+
)
|
17
|
+
|
18
|
+
OPTIONS = RACK_OPTIONS + BUB_BOT_OPTIONS
|
19
|
+
|
20
|
+
attr_accessor *OPTIONS
|
21
|
+
|
22
|
+
def rack_options_hash
|
23
|
+
RACK_OPTIONS.each_with_object({}) do |option_name, options_hash|
|
24
|
+
options_hash[option_name] = public_send(option_name)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def verify_options
|
29
|
+
# TODO: verify that deploy_targets, etc, are formatted correctly and print
|
30
|
+
# useful error messages otherwise.
|
31
|
+
true
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'bub_bot/repo'
|
2
|
+
require 'bub_bot/redis_connection'
|
3
|
+
require 'erb'
|
4
|
+
|
5
|
+
class BubBot::DeployManager
|
6
|
+
def deploy(server, target_name, branch)
|
7
|
+
if DeployState[server, target_name].deploying?
|
8
|
+
raise RespondableError.new("A deploy to #{target_name} on #{server} is already in progress. Not deploying.")
|
9
|
+
end
|
10
|
+
|
11
|
+
begin
|
12
|
+
DeployState[server, target_name].set(true)
|
13
|
+
target_config = target(target_name)
|
14
|
+
|
15
|
+
unless deploy_config = target_config['deploy']
|
16
|
+
raise "Missing deploy config for #{target_name}"
|
17
|
+
end
|
18
|
+
|
19
|
+
locals = {
|
20
|
+
server: server
|
21
|
+
}
|
22
|
+
|
23
|
+
# Handle each type of deploy here.
|
24
|
+
# TODO: maybe handle multiple deploys for each target? Right now the
|
25
|
+
# workaround to do that is to just have a script-type deploy that does that.
|
26
|
+
if deploy_git_remote = deploy_config['git']
|
27
|
+
deploy_git_remote = ERB.new(deploy_git_remote).result(get_binding(locals))
|
28
|
+
repo(target_name, server).push(branch, deploy_git_remote)
|
29
|
+
elsif deploy_script = deploy_config['script']
|
30
|
+
puts "xdeploying web script #{deploy_script}"
|
31
|
+
repo = repo(target_name, server)
|
32
|
+
puts "Checking out..."
|
33
|
+
repo.checkout(branch)
|
34
|
+
puts "Pulling..."
|
35
|
+
repo.pull
|
36
|
+
puts "Running script..."
|
37
|
+
success = Kernel.system("./#{deploy_script} #{repo.repo_dir} #{branch} #{server}")
|
38
|
+
puts "Success = #{success}"
|
39
|
+
unless success
|
40
|
+
raise RespondableError.new("Deploy script failed for server #{server} and target #{target_name}")
|
41
|
+
end
|
42
|
+
elsif deploy_url = deploy_config['http']
|
43
|
+
raise RespondableError.new('Sorry, deploys by http request are not supported yet')
|
44
|
+
end
|
45
|
+
|
46
|
+
DeployState[server, target_name].set(false)
|
47
|
+
rescue
|
48
|
+
DeployState[server, target_name].set(false)
|
49
|
+
raise
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def target_names
|
54
|
+
targets.keys - ['all']
|
55
|
+
end
|
56
|
+
|
57
|
+
def branches(target_name)
|
58
|
+
Repo.branches(target(target_name)['git'])
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def repo(target_name, server)
|
64
|
+
@repos ||= {}
|
65
|
+
target = target(target_name)
|
66
|
+
@repos[target_name + '__' + server] ||= Repo.new(target_name, target['git'], server)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns a hash of config data for the target with this name
|
70
|
+
def target(target_name)
|
71
|
+
targets.find { |name, _| name == target_name }.last
|
72
|
+
end
|
73
|
+
|
74
|
+
def targets
|
75
|
+
BubBot.configuration.deploy_targets
|
76
|
+
end
|
77
|
+
|
78
|
+
# Gets a binding object with the given variables defined in it. You'd *think*
|
79
|
+
# there'd be a simpler way. Well, ok, there is, but there's no simpler way that
|
80
|
+
# doesn't *also* polute the binding with variables from the outer scope.
|
81
|
+
def get_binding(variables)
|
82
|
+
obj = Class.new {
|
83
|
+
attr_accessor *variables.keys
|
84
|
+
def get_binding(); binding end
|
85
|
+
}.new
|
86
|
+
variables.each { |name, value| obj.public_send(:"#{name}=", value) }
|
87
|
+
obj.get_binding
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class DeployState
|
92
|
+
ROOT_KEY = 'bub_deploy_status'.freeze
|
93
|
+
|
94
|
+
def self.[](server, target)
|
95
|
+
(@_deploy_states ||= {})[key(server, target)] ||= DeployState.new(server, target)
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.key(server, target)
|
99
|
+
"#{server}__#{target}"
|
100
|
+
end
|
101
|
+
|
102
|
+
def initialize(server, target)
|
103
|
+
@server = server
|
104
|
+
@target = target
|
105
|
+
end
|
106
|
+
|
107
|
+
def key
|
108
|
+
self.class.key(@server, @target)
|
109
|
+
end
|
110
|
+
|
111
|
+
def deploying?
|
112
|
+
deployed_at = redis.hget(ROOT_KEY, key)
|
113
|
+
|
114
|
+
# If we have a super-old deployed_at, assume something went wrong in the
|
115
|
+
# deploy and we failed to capture that.
|
116
|
+
return deployed_at && Time.parse(deployed_at) > 30.minutes.ago
|
117
|
+
end
|
118
|
+
|
119
|
+
def set(is_deploying)
|
120
|
+
puts "set deploying to #{is_deploying} for #{key}"
|
121
|
+
if is_deploying
|
122
|
+
redis.hset(ROOT_KEY, key, Time.now)
|
123
|
+
else
|
124
|
+
redis.hdel(ROOT_KEY, key)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def redis
|
129
|
+
BubBot::RedisConnection.instance
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'singleton'
|
3
|
+
|
4
|
+
# Just a simple wrapper around a redis connection. Call .instance on this, then
|
5
|
+
# call anything you'd normally call on a Redis on object on this instead. Eg
|
6
|
+
# `RedisConnection.instance.hgetall(...)`.
|
7
|
+
class BubBot::RedisConnection
|
8
|
+
include Singleton
|
9
|
+
|
10
|
+
def method_missing(method, *args, &block)
|
11
|
+
redis.respond_to?(method) ? redis.public_send(method, *args, &block) : super
|
12
|
+
end
|
13
|
+
def redis
|
14
|
+
@redis ||= Redis.new(url: 'redis://' + BubBot.configuration.redis_host)
|
15
|
+
end
|
16
|
+
end
|
data/lib/bub_bot/repo.rb
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'git'
|
2
|
+
|
3
|
+
class Repo
|
4
|
+
# When we create a temproary remote to push to, use this name.
|
5
|
+
PUSH_REMOTE_NAME = 'push_remote'
|
6
|
+
|
7
|
+
def initialize(target, origin_remote, server)
|
8
|
+
puts "target = #{target}"
|
9
|
+
puts "remote = #{origin_remote}"
|
10
|
+
puts "server = #{server}"
|
11
|
+
@target = target
|
12
|
+
@origin_remote = origin_remote
|
13
|
+
@server = server
|
14
|
+
end
|
15
|
+
|
16
|
+
# Gets a list of branch names without actually cloning the repo.
|
17
|
+
def self.branches(origin_remote)
|
18
|
+
puts 'repo.branches'
|
19
|
+
# git ls-remote returns a list of lines like this:
|
20
|
+
# ea656e141760b0d8ba49d92506427322120ce945 refs/heads/some-branch-name
|
21
|
+
ls_remote_output = `git ls-remote --heads #{origin_remote}`
|
22
|
+
ls_remote_output.split("\n").map {|line| line.split('/').last }
|
23
|
+
end
|
24
|
+
|
25
|
+
def git
|
26
|
+
return @_git if @_git
|
27
|
+
|
28
|
+
if Dir.exists?(repo_dir) && Dir.exists?(repo_dir + '/.git')
|
29
|
+
puts "Repo exists!"
|
30
|
+
@_git = Git.open(repo_dir)
|
31
|
+
# TODO: handle other errors beside "dir doesn't exist"
|
32
|
+
else
|
33
|
+
puts "Cloning repo"
|
34
|
+
@_git = Git.clone(@origin_remote, repo_dir_name, path: dir, depth: 1, :log => Logger.new(STDOUT))
|
35
|
+
puts 'here'
|
36
|
+
end
|
37
|
+
@_git
|
38
|
+
end
|
39
|
+
|
40
|
+
# Push is async
|
41
|
+
def push(branch, remote, &on_complete)
|
42
|
+
# TODO: handle people pushing while another push is still going, since we
|
43
|
+
# have some shared state in the form of the filesystem
|
44
|
+
|
45
|
+
puts 'Pushing'
|
46
|
+
git.remotes.find{ |remote| remote.name == 'push_remote' }&.remove
|
47
|
+
puts 'Maybe removed old remote'
|
48
|
+
|
49
|
+
git.add_remote('push_remote', remote)
|
50
|
+
puts 'added remote'
|
51
|
+
|
52
|
+
Kernel.system("cd #{repo_dir}; git remote set-branches --add origin '#{branch}'")
|
53
|
+
puts "Pushing #{branch} to #{remote}"
|
54
|
+
|
55
|
+
puts "about to git fetch origin for branch #{branch}"
|
56
|
+
git.fetch('origin', branch: branch)
|
57
|
+
puts 'about to finally push'
|
58
|
+
git.push(remote, "+origin/#{branch}:master")
|
59
|
+
puts 'Finished final push'
|
60
|
+
on_complete.call if on_complete
|
61
|
+
end
|
62
|
+
|
63
|
+
def clean
|
64
|
+
# TODO: make sure there's no weird changes on the git repo
|
65
|
+
git.clean(force: true)
|
66
|
+
git.reset_hard
|
67
|
+
end
|
68
|
+
|
69
|
+
def fetch
|
70
|
+
git.fetch
|
71
|
+
end
|
72
|
+
|
73
|
+
def checkout(branch_name)
|
74
|
+
clean
|
75
|
+
cmd("remote set-branches origin '*'")
|
76
|
+
fetch
|
77
|
+
git.checkout(branch_name)
|
78
|
+
end
|
79
|
+
|
80
|
+
def pull
|
81
|
+
git.pull
|
82
|
+
end
|
83
|
+
|
84
|
+
# We name repo dirs after server + name (eg `burrito__core`). This lets
|
85
|
+
# multiple deploys to the same server (eg deploying both core and web to burrito
|
86
|
+
# at the same time) to work, as well as multiple deploys to the same target
|
87
|
+
# (eg deploying core on both burrito and gyro) to work.
|
88
|
+
#
|
89
|
+
# Note that simultanious deploys to the same target _and_ server (eg two deploys
|
90
|
+
# at once to burrito__core) won't work. Both deploys would use the same git
|
91
|
+
# working directory and step on eachother's toes. That's fine, though, because
|
92
|
+
# they'd _also_ step on eachother's toes in the actual target environment. We
|
93
|
+
# should prevent this case in the UI.
|
94
|
+
def repo_dir
|
95
|
+
"#{dir}/#{repo_dir_name}"
|
96
|
+
end
|
97
|
+
|
98
|
+
def repo_dir_name
|
99
|
+
"#{@server}__#{@target}"
|
100
|
+
end
|
101
|
+
|
102
|
+
def dir
|
103
|
+
"/tmp/bub_repos"
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
# The git library doesn't support everything, so sometimes we run arbitrary commands.
|
109
|
+
def cmd(command)
|
110
|
+
git_command = "git --git-dir=#{repo_dir}/.git --work-tree=#{repo_dir} #{command}"
|
111
|
+
puts "Running #{git_command}"
|
112
|
+
result = Kernel.system(git_command)
|
113
|
+
puts "Result=#{result}"
|
114
|
+
end
|
115
|
+
end
|