git-deploy-ng 0.8.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/CONTRIBUTING.md +35 -0
- data/LICENSE +18 -0
- data/MAINTAINERS.md +15 -0
- data/README.markdown +172 -0
- data/ROADMAP.md +31 -0
- data/bin/git-deploy +3 -0
- data/lib/git_deploy/configuration.rb +95 -0
- data/lib/git_deploy/generator.rb +28 -0
- data/lib/git_deploy/ssh_methods.rb +119 -0
- data/lib/git_deploy/templates/after_push.sh +17 -0
- data/lib/git_deploy/templates/before_restart.rb +44 -0
- data/lib/git_deploy/templates/restart.sh +3 -0
- data/lib/git_deploy.rb +104 -0
- data/lib/hooks/post-receive.sh +55 -0
- data/spec/cli_spec.rb +10 -0
- data/spec/commands_spec.rb +37 -0
- data/spec/configuration_spec.rb +99 -0
- data/spec/generator_spec.rb +17 -0
- data/spec/hooks_spec.rb +18 -0
- data/spec/setup_spec.rb +37 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/ssh_connection_spec.rb +26 -0
- metadata +120 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b9bac8c7043b8147444e762d919dbc943862b999a420b26112edcf8e893fb509
|
|
4
|
+
data.tar.gz: 00e10711fdcdafc3fc8a473f273f051d0eb09b9ee9c84c9db3796c4d1ee71abf
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: aa0e71d63684c7e958e90b118cb3cfe18fce10ec21993a8d84e307e9f74ba806a632647554bdea917e502b24089b5faa59029820e14a36eb8cd31b4c9abdcca4
|
|
7
|
+
data.tar.gz: f81335e3f58ff15a32d7b335acb888df020b7a139dfbaa24ea8cec5c2027c54789ddb201b634f160062f4eb21e189a7a37cde5858454c35390c40d424258653d
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thank you for contributing to git-deploy-ng. This project is a backwards-compatible continuation of [mislav/git-deploy](https://github.com/mislav/git-deploy).
|
|
4
|
+
|
|
5
|
+
## Getting started
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git clone https://github.com/npfedwards/git-deploy.git
|
|
9
|
+
cd git-deploy
|
|
10
|
+
bundle install
|
|
11
|
+
bundle exec rspec
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Pull requests
|
|
15
|
+
|
|
16
|
+
1. Fork the repo and create a feature branch from `master`.
|
|
17
|
+
2. Add or update tests for any behaviour change. We use RSpec and prefer integration-style tests through public interfaces.
|
|
18
|
+
3. Keep the CLI, hook defaults, and deploy callback contract backwards-compatible unless explicitly discussed in an issue.
|
|
19
|
+
4. Open a PR with a clear description of the user-facing behaviour change.
|
|
20
|
+
|
|
21
|
+
## Backwards compatibility
|
|
22
|
+
|
|
23
|
+
v0.8.x must not break existing users migrating from upstream 0.7.0:
|
|
24
|
+
|
|
25
|
+
- Same `git deploy` subcommands and flags
|
|
26
|
+
- Same default `post-receive` hook behaviour (improvements are opt-in via `git deploy hooks`)
|
|
27
|
+
- Existing `deploy/` callback scripts in deployed apps should keep working unchanged
|
|
28
|
+
|
|
29
|
+
## Releases
|
|
30
|
+
|
|
31
|
+
Releases are tagged (`v0.8.0`, etc.) and published to RubyGems as `git-deploy-ng`. Maintainers cut releases from `master` after CI passes.
|
|
32
|
+
|
|
33
|
+
## Internal planning
|
|
34
|
+
|
|
35
|
+
Working notes may live in a local `plans/` directory (gitignored). [ROADMAP.md](ROADMAP.md) lists ideas under consideration — open an issue to discuss priorities.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Copyright (c) 2009 Mislav Marohnić
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
5
|
+
the Software without restriction, including without limitation the rights to
|
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
7
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
8
|
+
subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
15
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
16
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/MAINTAINERS.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Maintainers
|
|
2
|
+
|
|
3
|
+
git-deploy-ng is a community continuation of [mislav/git-deploy](https://github.com/mislav/git-deploy).
|
|
4
|
+
|
|
5
|
+
## Original author
|
|
6
|
+
|
|
7
|
+
- **Mislav Marohnić** — created git-deploy and maintained it through v0.7.0
|
|
8
|
+
|
|
9
|
+
## Current maintainers
|
|
10
|
+
|
|
11
|
+
- **Nathan Edwards** ([@npfedwards](https://github.com/npfedwards)) — fork maintainer
|
|
12
|
+
|
|
13
|
+
## License
|
|
14
|
+
|
|
15
|
+
MIT — see [LICENSE](LICENSE).
|
data/README.markdown
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Easy git deployment
|
|
2
|
+
===================
|
|
3
|
+
|
|
4
|
+
Community continuation of [mislav/git-deploy](https://github.com/mislav/git-deploy), published as **git-deploy-ng**. The CLI is unchanged: `git deploy <command>`.
|
|
5
|
+
|
|
6
|
+
Straightforward, [Heroku][]-style, push-based deployment. Your deploys can become as simple as this:
|
|
7
|
+
|
|
8
|
+
$ git push production main
|
|
9
|
+
|
|
10
|
+
To get started, install the gem on the machine that runs setup (once per project):
|
|
11
|
+
|
|
12
|
+
gem install git-deploy-ng
|
|
13
|
+
|
|
14
|
+
Only the person who is setting up deployment for the first time needs to install
|
|
15
|
+
the gem. You don't have to add it to your project's Gemfile. Production servers
|
|
16
|
+
do not need Ruby — only bash and git.
|
|
17
|
+
|
|
18
|
+
See [ROADMAP.md](ROADMAP.md) for ideas under consideration and [CONTRIBUTING.md](CONTRIBUTING.md) to contribute.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
Migrating from mislav/git-deploy
|
|
22
|
+
--------------------------------
|
|
23
|
+
|
|
24
|
+
Requires **Ruby 2.7+** on the setup machine (including Ruby 4.x; system Ruby 2.6 on macOS is not supported).
|
|
25
|
+
|
|
26
|
+
1. `gem uninstall git-deploy` (optional)
|
|
27
|
+
2. `gem install git-deploy-ng`
|
|
28
|
+
3. `git deploy hooks -r production` (optional, per host — refreshes remote hooks)
|
|
29
|
+
|
|
30
|
+
Existing `deploy/` callback scripts in your apps do not need to change.
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
Which app languages/frameworks are supported?
|
|
34
|
+
---------------------------------------------
|
|
35
|
+
|
|
36
|
+
Regardless of the fact that this tool is mostly written in Ruby, git-deploy can be useful for any kind of code that needs deploying on a remote server. The default scripts are suited for Ruby web apps, but can be edited to accommodate other frameworks.
|
|
37
|
+
|
|
38
|
+
Your deployment is customized with per-project callback scripts which can be written in any language.
|
|
39
|
+
|
|
40
|
+
The assumption is that you're deploying to a single host to which you connect over SSH using public/private key authentication.
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
Initial setup
|
|
44
|
+
-------------
|
|
45
|
+
|
|
46
|
+
1. Create a git remote for where you'll push the code on your server. The name of this remote in the examples is "production", but it can be whatever you wish ("online", "website", or other).
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
git remote add production "user@example.com:/apps/mynewapp"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`/apps/mynewapp` is the directory where you want your code to reside on the
|
|
53
|
+
remote server. If the directory doesn't exist, the next step creates it.
|
|
54
|
+
|
|
55
|
+
2. Run the setup task:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
git deploy setup -r "production"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This will initialize the remote git repository in the deploy directory
|
|
62
|
+
(`/apps/mynewapp` in the above example) and install the remote git hook.
|
|
63
|
+
|
|
64
|
+
3. Run the init task:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
git deploy init
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
This generates default deploy callback scripts in the `deploy/` directory.
|
|
71
|
+
You should check them in git because they are going to be executed on the
|
|
72
|
+
server during each deploy.
|
|
73
|
+
|
|
74
|
+
4. Push the code.
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
git push production main
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Use whichever branch is checked out locally; `git deploy setup` configures the remote repo to match your current branch (`main` or `master`).
|
|
81
|
+
|
|
82
|
+
5. Login to your server and manually perform necessary one-time administrative operations. This might include:
|
|
83
|
+
* set up the Apache/nginx virtual host for this application;
|
|
84
|
+
* check your `config/database.yml` and create the production database.
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
Everyday deployments
|
|
88
|
+
--------------------
|
|
89
|
+
|
|
90
|
+
If you've set your app correctly, visiting <http://example.com> in your browser
|
|
91
|
+
should show it up and running.
|
|
92
|
+
|
|
93
|
+
Now, subsequent deployments are done simply **by pushing to the branch that is
|
|
94
|
+
currently checked out on the remote**:
|
|
95
|
+
|
|
96
|
+
git push production main
|
|
97
|
+
|
|
98
|
+
Because the deployments are performed with git, nobody else on the team needs to
|
|
99
|
+
install the git-deploy gem.
|
|
100
|
+
|
|
101
|
+
On every deploy, the default `deploy/after_push` script performs the following:
|
|
102
|
+
|
|
103
|
+
1. updates git submodules (if there are any);
|
|
104
|
+
2. runs `bundle install --deployment` if there is a Gemfile;
|
|
105
|
+
3. runs `rake db:migrate` if new migrations have been added;
|
|
106
|
+
4. clears cached CSS/JS assets in "public/stylesheets" and "public/javascripts";
|
|
107
|
+
5. restarts the web application.
|
|
108
|
+
|
|
109
|
+
You can customize all this by editing generated scripts in the `deploy/`
|
|
110
|
+
directory of your app.
|
|
111
|
+
|
|
112
|
+
Deployments are logged to `log/deploy.log` in your application's directory.
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
How it works
|
|
116
|
+
------------
|
|
117
|
+
|
|
118
|
+
The `git deploy setup` command installed a `post-receive` git hook in the remote
|
|
119
|
+
repository. This is how your code on the server is kept up to date. This script
|
|
120
|
+
checks out the latest version of your project from the current branch and
|
|
121
|
+
runs the following callback scripts:
|
|
122
|
+
|
|
123
|
+
* `deploy/setup` - on first push.
|
|
124
|
+
* `deploy/after_push` - on subsequent pushes. It in turn executes:
|
|
125
|
+
* `deploy/before_restart`
|
|
126
|
+
* `deploy/restart`
|
|
127
|
+
* `deploy/after_restart`
|
|
128
|
+
* `deploy/rollback` - executed for `git deploy rollback`.
|
|
129
|
+
|
|
130
|
+
All of the callbacks are optional. These scripts are ordinary Unix executables.
|
|
131
|
+
The ones which get generated for you by `git deploy init` are written in shell
|
|
132
|
+
script and Ruby.
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
Extra commands
|
|
136
|
+
--------------
|
|
137
|
+
|
|
138
|
+
* `git deploy hooks` - Updates git hooks on the remote repository
|
|
139
|
+
|
|
140
|
+
* `git deploy log [N=20]` - Shows last 20 lines of deploy log on the server
|
|
141
|
+
|
|
142
|
+
* `git deploy rerun` - Re-runs the `deploy/after_push` callback as if a git push happened
|
|
143
|
+
|
|
144
|
+
* `git deploy restart` - Runs the `deploy/restart` callback
|
|
145
|
+
|
|
146
|
+
* `git deploy rollback` - Undo a deploy by checking out the previous revision,
|
|
147
|
+
runs `deploy/rollback` if exists instead of `deploy/after_push`
|
|
148
|
+
|
|
149
|
+
* `git deploy upload <files>` - Copy local files to the remote app
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
Troubleshooting
|
|
153
|
+
---------------
|
|
154
|
+
|
|
155
|
+
### `unsupported key type ssh-ed25519`
|
|
156
|
+
|
|
157
|
+
Modern OpenSSH keys (ed25519) require optional gems that net-ssh does not bundle:
|
|
158
|
+
|
|
159
|
+
```sh
|
|
160
|
+
gem install ed25519 bcrypt_pbkdf
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
git-deploy-ng will print this command if the gems are missing when connecting.
|
|
164
|
+
|
|
165
|
+
### Still running upstream `git-deploy` 0.7.0?
|
|
166
|
+
|
|
167
|
+
The original gem targets older Ruby and net-ssh versions. Migrate to `git-deploy-ng`
|
|
168
|
+
on Ruby 2.7+ — see [Migrating from mislav/git-deploy](#migrating-from-mislavgit-deploy).
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
[heroku]: http://heroku.com/
|
data/ROADMAP.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Roadmap
|
|
2
|
+
|
|
3
|
+
Tracking for git-deploy-ng. **Nothing listed here is a commitment** — items may be reprioritised, dropped, or implemented differently. Any change that affects default behaviour will be called out explicitly in release notes.
|
|
4
|
+
|
|
5
|
+
## v0.8.0 (in progress)
|
|
6
|
+
|
|
7
|
+
Scoped fixes for this release:
|
|
8
|
+
|
|
9
|
+
| Fix | Upstream context |
|
|
10
|
+
|-----|------------------|
|
|
11
|
+
| Ruby 3.x compatibility | [#88](https://github.com/mislav/git-deploy/issues/88), [#67](https://github.com/mislav/git-deploy/issues/67) |
|
|
12
|
+
| Dynamic default branch (`main` / `master`) | Hardcoded `master` in setup |
|
|
13
|
+
| Dependency upgrades (Thor, net-ssh, net-scp) | OpenSSL / Ruby 3.2+ SSH failures |
|
|
14
|
+
| Modern CI (GitHub Actions, Ruby 2.7–4.0) | Travis CI retired |
|
|
15
|
+
| Publish as `git-deploy-ng` on RubyGems | Avoids namespace conflict with upstream gem |
|
|
16
|
+
|
|
17
|
+
## Ideas under consideration (post-v0.8.0)
|
|
18
|
+
|
|
19
|
+
Sourced from upstream open issues and community discussion. If something here interests you, open an issue — priorities are not fixed.
|
|
20
|
+
|
|
21
|
+
| Idea | Source | Notes |
|
|
22
|
+
|------|--------|-------|
|
|
23
|
+
| Multi-server deploy | [#89](https://github.com/mislav/git-deploy/issues/89) | Single push triggers deploy on N hosts |
|
|
24
|
+
| Multiple environments | [#71](https://github.com/mislav/git-deploy/issues/71) | `staging` / `production` config profiles |
|
|
25
|
+
| Custom deploy script directory | [#75](https://github.com/mislav/git-deploy/issues/75) | Not hardcoded to `deploy/` |
|
|
26
|
+
| Rails 6+ default templates | [#92](https://github.com/mislav/git-deploy/issues/92) | Zeitwerk, credentials, modern asset pipeline |
|
|
27
|
+
| Improved PATH in hooks | [#68](https://github.com/mislav/git-deploy/issues/68) | rbenv/nvm/pyenv in non-login hook context |
|
|
28
|
+
| Windows client support | [#80](https://github.com/mislav/git-deploy/issues/80) | May remain unsupported; needs discussion |
|
|
29
|
+
| Homebrew distribution | — | `brew install` for macOS setup machines |
|
|
30
|
+
| Download remote files | [#83](https://github.com/mislav/git-deploy/issues/83) | `git deploy download` — inverse of `upload` |
|
|
31
|
+
| Hook dry-run | — | `git deploy rerun --noop` on server |
|
data/bin/git-deploy
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
require 'uri'
|
|
2
|
+
require 'cgi'
|
|
3
|
+
require 'forwardable'
|
|
4
|
+
|
|
5
|
+
class GitDeploy
|
|
6
|
+
module Configuration
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
extend Forwardable
|
|
10
|
+
def_delegator :remote_url, :host
|
|
11
|
+
def_delegator :remote_url, :port, :remote_port
|
|
12
|
+
|
|
13
|
+
def deploy_to
|
|
14
|
+
@deploy_to ||= begin
|
|
15
|
+
if remote_url.path.start_with? '/~/'
|
|
16
|
+
remote_url.path[1..-1]
|
|
17
|
+
else
|
|
18
|
+
remote_url.path
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def remote_user
|
|
24
|
+
@user ||= begin
|
|
25
|
+
user = remote_url.user
|
|
26
|
+
user ? CGI.unescape(user) : `whoami`.chomp
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def branch
|
|
31
|
+
@branch ||= begin
|
|
32
|
+
ref = current_branch
|
|
33
|
+
ref && !ref.empty? ? normalize_branch(ref) : 'master'
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def git_config
|
|
38
|
+
@git_config ||= Hash.new do |cache, cmd|
|
|
39
|
+
git = ENV['GIT'] || 'git'
|
|
40
|
+
out = `#{git} #{cmd}`
|
|
41
|
+
if $?.success? then cache[cmd] = out.chomp
|
|
42
|
+
else cache[cmd] = nil
|
|
43
|
+
end
|
|
44
|
+
cache[cmd]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def remote_urls(remote)
|
|
49
|
+
git_config["remote -v"].to_s.split("\n").
|
|
50
|
+
select {|l| l =~ /^#{remote}\t.+/ }.
|
|
51
|
+
map {|l| l.split("\t")[1].sub(/\s+\(.+?\)$/, '') }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def remote_url(remote = options[:remote])
|
|
55
|
+
@remote_url ||= {}
|
|
56
|
+
@remote_url[remote] ||= begin
|
|
57
|
+
url = remote_urls(remote).first
|
|
58
|
+
if url.nil?
|
|
59
|
+
abort "Error: Remote url not found for remote #{remote.inspect}"
|
|
60
|
+
elsif url =~ /(^|@)github\.com\b/
|
|
61
|
+
abort "Error: Remote url for #{remote.inspect} points to GitHub. Can't deploy there!"
|
|
62
|
+
else
|
|
63
|
+
url = 'ssh://' + url.sub(%r{:/?}, '/') unless url =~ %r{^[\w-]+://}
|
|
64
|
+
begin
|
|
65
|
+
url = URI.parse url
|
|
66
|
+
rescue
|
|
67
|
+
abort "Error parsing remote url #{url}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
url
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def current_branch
|
|
75
|
+
git_config['symbolic-ref -q HEAD']
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def tracked_branch
|
|
79
|
+
branch = current_branch && tracked_for(current_branch)
|
|
80
|
+
normalize_branch(branch) if branch
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def normalize_branch(branch)
|
|
84
|
+
branch.sub('refs/heads/', '')
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def remote_for(branch)
|
|
88
|
+
git_config['config branch.%s.remote' % normalize_branch(branch)]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def tracked_for(branch)
|
|
92
|
+
git_config['config branch.%s.merge' % normalize_branch(branch)]
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require 'thor/group'
|
|
2
|
+
|
|
3
|
+
class GitDeploy::Generator < Thor::Group
|
|
4
|
+
include Thor::Actions
|
|
5
|
+
|
|
6
|
+
def self.source_root
|
|
7
|
+
File.expand_path('../templates', __FILE__)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def copy_main_hook
|
|
11
|
+
copy_hook 'after_push.sh', 'deploy/after_push'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def copy_restart_hook
|
|
15
|
+
copy_hook 'restart.sh', 'deploy/restart'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def copy_restart_callbacks
|
|
19
|
+
copy_hook 'before_restart.rb', 'deploy/before_restart'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def copy_hook(template, destination)
|
|
25
|
+
copy_file template, destination
|
|
26
|
+
chmod destination, 0744 unless File.executable? destination
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
class GitDeploy
|
|
2
|
+
module SSHMethods
|
|
3
|
+
private
|
|
4
|
+
|
|
5
|
+
def sudo_cmd
|
|
6
|
+
"sudo -p 'sudo password: '"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def system(*args)
|
|
10
|
+
puts "[local] $ " + args.join(' ').gsub(' && ', " && \\\n ")
|
|
11
|
+
super unless options.noop?
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def run(cmd = nil, opt = {})
|
|
15
|
+
cmd = yield(cmd) if block_given?
|
|
16
|
+
cmd = cmd.join(' && ') if Array === cmd
|
|
17
|
+
|
|
18
|
+
if opt.fetch(:echo, true)
|
|
19
|
+
puts "[#{options[:remote]}] $ " + cmd.gsub(' && ', " && \\\n ")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
unless options.noop?
|
|
23
|
+
status, output = ssh_exec cmd do |ch, stream, data|
|
|
24
|
+
case stream
|
|
25
|
+
when :stdout then $stdout.print data
|
|
26
|
+
when :stderr then $stderr.print data
|
|
27
|
+
end
|
|
28
|
+
ch.send_data(askpass) if data =~ /^sudo password: /
|
|
29
|
+
end
|
|
30
|
+
output
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def run_test(cmd)
|
|
35
|
+
status, output = ssh_exec(cmd) { }
|
|
36
|
+
status == 0
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def ssh_exec(cmd, &block)
|
|
40
|
+
status = nil
|
|
41
|
+
output = ''
|
|
42
|
+
|
|
43
|
+
channel = ssh_connection.open_channel do |chan|
|
|
44
|
+
chan.exec(cmd) do |ch, success|
|
|
45
|
+
raise "command failed: #{cmd.inspect}" unless success
|
|
46
|
+
# ch.request_pty
|
|
47
|
+
|
|
48
|
+
ch.on_data do |c, data|
|
|
49
|
+
output << data
|
|
50
|
+
yield(c, :stdout, data)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
ch.on_extended_data do |c, type, data|
|
|
54
|
+
output << data
|
|
55
|
+
yield(c, :stderr, data)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
ch.on_request "exit-status" do |ch, data|
|
|
59
|
+
status = data.read_long
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
channel.wait
|
|
65
|
+
[status, output]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# TODO: use Highline for cross-platform support
|
|
69
|
+
def askpass
|
|
70
|
+
tty_state = `stty -g`
|
|
71
|
+
system 'stty raw -echo -icanon isig' if $?.success?
|
|
72
|
+
pass = ''
|
|
73
|
+
while char = $stdin.getbyte and not (char == 13 or char == 10)
|
|
74
|
+
if char == 127 or char == 8
|
|
75
|
+
pass[-1,1] = '' unless pass.empty?
|
|
76
|
+
else
|
|
77
|
+
pass << char.chr
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
pass
|
|
81
|
+
ensure
|
|
82
|
+
system "stty #{tty_state}" unless tty_state.empty?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def scp_upload(files)
|
|
86
|
+
channels = []
|
|
87
|
+
files.each do |local, remote|
|
|
88
|
+
puts "FILE: [local] #{local.sub(LOCAL_DIR + '/', '')} -> [#{options[:remote]}] #{remote}"
|
|
89
|
+
channels << ssh_connection.scp.upload(local, remote) unless options.noop?
|
|
90
|
+
end
|
|
91
|
+
channels.each { |c| c.wait }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def ssh_connection
|
|
95
|
+
@ssh ||= begin
|
|
96
|
+
ssh = Net::SSH.start(host, remote_user, :port => remote_port || 22)
|
|
97
|
+
at_exit { ssh.close }
|
|
98
|
+
ssh
|
|
99
|
+
end
|
|
100
|
+
rescue NotImplementedError, Gem::MissingSpecError => e
|
|
101
|
+
abort ed25519_ssh_help if ed25519_ssh_error?(e)
|
|
102
|
+
raise
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def ed25519_ssh_error?(error)
|
|
106
|
+
error.message.downcase.include?('ed25519')
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def ed25519_ssh_help
|
|
110
|
+
<<~HELP
|
|
111
|
+
Error: Your SSH key requires ed25519 support, but the optional gems are not installed.
|
|
112
|
+
|
|
113
|
+
gem install ed25519 bcrypt_pbkdf
|
|
114
|
+
|
|
115
|
+
See https://github.com/net-ssh/net-ssh/issues/565
|
|
116
|
+
HELP
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -e
|
|
3
|
+
oldrev=$1
|
|
4
|
+
newrev=$2
|
|
5
|
+
|
|
6
|
+
run() {
|
|
7
|
+
[ -x $1 ] && $1 $oldrev $newrev
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
echo files changed: $(git diff $oldrev $newrev --diff-filter=ACDMR --name-only | wc -l)
|
|
11
|
+
|
|
12
|
+
umask 002
|
|
13
|
+
|
|
14
|
+
git submodule sync && git submodule update --init --recursive
|
|
15
|
+
|
|
16
|
+
run deploy/before_restart
|
|
17
|
+
run deploy/restart && run deploy/after_restart
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
oldrev, newrev = ARGV
|
|
3
|
+
|
|
4
|
+
def run(cmd)
|
|
5
|
+
exit($?.exitstatus) unless system "umask 002 && #{cmd}"
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
RAILS_ENV = ENV['RAILS_ENV'] || 'production'
|
|
9
|
+
use_bundler = File.file? 'Gemfile'
|
|
10
|
+
rake_cmd = use_bundler ? 'bundle exec rake' : 'rake'
|
|
11
|
+
|
|
12
|
+
if use_bundler
|
|
13
|
+
bundler_args = ['--deployment']
|
|
14
|
+
BUNDLE_WITHOUT = ENV['BUNDLE_WITHOUT'] || 'development:test'
|
|
15
|
+
bundler_args << '--without' << BUNDLE_WITHOUT unless BUNDLE_WITHOUT.empty?
|
|
16
|
+
|
|
17
|
+
# update gem bundle
|
|
18
|
+
run "bundle install #{bundler_args.join(' ')}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
if File.file? 'Rakefile'
|
|
22
|
+
tasks = []
|
|
23
|
+
|
|
24
|
+
if File.exist?('db/migrate')
|
|
25
|
+
num_migrations = `git diff #{oldrev} #{newrev} --diff-filter=A --name-only -z -- db/migrate`.split("\0").size
|
|
26
|
+
else
|
|
27
|
+
num_migrations = 0
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# run migrations if new ones have been added
|
|
31
|
+
tasks << "db:migrate" if num_migrations > 0
|
|
32
|
+
|
|
33
|
+
# precompile assets
|
|
34
|
+
changed_assets = `git diff #{oldrev} #{newrev} --name-only -z -- app/assets`.split("\0")
|
|
35
|
+
tasks << "assets:precompile" if changed_assets.size > 0
|
|
36
|
+
|
|
37
|
+
run "#{rake_cmd} #{tasks.join(' ')} RAILS_ENV=#{RAILS_ENV}" if tasks.any?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# clear cached assets (unversioned/ignored files)
|
|
41
|
+
run "git clean -x -f -- public/stylesheets public/javascripts"
|
|
42
|
+
|
|
43
|
+
# clean unversioned files from vendor/plugins (e.g. old submodules)
|
|
44
|
+
run "git clean -d -f -- vendor/plugins"
|
data/lib/git_deploy.rb
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
require 'thor'
|
|
2
|
+
require 'net/ssh'
|
|
3
|
+
require 'net/scp'
|
|
4
|
+
|
|
5
|
+
class GitDeploy < Thor
|
|
6
|
+
LOCAL_DIR = File.expand_path('..', __FILE__)
|
|
7
|
+
|
|
8
|
+
require 'git_deploy/configuration'
|
|
9
|
+
require 'git_deploy/ssh_methods'
|
|
10
|
+
include Configuration
|
|
11
|
+
include SSHMethods
|
|
12
|
+
|
|
13
|
+
class_option :remote, :aliases => '-r', :type => :string, :default => 'origin'
|
|
14
|
+
class_option :noop, :aliases => '-n', :type => :boolean, :default => false
|
|
15
|
+
|
|
16
|
+
desc "init", "Generates deployment customization scripts for your app"
|
|
17
|
+
def init
|
|
18
|
+
require 'git_deploy/generator'
|
|
19
|
+
Generator::start([])
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
desc "setup", "Create the remote git repository and install push hooks for it"
|
|
23
|
+
method_option :shared, :aliases => '-g', :type => :boolean, :default => false
|
|
24
|
+
method_option :sudo, :aliases => '-s', :type => :boolean, :default => false
|
|
25
|
+
def setup
|
|
26
|
+
sudo = options.sudo? ? "#{sudo_cmd} " : ''
|
|
27
|
+
|
|
28
|
+
unless run_test("test -x #{deploy_to}")
|
|
29
|
+
run ["#{sudo}mkdir -p #{deploy_to}"] do |cmd|
|
|
30
|
+
cmd << "#{sudo}chown $USER #{deploy_to}" if options.sudo?
|
|
31
|
+
cmd
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
run [] do |cmd|
|
|
36
|
+
cmd << "chmod g+ws #{deploy_to}" if options.shared?
|
|
37
|
+
cmd << "cd #{deploy_to}"
|
|
38
|
+
cmd << "git init #{options.shared? ? '--shared' : ''}"
|
|
39
|
+
cmd << "sed -i'' -e 's/master/#{branch}/' .git/HEAD" unless branch == 'master'
|
|
40
|
+
cmd << "git config --bool receive.denyNonFastForwards false" if options.shared?
|
|
41
|
+
cmd << "git config receive.denyCurrentBranch ignore"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
invoke :hooks
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
desc "hooks", "Installs git hooks to the remote repository"
|
|
48
|
+
def hooks
|
|
49
|
+
hooks_dir = File.join(LOCAL_DIR, 'hooks')
|
|
50
|
+
remote_dir = "#{deploy_to}/.git/hooks"
|
|
51
|
+
|
|
52
|
+
scp_upload "#{hooks_dir}/post-receive.sh" => "#{remote_dir}/post-receive"
|
|
53
|
+
run "chmod +x #{remote_dir}/post-receive"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
desc "restart", "Restarts the application on the server"
|
|
57
|
+
def restart
|
|
58
|
+
run "cd #{deploy_to} && deploy/restart 2>&1 | tee -a log/deploy.log"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
desc "rerun", "Runs the `deploy/after_push' callback as if a new revision was pushed via git"
|
|
62
|
+
def rerun
|
|
63
|
+
run <<-BASH, :echo => false
|
|
64
|
+
bash -e -c '
|
|
65
|
+
cd '#{deploy_to}'
|
|
66
|
+
declare -a revs=( $(git rev-parse HEAD@{1} HEAD) )
|
|
67
|
+
deploy/after_push ${revs[@]} 2>&1 | tee -a log/deploy.log
|
|
68
|
+
'
|
|
69
|
+
BASH
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
desc "rollback", "Rolls back the checkout to before the last push"
|
|
73
|
+
def rollback
|
|
74
|
+
run <<-BASH, :echo => false
|
|
75
|
+
bash -e -c '
|
|
76
|
+
cd '#{deploy_to}'
|
|
77
|
+
declare -a revs=( $(git rev-parse HEAD HEAD@{1}) )
|
|
78
|
+
git reset --hard ${revs[1]}
|
|
79
|
+
callback=after_push
|
|
80
|
+
[ -x deploy/rollback ] && callback=rollback
|
|
81
|
+
deploy/$callback ${revs[@]} 2>&1 | tee -a log/deploy.log
|
|
82
|
+
'
|
|
83
|
+
BASH
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
desc "log", "Shows the last part of the deploy log on the server"
|
|
87
|
+
method_option :tail, :aliases => '-t', :type => :boolean, :default => false
|
|
88
|
+
method_option :lines, :aliases => '-l', :type => :numeric, :default => 20
|
|
89
|
+
def log(n = nil)
|
|
90
|
+
tail_args = options.tail? ? '-f' : "-n#{n || options.lines}"
|
|
91
|
+
run "tail #{tail_args} #{deploy_to}/log/deploy.log"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
desc "upload <files>", "Copy local files to the remote app"
|
|
95
|
+
def upload(*files)
|
|
96
|
+
files = files.map { |f| Dir[f.strip] }.flatten
|
|
97
|
+
abort "Error: Specify at least one file to upload" if files.empty?
|
|
98
|
+
|
|
99
|
+
scp_upload files.inject({}) { |all, file|
|
|
100
|
+
all[file] = File.join(deploy_to, file)
|
|
101
|
+
all
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
if [ "$GIT_DIR" = "." ]; then
|
|
5
|
+
# The script has been called as a hook; chdir to the working copy
|
|
6
|
+
cd ..
|
|
7
|
+
unset GIT_DIR
|
|
8
|
+
fi
|
|
9
|
+
|
|
10
|
+
# try to obtain the usual system PATH
|
|
11
|
+
if [ -f /etc/profile ]; then
|
|
12
|
+
PATH=$(source /etc/profile; echo $PATH)
|
|
13
|
+
export PATH
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# get the current branch
|
|
17
|
+
head="$(git symbolic-ref HEAD)"
|
|
18
|
+
|
|
19
|
+
# read the STDIN to detect if this push changed the current branch
|
|
20
|
+
while read oldrev newrev refname
|
|
21
|
+
do
|
|
22
|
+
[ "$refname" = "$head" ] && break
|
|
23
|
+
done
|
|
24
|
+
|
|
25
|
+
# abort if there's no update, or in case the branch is deleted
|
|
26
|
+
if [ -z "${newrev//0}" ]; then
|
|
27
|
+
exit
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# check out the latest code into the working copy
|
|
31
|
+
umask 002
|
|
32
|
+
git reset --hard
|
|
33
|
+
|
|
34
|
+
logfile=log/deploy.log
|
|
35
|
+
restart=tmp/restart.txt
|
|
36
|
+
|
|
37
|
+
if [ -z "${oldrev//0}" ]; then
|
|
38
|
+
# this is the first push; this branch was just created
|
|
39
|
+
mkdir -p log tmp
|
|
40
|
+
chmod 0775 log tmp
|
|
41
|
+
touch $logfile $restart
|
|
42
|
+
chmod 0664 $logfile $restart
|
|
43
|
+
|
|
44
|
+
# init submodules
|
|
45
|
+
git submodule update --recursive --init 2>&1 | tee -a $logfile
|
|
46
|
+
|
|
47
|
+
# execute the one-time setup hook
|
|
48
|
+
[ -x deploy/setup ] && deploy/setup $oldrev $newrev 2>&1 | tee -a $logfile
|
|
49
|
+
else
|
|
50
|
+
# log timestamp
|
|
51
|
+
echo ==== $(date) ==== >> $logfile
|
|
52
|
+
|
|
53
|
+
# execute the main deploy hook
|
|
54
|
+
[ -x deploy/after_push ] && deploy/after_push $oldrev $newrev 2>&1 | tee -a $logfile
|
|
55
|
+
fi
|
data/spec/cli_spec.rb
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe GitDeploy do
|
|
4
|
+
it 'exposes the upstream CLI commands' do
|
|
5
|
+
commands = described_class.all_commands.keys
|
|
6
|
+
expect(commands).to include(
|
|
7
|
+
'init', 'setup', 'hooks', 'restart', 'rerun', 'rollback', 'log', 'upload'
|
|
8
|
+
)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'tmpdir'
|
|
3
|
+
|
|
4
|
+
describe GitDeploy do
|
|
5
|
+
describe '#upload' do
|
|
6
|
+
it 'copies local files to the remote app directory' do
|
|
7
|
+
instance = described_class.new([], remote: 'production', noop: true)
|
|
8
|
+
uploads = {}
|
|
9
|
+
|
|
10
|
+
allow(instance).to receive(:scp_upload) { |files| uploads.merge!(files) }
|
|
11
|
+
instance.send(:git_config)['remote -v'] = "production\tgit@example.com:/apps/demo (fetch)"
|
|
12
|
+
|
|
13
|
+
Dir.mktmpdir do |dir|
|
|
14
|
+
file = File.join(dir, 'config.yml')
|
|
15
|
+
File.write(file, 'test')
|
|
16
|
+
|
|
17
|
+
instance.upload(file)
|
|
18
|
+
|
|
19
|
+
expect(uploads[file]).to eq(File.join('/apps/demo', file))
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe '#restart' do
|
|
25
|
+
it 'runs the deploy restart script on the server' do
|
|
26
|
+
instance = described_class.new([], remote: 'production', noop: true)
|
|
27
|
+
commands = []
|
|
28
|
+
|
|
29
|
+
allow(instance).to receive(:run) { |cmd| commands << cmd }
|
|
30
|
+
instance.send(:git_config)['remote -v'] = "production\tgit@example.com:/apps/demo (fetch)"
|
|
31
|
+
|
|
32
|
+
instance.restart
|
|
33
|
+
|
|
34
|
+
expect(commands.first).to include('deploy/restart')
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe GitDeploy::Configuration do
|
|
4
|
+
|
|
5
|
+
subject {
|
|
6
|
+
mod = described_class
|
|
7
|
+
obj = Object.new
|
|
8
|
+
opt = options
|
|
9
|
+
(class << obj; self; end).class_eval do
|
|
10
|
+
include mod
|
|
11
|
+
mod.private_instance_methods.each {|m| public m }
|
|
12
|
+
define_method(:options) { opt }
|
|
13
|
+
end
|
|
14
|
+
obj
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let(:options) { {:remote => 'production'} }
|
|
18
|
+
|
|
19
|
+
def stub_git_config(cmd, value)
|
|
20
|
+
subject.git_config[cmd] = value
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def stub_remote_url(url, remote = options[:remote])
|
|
24
|
+
stub_git_config("remote -v", "#{remote}\t#{url} (fetch)")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
describe '#branch' do
|
|
28
|
+
it 'uses the current branch when HEAD is on main' do
|
|
29
|
+
stub_git_config('symbolic-ref -q HEAD', 'refs/heads/main')
|
|
30
|
+
expect(subject.branch).to eq('main')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'uses master when no symbolic ref is available' do
|
|
34
|
+
stub_git_config('symbolic-ref -q HEAD', nil)
|
|
35
|
+
expect(subject.branch).to eq('master')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'uses master when on the master branch' do
|
|
39
|
+
stub_git_config('symbolic-ref -q HEAD', 'refs/heads/master')
|
|
40
|
+
expect(subject.branch).to eq('master')
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe '#tracked_branch' do
|
|
45
|
+
it 'returns the branch name for the current HEAD' do
|
|
46
|
+
stub_git_config('symbolic-ref -q HEAD', 'refs/heads/main')
|
|
47
|
+
stub_git_config('config branch.main.merge', 'refs/heads/main')
|
|
48
|
+
expect(subject.tracked_branch).to eq('main')
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
describe '#remote_for' do
|
|
53
|
+
it 'returns the remote tracking a branch' do
|
|
54
|
+
stub_git_config('config branch.main.remote', 'production')
|
|
55
|
+
expect(subject.remote_for('refs/heads/main')).to eq('production')
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
describe "extracting user/host from remote url" do
|
|
60
|
+
context "ssh url" do
|
|
61
|
+
before { stub_remote_url 'ssh://jon%20doe@example.com:88/path/to/app' }
|
|
62
|
+
|
|
63
|
+
it { expect(subject.host).to eq('example.com') }
|
|
64
|
+
it { expect(subject.remote_port).to eq(88) }
|
|
65
|
+
it { expect(subject.remote_user).to eq('jon doe') }
|
|
66
|
+
it { expect(subject.deploy_to).to eq('/path/to/app') }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
context "scp-style" do
|
|
70
|
+
before { stub_remote_url 'git@example.com:/path/to/app' }
|
|
71
|
+
|
|
72
|
+
it { expect(subject.host).to eq('example.com') }
|
|
73
|
+
it { expect(subject.remote_port).to be_nil }
|
|
74
|
+
it { expect(subject.remote_user).to eq('git') }
|
|
75
|
+
it { expect(subject.deploy_to).to eq('/path/to/app') }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
context "scp-style with home" do
|
|
79
|
+
before { stub_remote_url 'git@example.com:~/path/to/app' }
|
|
80
|
+
|
|
81
|
+
it { expect(subject.host).to eq('example.com') }
|
|
82
|
+
it { expect(subject.remote_port).to be_nil }
|
|
83
|
+
it { expect(subject.remote_user).to eq('git') }
|
|
84
|
+
it { expect(subject.deploy_to).to eq('~/path/to/app') }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
context "pushurl only" do
|
|
88
|
+
before {
|
|
89
|
+
remote = options.fetch(:remote)
|
|
90
|
+
url = 'git@example.com:/path/to/app'
|
|
91
|
+
stub_git_config("remote -v", "#{remote}\t\n#{remote}\t#{url} (push)")
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
it { expect(subject.host).to eq('example.com') }
|
|
95
|
+
it { expect(subject.remote_user).to eq('git') }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'tmpdir'
|
|
3
|
+
require 'git_deploy/generator'
|
|
4
|
+
|
|
5
|
+
describe GitDeploy::Generator do
|
|
6
|
+
it 'generates deploy callback scripts in the current directory' do
|
|
7
|
+
Dir.mktmpdir do |dir|
|
|
8
|
+
Dir.chdir(dir) do
|
|
9
|
+
described_class.start([])
|
|
10
|
+
|
|
11
|
+
expect(File.executable?('deploy/after_push')).to be true
|
|
12
|
+
expect(File.executable?('deploy/restart')).to be true
|
|
13
|
+
expect(File.executable?('deploy/before_restart')).to be true
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/spec/hooks_spec.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe GitDeploy do
|
|
4
|
+
describe '#hooks' do
|
|
5
|
+
it 'uploads the post-receive hook to the remote repository' do
|
|
6
|
+
instance = described_class.new([], remote: 'production', noop: true)
|
|
7
|
+
uploads = {}
|
|
8
|
+
|
|
9
|
+
allow(instance).to receive(:scp_upload) { |files| uploads.merge!(files) }
|
|
10
|
+
allow(instance).to receive(:run)
|
|
11
|
+
instance.send(:git_config)['remote -v'] = "production\tgit@example.com:/apps/demo (fetch)"
|
|
12
|
+
|
|
13
|
+
instance.hooks
|
|
14
|
+
|
|
15
|
+
expect(uploads.values).to include('/apps/demo/.git/hooks/post-receive')
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/spec/setup_spec.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe GitDeploy do
|
|
4
|
+
describe '#setup' do
|
|
5
|
+
let(:instance) { described_class.new([], remote: 'production', noop: true, shared: false, sudo: false) }
|
|
6
|
+
let(:commands) { [] }
|
|
7
|
+
|
|
8
|
+
before do
|
|
9
|
+
allow(instance).to receive(:run_test).and_return(false)
|
|
10
|
+
allow(instance).to receive(:run) do |cmd = nil, **_opts, &block|
|
|
11
|
+
cmd = block.call([]) if block
|
|
12
|
+
commands << cmd
|
|
13
|
+
end
|
|
14
|
+
allow(instance).to receive(:invoke)
|
|
15
|
+
instance.send(:git_config)['remote -v'] = "production\tgit@example.com:/apps/demo (fetch)"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'initializes the remote repo with the current branch as HEAD' do
|
|
19
|
+
instance.send(:git_config)['symbolic-ref -q HEAD'] = 'refs/heads/main'
|
|
20
|
+
|
|
21
|
+
instance.setup
|
|
22
|
+
|
|
23
|
+
init_cmd = commands.flatten.join(' && ')
|
|
24
|
+
expect(init_cmd).to include('git init')
|
|
25
|
+
expect(init_cmd).to include("sed -i'' -e 's/master/main/' .git/HEAD")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'skips HEAD rewrite when the current branch is master' do
|
|
29
|
+
instance.send(:git_config)['symbolic-ref -q HEAD'] = 'refs/heads/master'
|
|
30
|
+
|
|
31
|
+
instance.setup
|
|
32
|
+
|
|
33
|
+
init_cmd = commands.flatten.join(' && ')
|
|
34
|
+
expect(init_cmd).not_to include('sed')
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe GitDeploy do
|
|
4
|
+
describe 'SSH connection' do
|
|
5
|
+
let(:instance) { described_class.new([], remote: 'production') }
|
|
6
|
+
|
|
7
|
+
before do
|
|
8
|
+
instance.send(:git_config)['remote -v'] = "production\tgit@example.com:/apps/demo (fetch)"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'advises installing ed25519 gems when SSH keys require them' do
|
|
12
|
+
allow(Net::SSH).to receive(:start).and_raise(
|
|
13
|
+
NotImplementedError,
|
|
14
|
+
"unsupported key type `ssh-ed25519'\n * ed25519 (>= 1.2, < 2.0)"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
expect { instance.send(:run_test, 'true') }.to raise_error(SystemExit, /gem install ed25519 bcrypt_pbkdf/)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 're-raises unrelated SSH errors' do
|
|
21
|
+
allow(Net::SSH).to receive(:start).and_raise(NotImplementedError, 'unexpected failure')
|
|
22
|
+
|
|
23
|
+
expect { instance.send(:run_test, 'true') }.to raise_error(NotImplementedError, 'unexpected failure')
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: git-deploy-ng
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.8.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Nathan Edwards
|
|
8
|
+
- Mislav Marohnić
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: thor
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.3'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.3'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: net-ssh
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '7.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '7.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: net-scp
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '4.0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '4.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: logger
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
69
|
+
description: A community continuation of mislav/git-deploy. Push-based, Heroku-like
|
|
70
|
+
deployment over SSH.
|
|
71
|
+
email: npfedwards@gmail.com
|
|
72
|
+
executables:
|
|
73
|
+
- git-deploy
|
|
74
|
+
extensions: []
|
|
75
|
+
extra_rdoc_files: []
|
|
76
|
+
files:
|
|
77
|
+
- CONTRIBUTING.md
|
|
78
|
+
- LICENSE
|
|
79
|
+
- MAINTAINERS.md
|
|
80
|
+
- README.markdown
|
|
81
|
+
- ROADMAP.md
|
|
82
|
+
- bin/git-deploy
|
|
83
|
+
- lib/git_deploy.rb
|
|
84
|
+
- lib/git_deploy/configuration.rb
|
|
85
|
+
- lib/git_deploy/generator.rb
|
|
86
|
+
- lib/git_deploy/ssh_methods.rb
|
|
87
|
+
- lib/git_deploy/templates/after_push.sh
|
|
88
|
+
- lib/git_deploy/templates/before_restart.rb
|
|
89
|
+
- lib/git_deploy/templates/restart.sh
|
|
90
|
+
- lib/hooks/post-receive.sh
|
|
91
|
+
- spec/cli_spec.rb
|
|
92
|
+
- spec/commands_spec.rb
|
|
93
|
+
- spec/configuration_spec.rb
|
|
94
|
+
- spec/generator_spec.rb
|
|
95
|
+
- spec/hooks_spec.rb
|
|
96
|
+
- spec/setup_spec.rb
|
|
97
|
+
- spec/spec_helper.rb
|
|
98
|
+
- spec/ssh_connection_spec.rb
|
|
99
|
+
homepage: https://github.com/npfedwards/git-deploy#readme
|
|
100
|
+
licenses:
|
|
101
|
+
- MIT
|
|
102
|
+
metadata: {}
|
|
103
|
+
rdoc_options: []
|
|
104
|
+
require_paths:
|
|
105
|
+
- lib
|
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '2.7'
|
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - ">="
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: '0'
|
|
116
|
+
requirements: []
|
|
117
|
+
rubygems_version: 4.0.3
|
|
118
|
+
specification_version: 4
|
|
119
|
+
summary: Simple git push-based application deployment
|
|
120
|
+
test_files: []
|