Chrome拡張の継続的デリバリー

こんにちは、なんとかChrome拡張を公開できて、少し安心してる正徳です。

昨日、Forkwell JobsのChrome拡張の記事を公開しましたが、本記事では技術的な話を書いてみたいと思います。

リポジトリ

リポジトリはGitHubのgrooves/forkwell_for_chromeで公開しています。 実際のコードを参考にしたい方はどうぞ。

Haml, Sass, CoffeeScript を使う

社内の他エンジニア・デザイナーも触りやすいように、Forkwell Jobsで使用している技術に合わせました。

それぞれ、gulp.jsを使ってHTML, CSS, JavaScriptに変換しています。

mocha + power-assert を使ったテスト

mochapower-assertでテストを実行できるように環境を整えました。

詳細なテストの環境についてはリポジトリを読んで頂くとして、要点だけいくつか書いておきます。

mochaでクライアントサイドJavaScriptのテストをするとエラー

Chrome拡張のJavaScriptはHTML上で動く、普通のクライアントサイドJavaScriptです。 このため、Node.jsで動くmochaを使ってテストしても…

  • ReferenceError: window is not defined
  • Error: jQuery requires a window with a document

などのエラーが出てしまいます。 また、クライアントサイドのJavaScriptでは globalmodule.exports も使えません。

browserify + jsdom を使う

上述した問題に対応するため、Browserify を使い、クライアントサイドのJavaScriptでもNode.jsっぽく書けるようにしました。 また、jQueryがwindowを使うためtmpvar/jsdomも導入しておきます。

これにより、requireを使ったり、global.$をテスト側で上書きしたりが出来るようになります。

こんな感じ。

global.$ = require('jquery')(require('jsdom').jsdom().parentWindow)
Service = require 'src/coffee/models/service'

プロジェクトルートをNODE_PATHに追加

テストを書く際にrequire '../../src/coffee/models/service'のように書くと分かりづらいので、プロジェクトルートをNODE_PATHに追加しています。

test/mocha_resolve_test_path.coffee

path = require 'path'

process.env.NODE_PATH = [
  process.env.NODE_PATH,
  path.resolve(__dirname, '..')
].join(':')

require('module').Module._initPaths()

これをmochaの実行時に引数で渡すと効くようになります。

$ mocha --require test/mocha_resolve_test_path.coffee

Coveralls の設定

カバレッジを出したくて導入しました。少しハマった所があったので、そこだけ書いておきます。

istanbul-jsがCoffeeScriptに対応していない

JavaScriptでカバレッジを計測するためにistanbul-jsを導入したのですが、CoffeeScriptではうまく動きませんでした。 これ解決するためにはbenbria/coffee-coverageを別途インストールする必要があります。

gulpfile.coffeeを無視する

coffee-coverageのデフォルト設定だとgulpfile.coffeeのカバレッジまで測定されるため、これを対象外にします。

coffee-coverage-loader.js

var coffeeCoverage = require('coffee-coverage');
var coverageVar = coffeeCoverage.findIstanbulVariable();
var writeOnExit = coverageVar == null ? true : null;

coffeeCoverage.register({
    instrumentor: 'istanbul',
    basePath: process.cwd(),
    exclude: ['/test', '/node_modules', '/.git', '/gulpfile.coffee'],
    coverageVar: coverageVar,
    writeOnExit: writeOnExit ? ((_ref = process.env.COFFEECOV_OUT) != null ? _ref : 'coverage/coverage-coffee.json') : null,
    initAll: (_ref = process.env.COFFEECOV_INIT_ALL) != null ? (_ref === 'true') : true
});

デフォルト設定であるcoffee-coverage/register-istanbulの代わりに、作成したcoffee-coverage-loader.jsの設定を読み込むようにします。

$ mocha --require test/coffee-coverage-loader.js

Google APIを使い、Chromeウェブストアに自動デプロイ

Chrome拡張のzipファイルを作成し、手動でアップロードするのは非常に面倒なので、Google APIと werckerで自動デプロイの環境を作りました。

Google Developers Consoleの登録

まず、Google APIを使うためにGoogle Developers Consoleでプロジェクトを登録します。

手順はUsing the Chrome Web Store Publish API - Google Chromeに書いてあるので、この手順通りに作業を進めるとOAuth 2.0のCLIENT_IDCLIENT_SECRET, REFRESH_TOKENが手に入ります。

アクセストークンの有効期限

Google APIのアクセストークンの有効期限は 40分 です。 このため、毎回のデプロイ時にアクセストークンを再取得する必要があります。

参考にしたAPIのドキュメント一覧

wercker.ymlの例

werckerで以下の環境変数を設定してあります。

環境変数の名前 内容
GITHUB_TOKEN GitHubのpush権限のあるユーザーのtoken
APP_ID Chromeストアに公開しているアプリID
GOOGLE_APP_ID Google API の CLIENT_ID
GOOGLE_APP_SECRET Google API の CLIENT_SECRET
GOOGLE_REFRESH_TOKEN Google API の REFRESH_TOKEN

wercker.yml

deploy:
  steps:
    - script:
        name: get app_version from gulp
        code: export APP_VERSION=$(npm run app_version --silent)
    - script:
        name: configure git
        code: |
          git config --global user.email "pleasemailus@wercker.com"
          git config --global user.name "wercker"
          git remote set-url origin https://$GITHUB_TOKEN@github.com/$WERCKER_GIT_OWNER/$WERCKER_GIT_REPOSITORY.git
    - script:
        name: create tag
        code: git tag $APP_VERSION -m "$APP_VERSION release"
    - script:
        name: push tag
        code: git push origin $APP_VERSION
    - github-create-release:
        token: $GITHUB_TOKEN
        tag: $APP_VERSION
    - github-upload-asset:
        token: $GITHUB_TOKEN
        file: dist-$APP_VERSION.zip
    - script:
        name: Refresh a access token
        code: |
          JSON=`curl -d "client_id=$GOOGLE_APP_ID" -d "client_secret=$GOOGLE_APP_SECRET" -d "refresh_token=$GOOGLE_REFRESH_TOKEN" -d "grant_type=refresh_token" https://www.googleapis.com/oauth2/v3/token`
          export GOOGLE_APP_TOKEN=`node -p "($JSON).access_token;"`
    - script:
        name: Upload a package to update an existing store item
        code: |
          curl -H "Authorization: Bearer $GOOGLE_APP_TOKEN" -H "x-goog-api-version: 2" -X PUT -T dist-$APP_VERSION.zip -v https://www.googleapis.com/upload/chromewebstore/v1.1/items/$APP_ID
    - script:
        name: Publish an item to the public
        code: |
          curl -H "Authorization: Bearer $GOOGLE_APP_TOKEN" -H "x-goog-api-version: 2" -H "Content-Length: 0" -X POST -v https://www.googleapis.com/chromewebstore/v1.1/items/$APP_ID/publish

こんな感じのwercker.ymlを作成すると、自動で

  • GitHub のTagの作成
  • GitHub のReleasesの作成
  • Chromeウェブストアにzipのアップロード
  • Chromeウェブストアで新しいバージョンを公開

が行われます。便利です。

まとめ

今回のChrome拡張は出来たばかりで、これから少しずつ改良していきたいと思っています。

要望などあれば是非、Twitterで#forkwellのハッシュタグを付けてツイート頂くか、公式アカウントの@Forkwell_jaにご連絡ください。

最後になりましたが、この記事がChrome拡張を作る方、作っている方の参考になれば幸いです。