読者です 読者をやめる 読者になる 読者になる

GitHubリポジトリが突然消えたときにやるべきこと

新年あけましておめでとうございます。ことよろ。
最近 Forkwell のポートフォリオ機能を弄っている@sinsoku です。

今日は弊社で起きた「リポジトリが突然消えた事件」について書きたいと思います。

事の始まり

昨年末の26日の朝、 fork したけど使っていないリポジトリがあったので、何気なく削除しました。 その30分後、同僚のapp2641に声をかけられました。

app2641:「なぜか(メイン)リポジトリが404なんですが、sinsoku さん何か知ってます?」
sinsoku:「え、いや、分からないです。私の方でも調べてみます。(もしかして...)」

自分でもリポジトリのページを表示してみました。

f:id:sinsoku:20170118110130p:plain

404 ですね。マジか...。Audit log を確認してみるか。

f:id:sinsoku:20170118110145p:plain

見覚えあるアイコンの横に repo.destroy て書いてあるじゃないか...o..rz

GitHub のサポートへ連絡

下記の内容で問合せました。

Hi, GitHub Team.

I accidentally deleted a "grooves/xxx" repository. Can you resurrect it?

その後、サポートの人から返ってきた返信を要約すると下記の通り。

  • GitHub ではリポジトリを復元できるようにアーカイブに保存している
  • grooves/xxx のリポジトリは検索したけど、アーカイブに存在しない
  • sinsoku/xxx のリポジトリはあるけど、これ?

GitHub では削除した後に問い合わせれば復元できるっぽいですね。 通常なら

I can't see a "grooves/xxx" repo.

https://github.com/grooves/xxx

And Audit log shows "repo.destroy".

と送ったところ、「技術的な問題があって復元できなかった。エンジニアチームに尋ねた」と返ってきた。

(´-`).。oO(もしかして GitHub のバグを踏んでしまいリポジトリが死んだのでは...)

そして新年

年末年始で返信もなく GitHub も休んでそうだったので、連休明けの 1/11(水) に進捗を確認してみた。

Could you tell us about the current status?
The repository is not restored yet. How long will it take?

翌日1/12(木)の夜には返信があって、無事にリポジトリが復活した。

Sorry for the delay on this one but the repository should now be restored.

リポジトリが無い期間の開発

初日

すぐ復活するだろうと思っていたのと小さい変更だったので、2つだけ Slack上で diff をレビュー してデプロイしました。

f:id:sinsoku:20170118110210p:plain

「手順書を用意 => バックアップ作成 => パッチ適用 => ホットデプロイ」を手動でやるとか、旧世代のデプロイに懐かしさを感じた1日。

2日目以降

リポジトリの復元に時間かかりそうな返信内容だったのと、さすがに 2017年にもなって手動デプロイとか無理 なので、一時リポジトリを作成して作業することになりました。

  • 一時リポジトリの作成
  • CI の環境構築
  • Capistrano の設定変更

こういうとき Git だとリポジトリの複製が楽で良いですね。

リポジトリが復活した後

Capistrano の設定を戻し、一時リポジトリの Pull Request, Review Comment はスクリプトを書いてバックアップを保存しました。

#!/usr/bin/env ruby
# frozen_string_literal: true
require 'fileutils'
require 'octokit'

OWNER = 'grooves'
REPO = 'tmp_repo'
FULL_NAME = "#{OWNER}/#{REPO}"
DOC_PATH = File.expand_path('../../doc', __FILE__)
PULLS_PATH = "#{DOC_PATH}/#{REPO}/pulls"
COMMENTS_PATH = "#{DOC_PATH}/#{REPO}/review_comments"

Octokit.auto_paginate = true

class RepoBackup
  attr_reader :token

  def initialize
    @token = ENV['TOKEN']
  end

  def save_pulls
    mkdir_p(PULLS_PATH)

    pulls = client.pulls(FULL_NAME, state: :all)
    pulls.each do |pull|
      write_json("#{PULLS_PATH}/#{pull[:number]}", pull)
      puts "saved pulls##{pull[:number]}"
    end
  end

  def save_comments
    mkdir_p(COMMENTS_PATH)

    comments = client.reviews_comments(FULL_NAME)
    comments.each do |comment|
      write_json("#{COMMENTS_PATH}/#{comment[:id]}", comment)
      puts "saved review_comment##{comment[:id]}"
    end
  end

  private

  def client
    @client ||= Octokit::Client.new(access_token: token)
  end

  def mkdir_p(path)
    FileUtils.mkdir_p(path) unless FileTest.exist?(path)
  end

  def write_json(path, obj)
    File.write(path, JSON.pretty_generate(obj.to_h))
  end
end

backup = RepoBackup.new
if backup.token
  puts 'Start backup script'

  backup.save_pulls
  backup.save_comments

  puts "Backup finished and saved to #{DOC_PATH}."
else
  puts 'USAGE: TOKEN=xxx ./script/backup_forkwell_tmp'
end

対策方法

通常なら GitHub に問い合わせれば復元できるっぽいので、厳重な対策はしなくて良いかもしれません。 ただ、公表されているサポートって訳でも無いですし、数時間でもリポジトリが消えたら困るってケースもあると思うので、いくつか方法を挙げておきます。

1. 削除時の確認

今回の件で初めて知ったのですが、リポジトリを削除するときの確認画面でリポジトリ名だけでなく ユーザー名 も入力することが可能です。

f:id:sinsoku:20170118110233p:plain

削除時は sinsoku/xxx のようにユーザー名(もしくは Organization 名)を含めて入力し、念のためスクリーンショットを保存しておくのが良さそうです。

2. 開発者に Admin 権限をつけない

Terraform の GitHub provider を使えば、Organization のリポジトリをコードで管理することが可能です。

  1. Admin を1人だけ作成する
  2. リポジトリを削除するときは Pull Request でレビューしてからマージする
  3. マージ後 Admin の token を使って terraform apply でリポジトリを削除する

上記のような運用を行うことで、より安全なリポジトリ管理ができるかと思います。

3. 外部サービス

f:id:sinsoku:20170118110246p:plain

BackHub という GitHub のリポジトリを定期的にバックアップしてくれるサービスを見つけました。 使ったことがないので詳細は不明ですが Issue, Pull Request などを定期的に保存するなら便利そうです。

まとめ

GitHub のバグなのか、私の操作ミスなのか最終的な原因は分かりませんでしたが、リポジトリが無事に復旧して良かったです。

この記事が皆様の 参考にならない事 を祈っていますが、もしリポジトリが消えてしまった時は本記事を参考にして頑張ってください。

f:id:sinsoku:20170118110254p:plain