【プログラミング初心者OK】アプリのスクレイピングの方法教えます!【Androidアプリ編】【実装サンプル付】
スクレイピングといえば一般的には「Webブラウザで行うもの」と思われているかもしれませんが、実はiPhoneアプリやAndroidアプリでもできることをご存知でしょうか?
例えばアプリだけで展開しているサービスだったり、ページングが多くてWebブラウザでスクレイピングするのが大変などといった時にアプリのスクレイピングが有効になります。
この記事ではアプリでのスクレイピングの方法を一からお教えします。ある程度のプログラミング知識があることが望ましいですが、初心者の方でも簡単に行っていただけるよう丁寧にご説明しますのでご安心ください。
想定読者
この記事は以下のような読者の方を想定しています。 当てはまる方はぜひ読み進めてください。
読了後にできるようになること
この記事を最後までお読みいただくと、読者の方ご自身で以下のことができるようになります。
本題に入る前に
先程から名前だけは出ていましたが、この記事ではアプリのスクレイピングにAppiumというツールを利用します。アプリの自動化(例えばエンドツーエンドテスト等)を開発する上で世界で最も広く使われているツールです。
また記事内にいくつか専門的な言葉が出てくるのであらかじめこちらで簡単に説明しておきます。
- 実機:物理的な端末のこと。iPhone実機といったら実際お持ちのiPhone端末のことを指します。
- iPhoneシミュレーター:Mac上で動作するiPhoneの仮想マシンのこと。iPhone実機を持っていなくてもiPhoneアプリを動かすことができます。
- Androidエミュレーター:Android端末の仮想マシンのこと。Android実機を持っていなくてもAndroidアプリを動かすことができます。
- Apple Developerアカウント:iPhoneアプリを開発・リリースするのに必要なアカウントのこと。スクレイピングのみでしたら無料で行えます。
前提知識
Appiumでスクレイピングを行う上で前提となる知識をご紹介します。
- iPhoneシミュレーター内にはAppStoreが存在しない、つまりアプリをインストールできないためスクレイピングには利用できない
- iPhone実機でスクレイピングするにはApple Developerアカウントが必要
したがって、あなたがiPhoneアプリとAndroidアプリどちらをスクレイピングしたいかによって必要となるものが違います。
iPhoneアプリをスクレイピングしたい場合
Androidアプリをスクレイピングしたい場合
上記をご覧いただくと分かるように、スクレイピングを始めるに当たっては、iPhoneアプリのスクレイピングの方が若干ハードルが高いです。特にこだわりが無いのであればAndroidアプリでスクレイピングを行っていただくのが良いと思います。
この記事で話すこと
この記事ではタイトルの通り、Androidアプリのスクレイピングについて解説します。iPhoneアプリのスクレイピングに関してはもしご希望が多ければ別記事を書くかもしれません。
また、AndroidアプリのスクレイピングはMacでなければいけない理由はありませんが、この記事ではMacを利用して解説します。Windows等をご利用の方はこの記事を参考にしつつ、ご自身の環境に合わせて微調整を行ってください。
この記事で話さないこと
それでは早速やっていきましょう。
この続きは下のリンクよりご覧いただけます。(※有料Note)
E2EテストのアサーションをテストコードとPageObjectどちらに持つべきかという話
E2EテストのデザインパターンとしてPageObjectというものがあります。 今回はPageObject自体の説明は省きますが、簡潔に述べるとWebページなどの詳細(idとかclassとかDOM階層とかとか)を隠して利用側(テストコード)が利用しやすいインターフェースを提供する役目という認識です私は。
より詳しい内容はマーティン・ファウラーのブログをご参照ください。
さて表題の件ですが、テストを書いているとアサーションをどこで持つべきかという課題にぶち当たります。
シンプルな例
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(単一責任の原則)にも通ずる話かと思います。
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
事前に準備するもの
- Slackにメッセージを送信するためのincoming webhook URL(参照)
- Google Play Publisher API用のprivate key(JSON形式)(参照)
- 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
エディタ関連
- 画面分割がなんか賢くなる(あんまり興味ない)
- エディタの右にファイルのミニマップが出せるようになる
- 他のエディタではよく見るようなやつ
//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
デバッグツール関連
- ネットワークのスループットをシュミレートできるようになる
- 今まで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インスタンスを、複数の
UITextFieldView
のleftView
に同時に表示することはできない - 同時には表示することができないので、
leftViewMode
を.whileEditing
に指定すれば1つのViewインスタンスを使い回すことも可能 - この辺りの仕様は公式ドキュメントには記載されてないのでツライ
やりたかったこと
UITableViewCell
のsubViewにUITextField
を配置するUITextField
のleftView
にUIImageView
を常に表示するUITextField
のendEditingのコールバックでsuperViewのUITableViewCell
をリロードする
問題
初回表示時は問題なくleftView
が表示されましたが、Cellをリロードした直後にアプリがハングし、以後メモリ使用量が増え続ける問題が発生しました。
仮説1
メモリが増え続けていたことから、まずはメモリリークを疑いました。 XcodeのInstrumentsで調べてみましたが、これといったリーク箇所を見つけることができませんでした。
仮説2
Cellのリロード直後に問題が発生していたことから、再レンダリングされる時にleftView
に指定しているViewのインスタンスが解放されていることを疑いました。
leftView
をstatic変数にするなど試してみましたが解決しませんでした。
デバッグ
UITextField
のleftViewMode
を.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回スクリーン上に表示することはできない」。
公式ドキュメントには書いてないけど・・・。
結論
公式ドキュメントには特に記載がありませんが、1つのView
インスタンスを複数のUITextField.leftView
に同時に表示することはできない、という仕様ということが判明しました。
UITableViewCell
のケースでは、Cellが再利用される(複数のViewに利用される)ことでアプリがハングしていたと考えられます。
Apple IDの2FA必須化に伴うCI環境でのfastlane実行の問題と対応
問題
Travis CIでfastlaneの実行に利用していたApple ID(foo@example.com
とする)に2FAを設定したらfastlaneがうまいこと動かなくなった。
対策1
fastlane公式ドキュメントにちゃんと対応方法が書いてある👏
以下の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 namedFASTLANE_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.com
のApple IDで作成したもの - でも
fastlane deliver
はbar@example.com
のアカウントで行うように設定してる - つまり
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD
がなにか余計なことしてる?
ということで対策1で環境変数にセットしていたFASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD
を削除してみたところ、無事にipaのアップロードに成功🎉
結論
DeployerでColofulBoxにLaravelアプリケーションをデプロイする
対象読者
- ColorfulBoxにPHPアプリケーションをデプロイしたい
- GitなどでLaravelアプリケーションのソースコードをバージョン管理してる
- composerを利用してて
vendor
ディレクトリはバージョン管理対象から外してる
ColorfulBoxの制限
ColorfulBoxはいわゆる共用サーバです。VPSや専用サーバとは違って他のユーザに影響を与えないようにいくつか制限があります。
さくらインターネットなど他の共用サーバを利用したことがないのでこれらが一般的なのか分かりませんが、ColorfulBoxには現状気づいてるだけでも以下の制限が存在しています。
- CLIのPHPバージョンはcPanelからは設定できない
- php.iniの
allow_url_fopen=Off
なのでcomposer install
とかできない - サーバにrsyncがインストールされてないのでrsyncできない
これらの制約がある中でも、これから紹介する方法を利用すれば、ローカル開発環境からコマンド1つでデプロイすることが可能になります。
ちなみに、CLIのPHPバージョンを変更する方法は以下の記事で解説しています。
tofucodes.hatenablog.jp
Deployerとは
人気のフレームワークをサポートしている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サーバのCLIのPHPバージョンはデフォルト5.6となっていて最新版のLaravelをダウンロードすることができません。そのためDeployerが利用するCLIのPHPをバージョン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
が利用されるようにします。