Loading... Search articles

Search for articles

Sorry, but we couldn't find any matches...

But perhaps we can interest you in one of our more popular articles?
Flutter用CI/CDはfastlaneとCodemagicで簡単に実現できます

Flutter用CI/CDはfastlaneとCodemagicで簡単に実現できます

Jan 11, 2022

執筆者、Maks Majer

モバイルアプリのテスト自動化やCI/CDのセットアップは、私たちITCraftshipのチームが当社のプロジェクトで一般的にやらなければならないことです。これは繰り返し行う作業ですが、これを設定するアプリごとに、かなりの時間(最大で2日間)かかっていました。

ネイティブとハイブリッドモバイル技術(Cordova、Capacitor、React Native、Flutter)用にfastlaneを使用してiOSとAndroidビルドを構成した5年間の経験に基づいて、私たちは新しいプロジェクトのCI/CD設定をジャンプスタートするために使用するテンプレートを確立しました。

本チュートリアルでは、Codemagic CIをfastlaneでセットアップし、あなたご自身の開発者アカウントを使用して、App StoreとGoogle Playの両方に、Truth or Dareゲームを公開する方法をステップバイステップでご説明いたします。この手順では、アプリ名をカスタマイズすることができますので、これを特定のアプリケーション用に修正のはごく簡単です。

アプリのリリースを頑張ってください!

Truth or Dare(真実か挑戦)-デモアプリケーション

Truth or Dare Feature graphic

このデモプロジェクトは、Flutterで作成した非常にシンプルなオープンソースのTruth or Dareアプリです。この特定のチュートリアルのための例として作成されましたが、私たちはもう少し楽しいものにしたいと思いました😉

このアプリをご紹介するショートアニメーションをご覧ください:

Truth or Dare Flutter app

このアプリケーションは、App StoreGoogle Playからダウンロードできます。

免責事項

OS

iOS用のアプリを作るには、macOSが搭載されたコンピューターが必要なので、ここではその環境を想定しています。LinuxベースのOSをお使いの場合は、多くの手順が同じ感じです。ただし、Windowsの場合は、Linux用Windowsサブシステム(Windows Subsystem for Linux)を使って、このガイドに従うことをお勧めいたします。

Shell

ここでは、デフォルトのシェルとしてzshbashを想定しています。他のものを使っている場合(例えばfish)、環境変数の設定など、いくつかのことに対して適切な構文を使用する必要があります。 この後、私たちが「環境変数を設定する必要があります」とお伝えする場合は常に(あなたの現在のターミナルインスタンスでのexportコマンドの実行に加えて)、このexportをシェルプロファイルに追加してください (例えば、.zshrc, .bashrc, .bash_profile )。

Flutter & XCodeバージョン

このアプリを書いた時点では、Flutterのバージョンは1.22.5で、XCodeのバージョンは12.3でした。Codemagicでは、これらのバージョンをYAML構成で設定できるので、あなたのCIでは何も変更されないはずです。ですが、FlutterやXCodeのバージョンで大きなブレークチェンジがあった場合、ローカルマシンでのアプリのビルドに問題が発生する可能性があります。

リポジトリのフォーク

あなたのアカウントにリポジトリをコピーするには、リポジトリの右上にあるForkボタンを押します。しばらくすると、リポジトリの1つとして表示されるはずです。

Forking the repository

準備

フォークのクローン

まず、リポジトリを任意の場所にクローンして、そのディレクトリにcdすることから始めます。

前提条件をインストール

プロジェクトをローカルにビルドして実行する前に、READMEに従って必要な依存関係をすべてインストールする必要があります。新しいflutterプロジェクトの作成方法はすでにご存知だと思いますが、必要なものをすべてインストールしたことを確認するために、READMEからいくつかステップをご紹介します:

brew tap dart-lang/dart brew install dart
brew install dart

RVM & Ruby

ruby環境が必要です。RVMを使用することをお勧めします。

  1. Macの場合、まずGPGをインストールする必要があります。例:https://gpgtools.org
  2. その後、以下を実行してRVMをインストールします:
gpg2 --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB && \
 curl -sSL https://get.rvm.io | bash -s stable
  1. RVMをインストールしたら、Rubyもインストールしてください:
rvm install 2.4.1

rubyのバージョンは .ruby-version ファイルで指定します。

プロジェクトの構築

プロジェクトをビルドするには、まず、コマンドを実行する必要があります:

flutter pub get

Android Studioを使用している場合、pubspec.yamlファイルを入力して、右上の取得ボタンを押すことも可能です。 次に、以下のコマンドを使用します:

flutter run

すべてが正常に動作し、アプリが構築されているかどうかを確認します。

fastlane

公開プロセスをより簡単に、より速くするために、fastlaneを使用することにします。Gemfileの中にすでに含まれているので、あとは実行するだけです:

bundle install

これで準備は整ったはずです。


iOSの設定

バンドル識別子

App Storeにアプリを公開するためにまず必要なことは、バンドル識別子をユニークなものに変更することです。 手っ取り早くするために、検索とすべて置換の機能を使うことにします。 com.itcraftship.truth-or-dareが出現する箇所をすべて、選んだバンドル識別子に置き換えます。

  • Android Studioの場合、Macでは⌘⇧R 、WindowsではCtrl+Shift+Rを使用します。
  • Visual Studio Codeの場合、Macでは⌘⇧F 、WindowsではCtrl+Shift+Fを使い、左上の矢印をクリックして置換を有効にする必要があります。 影響を受けるはずのファイルは以下の通りです:
fastlane/Appfile
fastlane/Fastfile
fastlane/Matchfile
ios/Runner.xcodeproj/project.pbxproj

チュートリアルのMarkdownの値も置き換わりますが、気にする必要はありません😉

チームID

次に、Appfile内のチームIDを更新する必要があります。それにアクセスするには、https://developer.apple.com/accountにアクセスし、アカウントにログインして、メンバーシップタブをクリックします。チームIDは上から3段目にあるはずです。

ここで、Appfileに移動して、team_idの値をご自分のものに置き換えます。

Apple ID

また、Apple IDを環境変数に追加すると、fastlaneまたはMatchが必要とするたびに入力するのを避けることができます。Apple IDは、ログインに使用するメールアドレスです。

export TOD_APPLE_ID=<your apple id>

アプリケーション固有のパスワード

CIがビルドをApp Store Connectにアップロードできるようにするには、アプリケーション固有のパスワードを生成する必要があります。こちらにその方法のチュートリアルがあります。

前回同様、環境変数に追加してください:

export TOD_APP_SPECIFIC_PASSWORD=<your app specific password for CI/CD>

fastlaneでアプリケーションを作成

それでは、App Store Connect内にアプリを作成したいと思います。そのために、以下のコマンドを実行します:

bundle exec fastlane produce --app_name <application name of your choice> --language <primary language e.g. en-US>

例として、以下を呼び出すことができます:

bundle exec fastlane produce --app_name "Tod Tutorial" --language en-US

その後、新しいiOSアプリケーションは、App Store ConnectでAppfileから取得したBundle IDで表示されるはずです。 アプリを作成したら、App Store Connectにアクセスし、アプリ情報(App Information)からアプリのApple IDを取得します。

このApple IDをコピーして、パイロットアクション用のFastfileに設定する必要があります。ご参考までに以下のコードをご覧ください:

pilot(
  username: APPLE_ID,
  team_id: '120815547',
  skip_submission: true,
  skip_waiting_for_build_processing: true,
  apple_id: "1546377180" # ここで設定します
)

Match

matchというツールを使いたいと考えています。このツールは、アプリケーションコード署名証明書とプロビジョニングプロファイルの管理を担当するものです。これは、チーム/会社全体でコード署名を同期させるための素晴らしい方法です。この手法については、コード署名ガイドに詳しく書かれています。

まず、証明書を格納するプライベートリポジトリを作成します(例:GitHub上に)。 次に、リポジトリのSSH URLを環境変数に追加します:

export TOD_MATCH_REPO=<repository url>

注意:後でCodemagicでMatchを使うには、ビルドマシンからのSSHアクセスが必要なため、アプリリポジトリと証明書リポジトリの両方用に、ローカルでもSSHを使用するのがベストプラクティスです。

この環境変数を設定するのは、後でMatchがコード署名の際に使用するからです。Matchfileをご参照ください。

追加のセキュリティレイヤーを含めて、証明書リポジトリにパスワードを追加したい場合(推奨)、Matchを初めて実行するときにパスワードの入力を求められます。それが終わったら、もちろんあなたの環境変数にも追加してください:

export TOD_MATCH_PASSPHRASE=[the password to encrypt/decrypt your match repository]

後で、何らかのマッチコマンドを実行する際に、このパスワードを別の環境変数に切り替える必要があります。MATCH_PASSWORDが、fastlane がデフォルトで使用するものです。以下はコマンドの例です:

MATCH_PASSWORD=$TOD_MATCH_PASSPHRASE bundle exec fastlane match development

こうすることで、Matchは毎回パスワードの入力を求めることはありません。環境変数の接頭辞を付けるというトリックは、複数の異なるアプリケーションを構築し署名するためにローカル環境をセットアップする際に非常に便利です。そのため、このTruth or Dareリポジトリの場合に、TOD_ という接頭辞を付けています。他のプロジェクトでは、TETRIS_PRO_など、プロジェクトの略称として決めた別の接頭辞を付けることができます。

これらの環境変数の設定が終わったら、新しいキーチェーンの設定を行います。これも、さまざまなプロジェクトをコード署名する際に役立つトリックです。また、異なるビルドサーバーで独自のキーチェーンを作成するのも非常に便利です。そうすることで、ご自身のキーチェーンパスワードを指定し、ロック解除と正しいキーチェーンの選択をより簡単できます。

このパスワードはそこまで機密性が高くはありませんが(コンピュータのロックを解除しない限り)、セキュリティを強化するために環境変数に移動したいとお考えになるかもしれません。また、プロジェクト用に別のキーチェーンを持つことで、クリーンな状態を維持し、ログインのキーチェーンをコード署名証明書やプロファイルで汚染しません。キーチェーンを作成するには、次のコマンドを使用します:

bundle exec fastlane ios setup_keychain

By default the keychain will be created in:

/Users/<username>/Library/Keychains/itcKeychain-db

将来的に、itcという接頭辞をプロジェクトや会社に関連するものに置き換えることもできるでしょうが、記憶の中で私たちを暖かく見守っていただくために、このままにしておいてもいいかもしれませんね 😉 ここで、証明書を作成するには、以下のコマンドを実行します:

bundle exec fastlane refresh_all_profiles

注意:アプリのプロビジョニングプロファイルがない場合、このコマンドで新しいものを作成できます。また、開発プロファイルやアドホックプロビジョニングプロファイルに新しいデバイスを追加するときは、常にこのコマンドを使用します。この特定の目的のために、Fastfileに便利なコマンドを追加しました:bundle exec fastlane add_device

Match SSH鍵

gitリポジトリに接続するための最も安全な方法は、SSH鍵を使用することです。Codemagicでデプロイビルドを開始すると、Matchを使用して署名証明書とプロビジョニングプロファイルのダウンロードを試行します。しかし、初めて試すと「不正なエラー(unauthorized error)」と表示されます。これは、証明書リポジトリが非公開であるためです。正常に接続するために、Codemagicはリポジトリへの接続に使用するSSH鍵を必要とします。秘密鍵は外部のCIツールと共有するのは危険とされているので、Codemagicが使用するだけのために、新たに作成することにします。

SSH鍵を生成するには、以下を実行します:

ssh-keygen -t ed25519

ファイル名は自由につけることができます。 パスワードのプロンプトは必ずスキップしてください(それが求められたらEnterキーを押します)。SSH鍵にパスワードを設定した場合、Codemagicはビルドエージェントのセットアップ時にこのキーを読み込むことができません。このSSH鍵は、後でCodemagicの素晴らしい環境変数暗号化機能を使って暗号化するので、ご心配なさらないでください。

ここで、プライベート証明書リポジトリを作成したアカウントに鍵を追加します。GitHubのガイドはこちらです。GitHubとは別のgitプロバイダーを使用している場合は、適切なドキュメントを参照してSSH鍵を追加してください。 この鍵は、後ほどCodemagicのセットアップ時に使用する予定です。

テスターのセットアップ

テスターを設定するには、https://appstoreconnect.apple.comにアクセスしてログインし、My Appsセクションに移動します。作成したアプリを選択し、テストフライトタブに移動します。そこで、左側の「App Store Connect Users」を選択します。

Testersの横のプラス記号を押して、アプリケーションをテストする人をチェックします。

ローカルマシンからアプリをリリース

アプリを初めてビルドする前に、以下を使用してすべてのPodをインストールしてください:

find . -name "Podfile" -execdir pod install \;

これで、ローカルマシンからアプリをリリースするための準備はすべて整ったはずです。以下のコマンドを実行するだけです:

sh ci/build_ios_qa.sh

すべてが計画通りに進んでいれば、テストフライトに輝かしい新しいビルドがアップロードされているはずです🚀


Androidの設定

Keystore

Androidアプリケーションに署名するには、キーストアファイルを生成する必要があります。以下のコマンドを実行します:

keytool -genkey -v -keystore <キーに使用するパス>/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias <選択したキーエイリアス>

その後、<app dir>/android/key.properties 内に、以下の内容でkey.properties ファイルを作成する必要があります:

storePassword=<前のステップのパスワード>
keyPassword=<前のステップのパスワード>
keyAlias=<前のステップのキーエイリアス>
storeFile=<android/appディレクトリに対する前のステップのキーストアの場所>

キーストアのパスを<app dir>/android内のどこかに設定し、android/key.propertiesファイルを正しく更新してください。

以下はコマンドの例です:

keytool -genkey -v -keystore android/app/itc-release.keystore -keyalg RSA -keysize 2048 -validity 10000 -alias tod

そして、key.propertiesファイルの内容です:

storePassword=mySuperSecretP@ss
keyPassword=mySuperSecretP@ss
keyAlias=tod
storeFile=itc-release.keystore

警告:これらのファイルを公開ソースコントロールにチェックしないことを覚えておいてください!

Androidのアプリ識別子の名前を変更

App Storeにアプリを公開するためにまず必要なことは、バンドル識別子をユニークなものに変更することです。 手っ取り早くするために、検索とすべて置換の機能を使うことにします。

com.itcraftship.truth-or-dareが出現する箇所をすべて、選んだバンドル識別子に置き換えます。

  • Android Studioの場合、Macでは⌘⇧R 、WindowsではCtrl+Shift+Rを使用します。
  • Visual Studio Codeの場合、Macでは⌘⇧F 、WindowsではCtrl+Shift+Fを使い、左上の矢印をクリックして置換を有効にする必要があります。 影響を受けるはずのファイルは以下の通りです:
android/app/build.gradle
android/app/src/debug/AndroidManifest.xml
android/app/src/main/AndroidManifest.xml
android/app/src/main/kotlin/com/itcraftship/truth_or_dare/MainActivity.kt
android/app/src/profile/AndroidManifest.xml
fastlane/Fastfile

ここでも、これによりチュートリアルのMarkdownの値が置き換わります。

Google Playでアプリケーションを作成

https://play.google.com/console にアクセスし、すべてのアプリ(All apps)タブを選択します。右上にあるアプリ作成ボタンを押します。

無料ゲームのカテゴリを選択し、宣言に同意してアプリを作成する(Create app)をクリックします:

ここで、アプリのセットアップが必要です。こんな感じになるはずです:

タスクの表示(View tasks)をクリックすると、このように表示されるはずです:

これから各セクションを見ていきます。アプリのアクセス(App access)に移動し、最初のオプションを選択します:

保存を押して、ダッシュボードに戻ります。次に、広告(Ads)に進みます:

広告なしを選択し、保存を押して次のセクション、コンテンツの評価(Content ratings)に進みます。メールアドレスを入力し、ゲームカテゴリを選択します:

次に、アンケートを開始し、可能な限りすべての選択肢でいいえ(No)を選択します:

概要画面で送信(Submit)を押し、再度ダッシュボードに移動して次のセクション、ターゲット層とコンテンツ(Target audience and content)を選択します。

年齢を13歳以上から選択し、それ以外の場合はプライバシーポリシーを提供する必要があります(もちろん、希望すれば可能です)。

次の画面でいいえ(No)を押します。

概要へ移動し、保存を押して、再度ダッシュボードに戻り、次のセクション、ニュースアプリ(News app)へ移動します。

いいえ(No)を選択し、保存して次のセクション、設定を保存する(Store settings)に進みます。

ゲームカードのカテゴリを選択します。メールアドレスを入力し、保存をクリックします。進む必要がある次のセクションは、メインストアのリスト(Main store listing)です。

ここでは、アプリ名と2種類の説明文を入力する必要があります。これは完全にあなた次第です。その後、アプリのアイコンとスクリーンショットをいくつか追加する必要があります。その後、保存を押せば、アプリの設定は完了のはずです。ここで、ダッシュボードに戻り、2つ目のセルを展開します:

テスターの選択(Select testers)に進みます。

必要なリスト名を選択するか、何もない場合は作成します。変更を保存する(Save changes)を押します。 ここで、新しいビルドを手動でアップロードする必要がありますが、まず、GooglePlay.jsonファイルを取得する必要があります。

Google Play JSON

再度、https://play.google.com/consoleにアクセスしてログインします。
アカウントを入力後、「設定」->「APIアクセス」と進みます。

リンクするプロジェクトを選択する(Choose a project to link)ボタンを押します。その後、新しいサービスアカウントを作成する(Create new service account)ボタンを押します。

小さなウィンドウが表示されるはずです:

Google Cloud Platformにアクセスし、サービスアカウントの作成(Create service account)ボタンをもう一度押します。

最初のステップでは、選択した名前と説明を入力します:

ステップ2に進み、Editorのロールを選択します:

ステップ3をスキップして、完了(Done)をクリックできます。次に、キー(Keys)セクションまで下にスクロールします。キーを追加(Add Key)新しいキーを作成する(Create new key)の順にボタンを押します。

JSON形式を選択します:

これで、お使いのマシンにダウンロードされるはずです。その後、リストに表示されるはずです:

ここで、Google Play Consoleに戻ると、新しいサービスアカウントが表示されるはずです。その横にあるアクセス権を付与する(Grant access)を押します:

下部にあるタブをアプリのアクセス許可(App permissions)に切り替えます:

アプリの追加(Add app)をクリックし、truth or dareアプリを選択します。

次の画面で次へ(Next)を押します:

そして、招待状の送信を確認します:

最後に、これが表示されるはずです:

s

最初のアップロード

プロジェクトのルートディレクトリ内にgoogle_play.jsonファイルを置きます。

警告:このファイルを公開コードレポジトリにチェックインしないことを覚えておいてください。

これで、最初の.aabを構築する準備が整ったはずです。ターミナルに移動し、プロジェクトのディレクトリに移動して、実行します:

flutter build appbundle

すべてがうまくいけば、.abbへのパスがプリントされるはずです。 ここでGoogle Playコンソールに移動し、ダッシュボードに入り、新しいリリースを作成する(Create a new release)セクションに移動します:

新規リリースを作成する(Create new release)を押し、画面中央の続行(Continue)をクリックしてから、先ほどビルドした.abbをアップロードします。

その後、保存(Save) を押し、次に リリースをレビュー(Review Release) を押し、最後に ロールアウト を押します。

これで、fastlaneを使ってGoogle Playへのローカルリリースを作成する準備が整ったはずです。そのためには、次のコマンドを実行します:

BUILD_NUMBER=0 sh ci/build_android_qa.sh

環境変数$BUILD_NUMBERは一意な値である必要があり、そうでない場合はGoogle Playがアップロードを拒否します。そのため、次回このコマンドを実行するときは、ビルドをインクリメントする必要があります。また、Codemagicは、ビルドごとにこの変数の値を設定することを覚えておいてください。ローカルで特定の番号を使用すると、同じ番号のビルドはCodemagic上で失敗します。


Codemagicのセットアップ

プロジェクトの設定

Codemagicのアカウントにログイン後、チーム(Teams)タブに移動します。

まだの方は、新しいチームを作成してください。

チーム名、ユーザー制限、課金情報など後ほど入力します。その後、以下のような画面が表示されるはずです:

チームインテグレーション(Team integrations)に移動し、使用しているサービスに接続します(e.x. GitHub)。

接続に成功すると、緑色のランプが表示されます:

ここで、共有アプリケーション(Shared application)に移動し、truth or dareのリポジトリを選択します:

次に、アプリ(Apps)タブに移動します。そこにあなたのアプリケーションが表示されるはずです。その横にあるビルドセットアップを完了させる(Finish build setup)ボタンを押して、Flutter Appを選択します。

この時点でほぼ完了です。
次に、画面右側の環境変数の暗号化(Encrypt environmental variables)をクリックします。以下のように表示されるはずです:

さて、この部分はちょっと手間がかかります。ここで、このチュートリアルで設定した環境変数をすべて貼り付けて、結果を Codemagic.yaml ファイルにコピーする必要があります。言及したファイルを入力すると、必要なすべての変数のリストが見れます。

以下はその一例です:

TOD_APPLE_ID:  Encrypted(Z0FBQUFBQmYzZ2dSVkIxQzRZYXlCS2FaMXQ1bS0waFNialQwX0NfZWxIUlNYOE9kWG5heWdPRXlIZzB3ZJHD8HFa2dtbzNiZGNScWIxZFlRVjhJZXV4MUdnNExBam9tTy1JTzVEd1hYaWY5WEk2dDBKTFVCdkk9)

以下は、publish-qaワークフローで設定する必要のあるすべての値です:


TOD_MATCH_REPO: Encrypted(...)
TOD_APPLE_ID: Encrypted(...)
TOD_APP_SPECIFIC_PASSWORD: Encrypted(...)
TOD_MATCH_PASSPHRASE: Encrypted(...)
ANDROID_KEYSTORE: Encrypted(...)
MATCH_SSH_KEY: Encrypted(...)
ANDROID_KEY_PROPERTIES: Encrypted(...)
GOOGLE_PLAY_JSON: Encrypted(...)

変更を加えた後は、コミットしてバージョン管理システムにプッシュするのを忘れないでください。

バージョンのバンプ

新しいビルドをトリガーする前に、pubspec.yamlファイルのバージョンを上げてください。major.minor.patchのバージョンか、プレリリースサフィックス-Xのどちらかを増やす必要があります。

ビルド通知の受信者を更新

ビルドの成功や失敗を通知するために、codemagic.yamlファイルの受信者(recipients) セクションを更新して、必ずご自身のメールアドレスを使用してください。

Codemagicでのビルドの実行

完了したら、新規ビルドを開始(Start new build)ボタンを押して、ウィンドウの下でcodemagic.yamlからワークフローを選択(Select workflow from codemagic.yaml)を押し、使用しているブランチを選択して、publish-qaワークフローを選択します。新規ビルドを開始(Start)を押して、ビルドが完了するのを待ちます。すべてがうまくいけば、新規ビルドがApp Store ConnectとGooglePlayにアップロードされるはずです。

ビルドのトリガー

私たちが用意したcodemagic.yamlの設定は、2つのイベントでビルドをトリガーします:

  1. mainブランチに対してPRを作成すると、pr ワークフローがトリガーされます。
  2. mainブランチのコミットにタグを付けてリモートにプッシュすると、publish-qaワークフローがトリガーされます。 これは、モバイルアプリに適切な継続的デリバリーを実装したい場合に有効です。

これを既存のFlutterアプリに使用する

新しいアプリをセットアップする際には、このチュートリアルとリポジトリREADMEをガイドとして使用し、あなたご自身の組織用に同様のボイラープレートを作成できます。新しいflutterプロジェクトにとって最も重要なのは、以下をコピーできることです:

  • codemagic.yaml
  • fastlaneディレクトリの下にある設定
  • ciディレクトリの下にあるスクリプト。さらに
  • .gitignore あとはチュートリアルに従うと準備もできるはずです。

最後に

このチュートリアルは、Codemagicとfastlaneを使用してFlutterモバイルアプリケーションのビルド自動化を設定するのに必要な時間を短縮するための包括的なガイドになるように努めました。それに従って実行した結果、新しいアプリの識別子を作成するのに2時間かからずに完了しました。このチュートリアルをご活用いただき、プロジェクトをよりスムーズに進めるのにお役に立てますと幸いです。このチュートリアルで何かご不明な点や間違いに気づかれた場合は、リポジトリでの問題提起やPRを送信していただけますと助かります。

最後までお読みいただきありがとうございます! 楽しんでビルドしましょう! 🛠 P.S. Flutter、Ionic、React Nativeに特化した経験豊富なハイブリッドモバイル開発チームをお探しの場合、contact@itcraftship.comまでお気軽にご連絡ください。

How did you like this article?

Oops, your feedback wasn't sent

Latest articles

Show more posts