{"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-391925922\" class=\"dev95-- dev95-entity-placement\"><script async=\"async\" data-cfasync=\"false\" src=\"https:\/\/pl27862732.profitableratecpmnetwork.com\/2ad7a50e0bbc23ac6801d7b77c501463\/invoke.js\"><\/script>\r\n<div id=\"container-2ad7a50e0bbc23ac6801d7b77c501463\"><\/div><\/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>\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><div id=\"dev95-2594321857\" class=\"dev95- dev95-entity-placement\"><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><\/div>\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 medium\" aria-hidden=\"true\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" version=\"1.0\" viewBox=\"0 0 502 315\" preserveAspectRatio=\"xMidYMid meet\"><g transform=\"translate(0,332) scale(0.1,-0.1)\" fill=\"\" stroke=\"none\"><path d=\"M2394 3279 l-29 -30 -3 -207 c-2 -182 0 -211 15 -242 39 -76 157 -76 196 0 15 31 17 60 15 243 l-3 209 -33 29 c-26 23 -41 29 -80 29 -41 0 -53 -5 -78 -31z\"\/><path d=\"M3085 3251 c-45 -19 -58 -50 -96 -229 -47 -217 -49 -260 -13 -295 52 -53 146 -42 177 20 16 31 87 366 87 410 0 70 -86 122 -155 94z\"\/><path d=\"M1751 3234 c-13 -9 -29 -31 -37 -50 -12 -29 -10 -49 21 -204 19 -94 39 -189 45 -210 14 -50 54 -80 110 -80 34 0 48 6 76 34 21 21 34 44 34 59 0 14 -18 113 -40 219 -37 178 -43 195 -70 221 -36 32 -101 37 -139 11z\"\/><path d=\"M1163 3073 c-36 -7 -73 -59 -73 -102 0 -56 133 -378 171 -413 34 -32 83 -37 129 -13 70 36 67 87 -16 290 -86 209 -89 214 -129 231 -35 14 -42 15 -82 7z\"\/><path d=\"M3689 3066 c-15 -9 -33 -30 -42 -48 -48 -103 -147 -355 -147 -375 0 -98 131 -148 192 -74 13 15 57 108 97 206 80 196 84 226 37 273 -30 30 -99 39 -137 18z\"\/><path d=\"M583 2784 c-38 -19 -67 -74 -58 -113 9 -42 211 -354 242 -373 16 -10 45 -18 66 -18 51 0 107 52 107 100 0 39 -1 41 -124 234 -80 126 -108 162 -133 173 -41 17 -61 16 -100 -3z\"\/><path d=\"M4250 2784 c-14 -9 -74 -91 -133 -183 -95 -150 -107 -173 -107 -213 0 -55 33 -94 87 -104 67 -13 90 8 211 198 130 202 137 225 78 284 -27 27 -42 34 -72 34 -22 0 -50 -8 -64 -16z\"\/><path d=\"M2275 2693 c-553 -48 -1095 -270 -1585 -649 -135 -104 -459 -423 -483 -476 -23 -49 -22 -139 2 -186 73 -142 361 -457 571 -626 285 -228 642 -407 990 -497 242 -63 336 -73 660 -74 310 0 370 5 595 52 535 111 1045 392 1455 803 122 121 250 273 275 326 19 41 19 137 0 174 -41 79 -309 363 -465 492 -447 370 -946 591 -1479 653 -113 14 -422 18 -536 8z m395 -428 c171 -34 330 -124 456 -258 112 -119 167 -219 211 -378 27 -96 24 -300 -5 -401 -72 -255 -236 -447 -474 -557 -132 -62 -201 -76 -368 -76 -167 0 -236 14 -368 76 -213 98 -373 271 -451 485 -162 444 86 934 547 1084 153 49 292 57 452 25z m909 -232 c222 -123 408 -262 593 -441 76 -74 138 -139 138 -144 0 -16 -233 -242 -330 -319 -155 -123 -309 -223 -461 -299 l-81 -41 32 46 c18 26 49 83 70 128 143 306 141 649 -6 957 -25 52 -61 116 -79 142 l-34 47 45 -20 c26 -10 76 -36 113 -56z m-2057 25 c-40 -58 -105 -190 -130 -263 -110 -324 -59 -707 132 -981 25 -35 42 -64 37 -64 -19 0 -241 119 -326 174 -188 122 -406 314 -532 468 l-58 71 108 103 c185 178 428 349 672 473 66 33 121 60 123 61 2 0 -10 -19 -26 -42z\"\/><path d=\"M2375 1950 c-198 -44 -350 -190 -395 -379 -18 -76 -8 -221 19 -290 114 -284 457 -406 731 -260 98 52 188 154 231 260 27 69 37 214 19 290 -38 163 -166 304 -326 360 -67 23 -215 33 -279 19z\"\/><\/g><\/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","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 medium\" aria-hidden=\"true\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" version=\"1.0\" viewbox=\"0 0 502 315\" preserveaspectratio=\"xMidYMid meet\"><g transform=\"translate(0,332) scale(0.1,-0.1)\" fill=\"\" stroke=\"none\"><path d=\"M2394 3279 l-29 -30 -3 -207 c-2 -182 0 -211 15 -242 39 -76 157 -76 196 0 15 31 17 60 15 243 l-3 209 -33 29 c-26 23 -41 29 -80 29 -41 0 -53 -5 -78 -31z\"\/><path d=\"M3085 3251 c-45 -19 -58 -50 -96 -229 -47 -217 -49 -260 -13 -295 52 -53 146 -42 177 20 16 31 87 366 87 410 0 70 -86 122 -155 94z\"\/><path d=\"M1751 3234 c-13 -9 -29 -31 -37 -50 -12 -29 -10 -49 21 -204 19 -94 39 -189 45 -210 14 -50 54 -80 110 -80 34 0 48 6 76 34 21 21 34 44 34 59 0 14 -18 113 -40 219 -37 178 -43 195 -70 221 -36 32 -101 37 -139 11z\"\/><path d=\"M1163 3073 c-36 -7 -73 -59 -73 -102 0 -56 133 -378 171 -413 34 -32 83 -37 129 -13 70 36 67 87 -16 290 -86 209 -89 214 -129 231 -35 14 -42 15 -82 7z\"\/><path d=\"M3689 3066 c-15 -9 -33 -30 -42 -48 -48 -103 -147 -355 -147 -375 0 -98 131 -148 192 -74 13 15 57 108 97 206 80 196 84 226 37 273 -30 30 -99 39 -137 18z\"\/><path d=\"M583 2784 c-38 -19 -67 -74 -58 -113 9 -42 211 -354 242 -373 16 -10 45 -18 66 -18 51 0 107 52 107 100 0 39 -1 41 -124 234 -80 126 -108 162 -133 173 -41 17 -61 16 -100 -3z\"\/><path d=\"M4250 2784 c-14 -9 -74 -91 -133 -183 -95 -150 -107 -173 -107 -213 0 -55 33 -94 87 -104 67 -13 90 8 211 198 130 202 137 225 78 284 -27 27 -42 34 -72 34 -22 0 -50 -8 -64 -16z\"\/><path d=\"M2275 2693 c-553 -48 -1095 -270 -1585 -649 -135 -104 -459 -423 -483 -476 -23 -49 -22 -139 2 -186 73 -142 361 -457 571 -626 285 -228 642 -407 990 -497 242 -63 336 -73 660 -74 310 0 370 5 595 52 535 111 1045 392 1455 803 122 121 250 273 275 326 19 41 19 137 0 174 -41 79 -309 363 -465 492 -447 370 -946 591 -1479 653 -113 14 -422 18 -536 8z m395 -428 c171 -34 330 -124 456 -258 112 -119 167 -219 211 -378 27 -96 24 -300 -5 -401 -72 -255 -236 -447 -474 -557 -132 -62 -201 -76 -368 -76 -167 0 -236 14 -368 76 -213 98 -373 271 -451 485 -162 444 86 934 547 1084 153 49 292 57 452 25z m909 -232 c222 -123 408 -262 593 -441 76 -74 138 -139 138 -144 0 -16 -233 -242 -330 -319 -155 -123 -309 -223 -461 -299 l-81 -41 32 46 c18 26 49 83 70 128 143 306 141 649 -6 957 -25 52 -61 116 -79 142 l-34 47 45 -20 c26 -10 76 -36 113 -56z m-2057 25 c-40 -58 -105 -190 -130 -263 -110 -324 -59 -707 132 -981 25 -35 42 -64 37 -64 -19 0 -241 119 -326 174 -188 122 -406 314 -532 468 l-58 71 108 103 c185 178 428 349 672 473 66 33 121 60 123 61 2 0 -10 -19 -26 -42z\"\/><path d=\"M2375 1950 c-198 -44 -350 -190 -395 -379 -18 -76 -8 -221 19 -290 114 -284 457 -406 731 -260 98 52 188 154 231 260 27 69 37 214 19 290 -38 163 -166 304 -326 360 -67 23 -215 33 -279 19z\"\/><\/g><\/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":0,"today_views":0},"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.5 - 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\":\"A comprehensive platform for data and knowledge, delivering reliable content that meets the aspirations of readers and enthusiasts.\",\"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":"A comprehensive platform for data and knowledge, delivering reliable content that meets the aspirations of readers and enthusiasts.","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}]}}