{"id":1884,"date":"2021-06-16T12:01:10","date_gmt":"2021-06-16T12:01:10","guid":{"rendered":"https:\/\/dev95.site\/ios-monorepo-ci-pipelines\/"},"modified":"2021-06-16T12:01:10","modified_gmt":"2021-06-16T12:01:10","slug":"ios-monorepo-ci-pipelines","status":"publish","type":"post","link":"https:\/\/dev95.site\/ar\/ios-monorepo-ci-pipelines\/","title":{"rendered":"<div>iOS Monorepo &#038; CI Pipelines<\/div>"},"content":{"rendered":"<div id=\"dev95-1755418382\" class=\"dev95-- dev95-entity-placement\" role=\"region\" aria-label=\"\u0627\u0644\u0625\u0639\u0644\u0627\u0646\u0627\u062a\"><script async=\"async\" data-cfasync=\"false\" src=\"https:\/\/pl27862732.profitableratecpmnetwork.com\/2ad7a50e0bbc23ac6801d7b77c501463\/invoke.js\"><\/script>\r\n<div id=\"container-2ad7a50e0bbc23ac6801d7b77c501463\"><\/div><\/div><div id=\"dev95-375686529\" class=\"dev95-before-content dev95-entity-placement\" role=\"region\" aria-label=\"\u0627\u0644\u0625\u0639\u0644\u0627\u0646\u0627\u062a\"><p style=\"text-align: center;\"><strong>Stop wasting time on dead links! \ud83d\uded1 This smart platform automatically detects your device and country to give you the exact best offer instantly. Check it out now 100% free! \ud83d\udc47<\/strong><br data-sfc-root=\"ep\" data-sfc-pl=\"|||[]\" data-complete=\"true\" data-copy-service-computed-style=\"font-family: Arial, sans-serif, &quot;Noto Color Emoji&quot;; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);\" \/><a href=\"https:\/\/www.profitableratecpmnetwork.com\/pvhx5mbcc?key=ff7d362db3c20bc86fad685cc94f1fc4\">\ud83d\udd17 <strong class=\"rQesXe MPyX\" data-sfc-cp=\"\" data-sfc-root=\"ep\" data-complete=\"true\" aria-owns=\"action-menu-parent-container\" data-copy-service-computed-style=\"font-family: Arial, sans-serif, &quot;Noto Color Emoji&quot;; font-size: 16px; font-weight: 700; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);\">[Click here]<\/strong><\/a><\/p>\n<\/div><div><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2021\/06\/GettyImages-1297512889.jpg?w=1280&#038;ssl=1\" alt=\"iOS Monorepo &amp; CI Pipelines\"><\/p><div id=\"dev95-1496814934\" class=\"dev95- dev95-entity-placement\" role=\"region\" aria-label=\"\u0627\u0644\u0625\u0639\u0644\u0627\u0646\u0627\u062a\"><center>\r\n<script>\r\n  atOptions = {\r\n    'key' : '4ba6b6513c00e0ba76511f798ae56401',\r\n    'format' : 'iframe',\r\n    'height' : 50,\r\n    'width' : 320,\r\n    'params' : {}\r\n  };\r\n<\/script>\r\n<script src=\"https:\/\/www.highrevenueformat.com\/4ba6b6513c00e0ba76511f798ae56401\/invoke.js\"><\/script>\r\n\t<\/center>\r\n<br\/><\/div>\n<p><em>Originally published on the <\/em><a href=\"https:\/\/medium.com\/takeaway-tech\/ios-monorepo-ci-pipelines-99a3b69240a9?ref=albertodebortoli.com\"><em>Just Eat Takeaway Engineering Blog<\/em><\/a><em>.<\/em><\/p>\n<p>We have presented our modular iOS architecture in a previous <a href=\"https:\/\/albertodebortoli.com\/2019\/12\/19\/modular-ios-architecture-at-just-eat\/\">article<\/a> and I gave a <a href=\"https:\/\/www.youtube.com\/watch?v=QzM3lsFewN4&amp;ref=albertodebortoli.com\" rel=\"noopener nofollow\">talk<\/a> at Swift Heroes 2020 about it. In this article, we\u2019ll analyse the challenges we faced to have the modular architecture integrated with our CI pipelines and the reasoning behind migrating to a monorepo.<\/p>\n<h2 id=\"the-problem\">The Problem<\/h2>\n<p>Having several modules in separate repositories brings forward 2 main problems:<\/p>\n<ol>\n<li>Each module is versioned independently from the consuming app<\/li>\n<li>Each change involves at least 2 pull requests: 1 for the module and 1 for the integration in the app<\/li>\n<\/ol>\n<p>While the above was acceptable in a world where we had 2 different codebases, it soon became unnecessarily convoluted after we migrated to a new, global codebase. New module versions are implemented with the ultimate goal of being adopted by the only global codebase in use, making us realise we could simplify the change process.<\/p>\n<p>The monorepo approach has been discussed at length by the community for a few years now. Many talking points have come out of these conversations, even leading to an interesting story as told by Uber. In short, it entails putting all the code owned by the team in a single repository, precisely solving the 2 problems stated above.<\/p>\n<h2 id=\"monorepo-structure\">Monorepo structure<\/h2>\n<p>The main advantage of a monorepo is a streamlined PR process that doesn\u2019t require us to raise multiple PRs, de facto reducing the number of pull requests to one.<\/p>\n<p>It also simplifies the versioning, allowing module and app code (ultimately shipped together) to be aligned using the same versioning.<\/p>\n<p>The first step towards a monorepo was to move the content of the repositories of the modules to the main app repo (we\u2019ll call it \u201cmonorepo\u201d from now on). Since we rely on <a href=\"https:\/\/cocoapods.org\/?ref=albertodebortoli.com\" rel=\"noopener nofollow\">CocoaPods<\/a>, the modules would be consumed as<a href=\"https:\/\/guides.cocoapods.org\/making\/making-a-cocoapod.html?ref=albertodebortoli.com#development\" rel=\"noopener nofollow\"> development pods<\/a>.<\/p>\n<p>Here\u2019s a brief summary of the steps used to migrate a module to the monorepo:<\/p>\n<ul>\n<li>Inform the relevant teams about the upcoming migration<\/li>\n<li>Make sure there are no open PRs in the module repo<\/li>\n<li>Make the repository read-only and archive it<\/li>\n<li>Copy the module to the Modules folder of the monorepo (it\u2019s possible to<a href=\"https:\/\/saintgimp.org\/2013\/01\/22\/merging-two-git-repositories-into-one-repository-without-losing-file-history\/?ref=albertodebortoli.com\" rel=\"noopener nofollow\"> merge 2 repositories to keep the history<\/a> but we felt we wanted to keep the process simple, the old history is still available in the old repo anyway)<\/li>\n<li>Delete the module <code>.git<\/code> folder (or it would cause a git submodule)<\/li>\n<li>Remove <code>Gemfile<\/code> and <code>Gemfile.lock<\/code> <code>fastlane<\/code> folder, <code>.gitignore<\/code> file, <code>sonar-project.properties<\/code>, <code>.swiftlint.yml<\/code> so to use those in the monorepo<\/li>\n<li>Update the monorepo\u2019s <code>CODEOWNERS<\/code> file with the module codeowners<\/li>\n<li>Remove the <code>.github<\/code> folder<\/li>\n<li>Modify the app <code>Podfile<\/code> to point to the module as a dev pod and install it<\/li>\n<li>Make sure all the modules\u2019 demo apps in the monorepo refer to the new module as a dev pod (if they depend on it at all). The same applies to the module under migration.<\/li>\n<li>Delete the CI jobs related to the module<\/li>\n<li>Leave the podspecs in the private Specs repo (might be needed to build old versions of the app)<\/li>\n<\/ul>\n<p>The above assumes that CI is configured in a way that preserves the same integration steps upon a module change. We\u2019ll discuss them later in this article.<\/p>\n<p>Not all the modules could be migrated to the monorepo, due to the fact the second-level dependencies need to live in separate repositories in order to be referenced in the podspec of a development pod. If not done correctly, CocoaPods will not be able to install them. We considered moving these dependencies to the monorepo whilst maintaining separate versioning, however, the main problem with this approach is that the version tags might conflict with the ones of the app. Even though CocoaPods supports tags that don\u2019t respect <a href=\"https:\/\/semver.org\/?ref=albertodebortoli.com\" rel=\"noopener nofollow\">semantic versioning<\/a> (for example prepending the tag with the name of the module), violating it just didn\u2019t feel right.<\/p>\n<p>EDIT: we\u2019ve learned that it\u2019s possible to move such dependencies to the monorepo. This is done not by defining <code>:path=&gt;<\/code> in the podspecs but instead by doing so in the Podfile of the main app, which is all Cocoapods needs to work out the location of the dependency on disk.<\/p>\n<h3 id=\"swift-package-manager-considerations\">Swift Package Manager considerations<\/h3>\n<p>We investigated the possibility of migrating from CocoaPods to Apple\u2019s <a href=\"https:\/\/swift.org\/package-manager\/?ref=albertodebortoli.com\" rel=\"noopener nofollow\">Swift Package Manager<\/a>. Unfortunately, when it comes to handling the equivalent of development pods, Swift Package Manager really falls down for us. It turns out that Swift Package Manager only supports<a href=\"https:\/\/stackoverflow.com\/a\/50095567?ref=albertodebortoli.com\" rel=\"noopener nofollow\"> one package per repo<\/a>, which is frustrating because the process of working with editable packages is surprisingly powerful and transparent.<\/p>\n<h2 id=\"version-pinning-rules\">Version pinning rules<\/h2>\n<p>While development pods don\u2019t need to be versioned, other modules still need to. This is either because of their open-source nature or because they are second-level dependencies (referenced in other modules\u2019 podspecs).<\/p>\n<p>Here\u2019s a revised overview of the current modular architecture in 2021.<\/p>\n<figure class=\"kg-card kg-image-card kg-width-wide\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2021\/06\/The-Just-Eat-iOS-Stack---Holistic-Design-2021.png?resize=950%2C653&#038;ssl=1\" class=\"kg-image\" alt=\"iOS Monorepo &amp; CI Pipelines\" loading=\"lazy\" width=\"950\" height=\"653\" srcset=\"https:\/\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/size\/w600\/2021\/06\/The-Just-Eat-iOS-Stack---Holistic-Design-2021.png 600w, https:\/\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2021\/06\/The-Just-Eat-iOS-Stack---Holistic-Design-2021.png 950w\"><\/figure>\n<p>We categorised our pods to better clarify what rules should apply when it comes to version pinning both in the <code>Podfile<\/code>s and in the <code>podspec<\/code>s.<\/p>\n<h3 id=\"open-source-pods\">Open-Source pods<\/h3>\n<p>Our open-source repositories on <a href=\"https:\/\/github.com\/justeat?ref=albertodebortoli.com\" rel=\"noopener nofollow\">github.com\/justeat<\/a> are only used by the app.<\/p>\n<ul>\n<li><em>Examples: <\/em>JustTweak, AutomationTools, Shock<\/li>\n<li><em>Pinning in other modules\u2019 podspec:<\/em> <strong>NOT APPLICABLE<\/strong> open-source pods don\u2019t appear in any podspec, those that do are called \u2018open-source shared\u2019<\/li>\n<li><em>Pinning in other modules\u2019 Podfile (demo apps): <\/em><strong>PIN<\/strong> (e.g. AutomationTools in Orders demo app\u2019s Podfile)<\/li>\n<li><em>Pinning in main app\u2019s Podfile: <\/em><strong>PIN<\/strong> (e.g. AutomationTools)<\/li>\n<\/ul>\n<h3 id=\"open-source-shared-pods\">Open-Source shared pods<\/h3>\n<p>The Just Eat pods we put open-source on <a href=\"https:\/\/github.com\/justeat?ref=albertodebortoli.com\" rel=\"noopener nofollow\">github.com\/justeat<\/a> and are used by modules and apps.<\/p>\n<ul>\n<li><em>Examples:<\/em> JustTrack, JustLog, ScrollingStackViewController, ErrorUtilities<\/li>\n<li><em>Pinning in other modules\u2019 podspec:<\/em> <strong>PIN<\/strong> w\/ optimistic operator (e.g. JustTrack in Orders)<\/li>\n<li><em>Pinning in other modules\u2019 Podfile (demo apps): <\/em><strong>PIN<\/strong> (e.g. JustTrack in Orders demo app\u2019s Podfile)<\/li>\n<li><em>Pinning in main app\u2019s Podfile: <\/em><strong>DON\u2019T LIST<\/strong> latest compatible version is picked by CocoaPods (e.g. JustTrack). <strong>LIST &amp; PIN<\/strong> if the pod is explicitly used in the app too, so we don\u2019t magically inherit it.<\/li>\n<\/ul>\n<h3 id=\"internal-domain-pods\">Internal Domain pods<\/h3>\n<p>Domain modules (yellow).<\/p>\n<ul>\n<li><em>Examples:<\/em> Orders, SERP, etc.<\/li>\n<li><em>Pinning in other modules\u2019 podspec:<\/em> <strong>NOT APPLICABLE<\/strong> domain pods don\u2019t appear in other pods\u2019 podspecs (domain modules don\u2019t depend on other domain modules)<\/li>\n<li><em>Pinning in other modules\u2019 Podfile (demo apps): <\/em><strong>PIN<\/strong> only if the pod is used in the app code, rarely the case (e.g. Account in Orders demo app\u2019s Podfile)<\/li>\n<li><em>Pinning in main app\u2019s Podfile: <\/em><strong>PIN<\/strong> (e.g. Orders)<\/li>\n<\/ul>\n<h3 id=\"internal-core-pods\">Internal Core pods<\/h3>\n<p>Core modules (blue) minus those open-source.<\/p>\n<ul>\n<li><em>Examples:<\/em> APIClient, AssetProvider<\/li>\n<li><em>Pinning in other modules\u2019 podspec:<\/em> <strong>NOT APPLICABLE<\/strong> core pods don\u2019t appear in other pods\u2019 podspecs (core modules are only used in the app(s))<\/li>\n<li><em>Pinning in other modules\u2019 Podfile (demo apps): <\/em><strong>PIN<\/strong> only if pod is used in the app code (e.g. APIClient in Orders demo app\u2019s Podfile)<\/li>\n<li><em>Pinning in main app\u2019s Podfile: <\/em><strong>PIN<\/strong> (e.g. NavigationEngine)<\/li>\n<\/ul>\n<h3 id=\"internal-shared-pods\">Internal shared pods<\/h3>\n<p>Shared modules (green) minus those open-source.<\/p>\n<ul>\n<li><em>Examples:<\/em> JustUI, JustAnalytics<\/li>\n<li><em>Pinning in other modules\u2019 podspec:<\/em> <strong>DON\u2019T PIN<\/strong> (e.g. JustUI in Orders podspec)<\/li>\n<li><em>Pinning in other modules\u2019 Podfile (demo apps): <\/em><strong>PIN<\/strong> (e.g. JustUI in Orders demo app\u2019s Podfile)<\/li>\n<li><em>Pinning in main app\u2019s Podfile: <\/em><strong>PIN<\/strong> (e.g. JustUI)<\/li>\n<\/ul>\n<h3 id=\"external-shared-pods\">External shared pods<\/h3>\n<p>Any non-Just Eat pod used by any internal or open-source pod.<\/p>\n<ul>\n<li><em>Examples:<\/em> Usabilla, SDWebImage<\/li>\n<li><em>Pinning in other modules\u2019 podspec:<\/em> <strong>PIN<\/strong> (e.g. Usabilla in Orders)<\/li>\n<li><em>Pinning in other modules\u2019 Podfile (demo apps): <\/em><strong>DON\u2019T LIST<\/strong> because the version is forced by the podspec. <strong>LIST &amp; PIN<\/strong> if the pod is explicitly used in the app too, so we don\u2019t magically inherit it. Pinning is irrelevant but good practice.<\/li>\n<li><em>Pinning in main app\u2019s Podfile: <\/em><strong>DON\u2019T LIST<\/strong> because the version is forced by the podspec(s). <strong>LIST &amp; PIN<\/strong> if the pod is explicitly used in the app too, so we don\u2019t magically inherit it. Pinning is irrelevant but good practice.<\/li>\n<\/ul>\n<h3 id=\"external-pods\">External pods<\/h3>\n<p>Any non-Just Eat pod used by the app only.<\/p>\n<ul>\n<li><em>Examples:<\/em> Instabug, GoogleAnalytics<\/li>\n<li><em>Pinning in other modules\u2019 podspec:<\/em> <strong>NOT APPLICABLE<\/strong> external pods don\u2019t appear in any podspec, those that do are called \u2018external shared\u2019<\/li>\n<li><em>Pinning in other modules\u2019 Podfile (demo apps): <\/em><strong>PIN<\/strong> only if the pod is used in the app code, rarely the case (e.g. Promis)<\/li>\n<li><em>Pinning in main app\u2019s Podfile: <\/em><strong>PIN<\/strong> (e.g. Adjust)<\/li>\n<\/ul>\n<p>Pinning is a good solution because it guarantees that we always build the same software regardless of new released versions of dependencies. It\u2019s also true that pinning every dependency all the time makes the dependency graph hard to keep updated. This is why we decided to allow some flexibility in some cases.<\/p>\n<p>Following is some more reasoning.<\/p>\n<h3 id=\"open-source\">Open-source<\/h3>\n<p>For \u201copen-source shared\u201d pods, we are optimistic enough (pun intended) to tolerate the usage of the optimistic operator <code>~&gt;<\/code> in podspecs of other pods (i.e Orders using JustTrack) so that when a new patch version is released, the consuming pod gets it for free upon running <code>pod update<\/code>.<\/p>\n<p>We have control over our code and, by respecting semantic versioning, we guarantee the consuming pod to always build. In case of new minor or major versions, we would have to update the podspecs of the consuming pods, which is appropriate.<\/p>\n<p>Also, we do need to list any \u201copen-source shared\u201d pod in the main app\u2019s Podfile only if directly used by the app code.<\/p>\n<h3 id=\"external\">External<\/h3>\n<p>We don\u2019t have control over the \u201cexternal\u201d and \u201cexternal shared\u201d pods, therefore we always pin the version in the appropriate place. New patch versions might not respect semantic versioning for real and we don\u2019t want to pull in new code unintentionally. As a rule of thumb, we prefer injecting external pods instead of creating a dependency in the podspec.<\/p>\n<h3 id=\"internal\">Internal<\/h3>\n<p>Internal shared pods could change frequently (not as much as domain modules). For this reason, we\u2019ve decided to relax a constraint we had and not to pin the version in the podspec. This might cause the consuming pod to break when a new version of an \u201cinternal shared\u201d pod is released and we run <code>pod update<\/code>. This is a compromise we can tolerate. The alternative would be to pin the version causing too much work to update the podspec of the domain modules.<\/p>\n<h2 id=\"continuous-integration-changes\">Continuous Integration changes<\/h2>\n<p>With modules in separate repositories, the CI was quite simply replicating the same steps for each module:<\/p>\n<ul>\n<li>install pods<\/li>\n<li>run unit tests<\/li>\n<li>run UI tests<\/li>\n<li>generated code coverage<\/li>\n<li>submit code coverage to SonarQube<\/li>\n<\/ul>\n<p>Moving the modules to the monorepo meant creating smart CI pipelines that would run the same steps upon modules\u2019 changes.<\/p>\n<p>If a pull request is to change only app code, there is no need to run any step for the modules, just the usual steps for the app:<\/p>\n<figure class=\"kg-card kg-image-card kg-width-wide\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2021\/06\/0_iE4VY6rYjdrFzu5K.png?resize=1280%2C146&#038;ssl=1\" class=\"kg-image\" alt=\"iOS Monorepo &amp; CI Pipelines\" loading=\"lazy\" width=\"1280\" height=\"146\" srcset=\"https:\/\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/size\/w600\/2021\/06\/0_iE4VY6rYjdrFzu5K.png 600w, https:\/\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/size\/w1000\/2021\/06\/0_iE4VY6rYjdrFzu5K.png 1000w, https:\/\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2021\/06\/0_iE4VY6rYjdrFzu5K.png 1600w\" sizes=\"auto, (min-width: 1200px) 1200px\"><\/figure>\n<p>If instead, a pull request applies changes to one or more modules, we want the pipeline to first run the steps for the modules, and then the steps for the app:<\/p>\n<figure class=\"kg-card kg-image-card kg-width-wide\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2021\/06\/0_JEC2Pxzbispwi531.png?resize=1280%2C122&#038;ssl=1\" class=\"kg-image\" alt=\"iOS Monorepo &amp; CI Pipelines\" loading=\"lazy\" width=\"1280\" height=\"122\" srcset=\"https:\/\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/size\/w600\/2021\/06\/0_JEC2Pxzbispwi531.png 600w, https:\/\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/size\/w1000\/2021\/06\/0_JEC2Pxzbispwi531.png 1000w, https:\/\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2021\/06\/0_JEC2Pxzbispwi531.png 1600w\" sizes=\"auto, (min-width: 1200px) 1200px\"><\/figure>\n<p>Even if there are no changes in the app code, module changes could likely impact the app behaviour, so it\u2019s important to always run the app tests.<\/p>\n<p>We have achieved the above setup through constructing our Jenkins pipelines dynamically. The solution should scale when new modules are added to the monorepo and for this reason, it\u2019s important that all modules:<\/p>\n<ul>\n<li>respect the same project setup (generated by CocoaPods w\/ the <code>pod lib create<\/code> command)<\/li>\n<li>use the same naming conventions for the test schemes (<code>UnitTests<\/code>\/<code>ContractTests<\/code>\/<code>UITests<\/code>)<\/li>\n<li>make use of Apple Test Plans<\/li>\n<li>are in the same location ( <code>.\/Modules\/<\/code> folder).<\/li>\n<\/ul>\n<p>Following is an excerpt of the code that constructs the modules\u2019 stages from the Jenkinsfile used for pull request jobs.<\/p>\n<pre><code class=\"language-Groovy\">scripts = load \".\/Jenkins\/scripts\/scripts.groovy\"\n\ndef modifiedModules = scripts.modifiedModulesFromReferenceBranch(env.CHANGE_TARGET)\n\ndef modulesThatNeedUpdating = scripts.modulesThatNeedUpdating(env.CHANGE_TARGET)\n\ndef modulesToRun = (modulesThatNeedUpdating + modifiedModules).unique()\n\nsh \"echo \"List of modules modified on this branch: ${modifiedModules}\"\"\n\nsh \"echo \"List of modules that need updating: ${modulesThatNeedUpdating}\"\"\n\nsh \"echo \"Pipeline will run the following modules: ${modulesToRun}\"\"\n\nfor (int i = 0; i &lt; modulesToRun.size(); ++i) {\n    def moduleName = modulesToRun[i]\n    stage('Run pod install') {\n        sh \"bundle exec fastlane pod_install module:${moduleName}\"\n    }\n\n    def schemes = scripts.testSchemesForModule(moduleName)\n    schemes.each { scheme -&gt;\n        switch (scheme) {\n            case \"UnitTests\":\n                stage(\"${moduleName} Unit Tests\") {\n                    sh \"bundle exec fastlane module_unittests \n                    module_name:${moduleName} \n                    device:'${env.IPHONE_DEVICE}'\"\n                }\n                stage(\"Generate ${moduleName} code coverage\") {\n                    sh \"bundle exec fastlane generate_sonarqube_coverage_xml\"\n                }\n                stage(\"Submit ${moduleName} code coverage to SonarQube\") {\n                    sh \"bundle exec fastlane sonar_scanner_pull_request \n                    component_type:'module' \n                    source_branch:${env.BRANCH_NAME} \n                    target_branch:${env.CHANGE_TARGET} \n                    pull_id:${env.CHANGE_ID} \n                    project_key:'ios-${moduleName}' \n                    project_name:'iOS ${moduleName}' \n                    sources_path:'.\/Modules\/${moduleName}\/${moduleName}'\"\n                }\n                break;\n            case \"ContractTests\":\n                stage('Install pact mock service') {\n                    sh \"bundle exec fastlane install_pact_mock_service\"\n                }\n                stage(\"${moduleName} Contract Tests\") {\n                    sh \"bundle exec fastlane module_contracttests \n                    module_name:${moduleName} \n                    device:'${env.IPHONE_DEVICE}'\"\n                }\n                break;\n            case \"UITests\":\n                stage(\"${moduleName} UI Tests\") {\n                    sh \"bundle exec fastlane module_uitests \n                    module_name:${moduleName} \n                    number_of_simulators:${env.NUMBER_OF_SIMULATORS} \n                    device:'${env.IPHONE_DEVICE}'\"\n                }\n                break;\n            default: break;\n        }\n    }\n}<\/code><\/pre>\n<p>and here are the helper functions to make it all work:<\/p>\n<pre><code class=\"language-Groovy\">def modifiedModulesFromReferenceBranch(String referenceBranch) {\n    def script = \"git diff --name-only remotes\/origin\/${referenceBranch}\"\n    def filesChanged = sh script: script, returnStdout: true\n    Set modulesChanged = []\n    filesChanged.tokenize(\"n\").each {\n        def components = it.split('\/')\n        if (components.size() &gt; 1 &amp;&amp; components[0] == 'Modules') { \n            def module = components[1]\n            modulesChanged.add(module)\n        }\n    }\n    return modulesChanged\n}\n\ndef modulesThatNeedUpdating(String referenceBranch) {\n    def modifiedModules = modifiedModulesFromReferenceBranch(referenceBranch)\n    def allModules = allMonorepoModules()\n    def modulesThatNeedUpdating = []\n    for (module in allModules) {\n        def podfileLockPath = \"Modules\/${module}\/Example\/Podfile.lock\"\n        def dependencies = podfileDependencies(podfileLockPath)\n        def dependenciesIntersection = dependencies.intersect(modifiedModules) as TreeSet\n        Boolean moduleNeedsUpdating = (dependenciesIntersection.size() &gt; 0)\n        if (moduleNeedsUpdating == true &amp;&amp; modifiedModules.contains(module) == false) {\n            modulesThatNeedUpdating.add(module)\n        }\n    }\n    return modulesThatNeedUpdating\n}\n\ndef podfileDependencies(String podfileLockPath) {\n    def dependencies = []\n    def fileContent = readFile(file: podfileLockPath)\n    fileContent.tokenize(\"n\").each { line -&gt;\n        def lineComponents = line.split('\\(')\n        if (lineComponents.length &gt; 1) {\n            def dependencyLineSubComponents = lineComponents[0].split('-')\n            if (dependencyLineSubComponents.length &gt; 1) {\n                def moduleName = dependencyLineSubComponents[1].trim()\n                dependencies.add(moduleName)\n            }\n        }\n    }\n    return dependencies\n}\n\ndef allMonorepoModules() {\n    def modulesList = sh script: \"ls Modules\", returnStdout: true\n    return modulesList.tokenize(\"n\").collect { it.trim() }\n}\n\ndef testSchemesForModule(String moduleName) {\n    def script = \"xcodebuild -project .\/Modules\/${moduleName}\/Example\/${moduleName}.xcodeproj -list\"\n    def projectEntitites = sh script: script, returnStdout: true\n    def schemesPart = projectEntitites.split('Schemes:')[1]\n    def schemesPartLines = schemesPart.split(\/n\/)\n    def trimmedLined = schemesPartLines.collect { it.trim() }\n    def filteredLines = trimmedLined.findAll { !it.allWhitespace }\n    def allowedSchemes = ['UnitTests', 'ContractTests', 'UITests']\n    def testSchemes = filteredLines.findAll { allowedSchemes.contains(it) }\n    return testSchemes\n}<\/code><\/pre>\n<p>You might have noticed the <code>modulesThatNeedUpdating<\/code> method in the code above. Each module comes with a demo app using the dependencies listed in its Podfile and it\u2019s possible that other monorepo modules are listed there as development pods. This not only means that we have to run the steps for the main app, but also the steps for every module consuming modules that show changes.<\/p>\n<p>For example, the Orders demo app uses APIClient, meaning that pull requests with changes in APIClient will generate pipelines including the Orders steps.<\/p>\n<h2 id=\"pipeline-parallelization\">Pipeline parallelization<\/h2>\n<p>Something we initially thought was sensible to consider is the parallelisation of the pipelines across different nodes. We use parallelisation for the release pipelines and learned that, while it seems to be a fundamental requirement at first, it soon became apparent not to be so desirable nor truly fundamental for the pull requests pipeline.<\/p>\n<p>We\u2019ll discuss our CI setup in a separate article, but suffice to say that we have aggressively optimized it and managed to reduce the agent pool from 10 to 5, still maintaining a good level of service.<\/p>\n<p>Parallelisation sensibly complicates the Jenkinsfiles and their maintainability, spreads the cost of checking out the repository across nodes and makes the logs harder to read. The main benefit would come from running the app UI tests on different nodes. In the WWDC<a href=\"https:\/\/developer.apple.com\/videos\/play\/wwdc2019\/413\/?ref=albertodebortoli.com\" rel=\"noopener nofollow\"> session 413<\/a>, Apple recommends generating the <code>.xctestrun<\/code> file using the build-for-testing option in xcodebuild and distribute it across the other nodes. Since our app is quite large, such file is also large and transferring it has its costs, both in time and bandwidth usage.<\/p>\n<p>All things considered, we decided to keep the majority of our pipelines serial.<\/p>\n<p>EDIT: In 2022 we have parallelised our PR pipeline in 4 branches:<\/p>\n<ul>\n<li>Validation steps (linting, Fastlane lanes tests, etc.)<\/li>\n<li>App unit tests<\/li>\n<li>App UI tests (short enough that there&#8217;s no need to share <code>.xctestrun<\/code> across nodes)<\/li>\n<li>Modified modules unit tests<\/li>\n<li>Modified modules UI tests<\/li>\n<\/ul>\n<h2 id=\"conclusions\">Conclusions<\/h2>\n<p>We have used the setup described in this article since mid-2020 and we are very satisfied with it. We discussed the pipeline used for the pull requests which is the most relevant one when it comes to embracing a monorepo structure. We have a few more pipelines for various use cases, such as verifying changes in release branches, keeping the code coverage metrics up-to-date with jobs running of triggers, archiving the app for internal usage and for App Store.<\/p>\n<p>We hope to have given you some useful insights on how to structure a monorepo and its CI pipelines, especially if you have a structure similar to ours.<\/p>\n<\/div>\n<div class=\"pvc_clear\"><\/div>\n<p id=\"pvc_stats_1884\" class=\"pvc_stats total_only  \" data-element-id=\"1884\" style=\"\"><i class=\"pvc-stats-icon large\" aria-hidden=\"true\"><svg aria-hidden=\"true\" focusable=\"false\" data-prefix=\"far\" data-icon=\"chart-bar\" role=\"img\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" viewBox=\"0 0 512 512\" class=\"svg-inline--fa fa-chart-bar fa-w-16 fa-2x\"><path fill=\"currentColor\" d=\"M396.8 352h22.4c6.4 0 12.8-6.4 12.8-12.8V108.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v230.4c0 6.4 6.4 12.8 12.8 12.8zm-192 0h22.4c6.4 0 12.8-6.4 12.8-12.8V140.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v198.4c0 6.4 6.4 12.8 12.8 12.8zm96 0h22.4c6.4 0 12.8-6.4 12.8-12.8V204.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v134.4c0 6.4 6.4 12.8 12.8 12.8zM496 400H48V80c0-8.84-7.16-16-16-16H16C7.16 64 0 71.16 0 80v336c0 17.67 14.33 32 32 32h464c8.84 0 16-7.16 16-16v-16c0-8.84-7.16-16-16-16zm-387.2-48h22.4c6.4 0 12.8-6.4 12.8-12.8v-70.4c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v70.4c0 6.4 6.4 12.8 12.8 12.8z\" class=\"\"><\/path><\/svg><\/i> <img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"16\" height=\"16\" alt=\"Loading\" src=\"https:\/\/i0.wp.com\/dev95.site\/wp-content\/plugins\/page-views-count\/ajax-loader-2x.gif?resize=16%2C16&#038;ssl=1\" border=0 \/><\/p>\n<div class=\"pvc_clear\"><\/div>\n<div id=\"dev95-677763360\" class=\"dev95-after-content dev95-entity-placement\" role=\"region\" aria-label=\"\u0627\u0644\u0625\u0639\u0644\u0627\u0646\u0627\u062a\"><p style=\"text-align: center;\"><strong>Finally, a link that actually works for your region and device! \ud83c\udf0d Get instant access to the top exclusive offers tailored just for you right now. Don&#8217;t miss out, click here! \ud83d\udc47<\/strong><br data-sfc-root=\"ep\" data-sfc-pl=\"|||[]\" data-complete=\"true\" data-copy-service-computed-style=\"font-family: Arial, sans-serif, &quot;Noto Color Emoji&quot;; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);\" \/><a href=\"https:\/\/www.profitableratecpmnetwork.com\/uzeja8ahze?key=bc876be53d6ad0ff6370ab8ea030e479\"><strong>\ud83d\udd17 [Click here]<\/strong><\/a><\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"<p>Originally published on the Just Eat Takeaway Engineering Blog. We have presented our modular iOS architecture in a previous article and I gave a talk at Swift Heroes 2020 about it. In this article, we\u2019ll analyse the challenges we faced<\/p>\n<div class=\"hosteria-entry-more\"><a href=\"https:\/\/dev95.site\/ar\/ios-monorepo-ci-pipelines\/\" class=\"no-underline font-light  group-hover:text-primary-800 dark:group-hover:text-primary-300 py-1\">Read more &gt;&gt;&gt;<\/a><\/div>\n<div class=\"pvc_clear\"><\/div>\n<p id=\"pvc_stats_1884\" class=\"pvc_stats total_only\" data-element-id=\"1884\" style=\"\"><i class=\"pvc-stats-icon large\" aria-hidden=\"true\"><svg aria-hidden=\"true\" focusable=\"false\" data-prefix=\"far\" data-icon=\"chart-bar\" role=\"img\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" viewbox=\"0 0 512 512\" class=\"svg-inline--fa fa-chart-bar fa-w-16 fa-2x\"><path fill=\"currentColor\" d=\"M396.8 352h22.4c6.4 0 12.8-6.4 12.8-12.8V108.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v230.4c0 6.4 6.4 12.8 12.8 12.8zm-192 0h22.4c6.4 0 12.8-6.4 12.8-12.8V140.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v198.4c0 6.4 6.4 12.8 12.8 12.8zm96 0h22.4c6.4 0 12.8-6.4 12.8-12.8V204.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v134.4c0 6.4 6.4 12.8 12.8 12.8zM496 400H48V80c0-8.84-7.16-16-16-16H16C7.16 64 0 71.16 0 80v336c0 17.67 14.33 32 32 32h464c8.84 0 16-7.16 16-16v-16c0-8.84-7.16-16-16-16zm-387.2-48h22.4c6.4 0 12.8-6.4 12.8-12.8v-70.4c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v70.4c0 6.4 6.4 12.8 12.8 12.8z\" class=\"\"><\/path><\/svg><\/i> <img loading=\"lazy\" decoding=\"async\" width=\"16\" height=\"16\" alt=\"Loading\" src=\"https:\/\/dev95.site\/wp-content\/plugins\/page-views-count\/ajax-loader-2x.gif\" border=\"0\" \/><\/p>\n<div class=\"pvc_clear\"><\/div>","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"fp_fajr_begins":"","fp_fajr_iqamah":"","fp_dhuhr_begins":"","fp_dhuhr_iqamah":"","fp_asr_begins":"","fp_asr_iqamah":"","fp_maghrib_begins":"","fp_maghrib_iqamah":"","fp_isha_begins":"","fp_isha_iqamah":"","fp_midnight":"","fp_midnight_name":"","fp_sunrise":"","fp_single_prayer_begins_title":"","fp_single_prayer_iqamah_title":"","fp_prayer_times_for_today":"","fp_hijra_date":"","fp_fajr_name":"","fp_dhuhr_name":"","fp_asr_name":"","fp_maghrib_name":"","fp_isha_name":"","fp_sunrise_name":"","fp_currentDate":"","fp_current_time":"","fp_current_title":"","fp_current_location":"","fp_masjid_name":"","fp_prayer_title":"","fp_next_prayer_iqamah_time":"","fp_next_prayer_iqamah_title":"","fp_next_prayer_begins_time":"","fp_next_prayer_begins_title":"","fp_next_prayer_title":"","_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[37],"tags":[],"class_list":["post-1884","post","type-post","status-publish","format-standard","hentry","category-posts"],"a3_pvc":{"activated":true,"total_views":22,"today_views":1},"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.8 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>iOS Monorepo &amp; CI Pipelines - Dev95<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/dev95.site\/ar\/ios-monorepo-ci-pipelines\/\" \/>\n<meta property=\"og:locale\" content=\"ar_AR\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"iOS Monorepo &amp; CI Pipelines - Dev95\" \/>\n<meta property=\"og:description\" content=\"Originally published on the Just Eat Takeaway Engineering Blog. We have presented our modular iOS architecture in a previous article and I gave a talk at Swift Heroes 2020 about it. In this article, we\u2019ll analyse the challenges we facedRead more &gt;&gt;&gt;\" \/>\n<meta property=\"og:url\" content=\"https:\/\/dev95.site\/ar\/ios-monorepo-ci-pipelines\/\" \/>\n<meta property=\"og:site_name\" content=\"Dev95\" \/>\n<meta property=\"article:published_time\" content=\"2021-06-16T12:01:10+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2021\/06\/GettyImages-1297512889.jpg\" \/>\n<meta name=\"author\" content=\"dev95\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"\u0643\u064f\u062a\u0628 \u0628\u0648\u0627\u0633\u0637\u0629\" \/>\n\t<meta name=\"twitter:data1\" content=\"dev95\" \/>\n\t<meta name=\"twitter:label2\" content=\"\u0648\u0642\u062a \u0627\u0644\u0642\u0631\u0627\u0621\u0629 \u0627\u0644\u0645\u064f\u0642\u062f\u0651\u0631\" \/>\n\t<meta name=\"twitter:data2\" content=\"14 \u062f\u0642\u064a\u0642\u0629\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/dev95.site\\\/ios-monorepo-ci-pipelines\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/ios-monorepo-ci-pipelines\\\/\"},\"author\":{\"name\":\"dev95\",\"@id\":\"https:\\\/\\\/dev95.site\\\/#\\\/schema\\\/person\\\/b807805ffe2916206b04d0938bce0298\"},\"headline\":\"iOS Monorepo &#038; CI Pipelines\",\"datePublished\":\"2021-06-16T12:01:10+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/ios-monorepo-ci-pipelines\\\/\"},\"wordCount\":2356,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/ios-monorepo-ci-pipelines\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/storage.ghost.io\\\/c\\\/ae\\\/f4\\\/aef4d625-32a2-417b-86f4-22c70a9b47a1\\\/content\\\/images\\\/2021\\\/06\\\/GettyImages-1297512889.jpg\",\"articleSection\":[\"Posts\"],\"inLanguage\":\"ar\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/dev95.site\\\/ios-monorepo-ci-pipelines\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/dev95.site\\\/ios-monorepo-ci-pipelines\\\/\",\"url\":\"https:\\\/\\\/dev95.site\\\/ios-monorepo-ci-pipelines\\\/\",\"name\":\"iOS Monorepo & CI Pipelines - Dev95\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/ios-monorepo-ci-pipelines\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/ios-monorepo-ci-pipelines\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/storage.ghost.io\\\/c\\\/ae\\\/f4\\\/aef4d625-32a2-417b-86f4-22c70a9b47a1\\\/content\\\/images\\\/2021\\\/06\\\/GettyImages-1297512889.jpg\",\"datePublished\":\"2021-06-16T12:01:10+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/ios-monorepo-ci-pipelines\\\/#breadcrumb\"},\"inLanguage\":\"ar\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/dev95.site\\\/ios-monorepo-ci-pipelines\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"ar\",\"@id\":\"https:\\\/\\\/dev95.site\\\/ios-monorepo-ci-pipelines\\\/#primaryimage\",\"url\":\"https:\\\/\\\/storage.ghost.io\\\/c\\\/ae\\\/f4\\\/aef4d625-32a2-417b-86f4-22c70a9b47a1\\\/content\\\/images\\\/2021\\\/06\\\/GettyImages-1297512889.jpg\",\"contentUrl\":\"https:\\\/\\\/storage.ghost.io\\\/c\\\/ae\\\/f4\\\/aef4d625-32a2-417b-86f4-22c70a9b47a1\\\/content\\\/images\\\/2021\\\/06\\\/GettyImages-1297512889.jpg\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/dev95.site\\\/ios-monorepo-ci-pipelines\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/dev95.site\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"iOS Monorepo &#038; CI Pipelines\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/dev95.site\\\/#website\",\"url\":\"https:\\\/\\\/dev95.site\\\/\",\"name\":\"Dev95\",\"description\":\"Your comprehensive digital platform for knowledge, services, tools, and entertainment.\",\"publisher\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/dev95.site\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"ar\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/dev95.site\\\/#organization\",\"name\":\"Dev95\",\"url\":\"https:\\\/\\\/dev95.site\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"ar\",\"@id\":\"https:\\\/\\\/dev95.site\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/i0.wp.com\\\/dev95.site\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/rbrrbr-6.png?fit=512%2C512&ssl=1\",\"contentUrl\":\"https:\\\/\\\/i0.wp.com\\\/dev95.site\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/rbrrbr-6.png?fit=512%2C512&ssl=1\",\"width\":512,\"height\":512,\"caption\":\"Dev95\"},\"image\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/dev95.site\\\/#\\\/schema\\\/person\\\/b807805ffe2916206b04d0938bce0298\",\"name\":\"dev95\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"ar\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/a70a73d950838b20cd80d7ebdc955737e802e8cd896044c5473b32b946c0662a?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/a70a73d950838b20cd80d7ebdc955737e802e8cd896044c5473b32b946c0662a?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/a70a73d950838b20cd80d7ebdc955737e802e8cd896044c5473b32b946c0662a?s=96&d=mm&r=g\",\"caption\":\"dev95\"},\"url\":\"https:\\\/\\\/dev95.site\\\/ar\\\/author\\\/mohammad\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"iOS Monorepo & CI Pipelines - Dev95","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/dev95.site\/ar\/ios-monorepo-ci-pipelines\/","og_locale":"ar_AR","og_type":"article","og_title":"iOS Monorepo & CI Pipelines - Dev95","og_description":"Originally published on the Just Eat Takeaway Engineering Blog. We have presented our modular iOS architecture in a previous article and I gave a talk at Swift Heroes 2020 about it. In this article, we\u2019ll analyse the challenges we facedRead more &gt;&gt;&gt;","og_url":"https:\/\/dev95.site\/ar\/ios-monorepo-ci-pipelines\/","og_site_name":"Dev95","article_published_time":"2021-06-16T12:01:10+00:00","og_image":[{"url":"https:\/\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2021\/06\/GettyImages-1297512889.jpg","type":"","width":"","height":""}],"author":"dev95","twitter_card":"summary_large_image","twitter_misc":{"\u0643\u064f\u062a\u0628 \u0628\u0648\u0627\u0633\u0637\u0629":"dev95","\u0648\u0642\u062a \u0627\u0644\u0642\u0631\u0627\u0621\u0629 \u0627\u0644\u0645\u064f\u0642\u062f\u0651\u0631":"14 \u062f\u0642\u064a\u0642\u0629"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/dev95.site\/ios-monorepo-ci-pipelines\/#article","isPartOf":{"@id":"https:\/\/dev95.site\/ios-monorepo-ci-pipelines\/"},"author":{"name":"dev95","@id":"https:\/\/dev95.site\/#\/schema\/person\/b807805ffe2916206b04d0938bce0298"},"headline":"iOS Monorepo &#038; CI Pipelines","datePublished":"2021-06-16T12:01:10+00:00","mainEntityOfPage":{"@id":"https:\/\/dev95.site\/ios-monorepo-ci-pipelines\/"},"wordCount":2356,"commentCount":0,"publisher":{"@id":"https:\/\/dev95.site\/#organization"},"image":{"@id":"https:\/\/dev95.site\/ios-monorepo-ci-pipelines\/#primaryimage"},"thumbnailUrl":"https:\/\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2021\/06\/GettyImages-1297512889.jpg","articleSection":["Posts"],"inLanguage":"ar","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/dev95.site\/ios-monorepo-ci-pipelines\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/dev95.site\/ios-monorepo-ci-pipelines\/","url":"https:\/\/dev95.site\/ios-monorepo-ci-pipelines\/","name":"iOS Monorepo & CI Pipelines - Dev95","isPartOf":{"@id":"https:\/\/dev95.site\/#website"},"primaryImageOfPage":{"@id":"https:\/\/dev95.site\/ios-monorepo-ci-pipelines\/#primaryimage"},"image":{"@id":"https:\/\/dev95.site\/ios-monorepo-ci-pipelines\/#primaryimage"},"thumbnailUrl":"https:\/\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2021\/06\/GettyImages-1297512889.jpg","datePublished":"2021-06-16T12:01:10+00:00","breadcrumb":{"@id":"https:\/\/dev95.site\/ios-monorepo-ci-pipelines\/#breadcrumb"},"inLanguage":"ar","potentialAction":[{"@type":"ReadAction","target":["https:\/\/dev95.site\/ios-monorepo-ci-pipelines\/"]}]},{"@type":"ImageObject","inLanguage":"ar","@id":"https:\/\/dev95.site\/ios-monorepo-ci-pipelines\/#primaryimage","url":"https:\/\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2021\/06\/GettyImages-1297512889.jpg","contentUrl":"https:\/\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2021\/06\/GettyImages-1297512889.jpg"},{"@type":"BreadcrumbList","@id":"https:\/\/dev95.site\/ios-monorepo-ci-pipelines\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/dev95.site\/"},{"@type":"ListItem","position":2,"name":"iOS Monorepo &#038; CI Pipelines"}]},{"@type":"WebSite","@id":"https:\/\/dev95.site\/#website","url":"https:\/\/dev95.site\/","name":"Dev95","description":"\u0645\u0646\u0635\u062a\u0643 \u0627\u0644\u0631\u0642\u0645\u064a\u0629 \u0627\u0644\u0634\u0627\u0645\u0644\u0629 \u0644\u0644\u0645\u0639\u0631\u0641\u0629 \u0648\u0627\u0644\u062e\u062f\u0645\u0627\u062a \u0648\u0627\u0644\u0623\u062f\u0648\u0627\u062a \u0648\u0627\u0644\u062a\u0631\u0641\u064a\u0647.","publisher":{"@id":"https:\/\/dev95.site\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/dev95.site\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"ar"},{"@type":"Organization","@id":"https:\/\/dev95.site\/#organization","name":"Dev95","url":"https:\/\/dev95.site\/","logo":{"@type":"ImageObject","inLanguage":"ar","@id":"https:\/\/dev95.site\/#\/schema\/logo\/image\/","url":"https:\/\/i0.wp.com\/dev95.site\/wp-content\/uploads\/2026\/07\/rbrrbr-6.png?fit=512%2C512&ssl=1","contentUrl":"https:\/\/i0.wp.com\/dev95.site\/wp-content\/uploads\/2026\/07\/rbrrbr-6.png?fit=512%2C512&ssl=1","width":512,"height":512,"caption":"Dev95"},"image":{"@id":"https:\/\/dev95.site\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/dev95.site\/#\/schema\/person\/b807805ffe2916206b04d0938bce0298","name":"dev95","image":{"@type":"ImageObject","inLanguage":"ar","@id":"https:\/\/secure.gravatar.com\/avatar\/a70a73d950838b20cd80d7ebdc955737e802e8cd896044c5473b32b946c0662a?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/a70a73d950838b20cd80d7ebdc955737e802e8cd896044c5473b32b946c0662a?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/a70a73d950838b20cd80d7ebdc955737e802e8cd896044c5473b32b946c0662a?s=96&d=mm&r=g","caption":"dev95"},"url":"https:\/\/dev95.site\/ar\/author\/mohammad\/"}]}},"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/posts\/1884","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/comments?post=1884"}],"version-history":[{"count":0,"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/posts\/1884\/revisions"}],"wp:attachment":[{"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/media?parent=1884"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/categories?post=1884"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/tags?post=1884"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}