平常運転

アニソンが好き

過去記事とかは記事一覧で見れます

capistrano3 を使ってデプロイしていてrepo_urlを変更する

追記

2015 03/25 14:20頃に末尾に追記しました



表題の通り。capistrano3でデプロイするとき、リポジトリのurlはconfig/deploy.rbにこういう感じで書くと思う。

set :scm, :git
set :repo_url, 'git@github.com:MYNAME/MYPROJECT.git'

ここで、リポジトリサーバの引っ越しとかで後からrepo_urlを変更したくなったとする。というかしたくなった。

set :repo_url, 'git@myGitServer:MYNAME/MYPROJECT.git'

この時、普通にrepo_urlを書き換えるだけではダメだったのと、とりあえずそれをなんとかするためにcapのタスクを書いた話をする。capistrano詳しい人がいたらもうちょっとよい解決策教えてください。

repo_urlを書き換えるだけではダメ

なぜ普通にrepo_urlを書き換えるだけではダメだったのかという話を先にする。
cap3でgitデプロイすると、デプロイ先のサーバ上でrepo_urlからgit cloneして、そこから実際のリリース先のpathに対してgit cloneしている。このgitミラーは2回目以降のデプロイではそのまま使われる(git remote updateして同期する)。
読んでてだいたい想像がついてきたのではと思うけど、2回目以降のデプロイでgitミラーが存在するか確認するときに、ミラーのgit remoteとCapfileのrepo_urlが一致するかは確認していない。なので、repo_urlだけを後から変更しても、実際のデプロイでは変更前のリポジトリからデプロイされるのだった。
冒頭に書いた例だといつまでたってもMyGitServerではなくてgithubからデプロイされる。

capistrano3によるgit デプロイ

上記の挙動自体はcapを実行してログを眺めてるとだいたい想像つくけど、なんでこうなってるかをソースを追いかけた。
cap3でcap production deployした時に実際に実行されるタスクは、公式のドキュメンテーションを見ると分かるようにこういう感じになっている。

deploy:starting    - start a deployment, make sure everything is ready
deploy:started     - started hook (for custom tasks)
deploy:updating    - update server(s) with a new release
deploy:updated     - updated hook
deploy:publishing  - publish the new release
deploy:published   - published hook
deploy:finishing   - finish the deployment, clean up everything
deploy:finished    - finished hook
http://capistranorb.com/documentation/getting-started/flow/

具体的にdeploy:updatingで実行されるタスクはこういう感じ

  task :updating => :new_release_path do
    invoke "#{scm}:create_release"
    invoke "deploy:set_current_revision"
    invoke 'deploy:symlink:shared'
  end

git:create_releasegit:update => git:cloneと依存しており、ソースコードこういう感じ

  desc 'Clone the repo to the cache'
  task clone: :'git:wrapper' do
    on release_roles :all do
      if strategy.test
        info t(:mirror_exists, at: repo_path)
      else
        within deploy_path do
          with fetch(:git_environmental_variables) do
            strategy.clone
          end
        end
      end
    end
  end

  desc 'Update the repo mirror to reflect the origin state'
  task update: :'git:clone' do
    on release_roles :all do
      within repo_path do
        with fetch(:git_environmental_variables) do
          strategy.update
        end
      end
    end
  end

  desc 'Copy repo to releases'
  task create_release: :'git:update' do
    on release_roles :all do
      with fetch(:git_environmental_variables) do
        within repo_path do
          execute :mkdir, '-p', release_path
          strategy.release
        end
      end
    end
  end

strategy.testが真ならgitミラーが存在するとみなしてそこからupdateして、偽ならgitミラーが存在しないのでstrategy.cloneでcloneしてくる。strategyとはなんぞやというと、これはcapistrano/git.rbの中でmodule DefaultStrategyとして定義されてる。このstrategy.testのコードを読むと、

    def test
      test! " [ -f #{repo_path}/HEAD ] "
    end

となっていて、見事にHEADの存在しか見ていない。なのでrepo_urlが変わっていてもcapistrano3のgitデプロイでは追従してくれないのである。

workaround

capistranoにはあんまり明るくないので、世の中にはなんか格好良くその辺対応してるStrategyがあるのかなーと思ったけど、とりあえず自前でgit:cloneの手前でrepo_urlが一致してるかを確かめることにした。雑だからコピペして使うのやめた方がいい。

namespace :git do
  task :delete_repo_if_repo_url_mismatch do
    on release_roles :all do
      if strategy.test
        if capture("cd #{repo_path}; git remote -v show").split("\n").select{ |i| i.include?("#{fetch(:repo_url)}") }.empty?
          puts "repo url mismatch!"
          execute(:rm, "-rf", repo_path);
        else
          # puts "repo url match, so ALRIGHT*"
        end
      end
    end
  end
  before :clone, :delete_repo_if_repo_url_mismatch
end

雑だけど、まぁとりあえず今のところはこれで要件は達成できそう。

結び

capistrano3について知見をお持ちの方がいらしたらもっとかっこいいソリューションとか是非共有いただけると幸いです。

追記(03/25 14:20)

同じリポジトリのurlが変わっただけ(=中身は同じリポジトリ)であるなら、mirrorを消さなくてもgit remote set-urlするだけでいいじゃん、との指摘をもらって、確かにと思ったので書き直したらタスクちょっとまともになった。

namespace :git do
  task :sync_remote_url do
    on release_roles :all do
      if strategy.test
        within repo_path do
          if capture(:git, "remote", "-v", "show").split("\n").select{ |i| i.include?("#{fetch(:repo_url)}") }.empty?
            puts "repo url mismatch!"
            execute :git, "remote", "set-url", "origin", "#{fetch(:repo_url)}"
          else
            # puts "repo url match, so ALRIGHT*"
          end
        end
      end
    end
  end
  before :clone, :sync_remote_url
end