2015/03/10

AppVeyorでセキュア変数を設定する

CI, AppVeyor

Windows環境でCIを行えるAppVeyorでも、Travis CIなどのほかのCIサービスと同様にセキュアな環境変数を設定できます。しかし、Travis CIの場合とは少し勝手が違います。以下にTravis CIの場合と比較したAppVeyorでの設定方法を書き付けておきます。

Travis CIの場合

Travis CIの場合は、CLIクライアントを使って環境変数のキーごと暗号化します。例えば、GITHUB_ACCESS_TOKEN=foobarを暗号化したいのであれば、プロジェクトディレクトリに移動した後、以下のようなコマンドで暗号化します。

$ travis encrypt GITHUB_ACCESS_TOKEN=foobar
Please add the following to your .travis.yml file:

  secure: " ... 暗号化された値 ... "

出力された文字列を、.travis.ymlファイルに以下のように貼り付けます。暗号化された変数のキーであるGITHUB_ACCESS_TOKENは設定ファイル上では隠蔽されます。

env:
  global:
    - secure: ' ... 暗号化された値 ... '

実際のビルド結果に以下のようなログがあれば、変数が正しく設定されていることになります。

Setting environment variables from .travis.yml
$ export GITHUB_ACCESS_TOKEN=[secure]

詳細な方法は以下のドキュメントで確認できます。

AppVeyor の場合

AppVeyorの場合は、Encrypt configuration dataという専用のWebページを使って、キーを含まずに環境変数の値だけ暗号化します。

例えば、

set GITHUB_ACCESS_TOKEN=foobar

を暗号化して行いたいのであれば、foobarのみをフォームに入力します(GITHUB_ACCESS_TOKEN=foobarを丸ごと入力すると期待した値は得られません)。暗号化された値が出力されたら、ドキュメントに記載されているように以下の形式で設定ファイルのappveyor.ymlに書き加えます。

environment:
  暗号化した環境変数のキー:
    secure: ' ... 暗号化された値 ... '

例のGITHUB_ACCESS_TOKENの場合は、以下のようになります。

environment:
  GITHUB_ACCESS_TOKEN:
    secure: ' ... 暗号化された値 ... '

Travis CIの場合と違い、暗号化された変数のキーであるGITHUB_ACCESS_TOKENは設定ファイル上では隠蔽されません。

同じsecureというキーワードを設定ファイル上で用いること、それでいてキー - バリューの関係が大きく異なることが手違いを生みやすいように思います。しかし、設定さえ正しく行えば、同一のセキュア変数を使って複数OS上でのテストが可能、という便利なCIを構築できます。

実際に、gulp-gh-pagesというプロジェクトではTravis CIAppVeyor両方で共通のセキュア変数を使ったテストを行っています。

2015/03/08

更新の滞ったプロジェクトを引き継ぐ

オープンソース, プログラミング

Node、ひいてはJavaScriptを取り巻く環境は目紛しく変化し、ライブラリやフレームワークもそれに合わせて日々更新され続けています。特にNodeモジュールの場合は、それ自体のコードはもちろん、頻繁に行われる依存パッケージのアップデートも精査の対象としなければなりません。そんな環境の中で、大量のコードまたは複雑なパッケージ依存関係を持つにも関わらず、何ヶ月、あるいは何年も更新されていないNodeモジュールを使うのは、少なからぬリスクがあると考えるべきでしょう。

放置されたモジュールをそのまま使わずに、フォークして自身の用途に合わせて修正を行うというのも(オープンソースらしい)一つの手段です。同様の目的を果たすモジュールをコードベースから書き直して作成する手もあります。あるいは、既に別の開発者が代替手段となるモジュールを作ってくれている場合もあるでしょう。

そういった、代替物を探す/作る解決方法に加えてもう一つ、自分がその放置されたプロジェクトを引き継ぐ、というのも有効な選択肢です。

実際に、筆者は更新頻度が少ない、あるいは更新が滞ったオープンソース・プロジェクトの作者に協力を提案して管理を引き継ぐ、ということを何度か行っています。

フォークとの違い

フォークに比べたデメリットとしては何よりも「連絡の手間がかかる」ことが挙げられるでしょう。gulp-renameの場合は連絡をもらえるまでに3ヶ月掛かっています。当該プロジェクトだけでなくGithubへのアクセス自体が少ない作者であれば、そもそも連絡をとる事が困難です。連絡がついたとしても提案が受理されるとは限りません(実際、断られたことも何度かあります)。早々にフォークしてしまい、独自にメンテナンスを続ける方がずっと簡単に済みます。

それでも筆者がフォークではなくメンテナンスの引き継ぎに拘るのは、似通ったモジュールの乱立を防ぐことができるからです。開発リソースが分散せずに一つのリポジトリに集約されることはもちろん、開発者の混乱を防ぐことにも繋がります。

プロジェクトへの参加を提案する方法

メンテナンスを任せても大丈夫な開発者だと認識してもらう必要があります。最も分かりやすいのは、プロジェクトに対してPull Requestなど何らかの貢献することです。継続的に何件か行っていると、作者に覚えてもらえる可能性も高まりますし、安心感も増すと思います。これまで行ってきたOSSプロジェクトのメンテナンス経験を提示するという方法もあります。

ことNodeのプロジェクトに関して言えば、リポジトリへのコミット権の付与だけでなく、作者と相談してnpmパッケージのオーナーとして登録してもらうと尚良いでしょう。リポジトリの更新をすぐにnpmパッケージに反映することができます。

リスク回避の手段として

もちろん、完全に廃れきったソフトウェアにしがみ付く必要はありませんし、見極めて早々に別の開発手段に移行するフットワークの軽さも肝心です。それでも、トレンドを追って様々なライブラリ/開発環境を渡り歩くよりは、自分が使っているソフトウェアのレガシー化を防ぐべくコミュニティに働きかけた方が、総合的に考えれば少ない労力で済む場合もあるかと思います。

2015/02/15

外部コマンドを実行するNodeプログラムの作り方

Node, JavaScript

Nodeではchild_process.spawn()などで特定のコマンドを別プロセスで実行することができますが、前提としてそのコマンドが実行可能な状態でなければなりません。当該コマンドにPATHが通っているかどうかはnode-whichなどで確認できますし、ドキュメントに「事前に◯◯というプログラムをインストールしてください」とリクワイアメントを書いておけば利用者は対応できます。

しかし、できることなら利用者にそのような手間をかけさせたくはありません。npm installコマンドひとつでNodeプログラムに必要なファイルは全て揃ってほしいものです。ではどうするのかというと、npmのパッケージと同じように必要ならバイナリーもダウンロードしてしまえば良いのです。このような、一見強引ながら極めて合理的な解決策を提供するのがbin-wrapperです。

bin-wrapperを使う

bin-wrapper の基本的な役割は、以下の通りです。

  1. 指定されたURLからファイルをダウンロードし、
  2. ダウンロードしたファイルがアーカイブであれば展開し、
  3. 指定したファイル名のバイナリーを参照するパスを提供する

ほかにもバイナリーの実行やバージョン確認の機能を備えています。

今回はGithubを使いやすくするCLIツールhubをダウンロードして実行するまでをNodeでやってみましょう。hubにはコンパイル済みバイナリーを含んだtarファイルをダウンロードできるURLがあるので、それを利用します。

'use strict';

var path = require('path');

var BinWrapper = require('bin-wrapper');

var prefix = 'https://github.com/github/hub/releases/download/v2.2.0/hub-';
var suffix = '-2.2.0.tar.gz';

var bin = new BinWrapper();

// ダウンロードURLを指定
// OS、アーキテクチャごとに異なるURLを指定可能
bin.src(prefix + 'linux-386' + suffix, 'linux', 'x86');
bin.src(prefix + 'linux-amd64' + suffix, 'linux', 'x64');
bin.src(prefix + 'windows-386' + suffix, 'win32', 'x86');
bin.src(prefix + 'mac-amd64' + suffix, 'darwin');
bin.src(prefix + 'windows-386' + suffix, 'win32', 'x86');
bin.src(prefix + 'windows-amd64' + suffix, 'win32', 'x64');

// アーカイブの展開先を指定
bin.dest(path.resolve('vendor'));

// バイナリーとして使用するファイルの指定
bin.use(process.platform === 'win32' ? 'hub.exe' : 'hub');

// ダウンロード、アーカイブ展開の実行後、インストールの成否の確認。結果をコールバックに渡す
bin.run(function (err) {
  if (err) {
    throw err;
  }

  console.log('Installed to ' + bin.path());
});

bin-wrapperをインストール (npm install bin-wrapper) した後に上記スクリプトを実行してみましょう。Installed to (カレントディレクトリ)/hub(WindowsならInstalled to (カレントディレクトリ)\hub.exe)と表示されたら成功です。vendorディレクトリにファイルがダウンロードされているはずです。

$ ls vendor
LICENSE     README.md   etc     hub     man

ここまでで、hubのアーカイブのダウンロードと展開を行いました。当初の目的であるhubコマンドの実行までを行うには、.path()メソッドを利用します。

.path()メソッドは.dest()メソッドと.use()メソッドで指定したバイナリのパスを返します。このパスを使ってchild_process.exec()などでコマンドを実行すればいいのです。簡単にhubのバージョンを表示してみましょう。

// 先のコードの bin.run(function(err) { ... }) 内を書き換え

bin.run(function (err) {
  if (err) {
    throw err;
  }

  var exec = require('child_process').exec;

  exec(bin.path() + ' --version', function(err, stdout, stderr) {
    if (err) {
      throw err;
    }

    process.stdout.write(stdout);
    process.stderr.write(stderr);
  });
});

上記コードを実行すると、以下のようにhub --versionの結果が出力されます。

git version 2.3.0
hub version 2.2.0

以上、Nodeだけでプログラムに必要なバイナリーを取得し、それを実行することができました。このプログラムを実行するのに、事前にhubをインストールしておく必要はありません。

インストールスクリプトを分離する

上記スクリプトは、バイナリーが正常にダウンロードされているかどうかを実行時に毎回確認します。プログラムを何度も実行する上では無駄な処理と言えるでしょう。解決策としては、

  1. npm installの際にバイナリーを取得する
  2. プログラム実行時にはバイナリーの取得処理をせず、ダウンロード済みのバイナリーへのパスだけを提供する

というように、インストール時と実行時それぞれ別のスクリプトを用意することが挙げられます。そこで、package.jsonscriptsフィールドを利用します。

実例として、gifsicleのラッパーであるgifsicle-binのリポジトリが参考になります。

gifsicleバイナリーのインストールを行うlib/install.jsは、エントリーポイントであるindex.jsからはrequireされていません。代わりに、postinstallスクリプトで呼び出されるようpackage.jsonで指定されています

{
  "scripts": {
    "postinstall": "node lib/install.js"
  }
}

つまり、gifsicle-binパッケージがインストールされた直後に一度だけバイナリーのインストールが行われます。このようにインストール処理をpostinstallスクリプトとして切り離し、メインのスクリプトでは一切行わないのが、bin-wrapperを使う上でのベストプラクティスであると言えます。