Gooseリポジトリのmerge時にバージョンを上げmigrationボタンをSlackに出す

databasemysqlcirclecigolang

GooseはGo製のDB Migrationツール。

mergeされるとボタンが出る

コード

こんなリポジトリを作成し、各自ブランチを切ってGoose形式のup/downのSQLを書き、終わったらPullRequestを出す。

goose/
  .keep
.circleci/config.yml
create_test_table.sql
$ cat create_test_table.sql
-- +goose Up
-- SQL in this section is executed when the migration is applied.
CREATE TABLE testtable (
  id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  n INT NOT NULL,
  c VARCHAR (20) NOT NULL UNIQUE
);

-- +goose Down
-- SQL in this section is executed when the migration is rolled back.
DROP TABLE testtable;

無事Approveされ、mergeされるとCircleCIが走り、 SQLをgooseディレクトリの中にバージョンを付けて移し、 SlackにpostMessageするエンドポイントにリクエストを飛ばす。

ここでバージョンを作成することによって、並列で作業し、レビューなどの関係で適用順が前後しても修正する必要をなくしている。ただ、pushされる前に複数のブランチを連続でmergeする場合うまく動かないのでそれはなんとかする必要がある。

CircleCI 2.0ではApprovalボタンが出せるんだが、 アクセスしにいくのがちょっと面倒なのと、周知も兼ねてSlackに出したかったので使っていない。

version: 2
jobs:
  build:
    docker:
      - image: circleci/golang:1.8
    branches:
      only:
        - master
    steps:
      - checkout
      - run:
          name: Create new version
          command: |
            if [ -e *.sql ]; then
              VERSION=$(ls -U1 goose | wc -l | xargs expr 1 + | xargs printf %05d)
              FILENAME=$(find . -maxdepth 1 -name "*.sql" | head | xargs basename)
              mv ${FILENAME} goose/${VERSION}_${FILENAME}
              git config --global user.email "[email protected]"
              git config --global user.name "CircleCI"
              git add .
              git commit -m "version ${VERSION}"
              git push origin master
              COMMIT=$(git rev-parse HEAD)
              curl -H "Authorization: Basic $(echo -n 'foobar:dolphins' | base64)" "https://*****/auth/message?version=${VERSION}&filename=${FILENAME}&commit=${COMMIT}"
            fi            
goose/
  .keep
  00001_create_test_table.sql
.circleci/config.yml

Migrationボタンが押されると、まずボタンを消してRunning状態とし、 処理が終わったら結果を上書きするようにしている。

SlackのInteractive messagesでボタンの入力を受け付ける - sambaiz-net

slackMessages.action('migrate', (payload, respond) => {
  let replacement = payload.original_message; 
  delete replacement.attachments[0].actions; 
  replacement.attachments[0].text = `start migration by ${payload.user.name} at ${moment().format()}`;
  replacement.attachments[0].fields = [
    { 
       "title": "State",
       "value": "Running",
       "short": false
    } 
  ]; 

  exec(
    // Attention to command injection
    `rm -rf ${repositoryName} && git clone [email protected]:${repositoryPath}.git && cd ${repositoryName}/goose && goose mysql "${mySQLConf}" up`, 
    (err, stdout, stderr) => {
      replacement.attachments[0].fields = [
        { 
          "title": "Result",
          "value": (err || stderr) ? `${stderr || err}` : "Success",
          "short": false
        }
      ];
      respond(replacement);
    }
  );
  
  return replacement;
});