<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Nate Ebel on Medium]]></title>
        <description><![CDATA[Stories by Nate Ebel on Medium]]></description>
        <link>https://medium.com/@n8ebel?source=rss-696aff678e4------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/2*cpzGs0OUFHSn2TmS-2zpAg.png</url>
            <title>Stories by Nate Ebel on Medium</title>
            <link>https://medium.com/@n8ebel?source=rss-696aff678e4------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 06:03:50 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@n8ebel/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Publishing Kotlin Multiplatform Swift Packages Using Google Cloud Storage and Cloud Run]]></title>
            <link>https://engineering.premise.com/publishing-kotlin-multiplatform-swift-packages-to-google-cloud-storage-be5c6987e5d?source=rss-696aff678e4------2</link>
            <guid isPermaLink="false">https://medium.com/p/be5c6987e5d</guid>
            <category><![CDATA[kotlin-multiplatform]]></category>
            <category><![CDATA[google-cloud-run]]></category>
            <category><![CDATA[mobile-app-development]]></category>
            <category><![CDATA[google-cloud-platform]]></category>
            <category><![CDATA[kotlin]]></category>
            <dc:creator><![CDATA[Nate Ebel]]></dc:creator>
            <pubDate>Wed, 18 Oct 2023 05:28:28 GMT</pubDate>
            <atom:updated>2023-10-18T05:47:23.219Z</atom:updated>
            <content:encoded><![CDATA[<p><strong><em>By Nate Ebel, Android developer</em></strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CDESlxjxJDwaBwAjIqIVdg.png" /></figure><p>In this post, we’ll detail our solution for publishing, and consuming, Kotlin Multiplatform Swift Packages. Our solution leverages a custom Gradle plugin publishing XCFrameworks to <a href="https://cloud.google.com/storage">Google Cloud Storage</a> and a <a href="https://cloud.google.com/run">Google Cloud Run</a> service to download <a href="https://developer.apple.com/documentation/xcode/swift-packages">Swift Package</a> binaries requested by XCode. With this solution in place, we’ve been able to more efficiently serve multiple Kotlin Multiplatform libraries to our iOS application.</p><p>This is a part of an ongoing series on our usage of Kotlin Multiplatform at Premise:</p><ul><li><strong>Part 1: </strong><a href="https://engineering.premise.com/kotlin-multiplatform-at-premise-b28d85825c9f"><strong>Kotlin Multiplatform at Premise</strong></a></li><li><a href="https://engineering.premise.com/kotlin-multiplatform-project-structure-brownfield-applications-58038800a6ea"><strong>Part 2: Kotlin Multiplatform Project Structure for Integrating with Brownfield Applications</strong></a></li><li><a href="https://engineering.premise.com/kotlin-multiplatforrm-github-actions-workflow-3e5e0fcb7081"><strong>Part 3: Building a CI Pipeline for Kotlin Multiplatform Mobile Using GitHub Actions</strong></a></li><li><strong>Part 4: Publishing Kotlin Multiplatform Swift Packages Using Google Cloud Storage and Cloud Run </strong>— This Post</li><li><strong>Part 5: Generating BuildConfig Files for a Kotlin Multiplatform Library</strong> — Coming Soon</li><li><strong>Part 6: Optimizing Local Build Times for Kotlin Multiplatform Mobile Projects</strong> — Coming Soon</li></ul><h3>Premise and Kotlin Multiplatform Swift Packages</h3><p>We’ve been <a href="https://engineering.premise.com/kotlin-multiplatform-at-premise-b28d85825c9f">using Kotlin Multiplatform in production</a> since early 2021 in the form of our mobile-shared project.</p><p>During that time, we’ve consumed our shared code as a Swift Package within our iOS application. The integration of that Swift Package has gone through several iterations.</p><ul><li>v1: Use the <a href="https://github.com/ge-org/multiplatform-swiftpackage"><em>multiplatform-swiftpackage</em> plugin</a> to build the Swift Package and store the XCFramework binary in GitHub</li><li>v2: Use our own custom Gradle plugin to build the Swift Package and store the XCFramework binary in GitHub</li></ul><p>These two solutions were very similar. Build the XCFramework. Generate the Package.swiftfile. Check both into git with the desired version tag.</p><p>These approaches worked fine for a while, but eventually we started to pay the price for our simple initial solution.</p><p>An XCFramework binary can be pretty large. Ours were in the ballpark of 100MB. So checking 2–3 of these into each commit (1 for each iOS architecture) started to really increase the overall download size of our git repo.</p><p>Why is this a problem?</p><p>Well, Swift Package Manager does a full clone of the git repository when it has to sync package dependencies or reset package caches. So, when our developers had to switch branches with difference library versions, or had to reset their Xcode caches, they had to download GBs worth of XCFramework binaries; thereby dramatically slowing down their Xcode sync.</p><p>Once we determined what was causing this slow down, we knew we needed a new approach. We decided to move to a <a href="https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html#target">remote url distribution model</a> for our Kotlin Multiplatform Swift Packages. Rather than check the binary into source control, we would upload the binary to some remote location, acquire the url to that resource, and add that url, along with the checksum, to our Package.swift file.</p><p>This is conceptually pretty simple. However, there wasn’t an ideal out-of-the-box solution at the time. So, we built a solution ourselves which has been serving us well in managing the publication and integration of Swift Packages for multiple Kotlin Multiplatform projects.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*U4C7axtkrAA_nhE56KKgGA.png" /></figure><p>The solution is broken down into three parts:</p><ul><li>Uploading XCFramework binaries to Google Cloud Storage</li><li>Publishing the Package.swift file to GitHub</li><li>Consuming the Swift Package from Xcode using Google Cloud Run</li></ul><h3>Uploading XCFramework Binaries to Google Cloud Storage</h3><p>As part of our Kotlin Multiplatform project, we have a custom Gradle plugin named SwiftPackagePlugin that adds several key tasks to our build.</p><pre>class SwiftPackagePlugin : Plugin&lt;Project&gt; {<br>    override fun apply(target: Project) {<br>        target.registerMoveXCFrameworkTask()<br>        target.registerZipFileTask()<br>        target.registerSwiftPackageTask()<br>        target.registerLocalSwiftPackageTask()<br>    }<br>}</pre><p>registerMoveXCFrameworkTask moves a build XCFramework from the build directory to our desired output directory location</p><pre>/**<br> * Creates a moveXCFramework Gradle task to move files from the<br> * /build/swiftpackage directory to a /swiftpackage output directory<br> */<br>private fun Project.registerMoveXCFrameworkTask() {<br>    tasks.register(&quot;moveXCFramework&quot;, Copy::class.java) {<br>        group = TASK_GROUP_NAME<br>        description = &quot;Moves XCFramework into swiftpackage output directory&quot;<br><br>        dependsOn(&quot;assemble${rootProject.name.capitalized()}XCFramework&quot;)<br><br>        from(&quot;${this@registerMoveXCFrameworkTask.buildDir}/$OUTPUT_FILE_DIR_NAME&quot;)<br>        into(File(OUTPUT_FILE_DIR_NAME))<br>    }<br>}</pre><p>registerZipFileTask creates a zip containing the build XCFrameworks</p><pre>/**<br> * Creates a zipXCFramework Gradle task<br> */<br>private fun Project.registerZipFileTask() {<br>    tasks.register(&quot;zipXCFramework&quot;, Zip::class.java) {<br>        group = TASK_GROUP_NAME<br>        description = &quot;Creates a ZIP file for the XCFramework&quot;<br><br>        // dependsOn assembleMobilesharedXCFramework task<br>        dependsOn(&quot;assemble${rootProject.name.capitalized()}XCFramework&quot;)<br><br>        archiveFileName.set(project.getZipFileName())<br>        destinationDirectory.set(File(&quot;${this@registerZipFileTask.buildDir}/$OUTPUT_FILE_DIR_NAME&quot;))<br>        from(&quot;${this@registerZipFileTask.buildDir}/XCFrameworks/release&quot;)<br>    }<br>}</pre><p>registerLocalSwiftPackageTask constructs a Package.swift file useful for locally testing changes in our iOS project</p><pre>/**<br> * Creates a createLocalSwiftPackage Gradle task that generates a<br> * Package.swift file needed for the generated XCFramework so it<br> * can be consumed as a local Swift Package<br> */<br>private fun Project.registerLocalSwiftPackageTask() {<br>    tasks.register(&quot;createLocalSwiftPackage&quot;) {<br>        group = TASK_GROUP_NAME<br>        description = &quot;Creates a local Swift package to distribute the XCFramework&quot;<br><br>        dependsOn(&quot;zipXCFramework&quot;)<br>        finalizedBy(&quot;moveXCFramework&quot;)<br><br>        doLast {<br>            val packageDotSwiftFile = createPackageDotSwiftFile(<br>                outputDirName = OUTPUT_FILE_DIR_NAME,<br>                filename = OUTPUT_PACKAGE_CONFIG_FILE_NAME<br>            )<br><br>            val packageConfiguration = SwiftPackageConfiguration.local(<br>                packageName = rootProject.name,<br>                zipFileName = project.getZipFileName()<br>            )<br><br>            packageDotSwiftFile.writePackageDotSwiftFile(packageConfiguration)<br>        }<br>    }<br>}</pre><p>registerSwiftPackageTask uploads a zipped set of XCFrameworks to Google Cloud Storage, builds a SwiftPackageConfigurationand then constructs a Package.swift file pointing to this remote file.</p><pre>/**<br> * Generates the Package.swift file needed for the generated<br> * XCFramework so it can be consumed as a Swift Package<br> */<br>private fun Project.registerSwiftPackageTask() {<br>    tasks.register(&quot;createSwiftPackage&quot;) {<br>        group = TASK_GROUP_NAME<br>        description = &quot;Creates the Swift package to distribute the XCFramework&quot;<br><br>        dependsOn(&quot;zipXCFramework&quot;)<br><br>        doLast {<br>            val packageDotSwiftFile = createPackageDotSwiftFile(<br>                outputDirName = OUTPUT_FILE_DIR_NAME,<br>                filename = OUTPUT_PACKAGE_CONFIG_FILE_NAME<br>            )<br><br>            uploadeToCloudStorage(<br>                projectId = project.swiftPackageGCPProjectId,<br>                bucketName = project.swiftPackageBucketName,<br>                packageName = project.getZipFileName(),<br>                filePath = &quot;${project.buildDir}/$OUTPUT_FILE_DIR_NAME/${project.getZipFileName()}&quot;<br>            )<br><br>            val packageConfiguration = SwiftPackageConfiguration.remote(<br>                packageName = rootProject.name,<br>                zipFileName = project.getZipFileName(),<br>                zipChecksum = project.zipFileChecksum().trim(),<br>                distributionUrl = project.swiftPackageDistributionURL<br>            )<br><br>            packageDotSwiftFile.writePackageDotSwiftFile(packageConfiguration)<br>        }<br>    }<br>}</pre><h4>Uploading to Google Cloud Storage</h4><p>The implementation of uploadToCloudStorage looks like the following. This relies on the com.google.cloud:google-cloud-storage:&lt;version&gt; dependency for interacting with Cloud Storage. By extension this also means the task must be run from an environment authenticated with Google Cloud. Locally, this generally means authentication via application-default credentials. In CI, we use the google-github-actions/auth GitHub Action to authenticate via a service account that has permissions to upload to our desired Cloud Storage bucket.</p><pre>/**<br> * Uploads a specified XCFramework zip file to a specified Cloud Storage bucket<br> *<br> * @param projectId The ID of your GCP project<br> * @param bucketName The Cloud Storage bucket, within the specific project, to upload zip files to<br> * @param packageName The name of the XCFramework zip file<br> * @param filePath Path to the file to upload<br> */<br>private fun uploadeToCloudStorage(projectId: String, bucketName: String, packageName: String, filePath: String) {<br>    val storage = StorageOptions.newBuilder().setProjectId(projectId).build().service<br>    val blobId: BlobId = BlobId.of(bucketName, packageName)<br>    val blobInfo: BlobInfo = BlobInfo.newBuilder(blobId).build()<br><br>    // Optional: set a generation-match precondition to avoid potential race<br>    // conditions and data corruptions. The request returns a 412 error if the<br>    // preconditions are not met.<br>    val precondition = if (storage[bucketName, packageName] == null) {<br>        // For a target object that does not yet exist, set the DoesNotExist precondition.<br>        // This will cause the request to fail if the object is created before the request runs.<br>        Storage.BlobWriteOption.doesNotExist()<br>    } else {<br>        // If the destination already exists in your bucket, instead set a generation-match<br>        // precondition. This will cause the request to fail if the existing object&#39;s generation<br>        // changes before the request runs.<br>        Storage.BlobWriteOption.generationMatch(<br>            storage[bucketName, packageName].generation<br>        )<br>    }<br><br>    storage.createFrom(blobInfo, Paths.get(filePath), precondition)<br>}</pre><h4>Building a Package.swift file</h4><p>To build the desired Package.swift file we construct an instance of a SwiftPackageConfiguration which will hold the values we want to write out to disk</p><pre>private data class SwiftPackageConfiguration private constructor(<br>    private val packageName: String,<br>    private val swiftToolsVersion: String,<br>    private val platforms: String,<br>    private val zipFileName: String,<br>    private val zipChecksum: String,<br>    private val isLocal: Boolean,<br>    private val distributionUrl: String,<br>) {<br><br>    val templateProperties = mapOf(<br>        &quot;toolsVersion&quot; to swiftToolsVersion,<br>        &quot;name&quot; to packageName,<br>        &quot;zipName&quot; to zipFileName,<br>        &quot;platforms&quot; to platforms,<br>        &quot;isLocal&quot; to isLocal,<br>        &quot;checksum&quot; to zipChecksum,<br>        &quot;url&quot; to &quot;$distributionUrl/$zipFileName&quot;<br>    )<br><br>    companion object {<br>        internal val templateFile =<br>            SwiftPackagePlugin::class.java.getResource(&quot;/templates/$OUTPUT_PACKAGE_CONFIG_FILE_NAME.template&quot;)<br><br>        fun local(<br>            packageName: String,<br>            swiftToolsVersion: String = DEFAULT_SWIFT_TOOLS_VERSION,<br>            platforms: String = DEFAULT_IOS_PLATFORM_VERSION,<br>            zipFileName: String<br>        ) = SwiftPackageConfiguration(packageName, swiftToolsVersion, platforms, zipFileName, &quot;&quot;, true, &quot;&quot;)<br><br>        fun remote(<br>            packageName: String,<br>            swiftToolsVersion: String = DEFAULT_SWIFT_TOOLS_VERSION,<br>            platforms: String = DEFAULT_IOS_PLATFORM_VERSION,<br>            zipFileName: String,<br>            zipChecksum: String,<br>            distributionUrl: String,<br>        ) = SwiftPackageConfiguration(packageName, swiftToolsVersion, platforms, zipFileName, zipChecksum, false, distributionUrl)<br>    }<br>}</pre><p>With this class, we then configure our instance as follows</p><pre>val packageConfiguration = SwiftPackageConfiguration.remote(<br>                packageName = rootProject.name,<br>                zipFileName = project.getZipFileName(),<br>                zipChecksum = project.zipFileChecksum().trim(),<br>                distributionUrl = project.swiftPackageDistributionURL<br>            )</pre><p>With an instance of this SwiftPackageConfiguration we then build our Package.swift file by using a template and subsituting in our desired configuration values.</p><pre>// Writes the Package.swift file by<br>// first loading from a template file and<br>// second subtituting in the config values<br>private fun File.writePackageDotSwiftFile(packageConfiguration: SwiftPackageConfiguration) {<br>    SimpleTemplateEngine()<br>        .createTemplate(SwiftPackageConfiguration.templateFile)<br>        .make(packageConfiguration.templateProperties)<br>        .writeTo(writer())<br>}</pre><p>The base template file lives in /src/main/resources/templates/Package.swift.template</p><pre>// swift-tools-version:$toolsVersion<br>import PackageDescription<br><br>let package = Package(<br>    name: &quot;$name&quot;,<br>    platforms: [<br>        $platforms<br>    ],<br>    products: [<br>        .library(<br>            name: &quot;$name&quot;,<br>            targets: [&quot;$name&quot;]<br>        ),<br>    ],<br>    targets: [<br>&lt;% if (isLocal) print &quot;&quot;&quot;        .binaryTarget(<br>            name: &quot;$name&quot;,<br>            path: &quot;./${zipName}&quot;<br>        ),&quot;&quot;&quot; else print &quot;&quot;&quot;        .binaryTarget(<br>            name: &quot;$name&quot;,<br>            url: &quot;$url&quot;,<br>            checksum: &quot;$checksum&quot;<br>        ),&quot;&quot;&quot; %&gt;<br>    ]<br>)</pre><p>When the configuration is applied to the template, it resulst in an output Package.swift file that looks like the following:</p><pre>// swift-tools-version:5.8.0<br>import PackageDescription<br><br>let package = Package(<br>    name: &quot;mobileshared&quot;,<br>    platforms: [<br>        .iOS(.v14)<br>    ],<br>    products: [<br>        .library(<br>            name: &quot;mobileshared&quot;,<br>            targets: [&quot;mobileshared&quot;]<br>        ),<br>    ],<br>    targets: [<br>        .binaryTarget(<br>            name: &quot;mobileshared&quot;,<br>            url: &quot;https://&lt;cloud run url&gt;/swiftpackage/mobileshared-&lt;version&gt;.zip&quot;,<br>            checksum: &quot;&lt;XCFramework zip checksum&gt;&quot;<br>        ),<br>    ]<br>)</pre><p>Note the url value. This is a url pointing to a Google Cloud Run service which will be described below. The purpose of this service is to give us a known location to build up our artifact urls so we can add them to this Package.swift file.</p><h4>Additional plugin code</h4><p>To round out the plugin implementation, we have a handful of of other configuration values and helper functions/classes.</p><pre>private const val DEFAULT_SWIFT_TOOLS_VERSION = &quot;5.8.0&quot;<br>private const val DEFAULT_IOS_PLATFORM_VERSION = &quot;.iOS(.v14)&quot;<br><br>private val Project.swiftPackageGCPProjectId<br>    get() = System.getenv(&quot;SWIFT_PACKAGE_BUCKET_GCP_PROJECT_ID&quot;) ?: &quot;&quot;<br><br>private val Project.swiftPackageBucketName<br>    get() = System.getenv(&quot;SWIFT_PACKAGE_BUCKET_NAME&quot;) ?: &quot;&quot;<br><br>private val Project.swiftPackageDistributionURL<br>    get() = System.getenv(&quot;SWIFT_PACKAGE_DISTRIBUTION_URL&quot;) ?: &quot;&quot;<br><br>private fun Project.createPackageDotSwiftFile(outputDirName: String, filename: String): File {<br>    return File(&quot;$buildDir/$outputDirName&quot;, filename).apply {<br>        parentFile.mkdirs()<br>        createNewFile()<br>    }<br>}<br><br><br>private const val TASK_GROUP_NAME = &quot;Premise Swift Package&quot;<br>private const val OUTPUT_PACKAGE_CONFIG_FILE_NAME =  &quot;Package.swift&quot;<br>private const val OUTPUT_FILE_DIR_NAME = &quot;swiftpackage&quot;<br>private fun Project.getZipFileName() = &quot;${rootProject.name}-${rootProject.version}.zip&quot;<br><br>internal fun Project.zipFileChecksum(): String {<br>    val outputPath = &quot;${buildDir}/$OUTPUT_FILE_DIR_NAME&quot;<br>    logger.info(&quot;checksum for path: $outputPath&quot;)<br>    logger.info(&quot;checksum for file: ${getZipFileName()}&quot;)<br>    return File(outputPath, getZipFileName())<br>        .takeIf { it.exists() }<br>        ?.let { zipFile -&gt;<br>            ByteArrayOutputStream().use { os -&gt;<br>                project.exec {<br>                    workingDir = File(outputPath)<br>                    executable = &quot;swift&quot;<br>                    args = listOf(&quot;package&quot;, &quot;compute-checksum&quot;, zipFile.name)<br>                    standardOutput = os<br>                }<br>                os.toString()<br>            }<br>        } ?: &quot;&quot;<br>}<br><br></pre><h3>Publishing the Package.swift File to GitHub</h3><p>With our SwiftPackagePlugin applied to our project, we can run the createSwiftPacakge task from CI to build the binaries, upload them to Google Cloud Storage, and generate a Package.swift referencing that version.</p><p>However, we will need to publish that Package.swift file to GitHub so Xcode can reference it, and use it to eventually download the binaries.</p><p>To do this, we are using GitHub actions. The following, is a snippet from our release build that includes the neccessary job steps to build and publish the Swift Package. (this does not include the neccessary gcloud env setup)</p><pre># runs our Gradle tasks for building and uploading<br>- name: Create Swift Package<br>  uses: gradle/gradle-build-action@v2.4.2<br>  with:<br>    arguments: createSwiftPackage<br><br># copies Package.swift to desired location in repo so Xcode sees it<br>- name: Prepare iOS Artifacts<br>  run: |<br>    cp -r shared/build/swiftpackage/Package.swift .<br><br># commits the updated Package.swift and tags it so Xcode can find it<br>- name: Tag Release<br>  run: |<br>    git add --all<br>    git commit -m &quot;[skip ci] Update Package.swift to version ${{ env.ARTIFACT_VERSION }}&quot;<br>    git tag -a ${{ env.ARTIFACT_VERSION }} -m &quot;mobile-shared v${{ env.ARTIFACT_VERSION }}&quot;<br>    git push<br>    git push origin ${{ env.ARTIFACT_VERSION }}</pre><h3>Consuming the Swift Package from Xcode Using Google Cloud Run</h3><p>From Xcode, we can now point our project at GitHub and ask it for a specific tagged version. Xcode and Swift Package Manager will then</p><ul><li>download the repo</li><li>examine the Package.swift file for the tagged commit</li><li>extract the binaryTarget url</li><li>attempt to download the binary</li></ul><p>When building the Package.swift file, we construct the url using the name of the shared artifact, the version of that artifact, and add those values to a known service url.</p><p>https://&lt;cloud run service url&gt;/swiftpackage/&lt;artifact name&gt;-&lt;version&gt;.zip</p><p>That service is our swift-package-distribution-service .</p><p>It’s a Google Cloud Run service that is permissioned to access our Cloud Storage bucket and stream the requested files back for download.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xJa_-CVQaRkgzJWOy_1A-A.png" /><figcaption>Xcode uses the url from Package.swift to make a request to swift-package-distribution-service to download the binaries from Google Cloud Storage</figcaption></figure><h4>Implementing swift-package-distribution-service</h4><p>How you would go about implementing your own version of this type of service is for you to decide. We built our service with Python using <a href="https://goblet.github.io/goblet/build/html/index.html">Goblet</a>. Goblet is a framework designed to streamline the creation og Google Cloud Services using Python.</p><p>With Goblet, our entire service looks like the following:</p><pre>import io<br>import os<br>from goblet import Goblet, goblet_entrypoint, Response<br>from flask import stream_with_context<br>from google.cloud import storage<br>import base64<br><br>CHUNK_SIZE = 8192<br>BUCKET_NAME = os.environ.get(&quot;CLOUD_STORAGE_BUCKET&quot;)<br>SWIFT_DISTRIBUTION_USERNAME = os.environ.get(&quot;SWIFT_DISTRIBUTION_USERNAME&quot;)<br>SWIFT_DISTRIBUTION_TOKEN = os.environ.get(&quot;SWIFT_DISTRIBUTION_TOKEN&quot;)<br><br>app = Goblet(function_name=&quot;swift-package-distribution-service&quot;, backend=&quot;cloudrun&quot;, routes_type=&quot;cloudrun&quot;)<br>goblet_entrypoint(app)<br><br><br>@app.route(&#39;/swiftpackage/{name}&#39;)<br>def home(name: str):<br>    auth_header = get_auth_token()<br><br>    if not is_token_valid(auth_header):<br>        return Response(&quot;Invalid token&quot;, None, 401)<br><br>    swiftpackage_filename = f&#39;{name}&#39;<br>    storage_client = storage.Client()<br><br>    bucket = storage_client.get_bucket(BUCKET_NAME)<br>    if bucket is None or not bucket.exists():<br>        missing_bucket_msg = f&#39;Bucket {bucket} does not exist&#39;<br>        app.log.info(missing_bucket_msg)<br>        return Response(missing_bucket_msg, None, 404)<br>    else:<br>        app.log.info(f&#39;Located bucket {bucket}&#39;)<br><br>    swiftpackage_blob = bucket.get_blob(swiftpackage_filename)<br>    if swiftpackage_blob is None or not swiftpackage_blob.exists():<br>        missing_blob_msg = f&#39;Requested Swift Package {swiftpackage_filename} does not exist&#39;<br>        app.log.info(missing_blob_msg)<br>        return Response(missing_blob_msg, None, 404)<br>    else:<br>        app.log.info(f&#39;Located file {swiftpackage_filename} and started download&#39;)<br><br>    swiftpackage_content = swiftpackage_blob.download_as_bytes()<br><br>    app.log.info(&quot;Streaming response&quot;)<br>    return stream_response(swiftpackage_content, swiftpackage_filename)<br><br><br>def stream_response(content: bytes, filename: str):<br>    return stream_with_context(<br>        read_bytes_chunks(content)), \<br>        200, \<br>        {<br>            &quot;Content-Type&quot;: &quot;application/octet-stream&quot;,<br>            &quot;Content-Disposition&quot;: f&#39;attachment; filename={filename}&#39;<br>        }<br><br><br>def read_bytes_chunks(content: bytes):<br>    app.log.info(&quot;Reading chunks&quot;)<br>    content_stream = io.BytesIO(content)<br>    with content_stream:<br>        while 1:<br>            buffer = content_stream.read(CHUNK_SIZE)<br>            if buffer:<br>                app.log.info(&quot;chunk&quot;)<br>                yield buffer<br>            else:<br>                app.log.info(&quot;no chunks left&quot;)<br>                break<br><br><br>def get_auth_token() -&gt; str:<br>    # We&#39;ll skip this specific implementation<br>    # We include a token taken from .netrc on developers&#39; and CI machines<br>    # If the token is not present, or incorrect, we will reject the request<br>    # This is used due to Swift Package Manager not allowing us to auth via gcloud<br><br><br>def is_token_valid(auth_header: str) -&gt; bool:<br>    # We&#39;ll skip this specific implementation<br><br><br>def decode_auth_header(auth_header: str) -&gt; str:<br>    # We&#39;ll skip this specific implementation</pre><p>With this service deployed, we can use its url when building our Package.swift file.</p><p>https://&lt;cloud run service url&gt;/swiftpackage/&lt;artifact name&gt;-&lt;version&gt;.zip</p><p>And now, when Xcode tries to download the file referenced in Package.swift it makes a request against this service, validates the auth token, and downloads the desired XCFramework binaries.</p><p>From there, our iOS build is off and running.</p><h3>How Has This Solution Worked?</h3><p>We’ve been using this solution for 6+ months now without issue.</p><p>By enabling us to move away from checking binaries into GitHub, it dramatically improved package sync time for our iOS devs that were regularly switching between branches requiring different versions of our mobile-shared project.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*E71Pl0gYAcpi_GkBg25yfg.png" /><figcaption>Today, we have multiple Kotlin Multiplatform projects leveraging this same solution for integration Swift Packages</figcaption></figure><p>Because the plugin, and service, are general purpose, we were able to apply the same plugin and deployment model for our second Kotlin Multiplatform project in the org.</p><p>Because the plugin and service were both already in place, applying this approach to the second project was trivial; not requiring any real new development work beyond configuring the plugin in the new project.</p><h3>What Are The Drawbacks To This Solution?</h3><p>Perhaps the biggest drawback to this solution is that it requires cloud resources for storage and request handling. In an organization already using cloud resources this is likely not an issue. However, for open source projects or hobbyists, this could be a significant barrier to entry; both in terms of cost required to run these resources and the administration/security of cloud resources/organizations/etc.</p><p>On a similar note, another drawback is that it’s not an off-the-shelf solution. It required dev work leveraging knowledge of Swift Package Manager, Gradle plugins, and Google Cloud. This might not be something every time has access to and could be an additional barrier to entry. For a more, off-the-shelf option, I recommend looking into <a href="https://github.com/touchlab/KMMBridge">KMMBridge from TouchLab</a>.</p><p>Finally, another, albiet smaller, limitation of the current implementation is the ordering of binary upload and Package.swift file creation. If uploading the XCFramework binary to Cloud Storage succeeds, but creation of the Package.swift file fails, it would result in a stranded artifact in our Cloud Storage bucket. In practice, this wouldn’t result in any significant cost unless it was happening repeatedly; and in 6+ months of using the current implementation I don’t believe we’ve ever encountered this issue.</p><p><strong><em>Premise is constantly looking to hire top-tier engineering talent to help us improve our front-end, mobile offerings, data and cloud infrastructure. Come find out why we’re consistently named among the top places to work in Silicon Valley by visiting our </em></strong><a href="https://bit.ly/3wnTrLn"><strong><em>Careers</em></strong></a><strong><em> page.</em></strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=be5c6987e5d" width="1" height="1" alt=""><hr><p><a href="https://engineering.premise.com/publishing-kotlin-multiplatform-swift-packages-to-google-cloud-storage-be5c6987e5d">Publishing Kotlin Multiplatform Swift Packages Using Google Cloud Storage and Cloud Run</a> was originally published in <a href="https://engineering.premise.com/">Engineering at Premise</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Deploy a Ktor Service Docker Image from Artifact Registry to Cloud Run Using Gradle]]></title>
            <link>https://medium.com/goobar/deploy-a-ktor-service-docker-image-from-artifact-registry-to-cloud-run-using-gradle-aceb1379af95?source=rss-696aff678e4------2</link>
            <guid isPermaLink="false">https://medium.com/p/aceb1379af95</guid>
            <dc:creator><![CDATA[Nate Ebel]]></dc:creator>
            <pubDate>Sat, 07 Oct 2023 06:20:55 GMT</pubDate>
            <atom:updated>2023-10-07T06:20:55.395Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*acFaF8lpN_ATVB6plr58VA.png" /></figure><p>In this post, we’ll walk through how to deploy a <a href="https://ktor.io/">Ktor</a> service Docker container from <a href="https://cloud.google.com/artifact-registry">Artifact Registry</a> to <a href="https://cloud.google.com/run">Cloud Run</a> Using Gradle. This post builds on our two previous posts:</p><ul><li><a href="https://goobar.dev/publish-a-ktor-docker-image-to-artifact-registry-using-ktor-plugin/">How to Publish a Ktor Docker Image to Artifact Registry Using the Ktor Plugin</a></li><li><a href="https://goobar.dev/publish-a-ktor-docker-image-to-container-registry-using-ktor-plugin/">How to Publish a Ktor Docker Image to Container Registry Using the Ktor Plugin</a></li></ul><p>Using the approaches outlined in the previous series posts, we can leverage Gradle to automate build tasks for our service deployment. Primarily, we can use Gradle to compile our code, build our Docker image and deploy it to some container registry such as Artifact Registry.</p><p>To complete our deployment pipeline, we’ll once again use Gradle to automate the deployment of a Docker image from Artifact Registry to Cloud Run. By doing so, we can leverage Gradle’s task avoidance characteristics to avoid building or deploying if our source code hasn’t changed. Additionally, we keep deployment logic contained in a common language and build system as the rest of our Ktor service code; making it easier for developers to maintain both the application and deployment code.</p><h3>Prerequisites</h3><p>Since we’ll be deploying our Ktor service container to Artifact Registry, that comes with a new Google Cloud Platform (GCP) prerequisites.</p><p>We need to have access to a GCP organization</p><ol><li><a href="https://cloud.google.com/resource-manager/docs/creating-managing-organization#acquiring">How to setup a GCP organization?</a></li><li>You’ll eventually need to set up a billing account for your organization. To try out GCP for learning purposes, there are often ways to find free, or discounted, billing credits<br>- <a href="https://cloud.google.com/free/docs/free-cloud-features">Trial period with $300 free credits for new users</a><br>- <a href="https://cloud.google.com/billing/docs/how-to/edu-grants">GCP credits for faculty and students</a><br>- <a href="https://cloud.google.com/innovators/innovatorsplus">Google Cloud Innovators Plugs program</a></li></ol><p>We need to have a project set up within that organization</p><ul><li><a href="https://cloud.google.com/resource-manager/docs/creating-managing-projects#creating_a_project">How to create and manage GCP projects?</a></li></ul><p>Artifact Registry must be enabled within that project</p><ul><li><a href="https://cloud.google.com/artifact-registry/docs/enable-service">How to enable Artifact Registry for a GCP project?</a></li></ul><p>Cloud Run must be enabled within that project</p><ul><li><a href="https://cloud.google.com/run/docs/setup">How to enable Cloud Run for a GCP project?</a></li></ul><h3>Reviewing Artifact Registry Deployment</h3><p>Before we start on deploying to Cloud Run, let’s review our Gradle buildscript configuration for publishing our Ktor service to Artifact Registry.</p><p>We have a single Gradle module named <strong><em>:service</em></strong> to start. We have a simple class <strong><em>ArtifactRegistry</em></strong> that implements the <strong><em>DockerImageRegistry</em></strong> interface coming from the Ktor plugin. This allows us to configure the needed information to publish our built Docker image to Artifact Registry.</p><pre>// :service/build.gradle.kts<br><br>private class ArtifactRegistry(<br>    projectName: Provider&lt;String&gt;,<br>    regionName: Provider&lt;String&gt;,<br>    repositoryName: Provider&lt;String&gt;,<br>    imageName: Provider&lt;String&gt;,<br>) : DockerImageRegistry {<br>    override val username: Provider&lt;String&gt; = provider { &quot;&quot; }<br>    override val password: Provider&lt;String&gt; = provider { &quot;&quot; }<br>    override val toImage: Provider&lt;String&gt; = provider {<br>        val region = regionName.get()<br>        val project = projectName.get()<br>        val repository = repositoryName.get()<br>        val image = imageName.get()<br><br>        &quot;$region-docker.pkg.dev/$project/$repository/$image&quot;<br>    }<br>}</pre><p>Next, we can configure an instance of our <strong><em>ArtifactRegistry</em></strong> class; pulling the configuration data from environment variables and project properties.</p><pre>// :service/build.gradle.kts<br><br>// deployment config<br>private val deploymentProjectName = System.getenv(&quot;GCP_CONTAINER_PROJECT_NAME&quot;)<br>private val deploymentRegion = System.getenv(&quot;GCP_REGION&quot;)<br>private val artifactRegistryContainerName = System.getenv(&quot;GCP_CONTAINER_NAME&quot;)<br>private val deploymentEnv = System.getenv(&quot;ENV&quot;)<br>private val deployedImageTag: String = &quot;${rootProject.version}&quot;<br>private val deployedImageName = rootProject.name<br><br>private val artifactRegistryConfig = ArtifactRegistry(<br>    projectName = provider { deploymentProjectName },<br>    regionName = provider { deploymentRegion },<br>    repositoryName = provider { artifactRegistryContainerName },<br>    imageName = provider { deployedImageName },<br>)</pre><p>Lastly, we put it all together by configuring the Ktor plugin to build the Docker image and set our <strong><em>ArtifactRegistry</em></strong> config property as the <strong><em>externalRegistry</em></strong> for publication.</p><pre>// :service/build.gradle.kts<br><br>ktor {<br>    docker {<br>        jreVersion.set(io.ktor.plugin.features.JreVersion.JRE_17)<br>        localImageName.set(deployedImageName)<br>        imageTag.set(deployedImageTag)<br><br>        portMappings.set(<br>            listOf(<br>                io.ktor.plugin.features.DockerPortMapping(<br>                    outsideDocker = 8080,<br>                    insideDocker = 8080,<br>                    io.ktor.plugin.features.DockerPortMappingProtocol.TCP,<br>                ),<br>            ),<br>        )<br>        externalRegistry.set(artifactRegistryConfig)<br>    }<br>}</pre><p>With our <strong><em>build.gradle.kts</em></strong> file configured in this way, we can run the <strong><em>publishImage</em></strong> Gradle task to build our Ktor project as a Docker image and publish it to Artifact Registry.</p><h3>Building a Custom Gradle Plugin to Deploy from Artifact Registry to Cloud Run</h3><h3>What are we trying to automate?</h3><p>To deploy an image from Artifact Registry to Cloud Run we have several options. Perhaps the most straightforward is using the gcloud cli. With gcloud, we could build up a deployment command like the following:</p><p>gcloud run deploy &lt;service name&gt; --image &lt;docker image path&gt; --region &lt;deployed gcp region&gt;</p><p>This command would take the specified Artifact Registry image, and deploy it to Cloud Run as the specified service name in the specified region.</p><p>There are plenty of ways we could automate this command for deployment. For our purposes, we will use Gradle so our build and deployment commands are consolidated into a single language/tool pairing.</p><h3>What should the final buildscript configuration look like?</h3><p>Our end goal is to be able to configure our Cloud Run deployment from our <strong><em>build.gradle.kts</em></strong> file in a similar way to how we configure our Artifact Registry deployment. The final result should look something like this:</p><pre>cloudRunDeploy {<br>    deployedName.set(deployedImageName)<br>    deployedRegion.set(deploymentRegion)<br>    image.set(artifactRegistryConfig.toImage)<br>    imageTag.set(deployedImageTag)<br>    env.set(deploymentEnv)<br>}</pre><h3>Creating the Gradle plugin</h3><p>To achieve this, we’ll start by creating a new Gradle plugin project named <strong><em>:</em></strong><strong><em>cloudrun-deploy-plugin</em></strong> and adding it as an <a href="https://docs.gradle.org/current/userguide/composite_builds.html">included build to our main Gradle project</a>. Our <strong><em>:service</em></strong> module will eventually apply the plugin provided by <strong><em>:cloudun-deploy-plugin</em></strong> to enable our Cloud Run deployment.</p><p>Before that, we need to update our root <strong><em>settings.gradle.kts</em></strong> in add this new Gradle project as an included build:</p><pre>// settings/gradle.kts<br><br>includeBuild(&quot;cloudrun-deploy-plugin&quot;)</pre><h4>Creating a configuration extension interface</h4><p>With the plugin project created, we can create an interface defining properties for configuring the Cloud Run deployment.</p><pre>// :cloudrun-deploy-plugin/build.gradle.kts<br><br>interface CloudRunDeployPluginExtension {<br>    val deployedName: Property&lt;String&gt;<br>    val deployedRegion: Property&lt;String&gt;<br>    val image: Property&lt;String&gt;<br>    val imageTag: Property&lt;String&gt;<br>    val env: Property&lt;String&gt;<br>}</pre><p>Notice that these properties match the properties we configured in our <strong><em>:</em></strong><strong><em>service/build.gradle.kts</em></strong> file.</p><p>Now that we have this interface, we can stub out how we will configure Cloud Run deployment.</p><p>We will add an extension method on the Gradle <strong><em>Project</em></strong> type that takes an instance of <strong><em>CloudRunDeployPluginExtension</em></strong> as a receiver. This will provide us the configuration syntax we’re looking for and allow us to retrieve these configuration values when needed during task execution.</p><pre>// :cloudrun-deploy-plugin/build.gradle.kts<br><br>fun Project.cloudRunDeploy(configure: Action&lt;CloudRunDeployPluginExtension&gt;) {<br>    (this as org.gradle.api.plugins.ExtensionAware).extensions.configure(EXTENSION_NAME, configure)<br>}</pre><h4>Defining a deployment Gradle task</h4><p>Now, we just need two more things to pull this all together. We need a custom Gradle task to do the deployment. And, we need to define the Gradle plugin that will apply that task to our Ktor project.</p><p>We’ll start with the task.</p><p>We’ll create a new class <strong><em>CloudRunDeployTask</em></strong> that extends Gradle’s <strong><em>DefaultTask</em></strong> type.</p><pre>// :cloudrun-deploy-plugin/build.gradle.kts<br><br>private abstract class CloudRunDeployTask : DefaultTask() {<br><br>    @get:Inject<br>    abstract val exec: ExecOperations<br>    @get:Input<br>    abstract val deployedName: Property&lt;String&gt;<br>    @get:Input<br>    abstract val image: Property&lt;String&gt;<br>    @get:Input<br>    abstract val imageTag: Property&lt;String&gt;<br>    @get:Input<br>    abstract val deployedRegion: Property&lt;String&gt;<br>    @get:Input<br>    abstract val env: Property&lt;String&gt;<br><br>    init {<br>        group = TASK_GROUP<br>    }<br><br>    @TaskAction<br>    fun deploy() {<br>        val deployCommand = &quot;gcloud run deploy ${deployedName.get()} --image ${image.get()}:${imageTag.get()} --region ${deployedRegion.get()} --set-env-vars KTOR_ENV=${env.get()}&quot;<br>        logger.warn(&quot;Executing: $deployCommand&quot;)<br>        exec.exec { commandLine(deployCommand.split(&quot; &quot;)) }<br>    }<br>}</pre><p>There are a few important items to note in the implementation of this task class</p><ol><li>We define a property of type <strong><em>ExecOperations</em></strong> so we can execute terminal commands such as gcloud</li><li>We define properties of type <strong><em>Property&lt;String&gt;</em></strong> so we can, at runtime, access the configuration values needed to deploy our service</li><li>We define a method <strong><em>deploy()</em></strong> and annotate it with <strong><em>@TaskAction</em></strong> to define what should run when this task is executed. Within this implementation we</li><li>Build up the desired gcloud deployment command</li><li>Use the <strong><em>ExecOperations</em></strong> property to run the gcloud command</li></ol><h4>Defining a custom Plugin class</h4><p>With this task type created, we can now define our custom plugin.</p><ol><li>We’ll create a new class <strong><em>CloudRunDeployPlugin</em></strong> that implements <strong><em>Plugin&lt;Project&gt;</em></strong></li><li>We must override the <strong><em>apply()</em></strong> method to register our custom deployment task</li><li>We’ll create an instance of our <strong><em>CloudRunDeployPluginExtension</em></strong> interface so it can be accessed and configured from the buildscript</li><li>We’ll register our <strong><em>CloudRunDeployTask</em></strong> type with a name of <strong><em>deployToCloudRun</em></strong></li><li>Within the register configuration, we can then set the task properties using the values set on the created <strong><em>CloudRunDeployPluginExtension</em></strong></li></ol><pre>// :cloudrun-deploy-plugin/build.gradle.kts<br><br>class CloudRunDeployPlugin : Plugin&lt;Project&gt; {<br>    override fun apply(project: Project) {<br>        val extensionConfig = project.extensions.create(EXTENSION_NAME, CloudRunDeployPluginExtension::class.java)<br>        project.tasks.register&lt;CloudRunDeployTask&gt;(TASK_NAME) {<br>            deployedName.set(extensionConfig.deployedName)<br>            image.set(extensionConfig.image)<br>            imageTag.set(extensionConfig.imageTag)<br>            deployedRegion.set(extensionConfig.deployedRegion)<br>            env.set(extensionConfig.env)<br>        }<br>    }<br>}</pre><h4>Applying the custom Gradle plugin</h4><p>With a plugin class created, we need to define the plugin metadata so our main <strong><em>:service</em></strong> project can locate our Plugin. We can do this by leveraging the <strong><em>java-gradle-plugin</em></strong> from the <strong><em>:cloudrun-deploy-plugin/build.gradle.kts</em></strong> buildscript:</p><pre>// :cloudrun-deploy-plugin/build.gradle.kts<br><br>version = &quot;1.0.0&quot;<br>gradlePlugin {<br>    plugins.create(&quot;cloud-run-deploy&quot;) {<br>        id = &quot;cloud-run-deploy&quot;<br>        implementationClass = &quot;dev.goobar.cloudrun.deploy.CloudRunDeployPlugin&quot;<br>    }<br>}</pre><p>All we need to do now before we configure the plugin is to apply it to our project. The exact method of applying it to your project will depend on how you’ve structured your project(s). In my case, I’ve defined the coordinates of my custom plugin in my version catalog file:</p><pre>// gradle/libs.versions.toml<br><br>[versions]<br>cloudRunDeployPluginVersion = &quot;1.0.0&quot;<br><br>[plugins]<br>cloudRunDeploy = { id = &quot;cloud-run-deploy&quot;, version.ref = &quot;cloudRunDeployPluginVersion&quot; }</pre><p>Now, I can apply the plugin to my Ktor service buildscript as follows:</p><pre>// :service/build.gradle.kts<br><br>plugins {<br>    application<br>    ...<br>    alias(libs.plugins.cloudRunDeploy)<br>}</pre><h3>Configuring the Cloud Run Deployment Plugin</h3><p>We now have a custom Gradle task, a custom Gradle plugin to register the task, and a registered extension interface.</p><p>With that, we can configure our deployment from our module-level <strong><em>build.gradle.kts</em></strong> script:</p><pre>// :service/build.gradle.kts<br><br>cloudRunDeploy {<br>    deployedName.set(deployedImageName)<br>    deployedRegion.set(deploymentRegion)<br>    image.set(artifactRegistryConfig.toImage)<br>    imageTag.set(deployedImageTag)<br>    env.set(deploymentEnv)<br>}</pre><h3>Deploying the Cloud Run Service</h3><p>With our cloudrun-deploy plugin applied to our project and configured for deployment, deploying the service to Cloud Run is simple. We simply run the <strong><em>deployToCloudRun</em></strong> gradle task we registered from our plugin.</p><p>This task relies on gcloud under the hood. Whether running from a local development machine or a CI machine, we need to be authenticated with gcloud.</p><h3>Conclusion</h3><p>In this post, we’ve explored how to streamline a Cloud Run deployment for a Ktor service by using a custom Gradle plugin. Building on the concepts from previous posts, we were able to define a Gradle task that takes in the necessary configuration data to deploy a container from Artifact Registry to Cloud Run.</p><p>By consolidating this logic in Gradle tasks, we reduce the complexity of building and deploying our Kotlin service. This also allows us to leverage the Ktor plugin’s support for building and publishing Docker containers for our service so we get Gradle task avoidance for free; further simplifying our build and deployment pipeline.</p><h3>Want to Learn More?</h3><p>If you want to learn more about Kotlin language basics, check out my <a href="https://www.youtube.com/playlist?list=PLEPZdzLLJH95CnKw-StVhMcwo7MIkXu4x">playlist on YouTube</a></p><p>And if you’re ready to learn more about Kotlin for server-side development, take a look at my LinkedIn Learning course <a href="https://www.linkedin.com/learning/transitioning-from-java-to-kotlin">Transitioning from Java to Kotlin for the Web</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=aceb1379af95" width="1" height="1" alt=""><hr><p><a href="https://medium.com/goobar/deploy-a-ktor-service-docker-image-from-artifact-registry-to-cloud-run-using-gradle-aceb1379af95">Deploy a Ktor Service Docker Image from Artifact Registry to Cloud Run Using Gradle</a> was originally published in <a href="https://medium.com/goobar">goobar</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to Publish a Ktor Docker Image to Artifact Registry Using the Ktor Plugin]]></title>
            <link>https://medium.com/goobar/how-to-publish-a-ktor-docker-image-to-artifact-registry-using-the-ktor-plugin-901837633d25?source=rss-696aff678e4------2</link>
            <guid isPermaLink="false">https://medium.com/p/901837633d25</guid>
            <category><![CDATA[google-cloud-platform]]></category>
            <category><![CDATA[artifact-registry]]></category>
            <category><![CDATA[ktor]]></category>
            <category><![CDATA[docker]]></category>
            <category><![CDATA[kotlin]]></category>
            <dc:creator><![CDATA[Nate Ebel]]></dc:creator>
            <pubDate>Sat, 04 Mar 2023 05:33:15 GMT</pubDate>
            <atom:updated>2023-03-04T05:33:15.428Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*acFaF8lpN_ATVB6plr58VA.png" /></figure><p>In this post, we’ll walk through the configuration of the Ktor plugin to deploy a Ktor Docker image to Artifact Registry using Gradle. We’ll also explore several other Gradle tasks that make working with a containerized version of our Ktor service easier.</p><p>Ktor is a framework from JetBrains enabling developers to build client and/or server applications using Kotlin. For organizations using Kotlin for server-side development, Ktor is an appealing framework as it’s built from the ground up to leverage Kotlin language features such as coroutines.</p><p>In many organizations today, especially those undergoing “cloud transformation”, <em>containers</em> are a popular choice for packaging, sharing, and deploying applications. Those containers can be used with tools like Docker to simplify the development, testing, and coordination of services.</p><p>Containers are a key aspect of many serverless deployment workflows within Google Cloud Platform. Serverless solutions like Cloud Run rely on containers to respond quickly to increases in server load by deploying new containers, when needed, to handle the incoming traffic, and then spin down those containers when traffic subsides.</p><p>If using Ktor for serverless applications, you’ll likely need to create a container image for your application. Once you create an image, you’ll need to deploy it to an external registry so it can eventually be deployed to a container and start receiving traffic.</p><p>It’s possible to do this in an entirely manual fashion; defining our own Dockerfile, building the image locally, and then pushing it to some external registry such as Google’s Artifact Registry.</p><p>However, the Ktor plugin provides mechanisms for simplifying this process in a way that makes it easy for those without extensive Docker, container, or GCP knowledge to leverage.</p><p>If you need to publish your Ktor Docker image to Container Registry, check out <a href="https://goobar.dev/publish-a-ktor-docker-image-to-container-registry-using-ktor-plugin/">this related post</a>.</p><h3>Prerequisites</h3><p>Since we’ll be deploying our Ktor service container to Artifact Registry, that comes with a new Google Cloud Platform (GCP) prerequisites.</p><p>We need to have access to a GCP organization</p><ol><li><a href="https://cloud.google.com/resource-manager/docs/creating-managing-organization#acquiring">How to setup a GCP organization?</a></li><li>You’ll eventually need to set up a billing account for your organization. To try out GCP for learning purposes, there are often ways to find free, or discounted, billing credits<br>- <a href="https://cloud.google.com/free/docs/free-cloud-features">Trial period with $300 free credits for new users</a><br>- <a href="https://cloud.google.com/billing/docs/how-to/edu-grants">GCP credits for faculty and students</a><br>- <a href="https://cloud.google.com/innovators/innovatorsplus">Google Cloud Innovators Plugs program</a></li></ol><p>We need to have a project set up within that organization</p><ul><li><a href="https://cloud.google.com/resource-manager/docs/creating-managing-projects#creating_a_project">How to create and manage GCP projects?</a></li></ul><p>Artifact Registry must be enabled within that project</p><ul><li><a href="https://cloud.google.com/artifact-registry/docs/enable-service">How to enable Artifact Registry for a GCP project?</a></li></ul><h3>How to configure the Ktor plugin to build a Docker image?</h3><p>To configure a Docker image, and publishing tasks, for our Ktor project, we’ll use the Ktor plugin dsl in our project’s Gradle buildscript. This will leverage the <a href="https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin">jib Gradle plugin</a> under the hood to configure and build images for your project.</p><p>// build.gradle.kts</p><pre>// build.gradle.kts<br>ktor {<br>   docker {<br>      // container configuration goes here<br>   }<br>}</pre><p>To start, we’ll specify a Java Runtime Environment (JRE) version to use when constructing the container image.</p><pre>ktor {<br>   docker {<br>       // set a JRE version that will be used to build the container<br>       jreVersion.set(io.ktor.plugin.features.JreVersion.JRE_17)<br>   }<br>}</pre><p>Next, we’ll configure port mappings for the image.</p><pre>ktor {<br>   docker {<br>       // set a JRE version that will be used to build the container<br>       jreVersion.set(io.ktor.plugin.features.JreVersion.JRE_17)<br><br>       // setup port mapping for the container/service<br>       portMappings.set(listOf(<br>           io.ktor.plugin.features.DockerPortMapping(<br>               outsideDocker = 8080,<br>               insideDocker = 8080,<br>               io.ktor.plugin.features.DockerPortMappingProtocol.TCP<br>           )<br>       ))<br>   }<br>}</pre><p>And finally, we’ll configure the external registry used when publishing the container. In our example, we’ll configure it to use Artifact Registry.</p><p>To configure the external registry, we need to create an instance of <strong><em>DockerImageRegistry</em></strong>. Ktor provides <a href="https://ktor.io/docs/docker.html#external-registry">implementations of this interface for Docker Hub</a> and Google Container Registry, but not Artifact Registry.</p><p>However, the interface is public, so we can create our own implementation.<br>The most important thing is to implement the <strong><em>toImage</em></strong> property to return a string representing the <a href="https://cloud.google.com/artifact-registry/docs/docker/pushing-and-pulling#tag">tag needed to publish our image to Artifact Registry</a>. That tag structure takes the following form: <strong><em>&lt;region&gt;-docker.pkg.dev/&lt;project&gt;/&lt;repository&gt;/&lt;image name&gt;</em></strong>.</p><pre>class ArtifactRegistry(<br>   projectName: Provider&lt;String&gt;,<br>   regionName: Provider&lt;String&gt;,<br>   repositoryName: Provider&lt;String&gt;,<br>   imageName: Provider&lt;String&gt;,<br>) : DockerImageRegistry {<br>   override val username: Provider&lt;String&gt; = provider { &quot;&quot; }<br>   override val password: Provider&lt;String&gt; = provider { &quot;&quot; }<br>   override val toImage: Provider&lt;String&gt; = provider {<br>       val region = regionName.get()<br>       val project = projectName.get()<br>       val repository = repositoryName.get()<br>       val image = imageName.get()<br><br>       &quot;$region-docker.pkg.dev/$project/$repository/$image&quot;<br>   }<br>}</pre><p>There are several key pieces of information needed for this Artifact Registry configuration:</p><ol><li><em>projectName</em> should be the name of your existing GCP project in which Artifact Registry has been enabled and where you want to upload the built container</li><li><em>regionName</em> should be the region identifier for the GCP region in which your desired artifact repository is set up in GCP. ex: <em>us-west1</em></li><li><em>repositoryName</em> should be the name of the specific artifact repository where the images should be published. ex: <em>my-org-docker-containers</em></li><li><em>imageName</em> should be the name for a specific set of images within the repository.</li><li>A reasonable default here could be to use the name of your Ktor project. You can pull this from the Gradle project using <strong><em>project.name</em></strong></li></ol><p>Once we have our custom implementation of <strong><em>DockerImageRegistry</em></strong> we can finish our Docker configuration.</p><pre>ktor {<br>   docker {<br>       // set a JRE version that will be used to build the container<br>       jreVersion.set(io.ktor.plugin.features.JreVersion.JRE_17)<br><br>       // setup port mapping for the container/service<br>       portMappings.set(listOf(<br>           io.ktor.plugin.features.DockerPortMapping(<br>               outsideDocker = 8080,<br>               insideDocker = 8080,<br>               io.ktor.plugin.features.DockerPortMappingProtocol.TCP<br>           )<br>       ))<br><br>       // configure how the container should be deployed to Artifact Registry<br>       externalRegistry.set(<br>           ArtifactRegistry(<br>               projectName = provider { &quot;&lt;your gcp project name&gt;&quot; },<br>               regionName = provider { &quot;&lt;repository’s region name&gt;&quot; },<br>               repositoryName = provider { &quot;&lt;target repository name&gt;&quot; },<br>               imageName = provider { &quot;&lt;desired image name&gt;&quot; },<br>           )<br>        )<br>   }<br>}</pre><p>Once this configuration is applied to the project, we can build our Docker image using the <strong><em>buildImage</em></strong> Gradle task. Check out <a href="https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#what-would-a-dockerfile-for-a-jib-built-image-look-like">this FAQ from the jib Gradle plugin repo</a> for more details on what an equivalent Dockerfile would look like for the generated image.</p><h3>How to deploy a Ktor Docker image to Artifact Registry?</h3><p>With the Ktor Docker configuration applied to the project, we can publish a Docker image for our project using the <strong><em>publishImage</em></strong> Gradle task.</p><p>This requires us to authenticate against the specified GCP project and Artifact Registry repository.</p><p>To do this, we can run the following command: <strong><em>gcloud auth configure-docker &lt;region name&gt;-docker.pkg.dev</em></strong> to set up gcloud as a Docker credential helper.</p><p>Then, we just need to be sure we’ve authenticated with gcloud using <strong><em>gcloud auth login</em></strong>.</p><p>When an image with tag who’s start matches <strong><em>&lt;region name&gt;-docker.pkg.dev</em></strong> is pushed, it will look for this credential helper and use local gcloud authorization for the request.</p><p>Once we are authenticated, running the <strong><em>publishImage</em></strong> task should successfully publish the built image to Artifact Registry.</p><h3>How to deploy a Ktor Docker image to local Docker service?</h3><p>Using the Ktor Docker configuration, we can also streamline publishing of our project’s Docker image to a local Docker.</p><ol><li>Start Docker on your development machine</li><li>Run the <strong><em>publishImageToLocalRegistry</em></strong> Gradle task</li></ol><h3>How to run the Ktor service container using generated Gradle tasks?</h3><p>Using the Ktor Docker configuration, we can build an image and start running it with the local Docker daemon by running the <strong><em>runDocker</em></strong> Gradle task.</p><h3>Conclusion</h3><p>The Ktor plugin’s Docker configuration provides a streamlined workflow for developers working with Ktor services via Docker images.</p><p>By configuring the plugin to publish images to Google Cloud’s Artifact Registry, developers can publish Docker images for their service to Artifact Registry using a single Gradle task. This task can be used by all developers, and by CI/CD workflows to provide a consistent deployment workflow.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=901837633d25" width="1" height="1" alt=""><hr><p><a href="https://medium.com/goobar/how-to-publish-a-ktor-docker-image-to-artifact-registry-using-the-ktor-plugin-901837633d25">How to Publish a Ktor Docker Image to Artifact Registry Using the Ktor Plugin</a> was originally published in <a href="https://medium.com/goobar">goobar</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to Publish a Ktor Docker Image to Container Registry Using the Ktor Plugin]]></title>
            <link>https://medium.com/goobar/how-to-publish-a-ktor-docker-image-to-container-registry-using-the-ktor-plugin-2c5aeb3f263b?source=rss-696aff678e4------2</link>
            <guid isPermaLink="false">https://medium.com/p/2c5aeb3f263b</guid>
            <dc:creator><![CDATA[Nate Ebel]]></dc:creator>
            <pubDate>Sat, 04 Mar 2023 05:27:53 GMT</pubDate>
            <atom:updated>2023-03-04T05:27:53.934Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*acFaF8lpN_ATVB6plr58VA.png" /></figure><p>In this post, we’ll walk through the configuration of the Ktor plugin for deploying a Ktor Docker image to Container Registry via a generated Gradle task. We’ll also explore several other Gradle tasks that make working with a containerized version of our Ktor service easier.</p><p>Ktor is a framework from JetBrains enabling developers to build client and/or server applications using Kotlin. For organizations wanting to use Kotlin for server-side development, Ktor is an appealing choice of framework as it’s built from the ground up to leverage Kotlin language features such as coroutines.</p><p>In many organizations today, especially those undergoing “cloud transformation” through adoption of serverless technologies, <em>containers</em> are a popular choice for packaging, sharing, and deploying applications. Those containers can be used with tools like Docker to simplify the development, testing, and coordination of services.</p><p>Containers are a key aspect of many serverless deployment workflows within Google Cloud Platform. Servless solutions like Cloud Run rely on containers to respond quickly to increases in server load by deploying new containers, when needed, to handle the incoming traffic, and then spin down those containers when traffic subsides.</p><p>If using Ktor for modern serverless development, it’s likely you’ll need to create a container image for your Ktor application and be able to deploy that image to some external repository where it may then be deployed and start receiving traffic.</p><p>It’s possible to do this in an entirely manual fashion; defining our own Dockerfile, building the container locally, and then pushing it to some external registry such as Google’s Container Registry.</p><p>However, the Ktor plugin provides mechanisms for simplifying this process in a way that makes it easy for those without extensive Docker, container, or GCP knowledge to leverage.</p><p>If you need to publish your Ktor Docker image to Artifact Registry, check out <a href="https://goobar.dev/publish-a-ktor-docker-image-to-artifact-registry-using-ktor-plugin/">this related post</a>.</p><h3>Prerequisites</h3><p>Since we’ll be deploying our Ktor service container to Container Registry, that comes with a new Google Cloud Platform (GCP) prerequisites.</p><p>We need to have access to a GCP organization:<br>- <a href="https://cloud.google.com/resource-manager/docs/creating-managing-organization#acquiring">How to setup a GCP organization?</a><br>- You’ll eventually need to set up a billing account for your organization. To try out GCP for learning purposes, there are often ways to find free, or discounted, billing credits</p><ul><li><a href="https://cloud.google.com/free/docs/free-cloud-features">Trial period with $300 free credits for new users</a></li><li><a href="https://cloud.google.com/billing/docs/how-to/edu-grants">GCP credits for faculty and students</a></li><li><a href="https://cloud.google.com/innovators/innovatorsplus">Google Cloud Innovators Plugs program</a></li></ul><p>We need to have a project set up within that organization:<br>- <a href="https://cloud.google.com/resource-manager/docs/creating-managing-projects#creating_a_project">How to create and manage GCP projects?</a></p><p>Container Registry must be enabled within that project:<br>- <a href="https://cloud.google.com/container-registry/docs/enable-service">How to enable Container Registry for a GCP project?</a></p><h3>How to configure the Ktor plugin to build a Docker image?</h3><p>To configure a Docker image, and publishing tasks, for our Ktor project, we’ll use the Ktor plugin dsl in our project’s Gradle buildscript. This will leverage the <a href="https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin">jib Gradle plugin</a> under the hood to configure and build images for your project.</p><p>// build.gradle.kts</p><pre>// build.gradle.kts<br>ktor {<br>   docker {<br>      // container configuration goes here<br>   }<br>}</pre><p>To start, we’ll specify a Java Runtime Environment (JRE) version to use when constructing the container image.</p><pre>ktor {<br>   docker {<br>       // set a JRE version that will be used to build the container<br>       jreVersion.set(io.ktor.plugin.features.JreVersion.JRE_17)<br>   }<br>}</pre><p>Next, we’ll configure port mappings for the image.</p><pre>ktor {<br>   docker {<br>       // set a JRE version that will be used to build the container<br>       jreVersion.set(io.ktor.plugin.features.JreVersion.JRE_17)<br><br>       // setup port mapping for the container/service<br>       portMappings.set(listOf(<br>           io.ktor.plugin.features.DockerPortMapping(<br>               outsideDocker = 8080,<br>               insideDocker = 8080,<br>               io.ktor.plugin.features.DockerPortMappingProtocol.TCP<br>           )<br>       ))<br>   }<br>}</pre><p>And finally, we’ll configure the external registry used when publishing the container. In our example, we’ll configure it to use Container Registry.</p><pre>ktor {<br>   docker {<br>       // set a JRE version that will be used to build the container<br>       jreVersion.set(io.ktor.plugin.features.JreVersion.JRE_17)<br><br>       // setup port mapping for the container/service<br>       portMappings.set(listOf(<br>           io.ktor.plugin.features.DockerPortMapping(<br>               outsideDocker = 8080,<br>               insideDocker = 8080,<br>               io.ktor.plugin.features.DockerPortMappingProtocol.TCP<br>           )<br>       ))<br><br>       // configure how the container should be deployed to Container Registry<br>       externalRegistry.set(<br>           DockerImageRegistry.googleContainerRegistry(<br>               projectName = provider { &quot;&lt;your gcp project name&gt;&quot; },<br>               appName = provider { &quot;&lt;desired repository name&gt;&quot; },<br>               username = provider { &quot;&quot; },<br>               password = provider { &quot;&quot; }<br>           )<br>       )<br>   }<br>}</pre><p>There are several key pieces of information needed for this Container Registry configuration:</p><ol><li><em>projectName</em> should be the name of your existing GCP project in which Container Registry has been enabled and where you want to upload the built container</li><li><em>appName</em> will be used to name the repository within Container Registry. If the repository doesn’t exist, it will be created when the container image is published.<br>- A reasonable default here could be the name of your Ktor project. You could pull this from the Gradle project using <strong><em>project.name</em></strong></li><li><em>username</em> and <em>password</em> we leave blank here. In the next section we’ll discuss how authentication happens when deploying the container image</li></ol><p>Once this configuration is applied to the project, we can build our Docker image using the <strong><em>buildImage</em></strong> Gradle task. Check out <a href="https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#what-would-a-dockerfile-for-a-jib-built-image-look-like">this FAQ from the jib Gradle plugin repo</a> for more details on what an equivalent Dockerfile would look like for the generated image.</p><h3>How to deploy a Ktor Docker image to Container Registry?</h3><p>With the Ktor Docker configuration applied to the project, we can publish a Docker image for our project using the <strong><em>publishImage</em></strong> Gradle task.</p><p>This task requires some form of authentication against the GCP project we are attempting to access. Specifically, we need access to the following resource url based on our project configuration: <em>gcr.io/&lt;project name&gt;/&lt;app name&gt;</em></p><p>As the Ktor Docker configuration uses the jib plugin under the hood, we can review documentation from jib on how to authenticate with different container registries.</p><p>For Container Registry, there straightforward options for authenticating the requests to publish our image:</p><ol><li>Setup Google Cloud Application Default credentials on your machine using an account that has access to necessary GCP project</li><li>Setup gcloud as a credential helper for any Docker-supported registries</li></ol><p>To <a href="https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login">setup auth via application default credentials</a>, run the following terminal command before running the <strong><em>publishImage</em></strong> task: <strong><em>gcloud auth application-default login</em></strong></p><p>To <a href="https://cloud.google.com/sdk/gcloud/reference/auth/configure-docker">setup auth via gcloud Docker credential helper</a>, run the following terminal commands before running the <strong><em>publishImage</em></strong> task:</p><ul><li><strong><em>gcloud auth configure-docker gcr.io</em></strong></li><li><strong><em>gcloud auth login</em></strong></li></ul><p>Once one of these (or another) authentication methods are applied, running the <strong><em>publishImage</em></strong> task should successfully publish the built image to Container Registry.</p><h3>How to deploy a Ktor Docker image to local Docker service?</h3><p>Using the Ktor Docker configuration, we can also streamline publishing of our project’s Docker image to a local Docker.</p><ol><li>Start Docker on your development machine</li><li>Run the <strong><em>publishImageToLocalRegistry</em></strong> Gradle task</li></ol><h3>How to run the Ktor service container using generated Gradle tasks?</h3><p>Using the Ktor Docker configuration, we can build an image and start running it with the local Docker daemon by running the <strong><em>runDocker</em></strong> Gradle task.</p><h3>Conclusion</h3><p>The Ktor plugin’s Docker configuration provides a streamlined workflow for developers working with Ktor services via Docker.</p><p>By configuring the plugin to publish images to Google Cloud’s Container Registry, developers can publish. This task can be used by all developers, and by CI/CD workflows to provide a consistent deployment workflow.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2c5aeb3f263b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/goobar/how-to-publish-a-ktor-docker-image-to-container-registry-using-the-ktor-plugin-2c5aeb3f263b">How to Publish a Ktor Docker Image to Container Registry Using the Ktor Plugin</a> was originally published in <a href="https://medium.com/goobar">goobar</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building a CI Pipeline for Kotlin Multiplatform Mobile Using GitHub Actions]]></title>
            <link>https://engineering.premise.com/kotlin-multiplatforrm-github-actions-workflow-3e5e0fcb7081?source=rss-696aff678e4------2</link>
            <guid isPermaLink="false">https://medium.com/p/3e5e0fcb7081</guid>
            <category><![CDATA[kotlin-multiplatform]]></category>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[github-actions]]></category>
            <category><![CDATA[continuous-integration]]></category>
            <category><![CDATA[mobile-app-development]]></category>
            <dc:creator><![CDATA[Nate Ebel]]></dc:creator>
            <pubDate>Thu, 06 Oct 2022 12:02:09 GMT</pubDate>
            <atom:updated>2023-10-17T16:44:19.561Z</atom:updated>
            <content:encoded><![CDATA[<p><strong><em>By Nate Ebel, Android Engineer</em></strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xzEJoB_AtCrQvVT0KB8OVw.png" /></figure><p>When evaluating a Kotlin Multiplatform solution, it was very important to our team that we understand how we could build a CI/CD pipeline for the project. It needed to be easy for our team to push changes and build a new version of the project.</p><p>Both our iOS and Android applications use GitHub Actions for our CI needs. So, in this post, we’ll walkthrough how to setup a GitHub Actions workflow for building a Kotlin Multiplatform Mobile project.</p><p>For this post, we are assuming that we already know how to build/integrate our shared code locally, and are focused on moving that process to CI.</p><p>For the tldr; you can jump to the end to see two sample workflows in their entirety. One for PR builds and one for Release builds.</p><p>One implementation note on the Swift Package distribution approach detailed in this post.</p><p>This approach is fairly straightforward as it simply publishes a built XCFramework, along with a Package.swift file to a GitHub repo. Xcode can then import this Swift package.</p><p>After using this approach for 1.5 years, we’ve identified a an issue with this approach. Xcode does not perform a shallow clone of the Swift Package repo, so when syncing new versions of our Swift Package, Xcode may need to fetch many versions of the built XCFramework which are stored in git history. After 1.5 years this became prohibitively slow.</p><p>Our team is currently migrating to a remote url distribution model for our Swift Package, and will write another post on that approach once completed. Additionally, the great folks over at <a href="https://touchlab.co/">Touchlab</a> are working on some tooling to help make this process easier for more teams to setup from the beginning.</p><p>This is a part of an ongoing series on our usage of Kotlin Multiplatform at Premise:</p><ul><li><strong>Part 1: </strong><a href="https://engineering.premise.com/kotlin-multiplatform-at-premise-b28d85825c9f"><strong>Kotlin Multiplatform at Premise</strong></a></li><li><a href="https://engineering.premise.com/kotlin-multiplatform-project-structure-brownfield-applications-58038800a6ea"><strong>Part 2: Kotlin Multiplatform Project Structure for Integrating with Brownfield Applications</strong></a></li><li><a href="https://engineering.premise.com/kotlin-multiplatforrm-github-actions-workflow-3e5e0fcb7081"><strong>Part 3: Building a CI Pipeline for Kotlin Multiplatform Mobile Using GitHub Actions</strong></a></li><li><strong>Part 4: </strong><a href="https://engineering.premise.com/publishing-kotlin-multiplatform-swift-packages-to-google-cloud-storage-be5c6987e5d"><strong>Publishing Kotlin Multiplatform Swift Packages to Google Cloud Storage</strong></a></li><li><strong>Part 5: Generating BuildConfig Files for a Kotlin Multiplatform Library</strong> — Coming Soon</li><li><strong>Part 6: Optimizing Local Build Times for Kotlin Multiplatform Mobile Projects</strong> — Coming Soon</li></ul><h3>Creating a GitHub Actions Workflow for Kotlin Multiplatform Mobile</h3><p>To start, we’ll create a new workflow file at .github/workflows/build-release.yml. In this file, we’ll defined our <a href="https://docs.github.com/en/actions/using-workflows/about-workflows">GitHub Actions workflow</a>.</p><pre>// .github/workflows/build-release.yml</pre><pre>name: Build Release</pre><p>In our case, we only trigger this workflow when a developer manually kicks off a build from the Actions tab. This is enabled by adding the workflow_dispatchtrigger to our workflow file.</p><pre>// .github/workflows/build-release.yml</pre><pre>name: Build Release<br><br><strong>on:<br>  workflow_dispatch:</strong></pre><p>If you wanted to publish a new version on any change to your main branch, you could add a push trigger with a filter for the main branch.</p><pre>// .github/workflows/build-release.yml</pre><pre>name: Build Release<br><br>on:<br>  workflow_dispatch:<br>  <strong>push:<br>    branches: &#39;main&#39;</strong></pre><h3>Setting Up Our Workflow Environment</h3><p>Next up, we’re going to set some environment variables for our build.</p><pre>...</pre><pre><strong>env:<br>  XCODE_VERSION: &#39;13.4.1&#39;<br>  JAVA_VERSION: &#39;14&#39;<br>  ARTIFACTORY_URL: ${{ secrets.ARTIFACTORY_URL }}<br>  ARTIFACTORY_REPO: ${{ secrets.ARTIFACTORY_REPO }}<br>  ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}<br>  ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}<br>  ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}</strong></pre><p>Let’s walk through why we’re using each of these.</p><p>We define constants for the Java and Xcode versions we’ll use for the build simply to pull those to the top of thee file and make theme easier to find.</p><pre>XCODE_VERSION: &#39;13.4.1&#39;<br>JAVA_VERSION: &#39;14&#39;</pre><p>The Artifactory variables are setup so we can make those values available to our JVM/Android artifact publication tasks. The actual values are pulled from GitHub Secrets so they can be hidden from the repo and rotated when needed.</p><pre>ARTIFACTORY_URL: ${{ secrets.ARTIFACTORY_URL }}<br>ARTIFACTORY_REPO: ${{ secrets.ARTIFACTORY_REPO }}<br>ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}<br>ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}</pre><p>And finally, we setup an access token that has read/push access to an additional GitHub repo within our org that we will ultimately used to serve our Swift Package artifact to the iOS application.</p><pre>ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}</pre><h3>Configuring a Job to Build a Kotlin Multiplatform Mobile Project</h3><p>Now, we can setup the <a href="https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobs">Job</a> that will actually build our project when our workflow is run.</p><p>The most important thing to call out here is that we’ll need to use a Mac OS runner if we want to build any iOS target. This is because we’ll need the Xcode Build Tools for our iOS compile/linking Gradle tasks.</p><p>Here, we’ve explicitly chosen the macos-12 runner, but you might also consider using macos-lateset if it has the tooling needed for your build.</p><p>You can find more details about available GitHub Actions runners <a href="http://macos-latest/">here</a>.</p><pre>...</pre><pre><strong>jobs:<br>  build:<br>    name: Build &amp; Test<br>    runs-on: macos-12<br>    steps:<br>      ...</strong></pre><p>Next, we can start adding individual build steps to our job.</p><h3>Checking Out Kotlin Multiplatform and Swift Package Repos</h3><p>The first build steps will be to checkout the two repos needed for the build:</p><ul><li>Repo 1 is our mobile-shared, Kotlin Multiplatform, project. This is the repo we will actually build and generate output artifacts from.</li><li>Repo 2 is our mobile-shared-swift-package project. This repo only exists as a location for us to publish the XCFramework and Package.swift file generated from the Kotlin Multiplatform build process</li></ul><p>To checkout each repo, we’ll use the standard actions/checkout GitHub Action.</p><p>First, we checkout the primary KMP repo. Because we’re going to checkout a second repo, we add explicit <em>repository</em> and <em>path</em> parameters here to clearly indicate which repo we are checking out, and where on disk it should be copied to. Knowing this location will be needed later when we start adding tags and committing changes to our Swift Package repository.</p><pre>steps:<br>  <strong>- name: Checkout<br>    uses: actions/checkout@v3<br>    with:<br>      repository: premisedata/mobile-shared<br>      path: mobile-shared</strong></pre><p>Next, we check out the repo that stores our Swift Package artifact. Again, here we are explicit about the <em>repository</em> and <em>path </em>so we can differentiate between it, and the primary repo that was checked out, when we need to push our new Swift Package to the repo.</p><p>We also explicitly pass a <em>token</em> to the checkout action. This is the token that was setup with our other environment variables. This extra token is needed to authorize requests to this second repo, because a workflow only has access to the repo its defined within by default.</p><pre>steps:<br>  - name: Checkout<br>    uses: actions/checkout@v3<br>    with:<br>      repository: premisedata/mobile-shared<br>      path: mobile-shared</pre><pre>  <strong>- name: Checkout<br>    uses: actions/checkout@v3<br>    with:<br>      repository: premisedata/mobile-tutorials-swift-package<br>      path: mobile-tutorials-swift-package<br>      token: ${{env.ACCESS_TOKEN}}</strong></pre><h3>Setting up Java and Xcode Tooling</h3><p>We want to be explicit in the versioning we use for our CI tooling so we get repeatable results. To help with this, we explicitly setup both the JVM and Xcode tooling versions on our runner.</p><pre>steps:<br>  ...</pre><pre>  <strong>- uses: actions/setup-java@v1<br>    with:<br>      java-version: ${{ env.JAVA_VERSION }}<br><br>  - uses: maxim-lobanov/setup-xcode@v1<br>    with:<br>      xcode-version: ${{ env.XCODE_VERSION }}</strong></pre><h3>Extracting Project Version for Git Tagging</h3><p>Once our XCFramework and Package.Swift file are generated, we ultimately want to consume them as a Swift Package from Xcode. To do this, we publish both files to our second mobile-shared-swift-package repo.</p><p>To resolve a given version of this package, Xcode will match a specified version code to a tag in the repo.</p><p>This means that any project version that is released needs to have a matching tag in pushed to the Swift Package repo.</p><p>Our version is defined as a Gradle project property within the project’sgradle.properties file.</p><pre>// gradle.properties</pre><pre>libraryVersion=1.0.0</pre><p>To extract this version, and make it available to future workflow steps, we define a build step prints available Gradle properties, extracts the version using <em>grep</em> and <em>cut</em>, and stores it as a env variable in the workflow.</p><ul><li><a href="https://stackoverflow.com/questions/44466728/gradle-project-version-from-build-gradle-via-shell-bash-script">Getting Gradle properties from the command line</a></li><li><a href="https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable">Setting output as env variables</a></li></ul><pre>steps:<br>  ...</pre><pre>  <strong>- name: Extract Version Info<br>    run: |<br>      cd mobile-shared<br>      export ARTIFACT_VERSION=$(./gradlew properties | grep ^version: | cut -c 10-)<br>      echo &quot;ARTIFACT_VERSION=$ARTIFACT_VERSION&quot; &gt;&gt; $GITHUB_ENV</strong></pre><p>In a later build step, we’ll access the saved env variable when tagging a new version of thee Swift Package.</p><pre>git tag -a ${{ env.ARTIFACT_VERSION }}</pre><h3>Building the Artifacts</h3><p>Now, it’s finally time to build the actual artifacts.</p><p>To do this, we’ll use gradle/gradle-build-action to run our Gradle tasks to build and test the project.</p><p>We explicitly define <em>build-root-directory</em> to point to our primary KMP repo when running the Gradle tasks. And the <em>arguments</em> parameters define the actual tasks we want to run; in this case build and createSwiftPackage</p><pre>steps:<br>  ...</pre><pre>  <strong>- name: Build Artifacts<br>    uses: gradle/gradle-build-action@v2<br>    with:<br>      build-root-directory: mobile-shared<br>      arguments: build createSwiftPackage</strong></pre><ul><li>The build task will compile and test the code</li><li>The createSwiftPackage task coordinates the generation of a Package.swift file and and zipping/copying of the XCFramework into a more findable location</li></ul><h3>Publishing the JVM Artifacts</h3><p>Once we’ve built the project, we can publish our artifacts.</p><p>The JVM/Android artifacts are published to our internal Maven repository; currently managed with an internal Artifactory instance. Here, we use those Artifactory env variables we defined initially to configure the publication tasks so they are uploaded to the correct locations.</p><pre>steps:<br>  ...</pre><pre>  <strong>- name: Publish JVM Artifacts<br>    uses: gradle/gradle-build-action@v2<br>    with:<br>      build-root-directory: mobile-shared<br>      arguments: artifactoryPublish artifactoryDeploy -PARTIFACTORY_URL=${{env.ARTIFACTORY_URL}} -PARTIFACTORY_REPO=${{env.ARTIFACTORY_REPO}} -PARTIFACTORY_USERNAME=${{env.ARTIFACTORY_USERNAME}} -PARTIFACTORY_PASSWORD=${{env.ARTIFACTORY_PASSWORD}}</strong></pre><h3>Publishing the Swift Package</h3><p>Publishing our Swift Package artifact is more involved than on the JVM side.</p><p>First, we run a build step that removes the existing XCFramework file from our mobile-shared-swift-package repo. This is done so there’s only one version of the framework in the repo for any given commit.</p><pre>steps:<br>  ...<br>  <strong>- name: Prepare iOS Artifacts<br>    run: |<br>      rm -f mobile-shared-swift-package/mobileshared-*<br>      cp -r mobile-shared/shared/swiftpackage/* mobile-shared-swift-package</strong></pre><p>Next, we “publish” the new Swift Package to our repo by committing the updated XCFramework and Package.swift file. In addition to committing the changes, we must be sure to tag the commit with the project version so Xcode can resolve this new named version of our Kotlin Multiplatform code.</p><pre>steps:<br>  ...<br>  - name: Prepare iOS Artifacts<br>    run: |<br>      rm -f mobile-shared-swift-package/mobileshared-*<br>      cp -r mobile-shared/shared/swiftpackage/* mobile-shared-swift-package</pre><pre>  <strong>- name: Publish iOS Artifacts<br>    run: |<br>      cd mobile-shared-swift-package<br>      git add --all<br>      git commit -m &quot;Update to version ${{ env.ARTIFACT_VERSION }}&quot;<br>      git tag -a ${{ env.ARTIFACT_VERSION }} -m &quot;mobile-shared v${{ env.ARTIFACT_VERSION }}&quot;<br>      git push<br>      git push origin ${{ env.ARTIFACT_VERSION }}</strong></pre><p>The last step here is to tag the Kotlin Multiplatform project itself so the source code has a tag matching the release tag. This step isn’t necessary for integration, but helps us match versions to the code that generated them.</p><pre>steps:<br>  ...<br>  - name: Prepare iOS Artifacts<br>    run: |<br>      rm -f mobile-shared-swift-package/mobileshared-*<br>      cp -r mobile-shared/shared/swiftpackage/* mobile-shared-swift-package</pre><pre><strong>  </strong>- name: Publish iOS Artifacts<br>    run: |<br>      cd mobile-shared-swift-package<br>      git add --all<br>      git commit -m &quot;Update to version ${{ env.ARTIFACT_VERSION }}&quot;<br>      git tag -a ${{ env.ARTIFACT_VERSION }} -m &quot;mobile-shared v${{ env.ARTIFACT_VERSION }}&quot;<br>      git push<br>      git push origin ${{ env.ARTIFACT_VERSION }}</pre><pre>  <strong>- name: Tag Release<br>    run: |<br>      cd mobile-shared<br>      git tag -a ${{ env.ARTIFACT_VERSION }} -m &quot;mobile-shared v${{ env.ARTIFACT_VERSION }}&quot;<br>      git push<br>      git push origin ${{ env.ARTIFACT_VERSION }}</strong></pre><p>Once these commits and tags are pushed to the mobile-shared-swift-package repo, Xcode should be able to resolve the new version of our library.</p><h3>Reviewing the Final Release Workflow Solution</h3><p>To help visualize everything together, here’s a complete look at our solution. Some bits of this have been modified for brevity, but all the core steps are here.</p><pre>// .github/workflows/build-release.yml</pre><pre>name: Build Release<br><br>on:<br>  workflow_dispatch:<br>  push:<br>    branches: &#39;release&#39;<br><br>env:<br>  XCODE_VERSION: &#39;13.4.1&#39;<br>  JAVA_VERSION: &#39;14&#39;<br>  ARTIFACTORY_URL: ${{ secrets.ARTIFACTORY_URL }}<br>  ARTIFACTORY_REPO: ${{ secrets.ARTIFACTORY_REPO }}<br>  ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}<br>  ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}<br>  ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}<br><br>jobs:<br>  build:<br>    name: Build &amp; Test<br>    runs-on: macos-12<br>    steps:<br>      - name: Checkout<br>        uses: actions/checkout@v3<br>        with:<br>          repository: premisedata/mobile-shared<br>          path: mobile-shared<br><br>      - name: Checkout<br>        uses: actions/checkout@v3<br>        with:<br>          repository: premisedata/mobile-shared-swift-package<br>          path: mobile-shared-swift-package<br>          token: ${{env.ACCESS_TOKEN}}<br><br>      - uses: actions/setup-java@v1<br>        with:<br>          java-version: ${{ env.JAVA_VERSION }}<br><br>      - uses: maxim-lobanov/setup-xcode@v1<br>        with:<br>          xcode-version: ${{ env.XCODE_VERSION }}<br><br>      - name: Extract Version Info<br>        run: |<br>          cd mobile-shared<br>          export ARTIFACT_VERSION=$(./gradlew properties | grep ^version: | cut -c 10-)<br>          echo &quot;ARTIFACT_VERSION=$ARTIFACT_VERSION&quot; &gt;&gt; $GITHUB_ENV<br><br>      - name: Build Artifacts<br>        uses: gradle/gradle-build-action@v1<br>        with:<br>          build-root-directory: mobile-shared<br>          arguments: build createSwiftPackage<br><br>      - name: Publish JVM Artifacts<br>        uses: gradle/gradle-build-action@v1<br>        with:<br>          build-root-directory: mobile-shared<br>          arguments: artifactoryPublish artifactoryDeploy -PARTIFACTORY_URL=${{env.ARTIFACTORY_URL}} -PARTIFACTORY_REPO=${{env.ARTIFACTORY_REPO}} -PARTIFACTORY_USERNAME=${{env.ARTIFACTORY_USERNAME}} -PARTIFACTORY_PASSWORD=${{env.ARTIFACTORY_PASSWORD}}<br><br>      - name: Prepare iOS Artifacts<br>        run: |<br>          rm -f mobile-shared-swift-package/mobileshared-*<br>          cp -r mobile-shared/shared/swiftpackage/* mobile-shared-swift-package<br><br>      - name: Publish iOS Artifacts<br>        run: |<br>          cd mobile-shared-swift-package<br>          git add --all<br>          git commit -m &quot;Update to version ${{ env.ARTIFACT_VERSION }}&quot;<br>          git tag -a ${{ env.ARTIFACT_VERSION }} -m &quot;mobile-shared v${{ env.ARTIFACT_VERSION }}&quot;<br>          git push<br>          git push origin ${{ env.ARTIFACT_VERSION }}<br><br>      - name: Tag Release<br>        run: |<br>          cd mobile-shared<br>          git tag -a ${{ env.ARTIFACT_VERSION }} -m &quot;mobile-shared v${{ env.ARTIFACT_VERSION }}&quot;<br>          git push<br>          git push origin ${{ env.ARTIFACT_VERSION }}</pre><h3>Reviewing a PR Workflow Solution</h3><p>We’ve walked through a version of our release workflow that builds, tests, and publishes artifacts for our Kotlin Multiplatform project. Because it’s doing a full release, it has some extra build steps that are not required if you simply want to setup a CI workflow for your PRs.</p><p>To round out our discussion, here’s an example of a PR workflow for a Kotlin Multiplatform project.</p><pre>// .github/workflows/pull-request.yml</pre><pre>name: Build Pull Request<br><br>on:<br>  pull_request:<br>    branches:<br>     - &#39;main&#39;<br>     - &#39;dev&#39;<br>     - &#39;release&#39;<br><br>env:<br>  XCODE_VERSION: &#39;13.4.1&#39;<br>  JAVA_VERSION: &#39;14&#39;<br><br>jobs:<br>  build:<br>    name: Build &amp; Test<br>    runs-on: macos-12<br>    steps:<br>      - name: Checkout<br>        uses: actions/checkout@v3<br><br>      - uses: actions/setup-java@v1<br>        with:<br>          java-version: ${{ env.JAVA_VERSION }}<br><br>      - uses: maxim-lobanov/setup-xcode@v1<br>        with:<br>          xcode-version: ${{ env.XCODE_VERSION }}<br><br>      - name: Build<br>        id: gradle<br>        uses: gradle/gradle-build-action@v2<br>        with:<br>          arguments: check</pre><h3>Final Thoughts</h3><p>These workflows have worked well for us in the past 1.5 years. Our publishing model for our Swift Package is evolving, but the workflow itself has worked great.</p><p>As linking native binaries for the XCFrameworks is slow, and running CI jobs is not free, it’s a good idea to ensure you’re only building native targets for iOS platforms you actually need. For example, you might find that you no longer need an X64 version of your code, and could stop building that in CI to save money.</p><p><strong><em>Premise is constantly looking to hire top-tier engineering talent to help us improve our front-end, mobile offerings, data and cloud infrastructure. Come find out why we’re consistently named among the top places to work in Silicon Valley by visiting our </em></strong><a href="https://bit.ly/3wnTrLn"><strong><em>Careers</em></strong></a><strong><em> page.</em></strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3e5e0fcb7081" width="1" height="1" alt=""><hr><p><a href="https://engineering.premise.com/kotlin-multiplatforrm-github-actions-workflow-3e5e0fcb7081">Building a CI Pipeline for Kotlin Multiplatform Mobile Using GitHub Actions</a> was originally published in <a href="https://engineering.premise.com/">Engineering at Premise</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Kotlin Multiplatform Project Structure for Integration with Brownfield Applications]]></title>
            <link>https://engineering.premise.com/kotlin-multiplatform-project-structure-brownfield-applications-58038800a6ea?source=rss-696aff678e4------2</link>
            <guid isPermaLink="false">https://medium.com/p/58038800a6ea</guid>
            <category><![CDATA[mobile-app-development]]></category>
            <category><![CDATA[ios-app-development]]></category>
            <category><![CDATA[kotlin-multiplatform]]></category>
            <category><![CDATA[android-app-development]]></category>
            <dc:creator><![CDATA[Nate Ebel]]></dc:creator>
            <pubDate>Thu, 06 Oct 2022 11:55:24 GMT</pubDate>
            <atom:updated>2023-10-17T16:44:38.753Z</atom:updated>
            <content:encoded><![CDATA[<p><strong><em>By Nate Ebel, Staff Software Engineer</em></strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0GwTqfdHDwnGDM4jCt1wzw.png" /></figure><p>Most Kotlin Multiplatform Mobile sample projects follow the same project structure — an Android project directory, an iOS project directory, and a common module shared between them in a single mono repo.</p><figure><img alt="A diagram showing an Android app, an iOS app, and Shared Code all existing within a single project" src="https://cdn-images-1.medium.com/max/400/1*Ia9N3sdPlY8EuS4kB8je1g.png" /><figcaption>A typical greenfield Kotlin Multiplatform project structure</figcaption></figure><p>This makes sense for a greenfield project.</p><p>I think this also makes a lot of sense for sample projects since it reduces project complexity and setup times; both of which are tremendously valuable in learning content for beginners.</p><p>However, this greenfield monorepo world is not the world that most teams are living in when evaluating whether or not Kotlin Multiplatform is a viable code sharing solution.</p><p>When our team began evaluating Kotlin Multiplatform, we knew we needed a solution that did not require us to merge our separate Android and iOS repos into a new monorepo structure. Instead, we took the approach of treating our shared code as an internal, first-party library that would be published to, and consumed from, some internal location.</p><blockquote>we took the approach of treating our shared code as an internal, first-party library</blockquote><p>In this paradigm, we end up with three separate repos- an Android repo, an iOS repo and our shared code in a new Kotlin Multiplatform repo.</p><figure><img alt="A diagram showing an Android app, an iOS app, and a Kotlin Multiplatform project all existing as separate repos" src="https://cdn-images-1.medium.com/max/1024/1*hsXpEhVVZgAdeylLs8GDpQ.png" /><figcaption>A Kotlin Multiplatform project structure for integrating with brownfield mobile applications</figcaption></figure><p>In this post, we’ll detail our exploration of both a single-feature project structure, and a structure that has supported us as we adopt Kotlin Multiplatform for additional functional areas over time.</p><p>This is a part of an ongoing series on our usage of Kotlin Multiplatform at Premise:</p><ul><li><strong>Part 1: </strong><a href="https://engineering.premise.com/kotlin-multiplatform-at-premise-b28d85825c9f"><strong>Kotlin Multiplatform at Premise</strong></a></li><li><a href="https://engineering.premise.com/kotlin-multiplatform-project-structure-brownfield-applications-58038800a6ea"><strong>Part 2: Kotlin Multiplatform Project Structure for Integrating with Brownfield Applications</strong></a></li><li><a href="https://engineering.premise.com/kotlin-multiplatforrm-github-actions-workflow-3e5e0fcb7081"><strong>Part 3: Building a CI Pipeline for Kotlin Multiplatform Mobile Using GitHub Actions</strong></a></li><li><strong>Part 4: </strong><a href="https://engineering.premise.com/publishing-kotlin-multiplatform-swift-packages-to-google-cloud-storage-be5c6987e5d"><strong>Publishing Kotlin Multiplatform Swift Packages to Google Cloud Storage</strong></a></li><li><strong>Part 5: Generating BuildConfig Files for a Kotlin Multiplatform Library</strong> — Coming Soon</li><li><strong>Part 6: Optimizing Local Build Times for Kotlin Multiplatform Mobile Projects</strong> — Coming Soon</li></ul><h3>Single Feature Module Structure</h3><p>How do you structure a Kotlin Multiplatform project with a single module?</p><p>The most straightforward project structure for this first-party library integration model is to have a single Gradle module within the Kotlin Multiplatform project. This module contains all shared code.</p><figure><img alt="A diagram showing a single Gradle module within the root Kotlin Multiplatform project repo" src="https://cdn-images-1.medium.com/max/998/1*lEH4SJVSC3Y6nQCETXJQOA.png" /><figcaption>A single Gradle module within the root Kotlin Multiplatform project repo</figcaption></figure><p>Internally, this module may be organized in several ways.</p><p>There could be a single <em>`commonMain`</em> source set that defines shared code for all targets.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/990/1*JDgB87ApVDujtU6Fo5TRJw.png" /><figcaption>A Kotlin Multiplatform Gradle module may contain a single source set for common code</figcaption></figure><p>There could be a `commonMain` plus several other source sets-one for each build target allowing for platform-specific code where needed.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/983/1*9ZEfVgD9rusQp5_NQQJGGg.png" /><figcaption>A Kotlin Multiplatform Gradle module may contain a several source sets mapping to the various target platforms</figcaption></figure><p>And within any of these source sets, we are free to define whatever package structure we may like.</p><h4>Build Output</h4><p>With this structure, our build would generate a single output artifact for each target. That artifact would contain all the code exposed to a given platform.</p><p>If the project targets Android and iOS, then we would end up with</p><ul><li>a .jar or .aar</li><li>an XCFramework</li></ul><h4><strong>Publishing JVM/Android Artifacts</strong></h4><p>To generate Maven publications for our Android and JVM targets, we can add the maven-publish plugin.</p><pre>// shared/build.gradle.kts</pre><pre>plugins <strong>{<br>    id(&quot;maven-publish&quot;)</strong><br><strong>}</strong></pre><pre>kotlin {<br>  android()<br>}</pre><p>And to ultimately publish those artifacts to an internal Artifactory server we configure something like the following:</p><pre>// shared/build.gradle.kts</pre><pre>...</pre><pre><strong><em>artifactory </em>{<br>    setContextUrl(<em>properties</em>[&quot;ARTIFACTORY_URL&quot;])<br>    publish(<em>delegateClosureOf</em>&lt;org.jfrog.gradle.plugin.artifactory.dsl.PublisherConfig&gt; {<br>        repository(<em>delegateClosureOf</em>&lt;org.jfrog.gradle.plugin.artifactory.dsl.DoubleDelegateWrapper&gt; {<br>            setProperty(&quot;repoKey&quot;, <em>properties</em>[&quot;ARTIFACTORY_REPO&quot;])<br>            setProperty(&quot;username&quot;, <em>properties</em>[&quot;ARTIFACTORY_USERNAME&quot;])<br>            setProperty(&quot;password&quot;, <em>properties</em>[&quot;ARTIFACTORY_PASSWORD&quot;])<br>            setProperty(&quot;maven&quot;, true)<br>        })<br>        defaults(<em>delegateClosureOf</em>&lt;groovy.lang.<em>GroovyObject</em>&gt; {<br>            invokeMethod(&quot;publications&quot;, <em>arrayOf</em>(&quot;androidRelease&quot;, &quot;kotlinMultiplatform&quot;))<br>        })<br>    })<br>}</strong></pre><h4><strong>Building iOS Artifacts</strong></h4><p>For iOS, building the XCFramework is pretty straightforward in the single-module approach.</p><p>We can configure our iOS targets to generate an XCFramework in our Gradle buildscript.</p><pre>// shared/build.gradle.kts<br>...</pre><pre><strong>kotlin {<br>    val xcf = XCFramework()<br><br>    ios {<br>        binaries.framework {<br>            baseName = &quot;shared&quot;<br>            xcf.add(this)<br>        }<br>    }<br>}</strong></pre><p>Adding this XCFramework() configuration will result in an assembleXCFramework task being created that will generate the XCFramework for us.</p><p>Consuming this XCFramework can be done in several ways. Our team publishes the framework as a Swift Package. That process is a bit more involved, and is described in more detail in this post:</p><p><a href="https://engineering.premise.com/kotlin-multiplatforrm-github-actions-workflow-3e5e0fcb7081">https://engineering.premise.com/kotlin-multiplatforrm-github-actions-workflow-3e5e0fcb7081</a></p><h4>Benefits to a single-module approach</h4><p>This approach is the easiest to setup as it requires the least amount of Gradle configuration. A single module also reduces any cognitive load in considering whether or not to add new code in a new module or not; everything just goes into the same place (maybe separated by package).</p><p>A single module also means a single output artifact per platform, which means fewer artifacts to version, publish, and consume.</p><h4>Drawbacks to a single-module approach</h4><p>Putting all code into a single module can lead to unintended dependencies between individual components as the lines between functional areas blur. A single module can also have a negative impact on build times as the project grows and any change requires the whole project to be rebuilt.</p><p>Organizing around a single module also means likely having a single library version for everything. This may, or may not, be a problem based on how you envision your project growing and your team working on it. If there’s a chance that separate functional areas may need to develop at a different pace, then a single module is not the ideal structure.</p><p>And finally, if all the shared code exists within, and is exported to, a single build output per target, there’s no way to consume only specific functional areas. For example, if one project needed both DTOs and Analytics and another only needed Analytics there would be no way to separate the use cases; both projects would receive all shared code at all times. As we’ll see in the next section, this is likely to be the case for iOS no matter what, but on the JVM/Android side we have more options.</p><h3>Multi-Feature Module Structure</h3><p>As we evaluated Kotlin Multiplatform, we initially started with only a shared analytics use case. However, we also envisioned an N+1 world in which we wanted to add more functional areas to our shared code.</p><p>Imagining that we might want multiple features such as <em>analytics</em>, <em>dtos</em>, <em>deeplinking</em>, etc lead us to explore a project structure in which each functional area could be a separate Gradle module within our Kotlin Multiplatform project.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/550/1*Ut67O7pWPhbLV944FmFNyw.png" /><figcaption>A Kotlin Multiplatform project may define multiple sub-projects (Gradle modules) that can contain specific functionality isolated from other modules.</figcaption></figure><p>This approach is straightforward for JVM and Android targets.</p><p>Each functional module may define it’s own publishing config in the same way as a single-module approach would. And ultimately those publishing configs will be used to generate a .jar or .aar for each module.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/783/1*_IT5OCBIva1ryp3TPo3Wbw.png" /><figcaption>Individual Gradle modules can publish JVM/Android artifacts</figcaption></figure><p>However, on the iOS side there is additional project configuration needed to support individual functional modules. A current limitation of the Kotlin Multiplatform tooling is that a single project can only produce a single output XCFramework.</p><p>This means we can’t generate one XCFramework per module as we can for JVM/Android targets.</p><p>Instead, we must define another Gradle module to act as a wrapper. This sharedmodule defines each functional module as a dependency and then configures the output XCFramework to include them all.</p><pre>// shared/build.gradle.kts</pre><pre>plugins <strong>{<br>    </strong><em>kotlin</em>(&quot;multiplatform&quot;)<br><strong>}</strong></pre><pre><em>kotlin </em><strong>{<br>    </strong>val xcf = <em>XCFramework</em>(<em>rootProject</em>.<em>name</em>)<br><br>    val nativeTargets = <em>listOf</em>(iosSimulatorArm64(), iosArm64())<br>    nativeTargets.<em>forEach </em><strong>{ </strong><em>target </em><strong>-&gt;<br>        </strong><em>target</em>.binaries.framework <strong>{<br>            </strong>baseName = <em>rootProject</em>.<em>name<br>            </em>xcf.add(this)<br><br>            export(project(&quot;:analytics&quot;))<br>            export(project(&quot;:network&quot;))<br>            export(project(&quot;:data&quot;))<br>            transitiveExport = true<br>        <strong>}<br>    }<br><br>    </strong><em>sourceSets </em><strong>{<br>        </strong>val iosSimulatorArm64Main by <em>getting<br>        </em>val iosArm64Main by <em>getting<br>        </em>val iosMain by <em>creating </em><strong>{<br>            </strong>iosSimulatorArm64Main.dependsOn(this)<br>            iosArm64Main.dependsOn(this)<br>        <strong>}<br>    }<br>}</strong></pre><p>The resulting project structure looks something like the following:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SFWkNu0dfer0DKRZcrnbJg.png" /><figcaption>Publishing multiple Gradle modules for iOS requires creating a <em>shared wrapper module that depends on all others and is used to generate the output XCFramework</em></figcaption></figure><h3>Why use separate modules?</h3><p>Separating functional areas into their own benefits has several benefits in our estimation:</p><ul><li>separation of concerns</li><li>improved build times when changing a single module</li><li>separate output artifacts for JVM/Android</li></ul><p>We felt these attributes would scale well if we continued to invest in KMP. In particular, it meant that we could integrate functional modules on a case-by-case basis. For example, our Android app could consume every shared feature, while our mobile-proxy backend service could consume only the Analytics module if desired.</p><h3>Drawbacks to a multi-module approach?</h3><p>The main drawback here is probably the added complexity.</p><p>We must configure an extra wrapper module for the iOS output. We must add additional JVM/Android publication configurations for each functional module. We must work harder to keep dependency versions synced across each module. There’s extra work involved in adding a new functional area to the project due to the cost of setting up a new module.</p><p>All of these are very solvable problems, but in our experience, has required members of the team that are comfortable with non-trivial Gradle configuration.</p><h3>Multi-Project Structure</h3><p>We’ve focused the discussion so far on how to structure a single project for one, or more, output artifacts. However, nothing is stopping us from just creating separate KMP projects altogether.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*V__Hs0YGDZcsHbcgiYqd_w.png" /><figcaption>Each feature could be moved into a separate Kotlin Multiplatform repo/project and treated as completely independent projects</figcaption></figure><p>This would provide a means of publishing/consuming separate XCFrameworks for iOS projects.</p><p>For a team with a single iOS app, this probably adds extra complexity for zero gain. But if your team maintains multiple iOS projects, having shared functionality split into separate artifacts could be beneficial.</p><p>For JVM/Android applications, this approach would likely provide little benefit to consumers. However, if development of individual functional areas was owned by separate teams/individuals, then separate projects <em>could</em> increase development velocity if those projects are not interdependent.</p><h3>Closing Thoughts</h3><p>Choosing the right project structure for you applications will depend on the applications themselves, your team structures, what code you’re trying to share, etc.</p><p>However, if you’re integrating your Kotlin Multiplatform code into existing applications, the chances are high that you’ll want to structure your KMP project using one of the approaches detailed above.</p><p>We’ve maintained a single KMP project, multi-module structure for over a year and have found it scaling well as we integrate additional functional areas and onboarding more developers.</p><h3>Diving deeper into how we are leveraging Kotlin Multiplatform</h3><p>Once you’ve settled on a project structure, you’ll likely move on to new challenges such as how to build your Kotlin Multiplatform project in CI and how to integrate your XCFramework into your iOS project.</p><ul><li><a href="https://engineering.premise.com/kotlin-multiplatforrm-github-actions-workflow-3e5e0fcb7081">Building A Kotlin Multiplatform Mobile CI Pipeline Using GitHub Actions</a></li><li><a href="https://engineering.premise.com/kotlin-multiplatform-at-premise-b28d85825c9f">Kotlin Multiplatform at Premise</a></li></ul><p><strong><em>Premise is constantly looking to hire top-tier engineering talent to help us improve our front-end, mobile offerings, data and cloud infrastructure. Come find out why we’re consistently named among the top places to work in Silicon Valley by visiting our </em></strong><a href="https://bit.ly/3wnTrLn"><strong><em>Careers</em></strong></a><strong><em> page.</em></strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=58038800a6ea" width="1" height="1" alt=""><hr><p><a href="https://engineering.premise.com/kotlin-multiplatform-project-structure-brownfield-applications-58038800a6ea">Kotlin Multiplatform Project Structure for Integration with Brownfield Applications</a> was originally published in <a href="https://engineering.premise.com/">Engineering at Premise</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Kotlin Multiplatform at Premise]]></title>
            <link>https://engineering.premise.com/kotlin-multiplatform-at-premise-b28d85825c9f?source=rss-696aff678e4------2</link>
            <guid isPermaLink="false">https://medium.com/p/b28d85825c9f</guid>
            <category><![CDATA[kotlin-multiplatform]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[mobile-app-development]]></category>
            <category><![CDATA[kotlin]]></category>
            <dc:creator><![CDATA[Nate Ebel]]></dc:creator>
            <pubDate>Thu, 06 Oct 2022 11:54:16 GMT</pubDate>
            <atom:updated>2023-10-17T16:43:15.032Z</atom:updated>
            <content:encoded><![CDATA[<p><strong><em>by Nate Ebel, Android Engineer</em></strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0GwTqfdHDwnGDM4jCt1wzw.png" /></figure><p>In January of 2021, we pushed the initial commit for our mobile-shared repo — a new Kotlin Multiplatform (KMP) initiative within the mobile engineering team here at Premise.</p><p>Fast forward to 2022, and our usage of Kotlin Multiplatform has increased along with our confidence in the choice to invest in Kotlin Multiplatform as a code sharing solution.</p><p>Much has been written comparing Kotlin Multiplatform to other code sharing solutions — debating the merits of one choice or another and often framed within the context of greenfield development or toy sample projects. While these resources are helpful, they’re often lacking a pragmatic grounding in the realities of scaling a production brown-field project.</p><p>The discussion is often focused on the core technology rather than on <em>why</em> Kotlin Multiplatform was chosen over other solutions, and <em>how</em> Kotlin Multiplatform was actually then adopted, integrated, and scaled across multiple teams and projects.</p><p>This is a part of an ongoing series on our usage of Kotlin Multiplatform at Premise:</p><ul><li><strong>Part 1: </strong><a href="https://engineering.premise.com/kotlin-multiplatform-at-premise-b28d85825c9f"><strong>Kotlin Multiplatform at Premise</strong></a></li><li><a href="https://engineering.premise.com/kotlin-multiplatform-project-structure-brownfield-applications-58038800a6ea"><strong>Part 2: Kotlin Multiplatform Project Structure for Integrating with Brownfield Applications</strong></a></li><li><a href="https://engineering.premise.com/kotlin-multiplatforrm-github-actions-workflow-3e5e0fcb7081"><strong>Part 3: Building a CI Pipeline for Kotlin Multiplatform Mobile Using GitHub Actions</strong></a></li><li><strong>Part 4: </strong><a href="https://engineering.premise.com/publishing-kotlin-multiplatform-swift-packages-to-google-cloud-storage-be5c6987e5d"><strong>Publishing Kotlin Multiplatform Swift Packages to Google Cloud Storage</strong></a></li><li><strong>Part 5: Generating BuildConfig Files for a Kotlin Multiplatform Library</strong> — Coming Soon</li><li><strong>Part 6: Optimizing Local Build Times for Kotlin Multiplatform Mobile Projects</strong> — Coming Soon</li></ul><p>I want to focus both on high-level thinking and decision making, <em>and</em> also highlight some of the key technical challenges we had to solve to begin building with Kotlin Multiplatform across both our iOS and Android teams.</p><p>We’ll start with how we identified the need for code sharing and why we choose KMP as our solution. And we’ll work our way through the validation, education, and scaling process we’ve gone through over this past year.</p><p>This post is the high-level story of our Kotlin Multiplatform journey — the <em>strategy. </em>Throughout this post, we will highlight key technical challenges, and link to other, more focused, posts from the series on the <em>implementation</em> of how we overcame those challenges.</p><h3>Why code sharing? And why now?</h3><p>Our code sharing story starts not with Kotlin Multiplatform, and not in comparing KMP to solutions like <a href="https://flutter.dev/">Flutter</a> or <a href="https://reactnative.dev/">React Native</a>. In fact, it doesn’t start from a strictly technical perspective at all.</p><p>It all started because we wanted more accurate and actionable analytics across our mobile platform.</p><blockquote><strong><em>Tip #1 — Let the problem define the solution</em></strong></blockquote><h3>Standardizing analytics across iOS and Android</h3><p>In early 2021, analytics across our mobile team was a bit of a mess.</p><p>Both iOS and Android cared about analytics, and tracked events to help understand our system. However, it was a bit like the wild west. Naming was very haphazard. Event types weren’t standardized. And there was significant room for error when implementing any given set of events.</p><p>This inconsistency, and uncertainty, hampered our ability to make data-driven decisions.</p><p>We knew we needed to change this as we scaled so data-driven decisions could be made for higher-impact with less cost.</p><p>To solve this problem, we formed a small team of engineering leaders, to standardize our thinking around analytics for the mobile team. These discussions were quite interesting in their own right, but not central to this story. What was important was the output of those discussions.</p><p>Our analytics working group generated several things:</p><ul><li>clear definitions for unique types of analytics event</li><li>specific naming conventions for each event type</li><li>enumerated value sets for key event name components</li></ul><p>This thinking was consolidated into a doc to act as a <em>source of truth</em> for developers looking to implement analytics in any new work. We had effectively developed “cross platform thinking” around analytics for our team.</p><p>As we reached the end of this process, our strike team had developed two new questions:</p><ol><li><em>Could we codify these rules so they’re strongly enforced in code rather than via a doc?</em></li><li><em>Could that enforcement be shared across both iOS, Android, and maybe even other teams?</em></li></ol><p>We had finally arrived at the question of code sharing.</p><h3>Sharing code for shared thinking</h3><p>Code sharing, particularly for mobile development, is a bit of a “holy grail.” Most would probably agree that some theoretical ideal solution would allow for 100% code reuse across all mobile platforms — while still offering peak performance and beautiful platform-native experiences.</p><p>Most that have walked this path would probably also agree that this fantasy solution rarely works out in practice; and certainly not without tradeoffs.</p><p>For our use case though, we didn’t need, or want, a 100% code sharing solution. We wanted something much more focused. Honestly, simply sharing event name constants would have been a viable v1 solution and was discussed as such.</p><p>So, our decision making backdrop at this point was</p><ul><li>We have a clear set of rules and definitions for analytics events.</li><li>We want to share this logic across iOS, Android (and maybe beyond).</li><li>We don’t need shared UI.</li><li>We need to share code using a tech stack that at least a significant portion of our team is familiar with.</li></ul><blockquote>Tip #2 — When evaluating a code sharing solution, consider both the technical, and human factors of each option.</blockquote><h3>Choosing Kotlin Multiplatform</h3><p>We chose Kotlin Multiplatform as our code sharing solution for serval reasons.</p><ul><li>KMP focuses on business logic — not UI.</li><li>KMP is relatively easy to integrate into a brown-field project.</li><li>We had a great deal of Kotlin knowledge on our Android team.</li><li>We had interest in KMP on our Android team.</li><li>We had equal interest in KMP from our iOS team.</li></ul><p>Kotlin Multiplatform was an obvious choice for us. Both our iOS, and Android tech leads arrived at this thought on their own before even having the full discussion.</p><p>It fit our use case of business logic sharing.</p><p>It was considered relatively low risk. If, for whatever reason, the KMP code didn’t work, the Android team could easily move the Kotlin code into our repo, and the iOS could translate it into Swift and bring it into their project.</p><p>At this point, we were comfortable enough to invest in a proof of concept validation.</p><h3>Validating Kotlin Multiplatform for brown-field projects</h3><p>Before we could realistically consider long-term investment into Kotlin Multiplatform, we needed to identify key challenges and risks.</p><p>We needed to understand how to solve the challenges and mitigate the risks before we felt comfortable adopting the proposed solution.</p><p>Some of these primary questions included:</p><ul><li>Where to store this new Kotlin Multiplatform project?</li><li>What multiplatform targets to choose?</li><li>How to structure a Kotlin Multiplatform project?</li><li>How to provide a productive local development experience?</li><li>How to ensure low-cost deployment via CI?</li><li>How to integrate the KMP code into both the existing iOS and Android projects?</li><li>How to onboard other developers?</li></ul><h3>Structuring our project</h3><p>Many Kotlin Multiplatform examples focus on the idea of building an app from scratch. In this use case, it’s common to structure your KMP project as a monorepo with the Shared, Android, and iOS projects all living side-by-side.</p><p>However, we already had existing iOS and Android projects in their own repos. So this typical project structure wouldn’t work for us.</p><p>We realized we had to shift our thinking a bit.</p><p>Rather than thinking of it as building a Kotlin Multiplatform app, we found it more helpful to think of the project as building a separate Kotlin Multiplatform library to then be consumed by each of the existing projects mobile projects.</p><p>With that in mind, we created a new Kotlin Multiplatform project named <em>mobile-shared</em> with a single :analytics module to start.</p><p>This structure would allow us to consolidate all the shared code into this specific repo, which would then need to be built, published, and consumed by our iOS and Android projects.</p><blockquote><strong><em>Tip #3 — For brownfield projects, think of building a multiplatform library rather than a new multiplatform app</em></strong></blockquote><h3>Choosing which platforms to support</h3><p>We needed our shared code to work for both our iOS and Android apps. Our first thought was that we could simply choose iOS and Android targets.</p><p>However, that’s not where we landed.</p><p>We envisioned a world in which our multiplatform experiment went well and in which we might want to share our <em>business event</em> logic with other projects. Many of our services, including our primary <em>mobile-proxy</em> service, are JVM-based — being a mix of Scala, Java, and Kotlin. We thought it would be interesting to see if we could generate an artifact that could be consumed by those projects as well.</p><p>So, rather than using an Android target for our proof-of-concept, we chose a JVM target. This allows us to still consume the JVM artifact in our Android app, but gives us the option to potentially reuse the code in other services down the line.</p><p>We were able to make this choice, because our shared analytics strategy was platform agnostic. We weren’t planning to rely on any Android-specific apis, or SDKs for what we were planning to build.</p><h3>Local development</h3><p>Our mobile team very much cares about developer productivity, and the overall developer experience of our projects. Because of this, it was vital for us to understand how developers could efficiently build, test, and consume the KMP project locally.</p><p>We envisioned developers working on a new feature, opening up the <em>mobile-shared</em> project, building and testing their code locally, before then eventually pushing back to GitHub and creating a PR.</p><p>These seem like relatively straightforward tasks on the surface, but they raised a handful of interesting questions for us.</p><blockquote>Tip #4 — Invest in the developer experience for any code sharing solution</blockquote><h4>How do developers build the Kotlin Multiplatform locally?</h4><p>Kotlin Multiplatform relies on the <a href="https://gradle.org/">Gradle build tool</a>, and by extension, the <a href="https://en.wikipedia.org/wiki/Java_virtual_machine">Java Virtual Machine (JVM)</a>. The Android target requires <a href="https://developer.android.com/studio/releases/build-tools">Android build tooling</a>, and the iOS target requires <a href="https://developer.apple.com/library/archive/technotes/tn2339/_index.html">XCode build tooling</a>.</p><p>This means that whether a dev was on the iOS or Android team, they likely had some tooling to install and configure before the KMP project could be built.</p><p>For iOS devs, they had to:</p><ul><li>Install the Android SDK.</li><li>Install a JDK.</li></ul><p>For Android devs they had to:</p><ul><li>Install XCode build tools.</li></ul><p>Once the tooling was installed and configured properly, we could run our KMP project’s <em>build</em> task using the <a href="https://docs.gradle.org/current/userguide/gradle_wrapper.html">gradle-wrapper</a> to assemble and test the project.</p><p>This left us with two primary artifacts:</p><ul><li>A .jar file for the JVM target</li><li>A XCFramework for the iOS target</li></ul><p>Now that we had these output artifacts, the next step was to understand how to actually consume those from within the iOS and Android projects.</p><h4>How do developers integrate the Kotlin Multiplatform artifacts locally?</h4><p>The fastest way to validate that our XCFramework and .jar could be integrated was to simply move those files into the iOS and Android project directories, and point the projects at them.</p><p>Both XCode and Gradle make it pretty easy to consume a framework or jar as a dependency.</p><p>This let us validate that our sample shared code could, in fact, be consumed from both iOS and Android using a single build process.</p><p>The next step was a move towards scalability.</p><p>Manually copying files around is not a great everyday development experience.</p><p>So, for the Android team, we explored a local deployment flow in which we could build our .jar, publish to our local Maven repository, and consume in our Android project like we would any other external dependency.</p><p>We applied the <a href="https://docs.gradle.org/current/userguide/publishing_maven.html">Maven Publish Plugin</a> to our <em>:analytics</em> module to gain access to the <em>publish</em> and <em>publishToMavenLocal</em> tasks.</p><ul><li>The publishtask generates metadata and associates it with a build artifact for publishing to some maven repository.</li><li>The publishToMavenLocal task deploys the maven publication (artifact + metadata) to the local Maven repository.</li></ul><p>Once the JVM output artifact was published to our local Maven repository, we could access that maven coordinate from the <a href="https://stackoverflow.com/questions/21048958/where-is-maven-local-repository"><em>mavenLocal()</em></a> repository and integrate the dependency into our project as if it were coming from <a href="https://search.maven.org/"><em>mavenCentral()</em></a> or an internal Maven repository.</p><p>This flow is really powerful for the team as it allows for efficient local dev/deploy/test feedback cycles without having to publish anything to our internal Maven repository.</p><h3>CI / CD</h3><p>Building and deploying as part of our CI/CD pipeline faced similar challenges to the local build/deploy workflow.</p><h4>Building a Kotlin Multiplatform project using GitHub Actions</h4><p>Our mobile teams use <a href="https://docs.github.com/en/actions">GitHub Actions</a> for our CI builds. We use the <a href="https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners">GitHub-hosted runners</a>, so we needed to make sure that whatever runner we chose included the necessary build dependencies on the VM.</p><p>This was actually a pretty easy decision though. Because we need the XCode build tools to build the iOS target of our KMP project, we need to use the MacOS runner.</p><p>This was generally fine. The list of <a href="https://github.com/actions/virtual-environments/blob/main/images/macos/macos-11-Readme.md">pre-installed dependencies</a> is quite long, and included everything we needed. The only real concern was the cost. The macOS runner is more expensive than the ubuntu-runner our Android team typically uses.</p><p>After running our first builds on that macOS runner though, it was determined that it was not prohibitively expensive since the builds were fast on such a small project.</p><p>This build step included building both the JVM and iOS artifacts when running the <em>build</em> Gradle task.</p><h4>Monitoring build performance using Gradle Enterprise</h4><p>From that early point in the project, we also integrated <a href="https://gradle.com/">Gradle Enterprise</a> so we could collect buildscans and monitor the build performance over time. This was done to keep the KMP builds as fast as possible and lessen the overall cost of using the macOS runner.</p><h4>Deploying Kotlin Multiplatform artifacts using GitHub Actions</h4><p>Deploying of the JVM artifact was quite simple. We wanted to deploy the .jar as a maven artifact within our internal Maven server. To do this, we used the <a href="https://www.jfrog.com/confluence/display/JFROG/Gradle+Artifactory+Plugin">Gradle Artifactory Plugin</a>. This plugin works in conjunction with the Maven Publish Plugin to deploy any generated Maven publications to some external Maven server.</p><p>Using the Artifactory plugin, we configured:</p><ul><li>the url of our internal Artifactory server</li><li>the publication credentials</li><li>which publications to include (jvm, kotlinMultiplatform)</li></ul><p>Once the plugin was applied to our <em>:analytics</em> module and configured, we could use the artifactoryPublishand artifactoryDeployGradle tasks to publish our .jar file to our internal Artifactory server. Once that was done, we could access the desired dependency from the internal server when building our Android project.</p><p>Deploying the iOS artifact was more challenging and much more interesting. Our goal, was to consume the iOS code as a <a href="https://developer.apple.com/documentation/swift_packages">Swift Package</a>.</p><p>To support the deployment, some changes were made to the project outside of any CI flow:</p><ul><li>We setup an additional GitHub repo to store a Swift Package and associated metadata.</li><li>We integrated the <a href="https://github.com/ge-org/multiplatform-swiftpackage">Multiplatform Swift Package plugin</a> which provides a Gradle task that packages a XCFramework as a Swift Package</li></ul><p>Once those changes were in place, the general CI flow for publishing the Swift Package was:</p><ul><li>build the JVM and iOS targets</li><li>generate the Swift Package from the XCFramework</li><li>publish the Swift Package, and metadata, to the external repo by pushing a commit</li></ul><p>Once the Swift Package is committed to the <em>mobile-shared-swift-package</em> repo, the XCode build can be pointed to that repo, with proper credentials, and can consume that package during the iOS CI build.</p><p>Once we reached this point, we had successfully validated all our questions and concerns. We had a working, end-to-end solution that made us feel comfortable continuing on with our investment into Kotlin Multiplatform. The next step was to present the findings to our team, and to ramp up in developer education.</p><h3>Developer education</h3><p>Once we had our working proof-of-concept, we wanted to share the findings with the team, and discuss our proposal for moving into the next phase of the project.</p><p>For this, we did several things:</p><ul><li>We prepared documentation on how to setup and build the project.</li><li>Multiple calls were scheduled with the team to walk through the technical aspects of the proposed solution.</li><li>A doc was prepared for review; outlining how our Analytics naming rules would be codified and enforced using Kotlin Multiplatform.</li><li>We hosted several working sessions with developers to help setup the project, and get ramped up on how to consume the multiplatform code.</li><li>Several working sessions were dedicated to rapid prototyping of our v1 release.</li></ul><p>Through these efforts, we moved towards a “live beta” phase of the project where we then continued to invest and expand on our usage of Kotlin Multiplatform.</p><h3>Testing Kotlin Multiplatform in production</h3><p>Even in early 2021, Kotlin Multiplatform was quite stable for use cases not requiring multithreading in native build targets such as iOS.</p><p>For our use case, we were generally only defining sealed classes, enums, constants, and a few interfaces to wire everything together. So, we viewed our shipping of KMP in production as a “low risk” endeavor.</p><p>That said, we still took a very measured approach to shipping Kotlin Multiplatform code into our iOS and Android apps — just in case something <em>did</em> go wrong.</p><p>iOS shipped Kotlin Multiplatform first. They added a single event that shadowed an existing event from the previous analytics implementation. This event was only fired in one single place, making it a low-cost investment. After several weeks in production, no issues were found, and it was deemed safe to continue adding more events from the shared code.</p><p>On Android, the experience was much the same. A single, commonly sent, event was added using shared code, and after weeks of observation, the endeavor was deemed safe for production.</p><h3>Revisiting our Kotlin Multiplatform investment one year later</h3><p>The process outlined in this post took several months from first conception to shipping our first Kotlin Multiplatform in production.</p><p>After a year, we’re still pretty bullish on Kotlin Multiplatform — at least for non user-facing use cases.</p><p>Our investment into the shared analytics use case has been very much a success. The ability to strongly enforce even naming consistency across both iOS and Android has been a win on its own. Being able to share the event definitions when teams work on projects at different times is another big win. Through developing our shared apis, we also took the opportunity to improve our thinking around event properties as well. In particular, we marked all events with a <em>schema</em> property making event schemas easier to identify in Amplitude.</p><p>There have been challenges as well. In particular, the iOS developer experience is not as polished as it is for the JVM and Android dev experience.</p><p>The two main challenges our iOS team have faced have been:</p><ol><li>Kotlin Multiplatform generates Objective-C code rather than Swift. So, some of the apis are difficult to work with from Swift when being consumed via Swift Package based on an XCFramework.</li><li>Syncing XCFrameworks (as Swift Packages) became slower and slower over time. This was due to committing the built XCFramework directly to our publication repo which causes Xcode’s deep clone of the repo to fetch a multi-gigabyte repo on fresh syncs.</li></ol><p>The issue with consuming KMP code from Swift is a real one. Our iOS team has started to build some small wrapper layers around the KMP code to make it easier to work with in Swift. This largely revolves around mapping Objective-C types into the desired Swift types as outlined in the KMP <a href="https://kotlinlang.org/docs/native-objc-interop.html">Objective-C/Kotlin interop guide</a>. There are additional tools and features coming online to help with this problem, so hopefully this part of the developer experience continues to evolve for the better.</p><p>The issue with slow Swift Package syncs is something we’re actively working on fixing at the moment. The solution in a nutshell is to move away from a binary distribution approach for the Swift Package to a remote url distribution approach. This allows us to store the XCFramework as a zip file outside of any git repository. In doin this, it means Xcode only ever has to sync a single version of the XCFramework so syncs stay fast over time.</p><h3>What’s next?</h3><p>Thanks to the success of the analytics use case, we’ve turned to Kotlin Multiplatform for new use cases including:</p><ul><li>Deeplinking</li><li>Performance monitoring</li><li>Recommendations</li><li>Networking</li></ul><p>There has been some experimentation into using Kotlin Multiplatform, in conjunction with Compose Multiplatform, to build some internal tools, but nothing significant has come from those efforts yet.</p><p>Will we ever embrace KMP to the level of shared presenters or even shared UI? I doubt it. But, we’re excited to see where the technology, and developer community are going.</p><p>I think we’ll continue to invest heavily into our analytics and deeplinking use cases, and think we’ll likely add new features into shared code as they come up.</p><p>For more on how we’ve been using Kotlin Multiplatform at Premise, check out Part 2 in our series:</p><p><a href="https://engineering.premise.com/kotlin-multiplatform-project-structure-brownfield-applications-58038800a6ea"><strong>Kotlin Multiplatform Project Structure for Integrating with Brownfield Applications</strong></a></p><p><strong><em>Premise is constantly looking to hire top-tier engineering talent to help us improve our front-end, mobile offerings, data and cloud infrastructure. Come find out why we’re consistently named among the top places to work in Silicon Valley by visiting our </em></strong><a href="https://bit.ly/3wnTrLn"><strong><em>Careers</em></strong></a><strong><em> page.</em></strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b28d85825c9f" width="1" height="1" alt=""><hr><p><a href="https://engineering.premise.com/kotlin-multiplatform-at-premise-b28d85825c9f">Kotlin Multiplatform at Premise</a> was originally published in <a href="https://engineering.premise.com/">Engineering at Premise</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Monitoring Changes In Firebase Remote Config Using Kotlin, Slack, and Google Cloud Functions]]></title>
            <link>https://engineering.premise.com/monitoring-changes-in-firebase-remote-config-using-kotlin-slack-and-google-cloud-functions-8d8c6f8199d7?source=rss-696aff678e4------2</link>
            <guid isPermaLink="false">https://medium.com/p/8d8c6f8199d7</guid>
            <category><![CDATA[firebase]]></category>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[mobile-app-development]]></category>
            <category><![CDATA[google-cloud-platform]]></category>
            <category><![CDATA[cloud-computing]]></category>
            <dc:creator><![CDATA[Nate Ebel]]></dc:creator>
            <pubDate>Wed, 27 Apr 2022 13:24:44 GMT</pubDate>
            <atom:updated>2022-05-23T15:04:00.700Z</atom:updated>
            <content:encoded><![CDATA[<p><strong><em>by Nate Ebel, Android Engineer</em></strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9N6d_oNrs1THhQvd3uj4sw.png" /></figure><p>Our mobile team here at Premise uses <a href="https://firebase.google.com/">Firebase</a> for a number of things, the most heavily used feature being <a href="https://firebase.google.com/docs/remote-config">Firebase Remote Config</a>. We use Remote Config to control feature flags. We also use it in our internationalization efforts as well as in conjunction with A/B Testing to gather insights into user behavior and application performance.</p><p>With this usage comes a fair bit of operational overhead.</p><p>As the number of Remote Config parameters and conditions increases, it becomes more difficult to keep track of individual values. This is exacerbated by increasing team sizes and more regular release cadences.</p><p>To help with this, we’ve been using <a href="https://cloud.google.com/functions">Google Cloud Function</a>s to monitor for changes in Remote Config values and to send notifications of those changes to Slack. This system has worked well, but over time, we’ve identified some potential improvements.</p><p>We recently built out a v2 of this Remote Config notification system, which included the following goals:</p><ul><li>Build the new Cloud Function in Kotlin so it’s easier for the Mobile team to maintain</li><li>Notify on changes to Remote Config parameters</li><li>Notify on changes to grouped Remote Config parameters</li><li>Notify on changes to Remote Config conditions</li><li>Clearly highlight diffs between parameter/condition versions</li></ul><p>In this post, we’ll walk through the system design, project setup, code, and basic deployment that we’ve used to monitor Remote Config changes in our DEV, QA, and PROD environments. You should be able to easily adapt this sample code to your team’s needs or even deploy it to your Google Cloud project as is and start monitoring Remote Config changes immediately.</p><p>In the end, you should end up with something similar to this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fx14ikzoyBbX7tiNTcv0yw.png" /></figure><p>If you want to skip right to the code, you can check out the sample project on GitHub.</p><p><a href="https://github.com/premisedata/mobile-tutorials/tree/main/002-remote-config-monitor-cloud-function">mobile-tutorials/002-remote-config-monitor-cloud-function at main · premisedata/mobile-tutorials</a></p><h3>Designing the Remote Config Notification System</h3><p>The end-to-end flow of our notification system has several steps:</p><ul><li>Changes to Firebase Remote Config trigger the remoteconfig.update event within GCP.</li><li>That event triggers our monitoring Cloud Function.</li><li>The Cloud Function parses the updated Remote Configversion from the event payload.</li><li>The previous, and new, Remote Config templates (sets of values) are loaded using the Firebase Admin SDK.</li><li>Those changes are compared to identify additions, removals, updates, etc.</li><li>Changes are sent to Slack using the Slack SDK.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VQFUhQwDS40AAa45rJ4J_Q.png" /><figcaption>System diagram for our Firebase Remote Config monitoring service</figcaption></figure><p>This workflow could be customized to suit your team’s needs in several ways:</p><ul><li>If you don’t care to see diffs in your Slack messages, then you could skip the use of the Firebase Admin SDK, and simply notify Slack of the updated version number any time the Cloud Function is run.</li><li>The comparison of new and old templates could be simplified, or made more complex, depending on specific output needs.</li><li>The notification could be sent to any other service, email, etc. It would be straightforward to send to multiple destinations as well.</li></ul><h3>Creating the Kotlin Cloud Function Project</h3><p>We’re going to use IntelliJ to create a new Kotlin project. <a href="https://cloud.google.com/functions/docs/writing/specifying-dependencies-java#maven">Cloud Functions supports the Java 11 runtime</a>, and we can include the required dependencies using either Maven or Gradle. We’re using Maven as the setup is a bit simpler and most of the Google docs use Maven as well.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4c5eYxZI2UkS8w6NFmbeEQ.png" /><figcaption>Use IntelliJ to create a new Kotlin project</figcaption></figure><p>Once we’ve created our project template, we need to add a dependency on the Cloud Functions API so we can define our entry point class.</p><pre>// pom.xml</pre><pre>&lt;dependencies&gt;<br>    &lt;dependency&gt;<br>        &lt;groupId&gt;com.google.cloud.functions&lt;/groupId&gt;<br>        &lt;artifactId&gt;functions-framework-api&lt;/artifactId&gt;<br>        &lt;version&gt;1.0.1&lt;/version&gt;<br>    &lt;/dependency&gt;<br>    ...<br>&lt;/dependencies&gt;</pre><p>Google Cloud Functions <a href="https://cloud.google.com/functions/docs/concepts/events-triggers">may be triggered</a> in response to HTTP requests and/or some GCP event coming from a Pub/Sub topic, a Cloud Storage change, or a variety of other events such as changes to Firebase Remote Config.</p><p>In our case, we only need to respond to the <a href="https://cloud.google.com/functions/docs/calling/firebase-remote-config">remoteconfig.update</a> event, so we will implement one of two available interfaces when defining our Cloud Function:</p><ul><li>RawBackgroundFunction</li><li>BackgroundFunction&lt;T&gt;</li></ul><p>Both interfaces provide an accept() method that we may override to defined the behavior of our Cloud Function. In the case of RawBackgroundFunction, the event payload will be returned as raw JSON, whereas the BackgroundFunction interface will return to us a typed object of our choosing.</p><p>We will use RawBackgroundFunction as it gives us more flexibility in how we log and parse the incoming event.</p><p>We’ll create a RemoteConfigUpdateEvent data class to hold the remoteconfig.update event payload.</p><pre>import com.google.gson.annotations.SerializedName</pre><pre>data class RemoteConfigUpdateEvent(<br>    @SerializedName(&quot;updateType&quot;)<br>    val type: String,<br>    @SerializedName(&quot;updateOrigin&quot;)<br>    val origin: String,<br>    @SerializedName(&quot;versionNumber&quot;)<br>    val version: Long,<br>    @SerializedName(&quot;updateUser&quot;)<br>    val user: User<br>) {<br>    data class User(val email: String)<br>}</pre><p>With this event, we can now define our Cloud Function.</p><pre>class RemoteConfigMonitorService<br>    : RawBackgroundFunction&gt; {<br>    override fun accept(<br>        json: String,<br>        context: Context<br>    ) {<br>        <em>TODO</em>(&quot;Not yet implemented&quot;)<br>    }<br>}</pre><p>Now, we can add some basic logging to help identify changes in our Cloud Function logs. This also helps familiarize ourselves with the structure of the event payload before we start using it for more complex things.</p><pre>class RemoteConfigMonitorService<br>    : RawBackgroundFunction {<br><br>    <strong>private val logger: Logger = Logger.getLogger(RemoteConfigMonitorService::class.<em>java</em>.<em>name</em>)</strong></pre><pre>    <strong>private val gson: Gson = Gson()</strong></pre><pre>    override fun accept(<br>        json: String,<br>        context: Context<br>    ) {<br>        <strong>val event: RemoteConfigUpdateEvent = <br>            gson.fromJson(json, RemoteConfigUpdateEvent::class.<em>java</em>)</strong>        <br>        <strong>logger.info(&quot;Remote Config updated to version ${event.version} by ${event.user.email}&quot;)</strong><br>    }<br>}</pre><p>With our function in place and the update event data available to us, we can start working on notifying Slack of the changes.</p><h3>Notifying Slack Of Remote Config Version Changes</h3><p>For our first pass here, we’ll send a notification message to Slack that simply includes the new version number and the email address of the person who made the change.</p><p>For this, we’ll use the Slack SDK for Java.</p><pre>// pom.xml</pre><pre>&lt;dependencies&gt;<br>    ...<br>    &lt;dependency&gt;<br>        &lt;groupId&gt;com.slack.api&lt;/groupId&gt;<br>        &lt;artifactId&gt;slack-api-client&lt;/artifactId&gt;<br>        &lt;version&gt;1.21.1&lt;/version&gt;<br>    &lt;/dependency&gt;<br>&lt;/dependencies&gt;</pre><p>To help work with the Slack SDK, we’ll create a small class SlackClient.</p><pre>import com.slack.api.Slack<br>import com.slack.api.methods.MethodsClient<br>import com.slack.api.methods.response.chat.ChatPostMessageResponse<br><br>class SlackClient(token: String, private val channelId: String) {<br><br>    private val methods: MethodsClient = Slack.getInstance().methods(token)<br><br>    fun notify(message: String): ChatPostMessageResponse = methods.chatPostMessage <strong>{ </strong>req <strong>-&gt;<br>        </strong>req.channel(channelId).text(message).mrkdwn(true)<br>    <strong>}<br></strong>}</pre><p>This class will manage the instantiation of a Slack MethodsClient and the sending of our notification message to the desired Slack channel. The authentication and routing of these calls is controlled via the token and channelId values passed to the SlackClient.</p><p>The Slack token must be authorized to post messages to the provided channelId. How to do that is beyond the scope of this post. I recommend reviewing the <a href="https://api.slack.com/start/building">“Building a Slack app” </a>guide on how to get started building a Slack bot that can post messages on your service’s behalf.</p><p>We will access the token and channelId as runtime environment variables. We’ll examine how to pass those values as part of the deployment section of this post.</p><p>Once we’ve retrieved the necessary Slack credentials, we can create a new instance of our SlackClient and notify the channel when the Remote Config version has been changed.</p><pre>class RemoteConfigMonitorService<br>    : RawBackgroundFunction {<br><br>    <strong>private val slackChannelId: String =<br>        System.getenv(&quot;SLACK_CHANNEL_ID&quot;)<br>    private val slackToken: String =<br>        System.getenv(&quot;SLACK_TOKEN&quot;)</strong><br><br>    <strong>private val slack =<br>        SlackClient(token = slackToken, channelId = slackChannelId)</strong><br>    private val logger: Logger =<br>       Logger.getLogger(RemoteConfigMonitorService::class.<em>java</em>.<em>name</em>)</pre><pre>    private val gson: Gson = Gson()</pre><pre>override fun accept(<br>        json: String,<br>        context: Context<br>    ) {<br>        val event: RemoteConfigUpdateEvent = <br>            gson.fromJson(json, RemoteConfigUpdateEvent::class.<em>java</em>)<br>        val msg = &quot;Remote Config updated to version ${event.version} by ${event.user.email}&quot;<br>        logger.info(msg)<br>        <strong>slack.notify(msg)</strong><br>    }<br>}</pre><p>At this point, we should have a functional (if still limited) notification service ready to be deployed to GCP.</p><h3>Deploying The Cloud Function From A Local Development Machine</h3><p>To actually put our new Cloud Function into service, we need to deploy it to the Google Cloud project that our Firebase project is in. If you’re not sure which GCP project backs your Firebase instance, you can review your Firebase Project Settings.</p><p>To deploy our Cloud Function from our local development machine, we need to do several things:</p><ul><li>Install <a href="https://cloud.google.com/sdk/gcloud">gcloud</a> cli</li><li>Authenticate gcloud with our desired GCP project</li><li>Run the functions deploy command with any necessary arguments</li></ul><p>The general deployment command format looks something like the following:</p><pre>gcloud functions deploy &lt;desired deployment name&gt; \<br>--entry-point &lt;class name&gt; \<br>--runtime &lt;target runtime&gt; \<br>--trigger-event &lt;target event&gt;</pre><p>We need to indicate the desired deployment name of our function, which class to load in response to an event, which event(s) should trigger the function, and which runtime environment to use for the deployment.</p><p>Beyond the basics, we can further customize the deployment by passing any necessary environment variables or any number of other properties.</p><p>In our case, the deployment command should look something like the following:</p><pre>gcloud functions deploy remote-config-monitor-service \<br>--entry-point com.premise.RemoteConfigMonitorService \<br>--runtime java11 \<br>--trigger-event google.firebase.remoteconfig.update \<br>--set-env-vars GOOGLE_CLOUD_PROJECT=&lt;target gcp proejct&gt;,SLACK_TOKEN=&lt;Slack Bot Token&gt;,SLACK_CHANNEL_ID=&lt;Id for target Slack channel&gt;</pre><p>The GOOGLE_CLOUD_PROJECT env variable will be used internally to ensure that the Cloud Function is authorized to interact with other GCP services such as Firebase. This variable is set automatically by some Cloud Function runtimes, but must be explicitly set when using the Java runtime.</p><p>Once this command has been run, it typically takes a couple of minutes to deploy to the target GCP project. Once deployed, you should be able to make a change in Remote Config and see a Slack notification within a few seconds.</p><h3>Sending Remote Config Diffs With Slack Notification</h3><p>At this point, our notification simply shows the new versionand the email of whoever made the change. It’s not particularly useful yet. So let’s dive deeper and take a look at how we can generate detailed diffs of each template change.</p><h4>Loading Remote Config template versions</h4><p>The remoteconfig.update event payload does not include the actual changes to the <a href="https://firebase.google.com/docs/remote-config/templates">Remote Config template</a>. Instead, the payload gives the version of the updated template. If we want to notify of the specific changes to our Remote Config parameters and conditions, we need to use the version to fetch that information.</p><p>We can use the <a href="https://firebase.google.com/docs/admin/setup">Firebase Admin SDK</a> to load this information from our Firebase project, and we’ll use coroutines to load both the old and new versions in parallel.</p><pre>// pom.xml</pre><pre>&lt;dependencies&gt;<br>    &lt;dependency&gt;<br>        &lt;groupId&gt;com.google.firebase&lt;/groupId&gt;<br>        &lt;artifactId&gt;firebase-admin&lt;/artifactId&gt;<br>        &lt;version&gt;8.1.0&lt;/version&gt;<br>    &lt;/dependency&gt;<br>    &lt;dependency&gt;<br>        &lt;groupId&gt;org.jetbrains.kotlinx&lt;/groupId&gt;<br>        &lt;artifactId&gt;kotlinx-coroutines-core&lt;/artifactId&gt;<br>        &lt;version&gt;1.6.1&lt;/version&gt;<br>        &lt;type&gt;pom&lt;/type&gt;<br>    &lt;/dependency&gt;<br>&lt;/dependencies&gt;</pre><p>With these dependencies added, we can now load the updated Remote Config version and version — 1 to get the complete set of parameters and conditions for each version.</p><pre><strong>private val projectId:String = System.getenv(&quot;GOOGLE_CLOUD_PROJECT&quot;)</strong></pre><pre><strong>init {        <br>    FirebaseApp.initializeApp()    <br>}</strong></pre><pre>override fun accept(<br>    event: RemoteConfigUpdateEvent,<br>    context: Context<br>)<strong>: Unit = <em>runBlocking</em></strong><em> </em><strong>{<br>    </strong>val event: RemoteConfigUpdateEvent = <br>            gson.fromJson(json, RemoteConfigUpdateEvent::class.<em>java</em>)</pre><pre><strong>    val newConfig = <em>async</em>(Dispatchers.IO) {<br>        FirebaseRemoteConfig<br>            .getInstance()<br>            .getTemplateAtVersion(event.version)<br>    }<br>    val previousConfig = <em>async</em>(Dispatchers.IO) {<br>        FirebaseRemoteConfig<br>            .getInstance()<br>            .getTemplateAtVersion(event.version - 1)<br>    }<br><br>    </strong>val msg = &quot;Remote Config updated to version ${event.version} by ${event.user.email}&quot;<br>    logger.info(msg)<br>    slack.notify(msg)<br><strong>}</strong></pre><p>Notice here that we are using runBlocking{} to provide a CoroutineScope and to block completion of accept() until all coroutines have completed. We then use the async{} coroutine builder to load both Remote Config template versions in parallel.</p><h4>Diffing Remote Config template versions</h4><p>Now that we have the full set of Remote Config parameters and conditions, we can diff the two versions, and use that diff to build a more helpful notification message.</p><p>We’ll start by creating several model classes to assist with the diff operations.</p><pre><em>/**<br> * Holds a diff between two Parameters or Conditions<br> */<br></em>data class RemoteConfigDiff&lt;T&gt;(<br>    val key: String,<br>    val original: T,<br>    val updated: T<br>)<br><br><em>/**<br> * A container for a set of changes to Parameters or Conditions<br> */<br></em>data class RemoteConfigDiffContainer&lt;T : Any&gt;(<br>    val label: String,<br>    val removed: List&lt;RemoteConfigDiff&lt;T&gt;&gt;,<br>    val added: List&lt;RemoteConfigDiff&lt;T&gt;&gt;,<br>    val updated: List&lt;RemoteConfigDiff&lt;T&gt;&gt;,<br>) {<br>    val numberOfChanges: Int = removed.size + added.size + updated.size<br>}<br><br><em>/**<br> * A container for the overall diff between two Remote Config template versions<br> */<br></em>data class RemoteConfigDiffCollection(<br>    val projectId: String,<br>    val update: RemoteConfigUpdateEvent,<br>    val parameters: RemoteConfigDiffContainer&lt;Parameter&gt;,<br>    val conditions: RemoteConfigDiffContainer&lt;Condition&gt;<br>) {<br>    val numberOfChanges = parameters.numberOfChanges + conditions.numberOfChanges<br>}</pre><p>Next, we’ll write a function to diff the updates into a resulting RemoteConfigDiffCollection, which will then be used to format our output notification for Slack.</p><pre><em>/**<br> * Constructs the diffs for Parameters and for Templates<br> */<br></em>fun getRemoteConfigDiff(<br>    projectId: String,<br>    update: RemoteConfigUpdateEvent,<br>    original: Template,<br>    updated: Template<br>) = RemoteConfigDiffCollection(<br>    projectId = projectId,<br>    update = update,<br>    parameters = <em>constructDiff</em>(<br>        label = &quot;Parameters&quot;,<br>        originalValues = original.<em>getAllParameters</em>(),<br>        updatedValues = updated.<em>getAllParameters</em>(),<br>        getDefault = <strong>{ </strong>Parameter() <strong>}<br>    </strong>),<br>    conditions = <em>constructDiff</em>(<br>        label = &quot;Conditions&quot;,<br>        originalValues = original.<em>conditions</em>.<em>associateBy </em><strong>{ </strong>condition <strong>-&gt; </strong>condition.<em>name </em><strong>}</strong>,<br>        updatedValues = updated.<em>conditions</em>.<em>associateBy </em><strong>{ </strong>condition <strong>-&gt; </strong>condition.<em>name </em><strong>}</strong>,<br>        getDefault = <strong>{ </strong>Condition(&quot; &quot;, &quot; &quot;) <strong>}<br>    </strong>)<br>)<br><br>private fun Template.getAllParameters() = <em>parameters </em>+ <em>buildMap </em><strong>{<br>    </strong><em>parameterGroups</em>.entries.<em>forEach </em><strong>{ </strong>entry <strong>-&gt;<br>        </strong>putAll(entry.value.<em>parameters</em>)<br>    <strong>}<br>}<br><br></strong><em>/**<br> * Uses Set math to identify which keys were updated, added, or removed<br> * Builds List&lt;RemoteConfigDiff&gt; for each of these sets of keys<br> */<br></em>private fun &lt;T : Any&gt; constructDiff(<br>    label: String,<br>    originalValues: Map&lt;String, T&gt;,<br>    updatedValues: Map&lt;String, T&gt;,<br>    getDefault: () -&gt; T<br>): RemoteConfigDiffContainer&lt;T&gt; {<br><br>    val keysInBothConfigVersion = originalValues.keys <em>intersect </em>updatedValues.keys<br>    val removedKeys = originalValues.keys <em>subtract </em>keysInBothConfigVersion<br>    val addedKeys = updatedValues.keys <em>subtract </em>keysInBothConfigVersion<br><br>    val removedDiffs: List&lt;RemoteConfigDiff&lt;T&gt;&gt; = removedKeys.<em>map </em><strong>{ </strong>key <strong>-&gt;<br>        </strong>RemoteConfigDiff(<br>            key = key,<br>            original = originalValues.getOrDefault(key, getDefault()),<br>            updated = getDefault()<br>        )<br>    <strong>}<br><br>    </strong>val addedDiffs: List&lt;RemoteConfigDiff&lt;T&gt;&gt; = addedKeys.<em>map </em><strong>{ </strong>key <strong>-&gt;<br>        </strong>RemoteConfigDiff(<br>            key = key,<br>            original = getDefault(),<br>            updated = updatedValues.getOrDefault(key, getDefault())<br>        )<br>    <strong>}<br><br>    </strong>val updatedDiffs: List&lt;RemoteConfigDiff&lt;T&gt;&gt; = keysInBothConfigVersion.<em>map </em><strong>{ </strong>key <strong>-&gt;<br>        </strong>val originalParameter = originalValues.getOrDefault(key, getDefault())<br>        val updatedParameter = updatedValues.getOrDefault(key, getDefault())<br>        Triple(key, originalParameter, updatedParameter)<br>    <strong>}</strong>.<em>filter </em><strong>{ it</strong>.second != <strong>it</strong>.third <strong>}<br>        </strong>.<em>map </em><strong>{<br>            </strong>RemoteConfigDiff(<br>                key = <strong>it</strong>.first,<br>                original = <strong>it</strong>.second,<br>                updated = <strong>it</strong>.third<br>            )<br>        <strong>}<br><br>    </strong>return RemoteConfigDiffContainer(<br>        label = label,<br>        removed = removedDiffs,<br>        added = addedDiffs,<br>        updated = updatedDiffs<br>    )<br>}</pre><p>Now that we understand how the Remote Config values have changed, we can format that data into something easier to glance at and understand in Slack.</p><h4>Formatting Remote Config diffs</h4><p>We will use the <a href="https://github.com/java-diff-utils/java-diff-utils">Java Diff Utils library</a> to format the diffs between Remote Config template versions. This library is particularly helpful because it can annotate changes using simple markdown, which allows us to do things like strikethrough removed values and bold added ones. This helps a lot when viewing the output in Slack.</p><pre>// pom.xml</pre><pre>&lt;dependencies&gt;<br>    &lt;dependency&gt;<br>        &lt;groupId&gt;io.github.java-diff-utils&lt;/groupId&gt;<br>        &lt;artifactId&gt;java-diff-utils&lt;/artifactId&gt;<br>        &lt;version&gt;4.11&lt;/version&gt;<br>    &lt;/dependency&gt;<br>&lt;/dependencies&gt;</pre><p>With the dependency added, we can create a RemoteConfigDiffFormatter class that will use a passed RemoteConfigDiffCollection to format an output string including:</p><ul><li>The total number of changes made</li><li>A link to the Firebase Remote Config dashboard</li><li>The email address of the person that made the change</li><li>Removed, Added, and Updated sections for Conditions</li><li>Removed, Added, and Updated sections for Parameters</li></ul><pre>private const val <em>REMOVAL_SLACK_EMOJI </em>= &quot;:github-changes-requested:&quot;<br>private const val <em>ADDITION_SLACK_EMOJI </em>= &quot;:github-check-mark:&quot;<br><br><em>/**<br> * Generates a markdown-formatted string for a given [RemoteConfigDiffCollection]<br> */<br></em>class RemoteConfigDiffFormatter(<br>    private val diff: RemoteConfigDiffCollection,<br>    private val firebaseProjectUrl: String,<br>) {<br>    private val generator = DiffRowGenerator.create()<br>        .showInlineDiffs(true)<br>        .inlineDiffByWord(true)<br>        .oldTag <strong>{ </strong>f: Boolean? <strong>-&gt; </strong>&quot;~&quot; <strong>}<br>        </strong>.newTag <strong>{ </strong>f: Boolean? <strong>-&gt; </strong>&quot;*&quot; <strong>}<br>        </strong>.build()<br><br>    fun toMarkdownString(): String {<br>        return diff.<em>run </em><strong>{<br>            </strong><em>buildString </em><strong>{<br>                </strong><em>appendLine</em>(&quot;$numberOfChanges changes made to &lt;$firebaseProjectUrl|Remote Config&gt; in $projectId&quot;)<br>                <em>appendLine</em>(&quot;Updated by: ${update.user.email}&quot;)<br><br>                <em>markdownFromContainer</em>(conditions)<br>                <em>markdownFromContainer</em>(parameters)<br>            <strong>}<br>        }<br>    </strong>}<br><br>    private fun Any.toJson(): String {<br>        return Gson().toJson(this)<br>    }<br><br>    private fun &lt;T : Any&gt; StringBuilder.markdownFromContainer(container: RemoteConfigDiffContainer&lt;T&gt;) {<br>        <em>removedSection</em>(container)<br>        <em>addedSection</em>(container)<br>        <em>updatedSection</em>(container)<br>    }<br><br>    private fun &lt;T : Any&gt; StringBuilder.updatedSection(container: RemoteConfigDiffContainer&lt;T&gt;) {<br>        if (container.updated.<em>isNotEmpty</em>()) {<br>            <em>appendLine</em>(&quot;&quot;)<br>            <em>appendLine</em>(&quot;&quot;)<br>            <em>appendLine</em>(&quot;$<em>ADDITION_SLACK_EMOJI</em> *Updated ${container.label}:*&quot;)<br>            <em>appendLine</em>(&quot;&quot;)<br><br>            container.updated.<em>forEach </em><strong>{ </strong>diff <strong>-&gt;<br>                </strong>val rows = generator.generateDiffRows(<br>                    <em>listOf</em>(diff.original.<em>toJson</em>()),<br>                    <em>listOf</em>(diff.updated.<em>toJson</em>())<br>                )<br>                <em>appendLine</em>(&quot;  • _${diff.key}_&quot;)<br>                <em>appendLine</em>(&quot;      • ${rows.<em>first</em>().<em>oldLine</em>}&quot;)<br>                <em>appendLine</em>(&quot;      • ${rows.<em>first</em>().<em>newLine</em>}&quot;)<br>            <strong>}<br>        </strong>}<br>    }<br><br>    private fun &lt;T : Any&gt; StringBuilder.addedSection(container: RemoteConfigDiffContainer&lt;T&gt;) {<br>        if (container.added.<em>isNotEmpty</em>()) {<br>            <em>appendLine</em>(&quot;&quot;)<br>            <em>appendLine</em>(&quot;&quot;)<br>            <em>appendLine</em>(&quot;$<em>ADDITION_SLACK_EMOJI</em> *Added ${container.label}:*&quot;)<br>            <em>appendLine</em>(&quot;&quot;)<br><br>            container.added.<em>forEach </em><strong>{ </strong>diff <strong>-&gt;<br>                </strong>val rows = generator.generateDiffRows(<br>                    <em>listOf</em>(&quot;&quot;),<br>                    <em>listOf</em>(diff.updated.<em>toJson</em>())<br>                )<br>                <em>appendLine</em>(&quot;  • _${diff.key}_: ${rows.<em>first</em>().<em>newLine</em>}&quot;)<br>            <strong>}<br>        </strong>}<br>    }<br><br>    private fun &lt;T : Any&gt; StringBuilder.removedSection(container: RemoteConfigDiffContainer&lt;T&gt;) {<br>        if (container.removed.<em>isNotEmpty</em>()) {<br>            <em>appendLine</em>(&quot;&quot;)<br>            <em>appendLine</em>(&quot;&quot;)<br>            <em>appendLine</em>(&quot;$<em>REMOVAL_SLACK_EMOJI</em> *Removed ${container.label}:*&quot;)<br>            <em>appendLine</em>(&quot;&quot;)<br><br>            container.removed.<em>forEach </em><strong>{ </strong>diff <strong>-&gt;<br>                </strong>val rows = generator.generateDiffRows(<br>                    <em>listOf</em>(diff.original.<em>toJson</em>()),<br>                    <em>listOf</em>(&quot;&quot;)<br>                )<br>                <em>appendLine</em>(&quot;  • _${diff.key}_: ${rows.<em>first</em>().<em>oldLine</em>}&quot;)<br>            <strong>}<br>        </strong>}<br>    }<br>}</pre><p>With our formatter in place, we can put it all together to send our more detailed notifications to Slack.</p><pre>class RemoteConfigMonitorService<br>    : RawBackgroundFunction {<br><br>    <strong>private val projectId: String = <br>        System.getenv(&quot;GOOGLE_CLOUD_PROJECT&quot;)</strong><br>    private val slackChannelId: String =<br>        System.getenv(&quot;SLACK_CHANNEL_ID&quot;)<br>    private val slackToken: String =<br>        System.getenv(&quot;SLACK_TOKEN&quot;)<br>    <strong>private val firebaseProjectUrl: String = <br>   &quot;https://console.firebase.google.com/project/${projectId}/config&quot;</strong><br><br><br>    private val slack =<br>        SlackClient(token = slackToken, channelId = slackChannelId)<br>    private val logger: Logger =<br>       Logger.getLogger(RemoteConfigMonitorService::class.<em>java</em>.<em>name</em>)</pre><pre>    private val gson: Gson = Gson()</pre><pre>    init {        <br>        FirebaseApp.initializeApp()    <br>    }</pre><pre>    override fun accept(<br>        json: String,<br>        context: Context<br>    ): Unit = <em>runBlocking </em><strong>{<br>        </strong>val event: RemoteConfigUpdateEvent = <br>            gson.fromJson(json, RemoteConfigUpdateEvent::class.<em>java</em>)<br><strong>        <br>        </strong>val newConfig = <em>async</em>(Dispatchers.IO) <strong>{<br>            </strong>FirebaseRemoteConfig<br>                .getInstance()<br>                .getTemplateAtVersion(event.version)<br>        <strong>}<br>        </strong>val previousConfig = <em>async</em>(Dispatchers.IO) <strong>{<br>            </strong>FirebaseRemoteConfig<br>                .getInstance()<br>                .getTemplateAtVersion(event.version - 1)<br>        <strong>}<br><br>        val diffCollection = <em>getRemoteConfigDiff</em>(<br>            projectId, <br>            event, <br>            previousConfig.await(), <br>            newConfig.await()<br>        )<br><br>        val response = slack.notify(<br>            message = RemoteConfigDiffFormatter(<br>                diff = diffCollection,<br>                firebaseProjectUrl = firebaseProjectUrl<br>            ).toMarkdownString())<br><br>        when (response.<em>isOk</em>) {<br>            true -&gt; logger.info(&quot;Slack channel $slackChannelId notified&quot;)<br>            false -&gt; logger.info(&quot;Failed to notify Slack channel $slackChannelId: ${response.<em>error</em>}&quot;)<br>        }</strong><br>    <strong>}<br></strong>}</pre><p>The resulting output in Slack should look something like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fx14ikzoyBbX7tiNTcv0yw.png" /><figcaption>Sample Remote Config diff sent to Slack</figcaption></figure><h3>What’s Next?</h3><p>This v2 of our Firebase Remote Config notification system has been a nice improvement so far, but we’re not done. Now that we have an improved framework on which to build, we’re looking for more ways to improve our monitoring of Remote Config changes.</p><p>The following are some of the planned improvements:</p><ul><li>Explicitly warn when a parameter’s type changes</li><li>Automated deployment via GitHub Actions</li><li>Improved notification formatting — possibly using <a href="https://api.slack.com/block-kit">Slack’s Block Kit</a> APIs</li></ul><p>You can find the full sample code for this post on GitHub:</p><p><a href="https://github.com/premisedata/mobile-tutorials/tree/main/002-remote-config-monitor-cloud-function">mobile-tutorials/002-remote-config-monitor-cloud-function at main · premisedata/mobile-tutorials</a></p><p><strong><em>Premise is constantly looking to hire top-tier engineering talent to help us improve our front-end, mobile offerings, data and cloud infrastructure. Come find out why we’re consistently named among the top places to work in Silicon Valley by visiting our </em></strong><a href="https://bit.ly/3wnTrLn"><strong><em>Careers</em></strong></a><strong><em> page.</em></strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8d8c6f8199d7" width="1" height="1" alt=""><hr><p><a href="https://engineering.premise.com/monitoring-changes-in-firebase-remote-config-using-kotlin-slack-and-google-cloud-functions-8d8c6f8199d7">Monitoring Changes In Firebase Remote Config Using Kotlin, Slack, and Google Cloud Functions</a> was originally published in <a href="https://engineering.premise.com/">Engineering at Premise</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Using :buildSrc Kotlin Extensions From Groovy-based Gradle Build Scripts]]></title>
            <link>https://engineering.premise.com/using-buildsrc-kotlin-extensions-from-groovy-based-gradle-build-scripts-eb79679ab0ff?source=rss-696aff678e4------2</link>
            <guid isPermaLink="false">https://medium.com/p/eb79679ab0ff</guid>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[android-app-development]]></category>
            <category><![CDATA[mobile-app-development]]></category>
            <category><![CDATA[gradle]]></category>
            <dc:creator><![CDATA[Nate Ebel]]></dc:creator>
            <pubDate>Tue, 19 Apr 2022 18:22:57 GMT</pubDate>
            <atom:updated>2022-04-19T18:22:57.621Z</atom:updated>
            <content:encoded><![CDATA[<h3>Using :buildSrc Kotlin Extensions From Groovy-based Gradle Build Scripts</h3><p><strong><em>by Nate Ebel, Android Developer</em></strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qhIC3BzaKkr0h6KcHOwvEQ.jpeg" /></figure><p>The buildSrc directory of a Gradle project is a special directory that is treated as an <a href="http://photooutputdto/"><em>included build</em></a><em> </em>and provides a convenient location to store common or complex build logic used throughout your Gradle project. A common example of this might be to declare statically typed dependency coordinates in a single location for across build scripts in a multi-module Gradle project.</p><p>Within the buildSrc directory, we can write custom build logic using both Groovy and Kotlin. This has the potential to make build scripts more approachable for developers that are unfamiliar with Groovy but comfortable with Kotlin — as is the case for most of the Android developers here at Premise.</p><p>When we define custom build logic in Kotlin within the buildSrc directory, we can easily interact with that code from any Gradle build scripts written in Kotlin — we simply import and access as we would from any other Kotlin file.</p><p>Recently, however, we wanted to reuse some Kotlin extensions in our Groovy-based build scripts. In doing so, we found it wasn’t quite as straightforward as reusing in Kotlin-based build scripts and thought we’d share a couple of tips in case others have run into the same hangups.</p><p>Check out our mobile-tutorials <a href="https://github.com/premisedata/mobile-tutorials/tree/main/001-buildsrc-kotiln-extensions-in-groovy">repo</a> for the full working sample detailed throughout this post.</p><p><a href="https://github.com/premisedata/mobile-tutorials/tree/main/001-buildsrc-kotiln-extensions-in-groovy">mobile-tutorials/001-buildsrc-kotiln-extensions-in-groovy at main · premisedata/mobile-tutorials</a></p><p>Let’s walk through a small example to illustrate this use case.</p><p>We’ll be working out of a basic “hello world” Android app. You can find the code in our mobile-tutorials <a href="https://github.com/premisedata/mobile-tutorials/tree/main/001-buildsrc-kotiln-extensions-in-groovy">repo</a>.</p><p>Within the root directory of that project, we’ll add our buildSrc directory.</p><ul><li>/app</li><li>/buildSrc</li><li>settings.gradle</li><li>build.gradle</li><li>etc…</li></ul><p>Now, let’s add some custom build logic to our new buildSrc directory.</p><p>We’ll add an extension function on Gradle’s Project type.</p><pre>// <a href="https://github.com/premisedata/mobile-tutorials/blob/main/001-buildsrc-kotiln-extensions-in-groovy/buildSrc/src/main/kotlin/com/premise/mobile/tutorials/extensions/ProjectPropertyExtensions.kt">buildSrc/src/main/.../extensions/ProjectPropertyExtensions.kt</a></pre><pre>fun Project.getProjectVersionName(): String? {    <br>  val versionName = findProperty(&quot;VERSION_NAME&quot;)    <br>  return if (versionName is String) versionName else null<br>}</pre><p>From a Kotlin build script file, we can leverage this extension quite simply.</p><pre>// app/build.gradle.kts</pre><pre>import com.premise.mobile.tutorials.extensions.getProjectVersionName<br>...</pre><pre>android {<br>  defaultConfig {<br>    versionName = <em>getProjectVersionName</em>()<br>  }<br>}</pre><p>However, from a Groovy file, if we try to use the extension the same way, we’ll get a build error during the script evaluation phase.</p><pre>// <a href="https://github.com/premisedata/mobile-tutorials/blob/main/001-buildsrc-kotiln-extensions-in-groovy/app/build.gradle">app/build.gradle</a></pre><pre>android {<br>  defaultConfig {<br>    // Error: groovy.lang.MissingMethodException<br>    versionName getProjectVersionName()<br>  }<br>}</pre><p>Notice though, that in the Kotlin build script example, we had explicitly imported the extension function. So, maybe we just need to import our function?</p><p>Still not there. If we try explicitly importing the extension first we get a different compilation error during the configuration phase of our build.</p><pre>// <a href="https://github.com/premisedata/mobile-tutorials/blob/main/001-buildsrc-kotiln-extensions-in-groovy/app/build.gradle">app/build.gradle</a></pre><pre>// Error: unable to resolve class getProjectVersionName<br>import com.premise.mobile.tutorials.extensions.getProjectVersionName</pre><pre>android {<br>  defaultConfig {<br>    versionName getProjectVersionName()<br>  }<br>}</pre><p>How then are we supposed to reference this getProjectVersionName() extension function from our Groovy build scripts? Surely we don’t have to migrate all our build scripts to Kotlin, right?</p><p>To answer that, let’s start by finding the lowest common denominator between our Groovy scripts and our Kotlin scripts.</p><p>Both Kotlin and Groovy ultimately compile down to JVM-compatible bytecode.</p><p>If we examine the /build directory of our :buildSrc module, we can locate the generated code for our getProjectVersionName() extension.</p><p>What we find is ultimately not surprising if you’re familiar with Java&lt;&gt;Kotlin interop. Under the hood, the Kotlin extension function generates a Java class named for the file in which it’s defined ProjectPropertyExtensionsKt.class .</p><p>So, we have a Kotlin extension, which generates a Java class, which we ultimately then want to consume from our Groovy build script.</p><p>If we return to our last example within our Groovy build script, we might now notice a key clue.</p><pre>// <a href="https://github.com/premisedata/mobile-tutorials/blob/main/001-buildsrc-kotiln-extensions-in-groovy/app/build.gradle">app/build.gradle</a></pre><pre>// Error: unable to resolve class getProjectVersionName<br>import com.premise.mobile.tutorials.extensions.getProjectVersionName</pre><pre>android {<br>  defaultConfig {<br>    versionName getProjectVersionName()<br>  }<br>}</pre><p>When attempting to import the extension function like we did from the Kotlin build script, we get an error indicating that the compiler can’t resolve the class name getProjectVersionName . In other words, it’s trying to import a class, rather than a specific function name. In Kotlin, this is handled for us, but when mixing Groovy and Kotlin here we need to explicitly import the name of the class generated by the compiler in which our extension function resides at the bytecode level.</p><p>With that in mind, we can now update the usage of our extension to call it as a static method on the generated class. The receiver type, Project, is then passed as the first argument to the invocation.</p><pre>// <a href="https://github.com/premisedata/mobile-tutorials/blob/main/001-buildsrc-kotiln-extensions-in-groovy/app/build.gradle">app/build.gradle</a></pre><pre>// import the generated class<br>import com.premise.mobile.tutorials.extensions.ProjectPropertyExtensionsKt</pre><pre>android {<br>  defaultConfig {<br>    // call the static function - passing in the project<br>    versionName ProjectPropertyExtensionsKt.<em>getProjectVersionName</em>(project)<br>  }<br>}</pre><p>Ultimately, we end up invoking the extension using the same syntax we would if we were calling it from Java.</p><p>The same thinking applies for extension properties as well.</p><p>Here, we’ve defined an extension property on Project to indicate whether or not we should be sending analytics events.</p><pre>// <a href="https://github.com/premisedata/mobile-tutorials/blob/main/001-buildsrc-kotiln-extensions-in-groovy/buildSrc/src/main/kotlin/com/premise/mobile/tutorials/extensions/ProjectPropertyExtensions.kt#L10">buildSrc/src/main/.../extensions/ProjectPropertyExtensions.kt</a></pre><pre>val Project.<em>shouldLogAnalytics</em>: Boolean<br>    get() {<br>        val shouldLogAnalytics = findProperty(&quot;LOG_ANALYTICS&quot;)<br>        return if (shouldLogAnalytics is String) {<br>            shouldLogAnalytics.<em>toBoolean</em>() <br>        } else {<br>            false<br>        }<br>    }</pre><p>Now, let’s say we want to use this extension in our Android project to generate a custom BuildConfig field.</p><pre>// <a href="https://github.com/premisedata/mobile-tutorials/blob/main/001-buildsrc-kotiln-extensions-in-groovy/app/build.gradle#L33-L40">app/build.gradle</a></pre><pre>buildTypes <strong>{<br>    </strong>debug <strong>{<br>        </strong>buildConfigField(&quot;boolean&quot;, &quot;SHOULD_LOG_ANALYTICS&quot;, ProjectPropertyExtensionsKt.<em>getShouldLogAnalytics</em>(project).toString())<br>    <strong>}<br>}</strong></pre><p>Notice a few things about our usage of the extension property here:</p><ul><li>Again, we access the extension using the generated class name ProjectPropertyExtensionskt.</li><li>Rather than using Kotlin’s property access syntax, we must use the extension property through a generated getter.</li><li>That getter prepends “get” to the property name to generate a method named getShouldLogAnalytics().</li><li>We must again pass the Project instance receive as the first parameter to the invocation of the method.</li></ul><p>This is a bit more complicated than with the extension function, but still mirroring how we would access an extension property from Java.</p><p>Understanding how to consume our Kotlin build script extensions from both Groovy and Kotlin allows us to reuse those extensions from any of our build scripts, achieving a greater level of consistency throughout our build.</p><p>It also allows us to migrate key build logic into these helper extensions without having to completely migrate our build scripts to Kotlin, which has both developer and build time costs.</p><p>Check out our mobile-tutorials <a href="https://github.com/premisedata/mobile-tutorials/tree/main/001-buildsrc-kotiln-extensions-in-groovy">repo</a> for the full working sample detailed throughout this post.</p><p><a href="https://github.com/premisedata/mobile-tutorials/tree/main/001-buildsrc-kotiln-extensions-in-groovy">mobile-tutorials/001-buildsrc-kotiln-extensions-in-groovy at main · premisedata/mobile-tutorials</a></p><p><strong><em>Premise is constantly looking to hire top-tier engineering talent to help us improve our front-end, mobile offerings, data and cloud infrastructure. Come find out why we’re consistently named among the top places to work in Silicon Valley by visiting our </em></strong><a href="https://bit.ly/3wnTrLn"><strong><em>Careers</em></strong></a><strong><em> page.</em></strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=eb79679ab0ff" width="1" height="1" alt=""><hr><p><a href="https://engineering.premise.com/using-buildsrc-kotlin-extensions-from-groovy-based-gradle-build-scripts-eb79679ab0ff">Using :buildSrc Kotlin Extensions From Groovy-based Gradle Build Scripts</a> was originally published in <a href="https://engineering.premise.com/">Engineering at Premise</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Using Gradle Enterprise to Detect Configuration Regressions In An Android Gradle Build]]></title>
            <link>https://engineering.premise.com/using-gradle-enterprise-to-detect-configuration-regressions-in-an-android-gradle-build-79b5a2b31a10?source=rss-696aff678e4------2</link>
            <guid isPermaLink="false">https://medium.com/p/79b5a2b31a10</guid>
            <category><![CDATA[android]]></category>
            <category><![CDATA[mobile-app-development]]></category>
            <category><![CDATA[android-app-development]]></category>
            <category><![CDATA[gradle]]></category>
            <dc:creator><![CDATA[Nate Ebel]]></dc:creator>
            <pubDate>Wed, 09 Jun 2021 18:57:42 GMT</pubDate>
            <atom:updated>2021-06-09T18:57:42.603Z</atom:updated>
            <content:encoded><![CDATA[<p><strong><em>By Nate Ebel, Android Developer</em></strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*OLwb1naxV0dQugiJ" /><figcaption>Photo by <a href="https://unsplash.com/@imattsmart?utm_source=medium&amp;utm_medium=referral">iMattSmart</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>The Mobile Engineering team here at Premise has made a concerted effort to improve developer efficiency over the past year.</p><p>One of the biggest areas of tech investment has been into understanding and reducing the Gradle build times of our Android project. We worked with engineers from <a href="https://gradle.org/">Gradle</a> to help us optimize and monitor our build using tools such as <a href="https://gradle.com/">Gradle Enterprise</a> and <a href="https://goobar.dev/benchmarking-gradle-builds-using-gradle-profiler/">gradle-profiler</a>. The results from this work have been terrific. We are seeing decreases in both local and CI build times of 50–80% from when we first started our work.</p><p>Now that we have our build under control, the story has changed a bit. Instead of a laser-focus on speeding up our builds, we now look towards adopting new build optimizations and towards maintaining existing build performance.</p><p>How can we monitor build trends over time? How do we debug local build issues for a team spread across multiple continents? What can we do to help identify and fix regressions?</p><p>It’s these questions that we are more often considering.</p><p>In this post, I want to walk you through a bit of a case study into this build monitoring process. We’re going to explore how we used Gradle Enterprise, and other tools to:</p><ul><li>Discover a build regression</li><li>Identify the root cause of that regression</li><li>Fix the issue</li><li>Improve monitoring so the issue is easier to deal with in the future</li></ul><p>If you want to jump to the final summary analysis, you can skip to the end.</p><p>If you want to walk through the full case study, then hopefully as you read through this, you might gain some insights into how you might identify Gradle build issues in your own projects and to leverage tools such as Gradle buildscans to diagnosis and fix those issues.</p><h3>Discovering A Build Regression</h3><p>The issue we uncovered was this:</p><ul><li>Over the course of a few days, our PR builds in GitHub Actions saw a 3x increase in Gradle configuration time</li><li>This resulted in a ~20% increase in total PR build times in GitHub Actions</li><li>Local build times didn’t seem to be impacted</li></ul><h3><strong>How Did We Detect The Increase In Gradle Configuration Time?</strong></h3><p>We detected this issue by reviewing build trend data in our Gradle Enterprise dashboard. If you aren’t familiar, Gradle Enterprise is a tool to help collect and analyze Gradle build data for your team or organization.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mKOrdYfRvw4msC5dQlpLhQ.png" /><figcaption>A sample of Gradle Enterprise buildscan trends for PR builds over the past 90 days</figcaption></figure><p>On reviewing our median PR build times over the past several weeks, we noticed a significant increase had occurred and persisted over several days. Without our Gradle Enterprise server collecting data for all Android builds, we likely would not have discovered this issue on our own.</p><p>Digging deeper into performance data for individual builds showed a significant increase in configuration time for one of our Gradle modules. This happened to be the one module in our app with native C++ code.</p><p>The was our first clue that maybe the issue was related to the NDK somehow.</p><h3><strong>Identifying The Root Cause Of The Regression?</strong></h3><p>With several days of increased build times for our :nativecode module, how did we then identify the root cause?</p><p>We started running through our list of Gradle debugging tools.</p><h4>Trying to reproduce locally</h4><p>First, we ran ./gradlew help locally to try and replicate the problem.</p><p>Running the help task will run just the configuration phase for a Gradle build as was the first place for us to try and reproduce the issue locally.</p><p>Unfortunately, this did not help as we were unable to replicate the build regression during a local build. Great for individual developer productivity, but not helpful in identifying our mystery issue.</p><h4>Identifying the causal commit</h4><p>Our next step, was to try and identify <em>when</em> the build regression started, and to identify any commits around that time that seemed likely to impact build performance.</p><p>Again, here we turned to Gradle Enterprise. We were able to view buildscan data over the past two weeks, and identify the approximate date where the build times started going up.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*i4smwmAmQpvYMupejUl4kw.png" /><figcaption>A sample buildscan within Gradle Enterprise</figcaption></figure><p>Once we had a timeframe in mind, we turned to our git history in GitHub.</p><p>On investigating the changes from that period, one change stuck out as the likely culprit — the updating of AGP to version 4.1 and Gradle to version 6.7</p><h4>Benchmarking build performance around possible problem commit</h4><p>To determine if the build regression was due to our bumping of AGP/Gradle versions, we turned to gradle-profiler to locally benchmark build performance before, and after, the version bump commit.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*R-TFqBFUoKe_lyZ8Suq0jg.png" /><figcaption>Sample gradle-profiler output from benchmarking the `help` task</figcaption></figure><p>If you aren’t familiar, gradle-profiler automates the running of Gradle tasks and the collection of build metrics to provide aggregate performance data and comparisons. By default, gradle-profiler will run a target task 10 times and summarize that build data for you to compare against other builds.</p><p>In our benchmarking, we identified several commits around the possible problem commit, and ran our benchmarks against each of them.</p><p>Given that we couldn’t reproduce the issue locally to begin with, it’s not surprising that the results of our benchmarking were inconclusive. We weren’t seeing any statistically significant difference in build times across any of the commits we tested.</p><p>Then, we realized we may be using gradle-profiler incorrectly.</p><p>We were measuring total build time, but our issue was specifically at configuration time. Maybe if we isolated configuration time, our benchmark results would turn up something more interesting?</p><p>As it turns out, gradle-profiler can be configured to include configuration time in its output.</p><p>After re-running our benchmarks with configuration time reported, sadly, we still saw nothing conclusive.</p><p>At this time, we considered admitting defeat and calling in reinforcements from the developer community. We decided to sleep on it, and give it one more try later that week.</p><h4>Reviewing buildscan output</h4><p>After taking a break, we decided to take a step back from the benchmarking and deep dives into commit history. Instead, we opened up one of the impacted buildscans and gave it a thorough examination.</p><p>We noticed something important upon review of the console output for that build. The output logs mentioned the download and installation of the NDK.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*53wZn50MNLp2ZwqZsA5iBg.png" /><figcaption>Example buildscan highlighting the automatic download of the NDK by Android Gradle Plugin</figcaption></figure><p>This struck us as odd.</p><p>Why are we downloading the NDK if our pinned version matches what is pre-installed on our virtual machine?</p><h4>Examining the pre-installed tooling of the GitHub Actions virtual environment</h4><p>We needed to double check the pre-installed version(s) of the NDK available in the GitHub Actions virtual environments.</p><p>The list of current GitHub Actions virtual environments is <a href="https://github.com/actions/virtual-environments">available on GitHub</a>. We found the listing for ubuntu-latest which, at the time, was ubuntu-18.*</p><p>If you open the <a href="https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu1804-README.md">README for a specific virtual environment</a>, you can find the pre-installed tooling descriptions. And in the case of our investigation, we searched for NDK to determine which NDK versions were available to us.</p><p>We compared the downloaded NDK version to the version pinned in our application module.</p><p>Why/how could this be?</p><h4>Reviewing AGP changes</h4><p>We checked the AGP release notes — specifically for NDK changes. We found that, sure enough, our updated version of AGP had a new default NDK version — a version not pre-installed on the GitHub Actions virtual environment.</p><h3><strong>Forming Our Hypothesis</strong></h3><p>We now knew <em>what</em> was happening, but not <em>why</em>?</p><p>The Android Gradle Plugin was downloading and installing a version of the NDK during the configuration phase of our build. The install was required, because that new version of the NDK was not available on the GitHub Actions virtual machine.</p><p>But why was AGP downloading a version <em>different</em> than what we had pinned in our application module?</p><p>We formed the following hypothesis:</p><p><em>Our desired NDK version was not set for all Android modules in the project — therefore leaving AGP to download whatever versions it needed if it didn’t detect a pinned version.</em></p><h3><strong>How Could We Reproduce And Test The Issue Locally?</strong></h3><p>To validate this hypotheses, we tried again to reproduce the issue locally.</p><p>We check the installed NDK versions on a local machine and found that the new default NDK had, in fact, been downloaded to the machine.</p><p>This would explain why local configuration times didn’t seem to be impacted. The NDK had been previously installed by either a dev or by AGP on a previous build.</p><p>We then deleted that version of the NDK from the local test machine, and ran ./gradlew help to replicate the issue.</p><p>This time, we saw that the task kicked off a download of the new NDK version.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/793/1*RZyg4dtdt_xOKCVlS5n2Vg.png" /></figure><p>Successful repro!!!</p><p>We then tested a possible fix. Again remove the new NDK version from the local machine. Then, we pinned our desired NDK version in each project module. Once again we ran the help task, and this time, the project built without downloading or installing any other version of the NDK.</p><h3>Fixing The Issue For Impacted CI Builds</h3><p>To fix this issue for good, so our CI builds were not facing increased build times, we needed to ensure we were pinning the same NDK version for each of our modules.</p><p>To accomplish this, we created a <a href="https://docs.gradle.org/current/userguide/custom_plugins.html#sec:precompiled_plugins">precompiled script plugin</a> named android-library.gradle file to consolidate any configuration that should be commonly applied to our Android library modules.</p><p>This included a single pining of the NDK which was then reused across modules to ensure consistency.</p><pre>android {<br>    ndkVersion &quot;&lt;our desired NDK version&gt;&quot;<br>}</pre><p>We then applied this android-library.gradle file across our build.gradle files.</p><h3><strong>Validating The Fix</strong></h3><p>Once we consolidated our buildscript logic, we pushed a branch with our changes and generated several Gradle buildscans for the new branch.</p><p>We examined the buildscans in Gradle Enterprise and verified that configuration time once again looked normal and output logs no longer included NDK installation</p><h3>Summary Analysis</h3><p>The Android Gradle Plugin (AGP) has a <a href="https://developer.android.com/studio/releases/gradle-plugin#4-2-0">default version</a> of the Native Development Kit (NDK). This is the version of the NDK that will be used if your project requires the NDK, but doesn’t specific a specific version of the NDK.</p><p>If you don’t specifically pin an NDK version for each module in your app, AGP will automatically download the default NDK if it doesn’t exist on your local machine.</p><p>The virtual environments provided by GitHub Actions have pre-installed versions of the NDK available. And for a while, the pre-installed versions matched AGP’s default NDK version for our builds.</p><p>However, after updating our Android Gradle Plugin version, the default NDK version was no longer present on the GitHub Actions virtual machine and had to be downloaded by AGP on each run.</p><p>Because our CI builds were not caching any/all installed NDK versions due to the large download size, our CI builds were having to download this new NDK on each build.</p><p>This download/installation time occurs during the configuration phase of the Gradle build for an Android project which is what was leading to the 5x increase in configuration time for our CI builds.</p><p>To avoid this, ensure that you’re using a consistent NDK version across all modules in your app so you don’t start downloading new versions when you update AGP in the future.</p><h3><strong>How Were We Able To Improve For The Future?</strong></h3><p>At this point, there was some minor celebration and we took pleasure in our newfound understanding of how Android Gradle Plugin works.</p><p>But very quickly, we started asking ourselves how we could identify and fix this issue more efficiently in the future? In trying to answer that question, we identified a number of ideas:</p><h4>Ensure NDK pinned properly across modules.</h4><p>This is the approach we implemented and helps us ensure that we’re only dealing with a single NDK version.</p><h4>Cleanup installed NDK versions</h4><p>By keeping multiple NDK versions around on local dev machines, we run the risk of not finding these types of issues during local development in the future. We took it as an opportunity to cleanup unused versions.</p><h4>Pay closer attention to the default NDK version when updating AGP</h4><p>Now that we know that Android Gradle Plugin will download and install missing versions of the NDK, we pay closer attention to the versions of the NDK that are installed on our GitHub Actions virtual environments.</p><p>If either the environment changes, or AGP’s default NDK version changes, we run the risk of facing the same configuration time regression.</p><h4>Benchmarking before merging</h4><p>This experience led to a greater understanding of the gradle-profiler tool. We now have this as a tool at our disposal when we think a change might impact build times.</p><p>Before merging in major changes to our build, we can benchmark build performance and better understand if our build will be significantly impacted in any way.</p><h4>Automated configuration checks</h4><p>Because the impact to configuration time is so dramatic when a new version of the NDK is downloaded, it’s pretty easy to spot the change if you are looking for it.</p><p>Wit this in mind, we created a workflow in GitHub Actions to help monitor and identify this issue in the future.</p><p>On a regular schedule, we run a job that simply runs Gradle’s help task.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/921/1*mKaL-a9YqQK7WNyGgkJCkw.png" /><figcaption>GitHub Action workflow runs to test the configuration of our project</figcaption></figure><p>This does several useful things for us</p><ul><li>Generates regular buildscans for the help task — making trends easier to analyze in Gradle Enterprise</li><li>Generates CI builds that have very consistent runtime characteristics. Because this job always completes in ~1 min, it’s very obvious that something is wrong with the job takes ~3–4 min.</li></ul><p>We’ve found that simply reviewing the job duration in GitHub has been enough to identify this regression — without even needing to review data in Gradle Enterprise.</p><h4>Suggesting improvements to Gradle Enterprise</h4><p>Gradle Enterprise already breaks build trends into several categories like “serial execution” and “cache avoidance”. We recognized that if Gradle Enterprise surfaced “configuration time” in this same way, it may make detecting this kinds of issues even easier.</p><p>We suggested the idea to Gradle engineers as a feature request for Gradle Enterprise.</p><h3>Takeaways for your own projects</h3><p>Hopefully you’ve found this to be an interesting and practical exploration of how to debug an Android Gradle build regression.</p><p>I just want to highlight a few key things for you that may be most helpful for your own builds:</p><ul><li><a href="https://gradle.com/">Gradle Enterprise</a> is a terrific tool for monitoring, debugging, and optimizing Gradle builds. It collects buildscan data from across your team or organization and helps identifies issues and trends that may otherwise go unnoticed.</li><li><a href="https://scans.gradle.com/">Gradle buildscans</a> are an invaluable tool for learning about a specific build, and for diagnosing issues in that build. We rely on them heavily for maintaining/improving our builds. If you don’t use Gradle Enterprise, you can still capture a buildscan by adding --scan to the end of your Gradle wrapper invocation.</li><li><a href="https://github.com/gradle/gradle-profiler">gradle-profiler</a> can be a very helpful tool for comparing the build impact of any project changes. You can test simple scenarios like our help task. Or, you can simulate complex scenarios like incremental builds across a multi-module project.</li><li>Be aware that Android Gradle Plugin will download missing NDK dependencies in the background if the needed version is not present on the building machine. The NDK is multiple Gb in size, so downloading this on every CI build is likely a cost you’d rather not pay each time if you can avoid it.</li><li>It’s a very good idea to use the same <a href="https://developer.android.com/studio/releases/gradle-plugin#4-2-0">NDK version</a> across all project modules — and to be explicit in setting that version across modules. Consolidating common build logic into a <a href="https://docs.gradle.org/current/userguide/custom_plugins.html#sec:precompiled_plugins">precompiled script plugin</a> is one very viable approach for ensuring Android Gradle Plugin is configured properly across all your modules.</li></ul><p><strong><em>Premise is constantly looking to hire top-tier engineering talent to help us improve our front-end, mobile offerings and cloud infrastructure. Come find out why we’re consistently named among the top places to work in Silicon Valley by visiting our </em></strong><a href="https://bit.ly/3wnTrLn"><strong><em>Careers</em></strong></a><strong><em> page.</em></strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=79b5a2b31a10" width="1" height="1" alt=""><hr><p><a href="https://engineering.premise.com/using-gradle-enterprise-to-detect-configuration-regressions-in-an-android-gradle-build-79b5a2b31a10">Using Gradle Enterprise to Detect Configuration Regressions In An Android Gradle Build</a> was originally published in <a href="https://engineering.premise.com/">Engineering at Premise</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>