tofucodes diary

にほんごのほう

【プログラミング初心者OK】アプリのスクレイピングの方法教えます!【Androidアプリ編】【実装サンプル付】

スクレイピングといえば一般的には「Webブラウザで行うもの」と思われているかもしれませんが、実はiPhoneアプリAndroidアプリでもできることをご存知でしょうか?

例えばアプリだけで展開しているサービスだったり、ページングが多くてWebブラウザスクレイピングするのが大変などといった時にアプリのスクレイピングが有効になります。

この記事ではアプリでのスクレイピングの方法を一からお教えします。ある程度のプログラミング知識があることが望ましいですが、初心者の方でも簡単に行っていただけるよう丁寧にご説明しますのでご安心ください。

想定読者

この記事は以下のような読者の方を想定しています。 当てはまる方はぜひ読み進めてください。

読了後にできるようになること

この記事を最後までお読みいただくと、読者の方ご自身で以下のことができるようになります。

本題に入る前に

先程から名前だけは出ていましたが、この記事ではアプリのスクレイピングにAppiumというツールを利用します。アプリの自動化(例えばエンドツーエンドテスト等)を開発する上で世界で最も広く使われているツールです。

また記事内にいくつか専門的な言葉が出てくるのであらかじめこちらで簡単に説明しておきます。

前提知識

Appiumでスクレイピングを行う上で前提となる知識をご紹介します。

したがって、あなたがiPhoneアプリAndroidアプリどちらをスクレイピングしたいかによって必要となるものが違います。

iPhoneアプリスクレイピングしたい場合

  1. Mac
  2. iPhone実機
  3. Apple Developerアカウント(無料アカウントで可)

Androidアプリをスクレイピングしたい場合

  1. PC(Mac以外でも可)
  2. Android実機もしくはAndroidエミュレーター

上記をご覧いただくと分かるように、スクレイピングを始めるに当たっては、iPhoneアプリスクレイピングの方が若干ハードルが高いです。特にこだわりが無いのであればAndroidアプリでスクレイピングを行っていただくのが良いと思います。

この記事で話すこと

この記事ではタイトルの通り、Androidアプリのスクレイピングについて解説します。iPhoneアプリスクレイピングに関してはもしご希望が多ければ別記事を書くかもしれません。

また、AndroidアプリのスクレイピングMacでなければいけない理由はありませんが、この記事ではMacを利用して解説します。Windows等をご利用の方はこの記事を参考にしつつ、ご自身の環境に合わせて微調整を行ってください。

この記事で話さないこと

それでは早速やっていきましょう。


この続きは下のリンクよりご覧いただけます。(※有料Note)

note.com

E2EテストのアサーションをテストコードとPageObjectどちらに持つべきかという話

E2EテストのデザインパターンとしてPageObjectというものがあります。 今回はPageObject自体の説明は省きますが、簡潔に述べるとWebページなどの詳細(idとかclassとかDOM階層とかとか)を隠して利用側(テストコード)が利用しやすいインターフェースを提供する役目という認識です私は。

より詳しい内容はマーティン・ファウラーのブログをご参照ください。

www.martinfowler.com

さて表題の件ですが、テストを書いているとアサーションをどこで持つべきかという課題にぶち当たります。

シンプルな例

AppiumによるiOS/AndroidアプリへのE2Eテストを例に挙げます。 アプリ内の特定のスクリーンに対してUI要素が期待通りにレンダリングされているかテストしたいとします。 簡潔ですが以下のようなコードになると思います。(pytestを想定)

PageObject

class SomePage:
    def __init__(self, driver)
        self.driver = driver

    def title(self):
        return self.driver.find_element_by_accessibility_id('title')

テストコード

def test_title_is_displayed(self):
    # WHEN navigate to SomePage
    some_page = SomePage(self.driver)

    # THEN title is displayed as expected
    assert some_page.title.is_displayed()

SomePageというスクリーンのタイトルが期待通り表示されているかのテストになります。 上記のコードではテストコードにアサーションを持たせていますのでPageObjectはあくまで具体的なUI要素へのアクセス方法を隠蔽する役目に徹しています。

ここで冒頭に紹介したマーティン・ファウラーのブログから彼の意見を参照してみます。

I favor having no assertions in page objects. I think you can avoid duplication by providing assertion libraries for common assertions - which can also make it easier to provide good diagnostics. [3]

Page objects are commonly used for testing, but should not make assertions themselves. Their responsibility is to provide access to the state of the underlying page. It's up to test clients to carry out the assertion logic.

つまり彼はPageObjectにアサーション持たないことを好むそうです。 PageObjectの役目はそのページの状態へのアクセスを提供することであるからだと述べています。 Single Responsibility Principle(単一責任の原則)にも通ずる話かと思います。

en.wikipedia.org

you can avoid duplication by providing assertion libraries for common assertions - which can also make it easier to provide good diagnostics.

この言及に関しては、ちょっと何言ってるか分からないです。

少し複雑な例

では先程のサンプルコードに戻ります。 今回はタイトルだけではなくページの主要なUI要素全てについてテストするとします。

PageObject

class SomePage:
    def __init__(self, driver)
        self.driver = driver

    def title(self):
        return self.driver.find_element_by_accessibility_id('title')

    def description(self):
        return self.driver.find_element_by_accessibility_id('description')

    def image(self):
        return self.driver.find_element_by_accessibility_id('image')

    def button(self):
        return self.driver.find_element_by_accessibility_id('button')

テストコード

def test_a_bit_complicated(self):
    # WHEN navigate to SomePage
    some_page = SomePage(self.driver)

    # THEN title is displayed as expected
    assert some_page.title().is_displayed()
    # THEN description is displayed as expected
    assert some_page.description().is_displayed()
    # THEN image is displayed as expected
    assert some_page.image().is_displayed()
    # THEN button is displayed as expected
    assert some_page.button().is_displayed()

いかがでしょうか?テストコードにアサーションが増えてきて少し煩雑なテストになってきたような気がします。まだこれくらいならマシかもしれませんが、もしテスト対象のUI要素が10個や20個になったらどうでしょう。もはやテストコードがアサーションだらけになってよろしくない気がします。

では試しにPageObject自体にアサーションを持たせてみましょう。

PageObject

class SomePage:
    def __init__(self, driver)
        self.driver = driver

    def assert_appearance(self):
        assert self.driver.find_element_by_accessibility_id('title').is_displayed()
        assert self.driver.find_element_by_accessibility_id('description').is_displayed()
        assert self.driver.find_element_by_accessibility_id('image').is_displayed()
        assert self.driver.find_element_by_accessibility_id('button').is_displayed()

テストコード

def test_a_bit_complicated(self):
    # WHEN navigate to SomePage
    some_page = SomePage(self.driver)

    # THEN all UI components are displayed as expected
    some_page.assert_appearance()

めちゃくちゃスッキリしました。たとえUI変更があってもPageObjectの変更だけで済むこともこのメリットの1つかなと思います。

ただ、もしPageObjectにアサーションを持たせるなら、テストコードにはアサーションを一切持たせたくない気がします。 なぜならそれぞれの役目の境界が曖昧になるからです。 コードを書く人もどっちにアサーション書けばいいか迷ってしまうでしょう。 では果たして「テストコードに一切アサーションを持たない」ことは可能なのでしょうか?

より複雑な例

では次に画面上のボタンをクリックしたらボタンがdisabledになるテストも追加するとします。 アサーションはPageObjectにのみ持つという方針の場合、このようなコードになるかと思います。

テストコード

def test_more_complicated(self):
    # WHEN navigate to SomePage
    some_page = SomePage(self.driver)

    # THEN all UI components are displayed as expected
    some_page.assert_appearance()

    # WHEN click on button
    some_page.button().click()

    # THEN button becomes disabled
    some_page.assert_button_is_disabled()

PageObject

class SomePage:
    def __init__(self, driver)
        self.driver = driver

    def assert_appearance(self):
        assert self.driver.find_element_by_accessibility_id('title').is_displayed()
        assert self.driver.find_element_by_accessibility_id('description').is_displayed()
        assert self.driver.find_element_by_accessibility_id('image').is_displayed()
        assert self.button().is_displayed()

    # Accessor for the button
    def button(self):
        return self.driver.find_element_by_accessibility_id('button')

    # Assertion for verifying disabled state
    def assert_button_is_disabled(self):
        assert not self.button().is_enabled()

まあこれはこれでいいのですが、assert_button_is_disabled()が冗長な気がします。同様の実装が増えていったらPageObjectが肥大化してしまって良くないと思います。

また、せっかくbuttonのアクセサが公開されているのだから、以下のようにテストコード内で直接buttonのstateをチェックした方がシンプルではないでしょうか。

テストコード

def test_more_complicated(self):
    # WHEN navigate to SomePage
    some_page = SomePage(self.driver)

    # THEN all UI components are displayed as expected
    some_page.assert_appearance()

    # WHEN click on button
    some_page.button().click()

    # THEN button becomes disabled
    # some_page.assert_button_is_disabled()  # Before
    assert not some_page.button().is_enabled()  # After

ただこうなると先程懸念していた「テストコードとPageObjectが共にアサーションを持つ曖昧な設計」になってしまいます。

ではではassert_appearance()の方をなんとかしてみることにします。こんなのはどうでしょうか?

PageObject

class SomePage:
    def __init__(self, driver)
        self.driver = driver

    def has_correct_appearance(self):
        return self.driver.find_element_by_accessibility_id('title').is_displayed()
            and self.driver.find_element_by_accessibility_id('description').is_displayed()
            and self.driver.find_element_by_accessibility_id('image').is_displayed()
            and self.button().is_displayed()

    # Accessor for the button
    def button(self):
        return self.driver.find_element_by_accessibility_id('button')

テストコード

def test_more_complicated(self):
    # WHEN navigate to SomePage
    some_page = SomePage(self.driver)

    # THEN SomePage has correct appearance
    assert some_page.has_correct_appearance()

    # WHEN click on button
    some_page.button().click()

    # THEN button becomes disabled
    assert not some_page.button().is_enabled()

一応これでテストコードだけにアサーションを持つというデザインに出来ました。 またたとえUI変更があったとしてもPageObjectのhas_correct_appearance()の実装を更新するだけで済む点もグッドです。 ただ完全にはしっくりこない気持ち。

結論

色々と検討してみましたが、完全にしっくりくる実装方法が見つかりません。誰か教えてください。

AppStoreとGooglePlayのユーザレビューを自動・無料・簡単にSlackに配信する方法

拝啓

アプリ開発者ならユーザがAppStoreあるいはGooglePlayに投稿してくれるレビューは非常に気になりますよね。

弊社ではストアの評価がKPIなんかになってたりもしますんでユーザレビューは超x3重要です。

とはいえAppStoreやGooglePlayにわざわざ見に行くなんて原始時代はとうに終わっています。

iPhoneユーザならばAppleのConnectアプリでPush通知をしてくれたりもしますが、結局は個人個人がストアに見に行く必要があるので、

なんかこう一斉に多数の人間が閲覧できてわちゃわちゃ議論したりできる仕組みが欲しいと思うわけです。

となるとやっぱりSlackになるわけで、AppStoreとGooglePlayから自動かつ無料でユーザレビューをスレッドに垂れ流すことは一定の需要がある気がしてます。

とはいえ極論各ストアの公開APIを叩けば取得できるわけなんで無料という部分は弱めですが、今回の内容を利用すればほぼコードを書かずに実現できます。

ポイントは自動・無料・簡単です。

本題

利用するもの
  • ReviewMe
  • Heroku
事前に準備するもの
  1. Slackにメッセージを送信するためのincoming webhook URL(参照
  2. Google Play Publisher API用のprivate key(JSON形式)(参照
  3. Herokuのアカウント

それぞれの詳細は省きますので参照リンクをご覧ください。

プロジェクト作成
$ mkdir review-me-myapp
$ cd review-me-myapp
$ npm init -y
$ npm install @trademe/reviewme --save
ReviewMeの設定ファイル作成
$ touch config.json
$ vim config.json
{
  "slackHook": "{Slack hook URL}",
  "verbose": true,
  "dryRun": false,
  "interval":300,
  "apps": [
    {
      "appId": "{Androidアプリのapp ID}",
      "publisherKey": "{Google Play Publisher API private keyのpath}"
    },
    {
      "appId": "{iOSアプリのapp ID}",
      "regions": false
    }
  ]
}

regionsをfalse(=全リージョン)にしてますが、日本にしかリリースしてないアプリとかはjp指定にしてもいいと思います。

publisherKeyはフルパスで指定してしまうとHerokuでそのパスが見つけられないので、相対パスで書きましょう。

レビュー取得用のスクリプト作成
$ touch index.js
$ vim index.js
var reviewme = require("@trademe/reviewme");
var config = require("./config.json");

reviewme.start(config);

ReviewMeのモジュールと作成した設定ファイルをロードして、Reviewの処理をスタートしています。

最低限必要なコードはこの3行のみです。

Heroku用のProcfile作成
$ touch Procfile
$ vim Procfile
worker: node index.js
Herokuにプロジェクトをデプロイ
$ heroku login
$ git push heroku master

まずは自分のHerokuアカウントにログインします。そしてプロジェクトをherokuにpushします。

Push後はHerokuが勝手にプロジェクトをビルドしてProcfileで指定した処理を開始してくれます。

Herokuのプロセスを確認して以下のようにworkerが立ち上がっていれば正常に動いていると思います。

$ heroku ps
Free dyno hours quota remaining this month: 408h 14m (74%)
Free dyno usage for this app: 141h 26m (25%)
For more information on dyno sleeping and how to upgrade, see:
https://devcenter.heroku.com/articles/dyno-sleeping

=== worker (Free): node index.js (1)
worker.1: up 2019/09/11 09:54:29 +0900 (~ 23h ago)

敬具

思い出しながら書いたので足りてないところあるかもしれません。

WWDC 2019 - What's New in Xcode 11

developer.apple.com

エディタ関連

  • 画面分割がなんか賢くなる(あんまり興味ない)
  • エディタの右にファイルのミニマップが出せるようになる
    • 他のエディタではよく見るようなやつ
    • //MARK: - XXXでセクション分割もしてくれる
    • 行数が長いファイルだとちょっと便利かもしれない
  • 関数のドキュメントが賢くなる
    • 引数追加したら追加した引数の分だけドキュメントにも追加してくれる
    • 引数名を変更したらドキュメントも一緒に更新してくれる
  • バージョン管理が賢くなる
    • コードの変更した行の上に、変更前のコードをインラインで表示することができる

Swift Package関連

  • XcodeにSwift Packageを完全に統合する(Swift Package使ったことないから今までどうだったか知らんけど)
  • Project 設定画面に新たにSwift Packageタブが追加される
    • 連携してるバージョン管理システムのアカウントから楽にパッケージを追加できる
    • 自分のレポジトリ、自分が属してるOrganizationのレポジトリ、スターをつけてるレポジトリが一覧で表示されるので選ぶだけ

デザインツール関連

  • StoryboardでLight/Darkモードの表示をすぐに切り替えられるボタン
  • カスタムのシンボルを作成することができる
    • 既存のSF Symbolをカスタマイズするとかそれ系だと思う
  • AssetカタログでLight/Dark用の画像をそれぞれ登録することで自動で画像切り替えが可能になる
  • AssetカタログでLight/Dark用のColorをそれぞれ登録することで自動で色の切り替えが可能になる
    • 例えばButtonColorという1つの名前でLight=青、Dark=オレンジのように
  • Assetカタログで画像のローカライズが可能になる
    • 方法については特に言及されず?だけど多分上記と同じ感じの方法なんだろうと思う
  • 新機能Environment Overwrite
    • デバッグバーに新しいボタンが追加されて、ボタン押すとXcode上に色々な設定が書かれてるポップアップが出現する
    • シミュレータの設定を実際に変更しなくても、設定を変更した状態のUI確認ができる
    • Light/Darkモードの切り替え、テキストサイズの変更とかとか
    • いちいちシュミレータの設定をいじらなくても様々なUIの確認ができて、めちゃくちゃ便利だと思う

デバッグツール関連

  • ネットワークのスループットをシュミレートできるようになる
    • 今までNetwork Link Conditioner使ってやってたやつがXcodeだけでできるようになる感じ
    • 実機でも利用可能
  • Thermal Stateをシュミレートできるようになる
    • 端末が熱かったり冷たかったりするやつ?
    • 何に利用するんだろう

テスト関連

  • 「テストプラン」を作成可能になる
    • 同じテストケースを色々な設定で実行可能になる?
    • 例えば環境変数とか言語とか

シミュレータ関連

  • Apple WatchアプリをApple Watchシミュレータ単独で実行することができる?(今までできなかったの?)
  • Metal上でシュミレータを動かすようにしたので色んなパフォーマンスが向上

Instruments関連

  • CPUとかMetalとかSwift UIとか色々言ってたけどよく分からんかった
  • 要は色んな指標が同時に見れるようになったよってことかな

Swift UI

  • 全く新しいUI実装のフローの実現
    • 今まではコード編集→アプリ起動→デバッグ、だったものがコード編集だけに
    • つまりSwift UIを利用すれば書いたUIコードが即座にプレビューできるということ(だと思ってる)

UITextFieldViewのleftViewに隠された仕様について

TL;DR

  • 1つのViewインスタンスを、複数のUITextFieldViewleftViewに同時に表示することはできない
  • 同時には表示することができないので、leftViewMode.whileEditingに指定すれば1つのViewインスタンスを使い回すことも可能
  • この辺りの仕様は公式ドキュメントには記載されてないのでツライ

やりたかったこと

  • UITableViewCellのsubViewにUITextFieldを配置する
  • UITextFieldleftViewUIImageViewを常に表示する
  • UITextFieldのendEditingのコールバックでsuperViewのUITableViewCellをリロードする

問題

初回表示時は問題なくleftViewが表示されましたが、Cellをリロードした直後にアプリがハングし、以後メモリ使用量が増え続ける問題が発生しました。

仮説1

メモリが増え続けていたことから、まずはメモリリークを疑いました。 XcodeのInstrumentsで調べてみましたが、これといったリーク箇所を見つけることができませんでした。

仮説2

Cellのリロード直後に問題が発生していたことから、再レンダリングされる時にleftViewに指定しているViewのインスタンスが解放されていることを疑いました。 leftViewをstatic変数にするなど試してみましたが解決しませんでした。

デバッグ

UITextFieldleftViewMode.alwaysから.whileEditingに変更してみたところ、問題が発生しなくなることが判明しました。.unlessEditingだと依然として問題は発生しました。

以上のことからUITableViewに起因する問題ではなく、UITextFieldそのものが起因の問題もしくは仕様なのではないかと考えられます。

検証

UITableViewを利用せず、以下のようなシンプルなサンプルを実装して検証してみました。

class ViewController: UIViewController {

    @IBOutlet weak var textField_1: UITextField!
    @IBOutlet weak var textField_2: UITextField!

    let icon = UIImageView(image: #imageLiteral(resourceName: "any"))

    override func viewDidLoad() {
        super.viewDidLoad()

        self.textField_1.leftView = icon
        self.textField_1.leftViewMode = .always

        self.textField_2.leftView = icon
        self.textField_2.leftViewMode = .always
    }
}

案の定、アプリが動かなくなり、メモリが増え続けます。前述した問題と同じ症状です。どうやらUITextFieldに何らかの仕様か問題があることが確定しました。ググりやすくなります。

ググる

https://stackoverflow.com/a/6967456/4834226

A single UIView (or UIImageView) cannot be displayed on screen twice at the same time. I fixed this by creating separate padding views for the UITextFields.

つまり、「1つのUIView(もしくはUIImageView)は同時に2回スクリーン上に表示することはできない」。

公式ドキュメントには書いてないけど・・・。

developer.apple.com

結論

公式ドキュメントには特に記載がありませんが、1つのViewインスタンス複数のUITextField.leftView同時に表示することはできない、という仕様ということが判明しました。

UITableViewCellのケースでは、Cellが再利用される(複数のViewに利用される)ことでアプリがハングしていたと考えられます。

Apple IDの2FA必須化に伴うCI環境でのfastlane実行の問題と対応

問題

Travis CIでfastlaneの実行に利用していたApple ID(foo@example.comとする)に2FAを設定したらfastlaneがうまいこと動かなくなった。

対策1

fastlane公式ドキュメントにちゃんと対応方法が書いてある👏

docs.fastlane.tools

以下の2つの環境変数を利用して2FAのアカウントを利用する際の対応方法を試してみた。

  • FASTLANE_SESSION
  • FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD

ところが、結局毎回6digitコードを聞かれてしまいダメだった。

[06:10:52]: Login to App Store Connect (foo@example.com)
Two Factor Authentication for account 'foo@example.com' is enabled
If you're running this in a non-interactive session (e.g. server or CI)
check out https://github.com/fastlane/fastlane/tree/master/spaceship#2-step-verification
Please enter the 6 digit code: 

No output has been received in the last 10m0s, this potentially indicates a stalled build or something wrong with the build itself.
Check the details on how to adjust your build configuration on: https://docs.travis-ci.com/user/common-build-problems/#Build-times-out-because-no-output-was-received

The build has been terminated

対策2

公式マニュアルより

The easiest way to get fastlane running on a CI system is to create a separate Apple ID that doesn't have 2-factor authentication enabled - doesn't have the Account Holder role

  • Account Holderじゃなくて
  • 2FAを設定してない

別のApple IDを利用するのが一番簡単とのこと。

別のApple ID(bar@example.comとする)を試してみる。

そして、エラーになる。

[09:00:07]: Making sure the latest version on App Store Connect matches '1.9.9' from the ipa file...
[09:00:08]: '1.9.9' is the latest version on App Store Connect
[09:00:11]: Uploading metadata to App Store Connect
[09:00:13]: Successfully uploaded set of metadata to App Store Connect
[09:00:14]: Starting with the upload of screenshots...
[09:00:14]: Successfully uploaded screenshots to App Store Connect
[09:00:14]: Uploading binary to App Store Connect
[09:00:16]: Fetching password for transporter from environment variable named `FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD`
[09:00:16]: Going to upload updated app to App Store Connect
[09:00:16]: This might take a few minutes. Please don't interrupt the script.
[09:00:21]: [Transporter Error Output]: Your Apple ID or password was entered incorrectly. (-20101)
-------------------------------------------------------------------------------------
Please provide your Apple Developer Program account credentials
The login information you enter will be stored in your macOS Keychain
You can also pass the password using the `FASTLANE_PASSWORD` environment variable
See more information about it on GitHub: https://github.com/fastlane/fastlane/tree/master/credentials_manager
-------------------------------------------------------------------------------------
The login credentials for 'bar@example.com' seem to be wrong
The password was taken from the environment variable
Please make sure it is correct
[09:06:08]: Please run this tool again to apply the new password
[09:06:08]: Transporter transfer failed.
[09:06:08]:
[09:06:08]: Your Apple ID or password was entered incorrectly. (-20101)
[09:06:08]: [iTMSTransporter] [2019-03-28 09:00:21 GMT] <main> DBG-X:   parameter StatisticsPreviousCallDurationInSecs = 0.338798522

[09:06:08]: [iTMSTransporter] [2019-03-28 09:00:21 GMT] <main> DBG-X:   parameter TransporterArguments = -m upload -u bar@example.com -p **hidden value** -f /tmp/1220373112.itmsp -t DAV -t Signiant -k 100000

[09:06:08]: [iTMSTransporter] [2019-03-28 09:00:21 GMT] <main> DBG-X:   parameter Version = 1.13.0

[09:06:08]: [iTMSTransporter] [2019-03-28 09:00:21 GMT] <main> DBG-X:   parameter iTMSTransporterMode = upload

[09:06:08]: [iTMSTransporter] [2019-03-28 09:00:21 GMT] <main>  INFO: id = 20190328090021-173

[09:06:08]: [iTMSTransporter] [2019-03-28 09:00:21 GMT] <main>  INFO: iTMSTransporter Correlation Key: 16925438-6961-40a7-a474-0b6386e4ea3e

[09:06:08]: [iTMSTransporter] [2019-03-28 09:00:21 GMT] <main> DBG-X: Apple's web service operation return value:

[09:06:08]: [iTMSTransporter] [2019-03-28 09:00:21 GMT] <main> DBG-X:   parameter Errors = [Your Apple ID or password was entered incorrectly. (-20101)]

[09:06:08]: [iTMSTransporter] [2019-03-28 09:00:21 GMT] <main> DBG-X:   parameter EnableJWTForAllCalls = false

[09:06:08]: [iTMSTransporter] [2019-03-28 09:00:21 GMT] <main> DBG-X:   parameter RestartClient = false

[09:06:08]: [iTMSTransporter] [2019-03-28 09:00:21 GMT] <main> DBG-X:   parameter ErrorCode = -20101

[09:06:08]: [iTMSTransporter] [2019-03-28 09:00:21 GMT] <main> DBG-X:   parameter ErrorMessage = Your Apple ID or password was entered incorrectly. (-20101)

[09:06:08]: [iTMSTransporter] [2019-03-28 09:00:21 GMT] <main> DBG-X:   parameter Success = false

[09:06:08]: [iTMSTransporter] [2019-03-28 09:00:21 GMT] <main> ERROR: Your Apple ID or password was entered incorrectly. (-20101)

[09:06:08]: [iTMSTransporter] [2019-03-28 09:00:21 GMT] <main> DBG-X: Returning 1

[09:06:08]: iTunes Transporter output above ^
[09:06:08]: Your Apple ID or password was entered incorrectly. (-20101)
Return status of iTunes Transporter was 1: Your Apple ID or password was entered incorrectly. (-20101)
The call to the iTMSTransporter completed with a non-zero exit status: 1. This indicates a failure.

CredentialsManagerを試してみる

See more information about it on GitHub: https://github.com/fastlane/fastlane/tree/master/credentials_manager

ということなのでリンク先を参考にfastlane-credentialを試してみる。

Traviss-Mac-6:ios-app travis$ fastlane fastlane-credentials add --username bar@example.com
[✔]
Password: ************
Credential bar@example.com:************ added to keychain.

が結果は変わらず。

想像力を働かせる

[09:00:14]: Uploading binary to App Store Connect
[09:00:16]: Fetching password for transporter from environment variable named FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD
...
The login credentials for 'bar@example.com' seem to be wrong

ビルドログを何度も眺め直してから、ある仮説を立てる。

  • ipaのアップロードにFASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORDを利用してるようだ
  • このパスワードはAccount Holderのfoo@example.comApple IDで作成したもの
  • でもfastlane deliverbar@example.comのアカウントで行うように設定してる
  • つまりFASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORDがなにか余計なことしてる?

ということで対策1で環境変数にセットしていたFASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORDを削除してみたところ、無事にipaのアップロードに成功🎉

結論

  • 2FA設定してるApple IDでfastlane deliverが動かない(たぶん自分の設定が悪いけど原因わからない)
  • 2FA設定してないApple IDでfastlane deliverするときは環境変数FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORDセットしちゃうとAppStoreConnectにログインできないので注意

DeployerでColofulBoxにLaravelアプリケーションをデプロイする

対象読者

  • ColorfulBoxにPHPアプリケーションをデプロイしたい
  • GitなどでLaravelアプリケーションのソースコードをバージョン管理してる
  • composerを利用しててvendorディレクトリはバージョン管理対象から外してる

ColorfulBoxの制限

ColorfulBoxはいわゆる共用サーバです。VPSや専用サーバとは違って他のユーザに影響を与えないようにいくつか制限があります。

さくらインターネットなど他の共用サーバを利用したことがないのでこれらが一般的なのか分かりませんが、ColorfulBoxには現状気づいてるだけでも以下の制限が存在しています。

  1. CLIPHPバージョンはcPanelからは設定できない
  2. php.iniのallow_url_fopen=Offなのでcomposer installとかできない
  3. サーバにrsyncがインストールされてないのでrsyncできない

これらの制約がある中でも、これから紹介する方法を利用すれば、ローカル開発環境からコマンド1つでデプロイすることが可能になります。


ちなみに、CLIPHPバージョンを変更する方法は以下の記事で解説しています。 tofucodes.hatenablog.jp

Deployerとは

deployer.org

人気のフレームワークをサポートしているPHP製のデプロイツールです。 LaravelやCakePHP、他にもたくさんのPHPフレームワークがサポートされています。 他のデプロイツールとの比較はできませんが、数行のスクリプトでLaravelのアプリケーションがデプロイできて感動しました。

方法

🛠Deployerのインストール

$ curl -LO https://deployer.org/deployer.phar
$ mv deployer.phar /usr/local/bin/dep
$ chmod +x /usr/local/bin/dep

👨‍🍳Laravel用のレシピを利用してDeployerのスクリプトを作成

$ dep init -t Laravel

📝作成されるdeploy.phpを編集(解説は後述します)

<?php
namespace Deployer;

require 'recipe/laravel.php';

set('application', 'アプリ名');

set('repository', 'リポジトリのURL');

set('git_tty', true);

set('writable_mode', 'chmod');

// 1.
set('bin/php', '/home/ユーザ名/bin/php');

// 2.
set('bin/composer', function () {
    if (commandExist('composer')) {
        $composer = locateBinaryPath('composer');
    }
    return '{{bin/php}} -d allow_url_fopen=1 ' . $composer;
});

host('ホスト名')
    ->set('deploy_path', 'デプロイ先のパス');

after('deploy:failed', 'deploy:unlock');

before('deploy:symlink', 'artisan:migrate');

🚢デプロイ実行

$ dep deploy [-vvv]

解説

1. bin/php

set('bin/php', '/home/ユーザ名/bin/php');

ColofulBoxサーバのCLIPHPバージョンはデフォルト5.6となっていて最新版のLaravelをダウンロードすることができません。そのためDeployerが利用するCLIPHPをバージョン7.1や7.2にする必要があります。

私はシステムのPHP7.1をユーザのbinディレクトリにコピーしてPATHを通して利用しているため上記のような設定になっていますが、バージョンさえ合っていればパスはどこでもいいと思います。

2. bin/composer

set('bin/composer', function () {
    if (commandExist('composer')) {
        $composer = locateBinaryPath('composer');
    }
    return '{{bin/php}} -d allow_url_fopen=1 ' . $composer;
});

制限2.で記載した通り、ColorfulBoxサーバではphp.iniのallow_url_fopen=Offになってる都合上、単純にcomposer installを実行することができません。そこでcomposer install時に-d allow_url_fopen=1をつけることで一時的にallow_url_fopen=Onの状態を作っています。またcomposer install時のphpに関しても先ほど2.で指定したbin/phpが利用されるようにします。