GitHub Webhooksで自動デプロイ

GitHubからcloneしたディレクトリを直接サーバーに配置してアプリケーションをデプロイしている場合、リモートでpushしたらサーバー側もpullしないとソースの同期がとれません。

リモートからpushがあるたびにいちいちサーバーに乗り込んでgit pullコマンドを発行するなんてメンドクサイ!

そんなときは!
GitHubのWebhooksを使いましょう!

Webhookとは?

Webhook(Webフック)とは、Webサービスがサービス内で起こったイベントをトリガーに、所定のURLにパラメータを渡してコールしてくれる機能です。
例えばGitHubならリポジトリへのpushを検知して、任意のURLにコミット内容などの情報をパラメータにのせて送り込んでもらえます。

自分の運用しているサーバーやサービスと連携するだけでなく、Webサービス同士を連携させるのにも使えます。
GitHubのプルリクエスト通知をSlackに投げるといった具合ですね。

GitHub Webhooksで何ができる?

GitHub Webhooksはリポジトリ単位で設定できます。
イベントトリガーはリポジトリに対して行われたほぼ全ての変更に対して設置できます。

  • ブランチ、タグの作成・削除
  • リポジトリのフォーク
  • イシューの作成・編集
  • プルリクエストの追加
  • リポジトリへのプッシュ
  • スターの追加・削除
などなど。

送り込んでくれるパラメータはPayloadと呼ばれ、そのイベントに対する情報が入ってます。
必要そうな情報は網羅されてます。

例えば↓はpush時に送り込んでくれるJSONです。
{
  "ref": "refs/tags/simple-tag",
  "before": "a10867b14bb761a232cd80139fbd4c0d33264240",
  "after": "0000000000000000000000000000000000000000",
  "created": false,
  "deleted": true,
  "forced": false,
  "base_ref": null,
  "compare": "https://github.com/Codertocat/Hello-World/compare/a10867b14bb7...000000000000",
  "commits": [

  ],
  "head_commit": null,
  "repository": {
    "id": 135493233,
    "node_id": "MDEwOlJlcG9zaXRvcnkxMzU0OTMyMzM=",
    "name": "Hello-World",
    "full_name": "Codertocat/Hello-World",
    "owner": {
      "name": "Codertocat",
      "email": "21031067+Codertocat@users.noreply.github.com",
      "login": "Codertocat",
      "id": 21031067,
      "node_id": "MDQ6VXNlcjIxMDMxMDY3",
      "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
      "gravatar_id": "",
      "url": "https://api.github.com/users/Codertocat",
      "html_url": "https://github.com/Codertocat",
      "followers_url": "https://api.github.com/users/Codertocat/followers",
      "following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
      "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
      "organizations_url": "https://api.github.com/users/Codertocat/orgs",
      "repos_url": "https://api.github.com/users/Codertocat/repos",
      "events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
      "received_events_url": "https://api.github.com/users/Codertocat/received_events",
      "type": "User",
      "site_admin": false
    },
    "private": false,
    "html_url": "https://github.com/Codertocat/Hello-World",
    "description": null,
    "fork": false,
    "url": "https://github.com/Codertocat/Hello-World",
    "forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks",
    "keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}",
    "collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}",
    "teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams",
    "hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks",
    "issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}",
    "events_url": "https://api.github.com/repos/Codertocat/Hello-World/events",
    "assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}",
    "branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}",
    "tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags",
    "blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}",
    "git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}",
    "git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}",
    "trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}",
    "statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}",
    "languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages",
    "stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers",
    "contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors",
    "subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers",
    "subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription",
    "commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}",
    "git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}",
    "comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}",
    "issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}",
    "contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}",
    "compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}",
    "merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges",
    "archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}",
    "downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads",
    "issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}",
    "pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}",
    "milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}",
    "notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}",
    "labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}",
    "releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}",
    "deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments",
    "created_at": 1527711484,
    "updated_at": "2018-05-30T20:18:35Z",
    "pushed_at": 1527711528,
    "git_url": "git://github.com/Codertocat/Hello-World.git",
    "ssh_url": "git@github.com:Codertocat/Hello-World.git",
    "clone_url": "https://github.com/Codertocat/Hello-World.git",
    "svn_url": "https://github.com/Codertocat/Hello-World",
    "homepage": null,
    "size": 0,
    "stargazers_count": 0,
    "watchers_count": 0,
    "language": null,
    "has_issues": true,
    "has_projects": true,
    "has_downloads": true,
    "has_wiki": true,
    "has_pages": true,
    "forks_count": 0,
    "mirror_url": null,
    "archived": false,
    "open_issues_count": 2,
    "license": null,
    "forks": 0,
    "open_issues": 2,
    "watchers": 0,
    "default_branch": "master",
    "stargazers": 0,
    "master_branch": "master"
  },
  "pusher": {
    "name": "Codertocat",
    "email": "21031067+Codertocat@users.noreply.github.com"
  },
  "sender": {
    "login": "Codertocat",
    "id": 21031067,
    "node_id": "MDQ6VXNlcjIxMDMxMDY3",
    "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/Codertocat",
    "html_url": "https://github.com/Codertocat",
    "followers_url": "https://api.github.com/users/Codertocat/followers",
    "following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
    "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
    "organizations_url": "https://api.github.com/users/Codertocat/orgs",
    "repos_url": "https://api.github.com/users/Codertocat/repos",
    "events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
    "received_events_url": "https://api.github.com/users/Codertocat/received_events",
    "type": "User",
    "site_admin": false
  }
}
イベントとペイロードの一覧はこちら。


GitHub Webhooksでの使い方

リポジトリの「Settings」タブから「Webhooks」を選び「Add webhook」を押して、webhookを追加します。

「Payload URL」に通知を飛ばしたいURLを設定します。クエリストリングを設定することもできます。

「Content type」には受け取るデータの形式をJSONかx-www-form-urlencodedで選択できます。

「Secret」に値を設定しておくとSHA1ハッシュをかけてリクエストヘッダに「X-Hub-Signature」のキーで送信してくれます。
これを使って呼び出されるAPI側で送信元の照合をかけることができます。

「Which events would you like to trigger this webhook?」では、どのイベントに対してトリガーするかを選べます。
「Just the push event.」はpush。
「Send me everything.」はすべてのイベント。
「Let me select individual events.」では任意のイベントを複数選択できます。

「Active」からチェックを外すと一時的にwebhookのトリガーを停止できます。

httpsのサイトを指定すると、SSL証明書の検証を行うかどうかも設定できます。


webhookを登録するとアクセスの検証に「ping」というイベントでURLをコールしてくれます。


リクエストヘッダとボディ、レスポンスの内容が見れるので、これでデバッグを行うことができます。
「Redeliver」ボタンを押すと再送できます。

「Recent Deliveries」にはwebhookの作動した履歴が表示されていきます。

実装例

実際にPHPで作ってみたもの。
各リポジトリ名に応じてpullするディレクトリを変えれるようになっています。
<?php
# deploy.php
$log_file = __DIR__ . "/../deploy.log";

// INIファイル読み込み
$arrayIniFile = parse_ini_file(__DIR__ . "/../deploy.ini", true);

// secretキーのチェック
if(!isset($_SERVER['https_X_HUB_SIGNATURE']) &&
   !($_SERVER['https_X_HUB_SIGNATURE'] === sha1($arrayIniFile['sercret_key']))){
    file_put_contents($log_file, date("Y-m-d H:i:s")."\t".'secret key error'.PHP_EOL, FILE_APPEND);
    die();
}

$payload = json_decode(file_get_contents('php://input'), true);

//payloadからpushされたリポジトリ名を取得し、iniファイルと突き合わせpullコマンドを発行する
foreach($arrayIniFile['deploy_setting'] as $repository_name => $local_repository_path){
    if($payload['repository']['name'] != $repository_name) continue;

    $command = "cd {$local_repository_path} && git --git-dir=.git pull 2>&1";
    exec($command, $output, $return_var);
    file_put_contents($log_file, date("Y-m-d H:i:s")."\t".$command."\t".var_export($output).PHP_EOL, FILE_APPEND);
}
;deploy.ini
sercret_key = "hogehoge"

[deploy_setting]
;repository name = local repository path
hogerepo = "/var/www/hogeapp/"
fugarepo = "/var/www/fugaapp/"
単一サーバーであれば、各リポジトリのwebhookにこれの呼び出しを登録、iniファイルを設定するだけで、各リポジトリへのpushに応じてpullが走るようになります。


簡単な設定でちょっとしたCIのようなことができるのは便利ですね。

ではまた。