diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 702a7f76..0c80cf2b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,8 +1,6 @@ -# Team Lead -* @manh-t - -# Team Members -* @doannimble @luongvo @markgravity +# Team +# @manh-t is the Team Lead +* @manh-t @doannimble @luongvo @markgravity # Engineering Leads CODEOWNERS @nimblehq/engineering-leads diff --git a/.github/wiki/Deployment.md b/.github/wiki/Deployment.md new file mode 100644 index 00000000..7465ba72 --- /dev/null +++ b/.github/wiki/Deployment.md @@ -0,0 +1,32 @@ +# Deployment Process + +## For iOS + +### Guide + +- Before deploying the iOS application, we have to prepare the necessary credentials and information: + + - An Apple developer account. + - Your application’s bundle id. This could be multiple bundle ids according to the number of application’s flavors. + - A new app's on Apple Store Connect that links to the bundle id. + - The access to Git’s [Match repository](https://codesigning.guide/). + +- Create certificates for distribution and [code signing](https://codesigning.guide/) so we are able to fetch the certificates through [Match](https://docs.fastlane.tools/actions/match/). +- Setup [Fastlane](https://docs.fastlane.tools/getting-started/ios/setup/) and [Match](https://docs.fastlane.tools/actions/match/). +- Adding these variables as the environment variable of CI: + + - **FASTLANE_USER**: This will be referred to in `Appfile` and `Fastfile`. Set your Apple ID to this variable. + - **FASTLANE_PASSWORD**: This variable will be referred to internally in Fastlane. Set the password of your Apple ID to this variable. + - **FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD**: We need to generate an `application specific password` to upload your app to TestFlight. Reference [here](https://support.apple.com/en-us/HT204397). + - **FASTLANE_SESSION**: When your Apple ID has Two-Factor Authentication or Two-Step verification information, we can’t validate your Apple ID with a prompt on your CI machine. We need to generate a login session for Apple ID in advance with `spaceauth`. Please check `Method 2: Two-step or two-factor authentication` in [here](https://docs.fastlane.tools/getting-started/ios/authentication/) to get the fastlane session and read the note in `Important note about session duration`. + - **MATCH_PASSWORD**: This will be referred to internally in `fastlane match` to decrypt your profiles. We can get the password from the one who has Admin access in Apple Store Connect. + - **KEYCHAIN_PASSWORD**: We can create a keychain password for ourself and it could be anything. + - **SSH_PRIVATE_KEY**: In order to fetch the distribution files from Git’s [Match repository](https://codesigning.guide/), we have to generate an SSH key and save it as the environment variable in CI. + +- Run the corresponding workflows CI or `fastlane` commands and enjoy the result! + +### Resource + +- [https://medium.com/flutter-community/deploying-flutter-ios-apps-with-fastlane-and-github-actions-2e87465e056e](https://medium.com/flutter-community/deploying-flutter-ios-apps-with-fastlane-and-github-actions-2e87465e056e) +- [https://docs.flutter.dev/deployment/cd](https://docs.flutter.dev/deployment/cd) +- [https://jaysavsani07.medium.com/flutter-ci-cd-with-github-actions-fastlane-part-2-ios-a4b281921d39](https://jaysavsani07.medium.com/flutter-ci-cd-with-github-actions-fastlane-part-2-ios-a4b281921d39) \ No newline at end of file diff --git a/.github/workflows/publish_docs_to_wiki.yml b/.github/workflows/publish_docs_to_wiki.yml new file mode 100644 index 00000000..b39ed12f --- /dev/null +++ b/.github/workflows/publish_docs_to_wiki.yml @@ -0,0 +1,17 @@ +name: Publish docs to Wiki +on: + push: + paths: + - .github/wiki/** + branches: + - main + +jobs: + publish_docs_to_wiki: + name: Publish Wiki + uses: nimblehq/github-actions-workflows/.github/workflows/publish_wiki.yml@0.1.0 + with: + USER_NAME: team-nimblehq + USER_EMAIL: bot@nimblehq.co + secrets: + USER_TOKEN: ${{ secrets.WIKI_ACTION_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..6565e67b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,25 @@ +name: CI +on: + # Trigger the workflow on push or pull request, + # but push action is only for the feature branch + pull_request: + types: [ opened, synchronize, reopened ] + push: + branches-ignore: + - develop + - 'release/**' + - main + +jobs: + test: + name: Template initializing scripts test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2.3.2 + + - uses: actions/setup-python@v2 + with: + python-version: '3.9' + + - name: Set new project + run: make run PACKAGE_NAME=co.nimblehq.flutter.template PROJECT_NAME=flutter_templates APP_NAME="Flutter Templates" diff --git a/.template/.github/workflows/ios_deployment.yml b/.template/.github/workflows/ios_deployment.yml new file mode 100644 index 00000000..a17645e0 --- /dev/null +++ b/.template/.github/workflows/ios_deployment.yml @@ -0,0 +1,50 @@ +name: ios-deployment +on: + # Trigger the workflow on push action + push: + branches: + - develop + +jobs: + build_and_upload_to_testflight: + name: Build And Upload iOS Application To TestFlight + runs-on: macOS-latest + env: + TEAM_ID: ${{ secrets.TEAM_ID }} + FASTLANE_USER: ${{ secrets.FASTLANE_USER }} + FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }} + FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} + FASTLANE_SESSION: ${{ secrets.FASTLANE_SESSION }} + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + steps: + - uses: actions/checkout@v1 + + - name: Install SSH key + uses: webfactory/ssh-agent@v0.4.1 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - uses: subosito/flutter-action@v1 + with: + channel: 'stable' + flutter-version: '2.10.3' + + - name: Get flutter dependencies + run: flutter pub get + + - name: Run code generator + run: flutter packages pub run build_runner build --delete-conflicting-outputs + + - name: Bundle install + run: cd ./ios && bundle install + + - name: Pod install + run: cd ./ios && pod install + + - name: Match AppStore + run: cd ./ios && bundle exec fastlane sync_appstore_staging_signing + + - name: Deploy to TestFlight + run: | + cd ./ios && bundle exec fastlane build_and_upload_testflight_app diff --git a/.template/ios/.gitignore b/.template/ios/.gitignore index 151026b9..4ce43025 100644 --- a/.template/ios/.gitignore +++ b/.template/ios/.gitignore @@ -23,8 +23,15 @@ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh +Flutter/tmp.xcconfig +Flutter/.last_build_id ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* +Build/ +DerivedData/ +fastlane/README.md +fastlane/report.xml +.envfile # Exceptions to above rules. !default.mode1v3 diff --git a/.template/ios/Gemfile b/.template/ios/Gemfile new file mode 100644 index 00000000..82d1e304 --- /dev/null +++ b/.template/ios/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +gem "fastlane" +gem "cocoapods" diff --git a/.template/ios/Gemfile.lock b/.template/ios/Gemfile.lock new file mode 100644 index 00000000..63873120 --- /dev/null +++ b/.template/ios/Gemfile.lock @@ -0,0 +1,284 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.5) + rexml + activesupport (6.1.5) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) + addressable (2.8.0) + public_suffix (>= 2.0.2, < 5.0) + algoliasearch (1.27.5) + httpclient (~> 2.8, >= 2.8.3) + json (>= 1.5.1) + artifactory (3.0.15) + atomos (0.1.3) + aws-eventstream (1.2.0) + aws-partitions (1.570.0) + aws-sdk-core (3.130.0) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.525.0) + aws-sigv4 (~> 1.1) + jmespath (~> 1.0) + aws-sdk-kms (1.55.0) + aws-sdk-core (~> 3, >= 3.127.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.113.0) + aws-sdk-core (~> 3, >= 3.127.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.4) + aws-sigv4 (1.4.0) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + claide (1.1.0) + cocoapods (1.11.3) + addressable (~> 2.8) + claide (>= 1.0.2, < 2.0) + cocoapods-core (= 1.11.3) + cocoapods-deintegrate (>= 1.0.3, < 2.0) + cocoapods-downloader (>= 1.4.0, < 2.0) + cocoapods-plugins (>= 1.0.0, < 2.0) + cocoapods-search (>= 1.0.0, < 2.0) + cocoapods-trunk (>= 1.4.0, < 2.0) + cocoapods-try (>= 1.1.0, < 2.0) + colored2 (~> 3.1) + escape (~> 0.0.4) + fourflusher (>= 2.3.0, < 3.0) + gh_inspector (~> 1.0) + molinillo (~> 0.8.0) + nap (~> 1.0) + ruby-macho (>= 1.0, < 3.0) + xcodeproj (>= 1.21.0, < 2.0) + cocoapods-core (1.11.3) + activesupport (>= 5.0, < 7) + addressable (~> 2.8) + algoliasearch (~> 1.0) + concurrent-ruby (~> 1.1) + fuzzy_match (~> 2.0.4) + nap (~> 1.0) + netrc (~> 0.11) + public_suffix (~> 4.0) + typhoeus (~> 1.0) + cocoapods-deintegrate (1.0.5) + cocoapods-downloader (1.6.1) + cocoapods-plugins (1.0.0) + nap + cocoapods-search (1.0.1) + cocoapods-trunk (1.6.0) + nap (>= 0.8, < 2.0) + netrc (~> 0.11) + cocoapods-try (1.2.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + concurrent-ruby (1.1.10) + declarative (0.0.20) + digest-crc (0.6.4) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.5.20190701) + unf (>= 0.0.5, < 1.0.0) + dotenv (2.7.6) + emoji_regex (3.2.3) + escape (0.0.4) + ethon (0.15.0) + ffi (>= 1.15.0) + excon (0.92.1) + faraday (1.10.0) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.3) + multipart-post (>= 1.2, < 3) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.0) + faraday (~> 1.0) + fastimage (2.2.6) + fastlane (2.205.1) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored + commander (~> 4.6) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (~> 2.0.0) + naturally (~> 2.2) + optparse (~> 0.1.1) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.3) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (>= 1.4.5, < 2.0.0) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3) + ffi (1.15.5) + fourflusher (2.3.1) + fuzzy_match (2.0.4) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.16.0) + google-apis-core (>= 0.4, < 2.a) + google-apis-core (0.4.2) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + webrick + google-apis-iamcredentials_v1 (0.10.0) + google-apis-core (>= 0.4, < 2.a) + google-apis-playcustomapp_v1 (0.7.0) + google-apis-core (>= 0.4, < 2.a) + google-apis-storage_v1 (0.11.0) + google-apis-core (>= 0.4, < 2.a) + google-cloud-core (1.6.0) + google-cloud-env (~> 1.0) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.2.0) + google-cloud-storage (1.36.1) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.1) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.1.2) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + memoist (~> 0.16) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.4) + domain_name (~> 0.5) + httpclient (2.8.3) + i18n (1.10.0) + concurrent-ruby (~> 1.0) + jmespath (1.6.1) + json (2.6.1) + jwt (2.3.0) + memoist (0.16.2) + mini_magick (4.11.0) + mini_mime (1.1.2) + minitest (5.15.0) + molinillo (0.8.0) + multi_json (1.15.0) + multipart-post (2.0.0) + nanaimo (0.3.0) + nap (1.1.0) + naturally (2.2.1) + netrc (0.11.0) + optparse (0.1.1) + os (1.1.4) + plist (3.6.0) + public_suffix (4.0.6) + rake (13.0.6) + representable (3.1.1) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.2.5) + rouge (2.0.7) + ruby-macho (2.5.1) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + security (0.1.3) + signet (0.16.1) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.0) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.8) + CFPropertyList + naturally + terminal-notifier (2.0.0) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.1) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + typhoeus (1.4.0) + ethon (>= 0.9.0) + tzinfo (2.0.4) + concurrent-ruby (~> 1.0) + uber (0.1.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.8.1) + unicode-display_width (1.8.0) + webrick (1.7.0) + word_wrap (1.0.0) + xcodeproj (1.21.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.3.0) + rexml (~> 3.2.4) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + zeitwerk (2.5.4) + +PLATFORMS + arm64-darwin-21 + +DEPENDENCIES + cocoapods + fastlane + +BUNDLED WITH + 2.3.10 diff --git a/.template/ios/Podfile b/.template/ios/Podfile index 1e8c3c90..9411102b 100644 --- a/.template/ios/Podfile +++ b/.template/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +platform :ios, '10.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/.template/ios/Podfile.lock b/.template/ios/Podfile.lock index 9fe1b2e5..21cd2dc7 100644 --- a/.template/ios/Podfile.lock +++ b/.template/ios/Podfile.lock @@ -29,6 +29,6 @@ SPEC CHECKSUMS: integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5 package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e -PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c +PODFILE CHECKSUM: a75497545d4391e2d394c3668e20cfb1c2bbd4aa COCOAPODS: 1.11.3 diff --git a/.template/ios/Runner.xcodeproj/project.pbxproj b/.template/ios/Runner.xcodeproj/project.pbxproj index f0fcd791..b7ed5335 100644 --- a/.template/ios/Runner.xcodeproj/project.pbxproj +++ b/.template/ios/Runner.xcodeproj/project.pbxproj @@ -368,6 +368,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 4TWS7E2EPE; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; @@ -377,6 +378,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = co.nimblehq.flutter.template.staging; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match AdHoc co.nimblehq.flutter.template.staging"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -498,6 +500,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 4TWS7E2EPE; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; @@ -507,6 +510,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = co.nimblehq.flutter.template.staging; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match AdHoc co.nimblehq.flutter.template.staging"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -521,7 +525,10 @@ APP_DISPLAY_NAME = "Flutter Templates"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 92; + DEVELOPMENT_TEAM = 4TWS7E2EPE; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; @@ -531,6 +538,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = co.nimblehq.flutter.template.staging; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match AdHoc co.nimblehq.flutter.template.staging"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -600,6 +608,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 4TWS7E2EPE; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; @@ -609,6 +618,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = co.nimblehq.flutter.template.staging; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match AdHoc co.nimblehq.flutter.template.staging"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -678,7 +688,9 @@ APP_DISPLAY_NAME = "Flutter Templates Production"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "iPhone Distribution"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 4TWS7E2EPE; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; @@ -688,6 +700,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = co.nimblehq.flutter.template; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore co.nimblehq.flutter.template"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -754,7 +767,9 @@ APP_DISPLAY_NAME = "Flutter Templates Staging"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "iPhone Distribution"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 4TWS7E2EPE; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; @@ -764,6 +779,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = co.nimblehq.flutter.template.staging; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore co.nimblehq.flutter.template.staging"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -829,7 +845,9 @@ APP_DISPLAY_NAME = "Flutter Templates Production"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "iPhone Distribution"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 4TWS7E2EPE; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; @@ -839,6 +857,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = co.nimblehq.flutter.template; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore co.nimblehq.flutter.template"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -903,6 +922,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 4TWS7E2EPE; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; @@ -912,6 +932,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = co.nimblehq.flutter.template.staging; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match AdHoc co.nimblehq.flutter.template.staging"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -975,7 +996,9 @@ APP_DISPLAY_NAME = "Flutter Templates Production"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "iPhone Distribution"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 4TWS7E2EPE; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; @@ -985,6 +1008,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = co.nimblehq.flutter.template; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore co.nimblehq.flutter.template"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/.template/ios/fastlane/Constants/Constants.rb b/.template/ios/fastlane/Constants/Constants.rb new file mode 100644 index 00000000..11d16180 --- /dev/null +++ b/.template/ios/fastlane/Constants/Constants.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +class Constants + ################# + #### PROJECT #### + ################# + + # Workspace path + def self.WORKSPACE_PATH + './Runner.xcworkspace' + end + + # Project path + def self.PROJECT_PATH + './Runner.xcodeproj' + end + + # bundle ID for Staging app + def self.BUNDLE_ID_STAGING + 'co.nimblehq.flutter.template.staging' + end + + # bundle ID for Production app + def self.BUNDLE_ID_PRODUCTION + 'co.nimblehq.flutter.template' + end + + ################# + #### BUILDING ### + ################# + + # a derived data path + def self.DERIVED_DATA_PATH + './DerivedData' + end + + # a build path + def self.BUILD_PATH + './Build' + end + + ################# + #### KEYCHAIN #### + ################# + + # Keychain name + def self.KEYCHAIN_NAME + 'github_action_keychain' + end + + def self.KEYCHAIN_PASSWORD + 'password' + end + + ################# + ### ARCHIVING ### + ################# + # an staging environment scheme name + def self.SCHEME_NAME_STAGING + 'staging' + end + + # a Production environment scheme name + def self.SCHEME_NAME_PRODUCTION + 'production' + end + + # an staging product name + def self.PRODUCT_NAME_STAGING + 'Flutter Template Staging' + end + + # a staging TestFlight product name + def self.PRODUCT_NAME_STAGING_TEST_FLIGHT + 'Flutter Template Staging' + end + + # a Production product name + def self.PRODUCT_NAME_PRODUCTION + 'Flutter Template' + end + + # a main target name + def self.MAIN_TARGET_NAME + 'Flutter Template' + end +end diff --git a/.template/ios/fastlane/Constants/Environments.rb b/.template/ios/fastlane/Constants/Environments.rb new file mode 100644 index 00000000..1044650a --- /dev/null +++ b/.template/ios/fastlane/Constants/Environments.rb @@ -0,0 +1,17 @@ +class Environments + def self.CI + ENV['CI'] + end + + def self.MANUAL_VERSION + ENV['MANUAL_VERSION'] + end + + def self.FASTLANE_USER + ENV['FASTLANE_USER'] + end + + def self.TEAM_ID + ENV['TEAM_ID'] + end +end diff --git a/.template/ios/fastlane/Fastfile b/.template/ios/fastlane/Fastfile new file mode 100644 index 00000000..868d8b70 --- /dev/null +++ b/.template/ios/fastlane/Fastfile @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require './Constants/Constants' +require './Constants/Environments' +require './Managers/BuildManager' +require './Managers/DistributionManager' +require './Managers/MatchManager' + +builder = BuildManager.new(fastlane: self) + +distribution_manager = DistributionManager.new( + fastlane: self, + build_path: Constants.BUILD_PATH +) + +match_manager = MatchManager.new( + fastlane: self, + keychain_name: Constants.KEYCHAIN_NAME, + keychain_password: Constants.KEYCHAIN_PASSWORD, + is_ci: Environments.CI +) + +before_all do + ensure_bundle_exec +end + +default_platform(:ios) + +platform :ios do + + # Code Sign + + desc 'Sync AppStore Staging match signing' + lane :sync_appstore_staging_signing do + match_manager.sync_app_store_signing(app_identifier: [Constants.BUNDLE_ID_STAGING]) + end + + desc 'Register new devices' + lane :register_new_device do + device_name = prompt(text: 'Enter the device name: ') + device_udid = prompt(text: 'Enter the device UDID: ') + device_hash = {} + device_hash[device_name] = device_udid + register_devices(devices: device_hash) + match(force: true) + end + + # Testflight + + desc 'Build and upload Staging app to Test Flight' + lane :build_and_upload_testflight_app do + set_app_version + bump_build + builder.build_app_store( + Constants.SCHEME_NAME_STAGING, + Constants.PRODUCT_NAME_STAGING, + Constants.BUNDLE_ID_STAGING, + false + ) + upload_build_to_testflight + end + + desc 'upload develop build to Test Flight' + private_lane :upload_build_to_testflight do + distribution_manager.upload_to_testflight( + product_name: Constants.PRODUCT_NAME_STAGING, + bundle_identifier: Constants.BUNDLE_ID_STAGING + ) + end + + # Private helper lanes + + desc 'check if any specific version number in build environment' + private_lane :set_app_version do + # Set up env var MANUAL_VERSION if we need to override the version number + if (Environments.MANUAL_VERSION || '') != '' + increment_version_number( + version_number: Environments.MANUAL_VERSION + ) + end + end + + desc 'set build number with number of commits' + private_lane :bump_build do + increment_build_number( + build_number: number_of_commits, + xcodeproj: Constants.PROJECT_PATH + ) + end +end diff --git a/.template/ios/fastlane/Gymfile b/.template/ios/fastlane/Gymfile new file mode 100644 index 00000000..9bf3f0b7 --- /dev/null +++ b/.template/ios/fastlane/Gymfile @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +clean(true) +export_team_id(Environments.TEAM_ID) +output_directory(Constants.BUILD_PATH) # .ipa +build_path(Constants.BUILD_PATH) # .xcarchive is stored +derived_data_path(Constants.DERIVED_DATA_PATH) # .app diff --git a/.template/ios/fastlane/Managers/BuildManager.rb b/.template/ios/fastlane/Managers/BuildManager.rb new file mode 100644 index 00000000..4ca437a9 --- /dev/null +++ b/.template/ios/fastlane/Managers/BuildManager.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class BuildManager + def initialize(fastlane:) + @fastlane = fastlane + end + + def build_app_store(scheme, product_name, bundle_identifier, include_bitcode) + @fastlane.gym( + scheme: scheme, + export_method: 'app-store', + export_options: { + provisioningProfiles: { + @bundle_identifier_staging.to_s => "match AppStore #{bundle_identifier}" + } + }, + include_bitcode: include_bitcode, + output_name: product_name + ) + end +end diff --git a/.template/ios/fastlane/Managers/DistributionManager.rb b/.template/ios/fastlane/Managers/DistributionManager.rb new file mode 100644 index 00000000..db05cb76 --- /dev/null +++ b/.template/ios/fastlane/Managers/DistributionManager.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class DistributionManager + def initialize(fastlane:, build_path:) + @fastlane = fastlane + @build_path = build_path + end + + def upload_to_testflight(product_name:, bundle_identifier:) + @fastlane.pilot( + ipa: "#{@build_path}/#{product_name}.ipa", + app_identifier: bundle_identifier, + notify_external_testers: false, + skip_waiting_for_build_processing: true + ) + end +end diff --git a/.template/ios/fastlane/Managers/MatchManager.rb b/.template/ios/fastlane/Managers/MatchManager.rb new file mode 100644 index 00000000..a32cb632 --- /dev/null +++ b/.template/ios/fastlane/Managers/MatchManager.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +class MatchManager + def initialize( + fastlane:, + keychain_name:, + keychain_password:, + is_ci: + ) + @fastlane = fastlane + @keychain_name = keychain_name + @keychain_password = keychain_password + @is_ci = is_ci + end + + def sync_adhoc_signing(app_identifier:) + if @is_ci + create_ci_keychain + @fastlane.match( + type: 'adhoc', + keychain_name: @keychain_name, + keychain_password: @keychain_password, + app_identifier: app_identifier, + readonly: true + ) + else + @fastlane.match(type: 'adhoc', app_identifier: app_identifier, readonly: true) + end + end + + def sync_app_store_signing(app_identifier:) + if @is_ci + create_ci_keychain + @fastlane.match( + type: 'appstore', + keychain_name: @keychain_name, + keychain_password: @keychain_password, + app_identifier: app_identifier, + readonly: true + ) + else + @fastlane.match(type: 'appstore', app_identifier: app_identifier, readonly: true) + end + end + + def create_ci_keychain + @fastlane.create_keychain( + name: @keychain_name, + password: @keychain_password, + default_keychain: true, + unlock: true, + timeout: 3600, + lock_when_sleeps: false + ) + end +end diff --git a/.template/ios/fastlane/Matchfile b/.template/ios/fastlane/Matchfile new file mode 100644 index 00000000..1ae56cd5 --- /dev/null +++ b/.template/ios/fastlane/Matchfile @@ -0,0 +1,15 @@ +git_url("git@github.com:nimblehq/match-certificates.git") + +storage_mode("git") + +# type("appstore") # The default type, can be: appstore, adhoc, enterprise or development + +# app_identifier(["tools.fastlane.app", "tools.fastlane.app2"]) +# username("user@fastlane.tools") # Your Apple Developer Portal username + +# For all available options run `fastlane match --help` +# Remove the # in the beginning of the line to enable the other options + +# The docs are available on https://docs.fastlane.tools/actions/match +readonly(true) +force(false) diff --git a/.template/pubspec.yaml b/.template/pubspec.yaml index 9cbdf5df..b4b7d3ae 100644 --- a/.template/pubspec.yaml +++ b/.template/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.6.0+7 +version: 0.7.0+8 environment: sdk: ">=2.16.1 <3.0.0" diff --git a/Makefile b/Makefile index 9c904f57..e8776343 100644 --- a/Makefile +++ b/Makefile @@ -13,12 +13,15 @@ APP_NAME= APP_VERSION=0.1.0 BUILD_NUMBER=1 -# Add the variable to env +RUNNING_TEST_MODE=1 # 0: false, 1: true + +export PROJECT_PATH export PACKAGE_NAME export PROJECT_NAME export APP_NAME export APP_VERSION export BUILD_NUMBER +export RUNNING_TEST_MODE .DEFAULT: help help: @@ -46,4 +49,5 @@ init: prepare-dev test: prepare-dev $(PYTHON) ./scripts/test.py +run: RUNNING_TEST_MODE=0 run: init test diff --git a/scripts/setup.py b/scripts/setup.py index 4709419c..b475c8d1 100644 --- a/scripts/setup.py +++ b/scripts/setup.py @@ -32,7 +32,7 @@ def __init__(self, p): self.project = p self.initial_folder = p.project_path + os.sep + "android" - def get_old_package(self): + def get_current_package_name(self): build_file = self.initial_folder + os.sep + ANDROID_MODULE + os.sep + "build.gradle" f = open(build_file, "r") file_text = f.read() @@ -42,7 +42,7 @@ def get_old_package(self): return line.strip().split(" ")[1].strip().replace("\"", "") return None - def get_old_app_name(self): + def get_current_app_name(self): string_res_file = self.initial_folder + os.sep + ANDROID_MODULE + os.sep + "src" + \ os.sep + "main" + os.sep + "res" + os.sep + "values" + os.sep + "strings.xml" f = open(string_res_file, "r") @@ -88,7 +88,7 @@ def move_folders(self, old_package): self.move_code_folder(str(f), old_package) def repackage(self): - old_package = self.get_old_package() + old_package = self.get_current_package_name() if old_package is not None and old_package != self.project.new_package: self.check_original_route(old_package) self.update_name(self.initial_folder, old_package, self.project.new_package) @@ -101,7 +101,7 @@ def repackage(self): print("Reusing old package name in Android!") def rename_app(self): - old_app_name = self.get_old_app_name() + old_app_name = self.get_current_app_name() if old_app_name is not None and old_app_name != self.project.new_app_name: self.update_name(self.initial_folder, old_app_name, self.project.new_app_name) print("✅ Rename Android app successfully!") @@ -147,7 +147,7 @@ def __init__(self, p): os.sep + "Runner.xcodeproj" + os.sep + "project.pbxproj" self.info_file = p.project_path + os.sep + "ios" + os.sep + "Runner" + os.sep + "Info.plist" - def get_old_package(self): + def get_current_package_name(self): f = open(self.project_file, "r") file_text = f.read() f.close() @@ -156,7 +156,7 @@ def get_old_package(self): return line.strip().split(" = ")[1].strip().replace(";", "").replace(".staging", "") return None - def get_old_app_name(self): + def get_current_app_name(self): f = open(self.project_file, "r") file_text = f.read() f.close() @@ -166,7 +166,7 @@ def get_old_app_name(self): .replace(";", "").replace(" Staging", "").replace(" Production", "") return None - def get_old_project_name(self): + def get_current_project_name(self): f = open(self.info_file, "r") file_text = f.read() f.close() @@ -194,7 +194,7 @@ def replace_text_in_file(self, file_path, contain_text, old_text, new_text): f.close() def repackage(self): - old_package = self.get_old_package() + old_package = self.get_current_package_name() if old_package is not None and old_package != self.project.new_package: self.replace_text_in_file(file_path=self.project_file, contain_text="PRODUCT_BUNDLE_IDENTIFIER", old_text=old_package, new_text=self.project.new_package) @@ -205,7 +205,7 @@ def repackage(self): print("Reusing old package name in iOS!") def rename_app(self): - old_app_name = self.get_old_app_name() + old_app_name = self.get_current_app_name() if old_app_name is not None and old_app_name != self.project.new_app_name: self.replace_text_in_file(file_path=self.project_file, contain_text="APP_DISPLAY_NAME", old_text=old_app_name, new_text=self.project.new_app_name) @@ -217,7 +217,7 @@ def rename_app(self): print("Reusing old app name in iOS!") def rename_project(self): - old_project_name = self.get_old_project_name() + old_project_name = self.get_current_project_name() if old_project_name is not None and old_project_name != self.project.new_project_name: self.replace_text_in_file(file_path=self.project_file, contain_text=old_project_name, old_text=old_project_name, new_text=self.project.new_project_name) @@ -411,16 +411,20 @@ def clean_up(files: list[str]): ) validate_parameters(project) - options = { - 'none' : 'none', - 'kebab (kebab-case)' : 'kebab', - 'snake (snake_case)' : 'snake', - 'pascal (PascalCase)' : 'pascal' - } - choice = enquiries.choose('Choose default json_serializable.field_rename: ', options.keys()) - project.json_serializable = JsonSerializable(options[choice]) - - print(f"=> 🐢 Staring init {project.new_project_name} with {project.new_package}...") + if os.environ.get("CI") != "true": + options = { + 'none': 'none', + 'kebab (kebab-case)': 'kebab', + 'snake (snake_case)': 'snake', + 'pascal (PascalCase)': 'pascal' + } + choice = enquiries.choose('Choose default json_serializable.field_rename: ', options.keys()) + project.json_serializable = JsonSerializable(options[choice]) + else: + # Skip enquiries on CI + project.json_serializable = JsonSerializable('snake') + + print(f"=> 🐢 Starting init {project.new_project_name} with {project.new_package}...") android = Android(project) android.run() ios = Ios(project) diff --git a/scripts/test.py b/scripts/test.py index 2e9b29d5..b0773611 100644 --- a/scripts/test.py +++ b/scripts/test.py @@ -6,29 +6,35 @@ from setup import Android, Project, Ios, Flutter -project = Project() -project.project_path = os.path.curdir -project.new_package = 'com.example' -project.new_app_name = '' -project.new_project_name = 'example_project' - -package_name = "co.nimblehq.flutter.template" -project_name = "flutter_templates" -app_name = "Flutter Templates" - +# After creating a new project, the new project will be created in the current directory. +expected_project_path = os.environ.get("PROJECT_PATH").replace("/.template", "") if os.environ.get("RUNNING_TEST_MODE") == "0" else os.environ.get("PROJECT_PATH") +expected_package_name = os.environ.get("PACKAGE_NAME") if bool(os.environ.get("PACKAGE_NAME")) else "co.nimblehq.flutter.template" +expected_app_name = os.environ.get("APP_NAME") if bool(os.environ.get("APP_NAME")) else "Flutter Templates" +expected_project_name = os.environ.get("PROJECT_NAME") if bool(os.environ.get("PROJECT_NAME")) else "flutter_templates" +expected_app_version = os.environ.get("APP_VERSION") if bool(os.environ.get("APP_VERSION")) else "0.1.0" +expected_build_number = os.environ.get("BUILD_NUMBER") if bool(os.environ.get("BUILD_NUMBER")) else "1" + +project = Project( + expected_project_path, + expected_package_name, + expected_app_name, + expected_project_name, + expected_app_version, + expected_build_number +) class AndroidTest(unittest.TestCase): def setUp(self): self.android = Android(project) def test_get_old_package(self): - self.assertEqual(self.android.get_old_package(), package_name) + self.assertEqual(self.android.get_current_package_name(), expected_package_name) def test_get_old_app_name(self): - self.assertEqual(self.android.get_old_app_name(), app_name) + self.assertEqual(self.android.get_current_app_name(), expected_app_name) def test_check_original_route(self): - old_package = self.android.get_old_package() + old_package = self.android.get_current_package_name() self.assertEqual(self.android.check_original_route(old_package), None) @@ -37,13 +43,13 @@ def setUp(self): self.ios = Ios(project) def test_get_old_package(self): - self.assertEqual(self.ios.get_old_package(), package_name) + self.assertEqual(self.ios.get_current_package_name(), expected_package_name) def test_get_old_app_name(self): - self.assertEqual(self.ios.get_old_app_name(), app_name) + self.assertEqual(self.ios.get_current_app_name(), expected_app_name) def test_get_old_project_name(self): - self.assertEqual(self.ios.get_old_project_name(), project_name) + self.assertEqual(self.ios.get_current_project_name(), expected_project_name) class FlutterTest(unittest.TestCase): @@ -51,7 +57,7 @@ def setUp(self): self.flutter = Flutter(project) def test_get_old_project_name(self): - self.assertEqual(self.flutter.get_old_project_name(), project_name) + self.assertEqual(self.flutter.get_old_project_name(), expected_project_name) if __name__ == '__main__':