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

GeekFactory

int128.hatenablog.com

Create React AppでChrome Extensionを開発する

JavaScript React

Reactのアプリを開発するにはFacebook謹製のcreate-react-appが便利です。しかし、create-react-appはWebアプリの開発に特化しているため、Chrome Extensionの開発には使えない問題があります。スクリプトを少し変えてwatch buildを行う方法を紹介します。

Chrome Extensionの開発に必要なフロー

Chrome Extensionを開発するには以下のようなフローが必要になります。

  1. アプリをビルドし、 build フォルダに出力する。
  2. Chrome拡張機能を開き、デベロッパーモードで build フォルダを読み込む。
  3. Chrome Extensionを実行する。
  4. ソースコードを修正する。
  5. アプリをビルドし、 build フォルダに出力する。
  6. 3に戻る。

create-react-appでnpm startを実行するとlocalhost:3000で開発サーバが実行されます。localhost:3000をそのまま開いても通常のWebアプリとして扱われるため、Bookmarks APIなどのChrome APIを実行できません。Chrome APIを実行するには上記の2が必要になります。

スクリプトのカスタマイズ

create-react-appは内部でWebpackの開発サーバを実行しています。これをwatchに変更します。

今回は以下の方針で修正します。

  • ejectして修正するとバージョンアップに追随するのが辛いのでやりたくない。
  • create-react-appが持っているWebpackの設定(webpack.config.dev.js)をなるべくそのまま利用したい。

まず、package.jsonを以下のように修正します。

  "scripts": {
    "start": "node scripts/start.js",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "zip": "zip -j - build/* > build/extension.zip",
    "clean": "rm -fr build"
  }

scripts/start.jsを作成します。

process.env.NODE_ENV = 'development';

var fs = require('fs-extra');
var paths = require('react-scripts/config/paths');
var webpack = require('webpack');
var config = require('react-scripts/config/webpack.config.dev.js');

// removes react-dev-utils/webpackHotDevClient.js at first in the array
config.entry.shift();

var compiler = webpack(config);
compiler.watch({}, function(err, stats) {
  if (err) {
    console.error(err);
  } else {
    copyPublicFolder();
  }
  console.error(stats.toString({
    chunks: false,
    colors: true
  }));
});

// as react-scripts/scripts/build.js
function copyPublicFolder() {
  fs.copySync(paths.appPublic, paths.appBuild, {
    dereference: true,
    filter: file => file !== paths.appHtml
  });
}

ちょっと雑ですが、create-react-appが持っているWebpackの設定を一部改変してwatchに渡すことで継続的にビルドを実行できます。

これでChrome Extensionの開発に必要な下表のタスクが実行できるようになりました。

コマンド やること
npm start ファイルが変更される度にdevelopmentでビルド
npm build productionでビルド
npm zip ビルドをZIPファイルに固める

ご参考まで。

Jenkinsfileによるジョブ管理のメリットと実例

jenkins CI

ジョブの設定をJenkinsfileで管理し始めてから3か月ぐらい経ったので、知見をまとめてみます。

Jenkinsfileを使うメリット

Jenkinsの画面でジョブを管理していると以下のような問題が起きることが多いと思います。

  • 誰かが勝手にJenkinsの設定を変更して動かなくなった
  • ジョブ設定を別リポジトリに横展開したいけど、ポチポチ設定するのが面倒

JenkinsfileをGitで管理することで、以下のメリットがあります。

  • いつ、誰が、なぜジョブ設定を変更したのか後から調べられる
  • Pull Requestでジョブ設定の変更をレビューできる
  • ブランチを使ってジョブ設定を試行錯誤しやすい

Jenkinsの運用ポリシー

前項のメリットを実現するには、Jenkinsを以下のポリシーで運用することが望ましいでしょう。

  • Jenkinsの設定は最小限に抑える
  • なるべく画面からジョブ設定を変更せずに済むようにする(GitHub OrganizationやMultibranch Pipelineを利用)
  • なるべくJenkinsにログインしなくてもオペレーションが回るようにする(ビルド結果をチャットに通知)
  • Jenkinsfileをポータブルにする(認証情報を書かない、Jenkins Agentをイミュータブルにする等)

Jenkinsfileのテンプレート

どの言語でも共通のテンプレートを用意しておくと便利です。以下のようなテンプレートを利用しています。

node {
  try {
    checkout scm

    stage('build') {
      try {
        // TODO: ビルドを実行する
        // TODO: チャットにビルド成功を通知する
      } finally {
        // TODO: テスト結果を収集する (junit, publishHTMLなど)
      }
    }
  } catch (e) {
    // TODO: チャットにビルドエラーを通知する
    throw e
  }
}

ビルド結果はチャットの専用チャンネルに通知するようにしています。ただし、masterブランチのビルドエラーが起きた場合はすぐに直す必要があるので普段のチャンネルに流すようにしています。

  } catch (e) {
    // TODO: チャットにビルドエラーを通知する
    if (env.BRANCH_NAME == 'master') {
      // TODO: チャットにmasterが壊れたと通知する
    }
    throw e
  }

GradleでJVM言語をビルドする

GradleでJava、Groovy、Kotlinなどをビルドする場合は以下のようになります。

node {
  try {
    checkout scm
    sh 'chmod +x gradlew'

    stage('check') {
      try {
        sh './gradlew check'
      } finally {
        // TODO: テスト結果を収集する
        junit allowEmptyResults: true, testResults: 'build/test-results/test/*.xml'
        publishHTML([allowMissing: true, alwaysLinkToLastBuild: true, keepAll: false, reportDir: 'build/reports/jacoco/test/html', reportFiles: 'index.html', reportName: 'Coverage'])
        publishHTML([allowMissing: true, alwaysLinkToLastBuild: true, keepAll: false, reportDir: 'build/reports/tests/test/html', reportFiles: 'index.html', reportName: 'Test'])
      }
    }

    // TODO: チャットにビルド成功を通知する
  } catch (e) {
    // TODO: チャットにビルドエラーを通知する
    throw e
  }
}

ビルド実行時の情報を使う

ビルド実行時に取得できる情報は、JenkinsのGlobal Variable Referenceで確認できます。例えば、 env.BRANCH_NAME でブランチ名を取得できるので、masterブランチの場合にのみ特殊な処理を行うといったことも簡単にできます。

    if (env.BRANCH_NAME == 'master') {
      // masterブランチの場合はSonarQubeの静的解析を行う
      sh './gradlew sonarqube'
    }

また、パスワードやアクセストークンのような認証情報はJenkinsで管理し、ビルド実行時に注入させることができます。

withCredentials([usernamePassword(credentialsId: 'xxx', passwordVariable: 'AWS_SECRET_ACCESS_KEY', usernameVariable: 'AWS_ACCESS_KEY_ID')]) {
  // S3にリリースする
}

まとめ

本稿ではJenkinsfileでジョブ設定を管理するメリットを説明し、Jenkinsfileの実例を紹介しました。

SonarQube GitHub PluginをGitBucketで使用する(失敗)

SonarQube GitBucket CI

SonarQube GitHub Pluginを使うと、静的解析の結果をPull Requestのステータスやコメントに反映できます。この便利な機能がGitBucketでも使えるか試してみましたが、残念ながら無理でした。何かの知見になればと残しておきます。

プラグインはPull Requestで変更されたファイルに対してのみ静的解析を行うようです。

How To Configure SonarQube GitHub Plugin With Jenkins - Stack Overflow

The analysis is automatically filtered based on the files in the pull request. We were testing with pull requests that only had changes in pom.xml and readme files. Once a functional change was introduced, everything lit up on the GitHub Pull Request view as expected.

プラグインはPull Requestで変更されたファイルを取得するため、以下のAPIを利用します。

https://developer.github.com/v3/pulls/#list-pull-requests-files

このAPIはGitBucketでは未実装(404が返される)のため、エラーになります。Gradle SonarQube Pluginを利用している場合はデバッグレベルで実行すると以下のようなログを確認できます。

21:15:42.972 [DEBUG] [sun.net.www.protocol.http.HttpURLConnection] sun.net.www.MessageHeader@5144a1927 pairs: {GET /api/v3/repos/example/hello/pulls/73/comments HTTP/1.1: null}{Authorization: token ****}{Accept-Encoding: gzip}{User-Agent: Java/1.8.0_102}{Host: git.example.com}{Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2}{Connection: keep-alive}
21:15:43.037 [DEBUG] [sun.net.www.protocol.http.HttpURLConnection] sun.net.www.MessageHeader@43fbb6477 pairs: {GET /api/v3/repos/example/hello/pulls/73/files HTTP/1.1: null}{Authorization: token ****}{Accept-Encoding: gzip}{User-Agent: Java/1.8.0_102}{Host: git.example.com}{Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2}{Connection: keep-alive}

今後の機能追加に期待です。Pull Requestの画面ではファイル一覧を表示しているので、APIで返すのはそれほど難しくないかもしれません。