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 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,3 @@
1
+ #!/usr/bin/env ruby
2
+ require 'git_deploy'
3
+ GitDeploy.start
@@ -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"
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ touch tmp/restart.txt
3
+ echo "restarting Passenger app"
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
@@ -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
@@ -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
@@ -0,0 +1,13 @@
1
+ require 'simplecov'
2
+ SimpleCov.minimum_coverage 80
3
+ SimpleCov.start
4
+
5
+ require 'rspec'
6
+ require 'rspec/mocks'
7
+ require 'git_deploy'
8
+
9
+ RSpec.configure do |config|
10
+ config.expect_with :rspec do |c|
11
+ c.syntax = :expect
12
+ end
13
+ end
@@ -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: []