Hey folks! We have a >100 member project with m...
# multiplatform
m
Hey folks! We have a >100 member project with multiple teams, and using Kotlin multiplatform to share code with iOS+Android. Currently we have 3 Git repos for that - but we have trouble when someone merges changes. Let’s say shared lib+Android+iOS is at version 99. Bob merges his breaking changes first to shared repo, but forgets to update Android+iOS to version 100. Now Alice comes and also merges some changes, now creating a shared lib version 101. ⚠️ now Alice has to update version 99 to 101, and now needs to resolve the breaking changes from 99 to 100 that Bob should have done. So far, we have developers talking to each other, but this always creates issues. How did you solve this in an automated fashion?
👀 2
plus1 2
e
the obvious solution would be "don't use 3 git repos"
as long as you're using any system with non-atomic commits, there is the potential for gaps like this
f
I just want to say
We have a >100 member project with multiple teams, and using Kotlin multiplatform
Nice! 🎉
m
Yes, migrating to a monorepo would be one possible solution. Then all tests + builds an all platforms need to be green in order to merge a PR. This is the obvious solution, but of course is not so easy to migrate to when we have to maintain a running project with >100 members.
a
Bob merges his breaking changes first to shared repo, but forgets to update Android+iOS to version 100.
can this check be automated?
m
Well: currently, Bob would need to create 2 additional PRs (for Android + iOS) by pulling in the new version and fixing the breaking changes on the platforms.
f
I don't really have the experience to answer you but I do 2 things that might help with this. • we run more versions of shared module in parallel (more epics) with different suffixes (e.g.
0.1.4-notifications
). In smaller groups (each working with one "branch" of module) this this will not be as big of a problem • writing change log for breaking changes - this might help in long run to educate everyone on what is a breaking change because not everyone might know This is really a naive look on the problem because I have no idea how such a big team works as I've never been in one. Sorry if this is offtopic 🙇
m
I guess it would come down to: “Here are 3 PRs, and they all need to be merged a the same time (atomic) or none of them”
e
that is something that gerrit/repo kinda handle but most other systems don't
m
Depending on your CI you could implement a auto approve/raising PR
m
To make things even more complicated: It’s not even possible to do right now. In order to merge the 2 platform PRs, they need to reference the new version - which only gets generated/built once the shared PR is merged and built.
So a not-so-intelligent algorithm could look like this: • stop world (prevent any PRs from being merged) • build shared PR + merge to master + build libs (new version) ◦ if failed: revert PR, resume world • update 2 Android/iOS PRs with new version, build - merge both ◦ if failed: revert, also revert shared PR, and resume world • if all succeeded: resume world
this is of course, not ideal (because we would stop the world for a long time), and really not trivial to do either. That’s why I’m asking for your advice here 🙂
@Filip Wiesner I like the approach using different suffixes. That could help in separating the work of different teams. In our case, we already build+deploy the shared libs for every PR, so you could say we already have a solution in place to separate team changes when it comes to multiplatform code - just on a PR level. However, we would still have the merge-fight when those devs try to add their changes to the main codebase, which can happens multiple times a day (ideally even more often) 😄
m
Can you not build a snapshot for the origin PR and test against that at the other repos? This would be even agnostic against order. The non trival thing is the order of merge I guess then
m
@Matthias Geisler we already do that. It helps with development, but doesn’t solve the issue of ‘one developer forgets to update platforms’ or ‘one developer will win the merge of the version line in Android/iOS’ part.
a
so presently you have 3 repos, iOS, Android, and shared-lib (which is KMM). Presumably iOS and Android both depend on the shared lib? What about dependency inversion/hexagonal architecture/ports and adapters? So split iOS/Android into • shared-lib-port (interfaces that describe required functionalities) • core (business logic, depends on shared-lib-port) • runtime (depends on core, port, and shared-lib) then shared-lib depends on android-shared-lib-port, and implements the required interfaces. So any breaking changes have to be fixed in the iOS/Android repo.
d
You might want to try the
includeBuild
option. It’s new with multiplaform and might not work in your case. But I’m using it in my company: merging a code on the project “lib” will force to rebuild all projects using “lib” with the latest version of the code (ie: the merged code). I did that to detect breaking change.
m
What is missing is the auto bump and running the test suite for the consumer, right? But this should be manageable depending on your CI. The shared repos should know its version, and a PR should be able to propagate it. The other 2 repos act than as gate for the origin PR.
e
using includeBuild instead of binary artifacts doesn't really change a whole ton aside from when the build happens
updating the shared git reference to be pulled in from the android repo becomes the equivalent up updating the version it's using
d
It depends. In my case, we’re doing a tag, waiting the jar to be published, changing the version in another projects (let’s say the android repo). With the includeBuild, the CI clone the
lib
project and build other projects (ie: android repo) without any modification on it, each time the
lib
project is updated. Because it’s an includeBuild, the dependency
lib
will be replace by the project
lib
. (so, no need to tag, updating version, …) So for me, it’s not equivalent. The later is continuous.
e
sure, it's easier than building and using an unpublished artifact. but you could have done that too
h
Is your problem not that you have 1 shared repo, which probably is huge because 100 developers are working on it, so breaking changes are happening multiple times a day. However I would assume that all the different daily breaking changes probably dont have much to do with eachother, so I would try to figure out if you can separate all those different subjects / usecases into different libraries, so that the breaking changes for one library will not bother the breaking changes of the other library. If bob merges a breaking change in lib A, updating its version from 99 to 100, but forgets to update the consumers of this library to also use version 100, thats fine. Then alice comes along and merges a breaking change in lib B, updating its version from 99 to 100, and she does not forget to update the consumers of this library to use version 100 of lib B, she can just do that without having to fix the breaking changes in lib A, since that would be an entire different PR for the consumers.
r
Github has a new feature in beta called mergequeues that might help somewhat, though I don't know how well it can handle checks across multiple repos https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue
We've been thinking about these sorts of problems a bunch at Touchlab, but it's tough to generalize because every team is different. If you have things versioned in the way you do, then at least the update doesn't have to be atomic. The apps can go on using version 99 until Bob and Alice can coordinate around making changes. But if other people are depending on the update then they get blocked.
You might want to think about having a process to limit breaking changes. Stability gets more important when you have a team that big
m
Wow, I am totally blown away by how many answers I got! ❤️ 👏 Thanks to everyone for sharing their thoughts - I’ll try to process these now.
@Hylke Bron I would love for this to work! This is a pretty ideal picture, and how it should be done™. Actually, we have multiple Gradle KMM modules in our shared repo, and for Android we publish the individual JARs which we then can import. For iOS, we need to publish a single XCFramework/Swift Package though, because loading dynamic libs on iOS can decrease app startup performance (and other reasons). In practice, it becomes a bit more complicated: some of the KMM modules can depend on each other, so they should be used in ‘a version mix that can be used together’. You are right that the breaking changes of the individual teams shouldn’t really interfere much - and they don’t. The disruption starts when pulling in the version mix / single XCFramework for iOS, and when adding the extra Android/iOS changes that are needed to account for those breaking changes. So I really like the theory, and I would have chosen to do this - but it wouldn’t solve our issues.
h
@Marc Reichelt Yeah i was afraid that would be the issues you ran into. However, im just speculating, what if your iOS codebase is actually a separate KMM codebase, that can use gradle to depend on all the shared code you require? So your iOS codebase does not depend on a multitude of xcframeworks, but on a multitude of maven dependencies, which, at compile time, it will put together into a single XCFramework as part of the KMM workflow?
m
This is an interesting idea. I remember thinking about it once, and dropped acting on it. Maybe because I feared that this will only offload the main problem - ‘developer B needs to fix things that developer A did’ - to a later point in time.
h
But is it? If executed properly, Developer A publishes a breaking change to Library A, but does not update the consumers of said library to use the latest version. Developer B only needs to fix what developer A did if Developer B requires the latest version of Library A (so actually needing the breaking changes of Developer A), or if Developer B also needs to make changes to Library A. (which would be OK since then developer B will also be at home at the domain space that Library A covers)