The Benefits of Continuous Delivery of Ionic Applications
This article is based on the speech that took place during Ionic Poland Meetup in Warsaw, organized by ITCraftship. Below we present the written version of Maks Majer’s presentation.
I’ve been building apps for over 14 years, mostly worked as a remote software engineer and this is why I like to promote this work style very much with our business and with what I’m doing. Skip to 5 years back, we (Maks and Maciej) actually were hired by a Norwegian startup to build a hybrid mobile application. When we started off as a 3-people team, we were supposed to build the application in the Xamarin framework.
We had a lot of experience with .NET and web development with .NET, but also desktop. Maciej had Xamarin experience – as the only one of us, but after working with it for a few weeks, we realized that we’re stumbling upon many hurdles and we decided that we need to experiment with something that’s going to get us results faster.
Then we decided to try out Cordova and Ionic framework. Ionic Framework is in beta now. We realized that it’s really great; the learning curve was not very steep for us. We’re web developers, so developing anything in Cordova was super easy. We were able to solve the problems we couldn’t solve with Xamarin – issues that took us weeks with the Xamarin framework.
Basically, for the last 5 years, I have really enjoyed working with the newer and newer versions of Ionic, so I have experience across all the different versions from 1 ’till 4. With version 4, I worked also with Vue.js and the source code for the presentation that I’m going to share with you today is actually with the React version.
What is the solution to the common bottlenecks?
Do you enjoy testing the features that other people on your team have completed? I guess not. I’ve seen that across many teams, when leading some Agile projects, and in many cases. Also, on the project that we were working all together with my team a few years ago – the QA column was constantly hitting limits. We were seeing issues and it was slowing us down. I like continuous improvement. I hate it when I hit the limits.
When I saw red on my board, I thought that we got to do something about that, and the solution was pretty straightforward – just hire a dedicated QA person. They will just test everything that we’ve built. Doesn’t it sound awesome? As a matter of fact, it did work really well because after we outsourced a part-time working person from a different company we have increased our velocity.
We increased our development velocity over two times after two sprints so there was the return on investment. If we wanted to develop more, the company would have to hire more developers, but that is very costly and wouldn’t necessarily produce any better results. Hiring a half–time QA person was much better ROI and the cost for the company was smaller, but then there’s a whole other set of challenges.
When you finally have a QA person, then you need to decide at what stages of your product development life cycle do you do the tests? What kind of tests are you doing? You could get away with one or two environments for like, the production, development but you need to have an environment for your QA employee (or QA employees) to be able to test the application and not be affected by whatever you screw up on the development side. You don’t want to push something into the production before you’ve actually tested it.
Another thing is basically how do you deliver the application to your QA people? When do you deliver that to the QA people so that they’re able to test? And lastly, how do you make sure that they’re testing the right version, the right build of your application? Because I’ve seen it many times that issues were being pulled back to the development just because the person downloaded the wrong build from App Center or HockeyApp or whatever we were using.
How to create a desired build flow?
Before I go into the technical part I want to share what is the desired continuous delivery flow that I wanted to set up and that I’ve set up in this and other projects as well.
First of all, we start with development. A developer is working on a feature – usually, you’re going with some kind of branching strategy, either GitFlow or Trunk–Based development. On your developer master branch, you have a version that is either deployed to staging or production and then when you work on a feature, you create a feature branch.
You do some commits. I personally like to commit frequently and push to the remote frequently so that I can get feedback from my CI server and see if anything didn’t pass if any automated tests failed. After you’re done with the feature, you want to obviously create a pull request or a merge request, something that you can make sure it’s completed. You identify the point at which the feature has been completed, and you want other people to take a look at that.
One low–hanging fruit that you can handle here as far as quality assurance process is obvious with using Typescript. You have linting, you have compilation which is something that you can do on the CI or do it as Git Hooks, for example, or pre-commit or pre-push. It depends on how much time that takes because you also don’t want to ensure these bottlenecks and mess up with your development experience if things like pushing to the repository take too long, but this is like the first step.
When I finish development I will create a Pull–Request or Merge–Request. When I was working on the feature I usually run the tests with a filter – to only run the relevant tests. I like creating a build with only that feature included. Included means added to the previous version because I don’t like to have my development or my master branch unstable. I want to have features merged only if they meet the acceptance criteria that were defined with the task, with the feature that we are working on.
That’s why upon making a PR I want to run all test automation and release an isolated version of the app that can be tested by a QA person. In our case we wanted to distribute this through Microsoft App Center for QA builds to have a convenient way of differentiating versions and testing the right release.
Here I also want to mention different branching strategies – you could work with git-flow or trunk-based development. Both of these have their benefits and disadvantages, but in both cases, I prefer to build a branch in a state that would happen after merging to the main branch (develop or master).
In order to do that with trunk-based development, you are usually rebasing your feature, you’re rebasing on top of master and when you are testing the branch, you’re testing a version that master will be after you’ve merged the PR.
With a git-flow approach, you can create a merged commit, then run the tests. Of course, so this is also possible, and what I want to do then is actually create a binary build so that the QA person can make sure that all the criteria, acceptance criteria, are being met, test the application, go through it, check the feature, and then approve it in their repository.
The CI server would run the automated builds and any linting compilation or whatever is needed, and then I would like to have another peer developer on the team to look at the code, just to have a quality check.
After approving the feature we merge the PR. Each commit to “develop/master” kicks off building a new version of the app. This is when I want to kick off another build because I want to have at least one build with the newest version of the development or the master branch so that the QA person can always have easy access to the newest version and they can continue with the integration tests. They can go through the critical paths of the application.
Make sure that they work prior to the release if you are planning that. Also, this is a good moment to run any of your UI tests, so any end–to–end tests that you want to execute. I do not recommend running them on every commit or even on every merge to the develop branch. It is something worth doing on a nightly basis – it usually takes a lot of time and can break. You sometimes have to just maintain them as well, and I don’t recommend having a large coverage of that. Focus only on the really important Passover application.
One additional suggestion is to keep a maintained version of UI & integration tests that include only critical paths of your application. You don’t want to have too many of those but having none can also cause you trouble in the long run. For example, if you run a mobile e-commerce app, then you would most likely test flows related to adding to cart, completing the order and payments. That will make your development cycle even more bulletproof.
You could also have some nice trigger. For example, if you’re making a tag on your master branch, you’re pushing that to remote, the CI server could pick up that the new tag has been pushed and it would actually run the release that would push it to the App Store or Google Play, and then you could manually submit that for review or publish the app.
What options do you have to deliver the application to your QA specialist and later to the App Store?
When you have a small team – i.e. one or two people – it’s ok to publish the builds manually. That’s how many projects started. You wouldn’t necessarily set up the whole CI/CD pipeline from the beginning. You want to go fast, develop fast, and this is not a time when you’re going to do pushing very frequently.
You can use the Ionic CLI to build the web version and then use one of the IDEs – XCode or Android Studio – to compile the app and distribute your binary manually by uploading to App Store Connect or Google Play. If you want to do a production release you’ll follow the same steps, but add code signing.
If you’re using Capacitor to actually build the binaries, sign them, upload them manually to the App Store, that’s all cool. This works well until your project and your team grows. Suddenly builds and releases need to happen more often and it starts to steal significant amounts of time from your development efforts.
There are also more and more features. You want to be able to test them and you are hitting the bottleneck that I described before. You’re switching context a lot, the developers are building instead of producing code. It’s becoming a mess. When that happens, you have several options to automate your build flow.
Ionic App Flow
The most obvious choice is using Ionic App flow. This is an obvious choice because it’s been created by the Ionic team, so it integrates really nicely with the whole ecosystem, but it’s also quite limited. If you want to do any complex integrations or any complex workflows – ex. manage code–signing for multiple different apps, run your UI tests and execute them in Appium, add more customization like integrations with Slack, update a Jira ticket with the link to the latest build, etc. – then this is not something that you can easily handle with Ionic App flow.
Custom Build Server
An important fact to keep in mind is that people will make errors. I have already mentioned that I’m very passionate about processes and continuous improvement. I really don’t like to give people the flexibility to make errors. I try to remove the human errors as much as possible like don’t testing on the right version. You can easily get rid of that problem by adding a link to the proper build version for your issue in Jira once the build was done and also pushing the comment to the Jira ticket.
If you do that, the QA person can just open up Jira on their mobile device, go to the ticket, scroll down to the comments, hit the last comment to the link there, pull up the version and test it on the device. No problems there, right? We are removing the human errors and everyone makes errors – software engineers, engineering managers, support staff and QA people on your team, so the more you can simplify that, the more productive they will be, and the process will be more streamlined.
This is why I would decide to go with a custom–build server. Currently, there are quite a few options that support the macOS platform which is critical for building iOS apps. Most of them also offer free plans for open source projects if you want to experiment before you commit to a particular one. However, a few years ago it was not possible.
We had to have a Mac mini in our office running Jenkins, maintain that, update that, make sure that the Xcode is updated all the time. I would not advise that today. Now you have CircleCI, Travis CI, Bitrise, they all handle macOS machines, and you have Azure DevOps which I want to explore more because it is a pretty nice alternative.
How to improve the compile and distribute parts of the process?
First of all, test your app and run test automation but not necessarily for each PR. Then compile the binary versions of the application – for the purpose of iOS we need to codesign them before we can release it. Later upload them either to a distribution center like App Center, previously HockeyApp, or directly to App Store Connect – so you can use it with TestFlight, or to Google Play – so you can use it with Alpha/Beta lines.
In this part, I will focus on the compile and distribute parts of the process.
Have you heard about Fastlane?
When I discovered Fastlane a few years ago there were no good alternatives to automating the builds. Ionic App flow didn’t exist and even Fastlane had no Ionic or Cordova support out of the box. Managing & unlocking the keychain on a build server was a nightmare. The whole setup had to be done through trial and error, so to make it all work I had to dig really deep into the build and signing process on both platforms.
Today doing this with Fastlane is trivial. Fastlane is a set of Ruby tools that help you streamline the whole process of building the application, managing the whole process of codesigning, but it’s much, much more than that.
Managed code signing
Code signing is one of the biggest reasons why I actually would use Fastlane with a plugin that’s called Match – it’s for the purpose of code signing and managing all the certificates and provisioning profiles within your team. Sharing that is a real nightmare. If everyone manages that with their Xcode, everyone creates all their own certificates for development, then if you’re creating certificates for distribution you have to share those with the team, basically with everyone who has to use them for publishing and all that.
Match removes all that for you. It actually stores all the certificates and provisioning profiles in an encrypted way in a git repository, so to get other people on your team to be able to use and produce an app with these certificates and provisioning profiles, you just need to give them access to the repository and the encryption-decryption key – it’s asymmetric key. You can take a look at codesigning.guide to read a little bit more about that and also about potential security vulnerabilities which there aren’t many, actually.
Lots of plugins
Fastlane, besides the whole code signing, has a lot of useful plugins that are out–of–the–box, so you can build Ionic apps. Underneath it, it uses the Ionic CLI. It has also Cordova plug–in – to easily release Cordova applications. You can even run the app and make screenshots to upload them to the App Store. You can upload D–box symbols to Sentry. You can use it to distribute to Microsoft App Center or directly to Google Play Store or App Store Connect, and if there aren’t plugins that support your specific need, you can write one. That’s because it has a pluggable architecture, so it’s pretty easy to manage it.
Finally, a shared build setting – the entire build configuration is organized with Fastfile and other accompanying configs – Gymfile, Matchfile, Appfile, etc. This is all stored in the repository (except the secrets) and provides a consistent building experience for the whole development team. If you’ve set up Fastlane nicely, you can easily move that to any CI server like Azure DevOps.
How much do you know about Azure DevOps features?
In our recent project we used DevOps in connection with the Atlassian suite – Bitbucket, JIRA, Confluence, but for the purpose of this presentation I’ve created a public Azure DevOps project. I wanted to explore the other features of these platforms and be able to say a few words about each of them and not just the build pipelines.
The first thing is what they call Boards and it’s basically an issue tracking feature. It’s simple, but it will do its job for most projects – especially at the early stages. There are both Kanban & SCRUM boards, 3 levels of work items: Epics, Issues, and Tasks that you can organize in a parent/child hierarchy. It also integrates nicely with other Azure features (repos) as well as external tools – ex. Github (you can link issues for example).
The next part of Azure DevOps is obviously repositories. Microsoft acquired GitHub, I think they have now some nice knowledge around that part in their company, and they are handling the repositories pretty well. Actually, I haven’t seen pushes going so fast into any repository previously, like to Azure DevOps. You can create as many repositories as you want, so under one DevOps project, you can have the back end, the front end, some NPM packages. If you are sharing code between front and back, you can actually publish them to Artifacts which I’ll show last.
Pipelines are the most interesting part for us concerning today’s topic. Azure DevOps allows you to set up CI/CD pipelines on Windows, Ubuntu Linux, and macOS machines. This gives plenty of flexibility for mobile use cases. It’s very similar to anything you may have used previously, like Travis or Circle, cloud CIs where you configure the whole build process with a YAML file – so you sort a configuration in code, which is really great for sharing that with the team working with the versioning.sync.
Actions already implemented
The good thing about that is that it has this nice UI to use some actions that are already implemented there. There’s a lot of them out of the box, so you don’t need to write custom scripts like Notes scripts or batch scripts to do the things because they already implemented that on their end. Of course, that ties you to the platform. I know for a fact that I’ve seen caring too much about porting between different platforms doesn’t necessarily pose a huge challenge because you rarely do that and planning and architecting for that is, in many cases, a waste of resources.
Finally, there are also Test Plans which is a great alternative to testing scenario tools like Gurock TestRail. It makes it easy for QA testers to organize their work and record the progress of testing and test run history. In regards to the process, I’m describing, this can be very helpful.
Lastly, there are artifacts where you can release your own Npm packages or similar artifacts (ex. Python, Maven or NuGet packages). I haven’t explored this but had to use other tools before for this – like Gemfury.
In the past we needed to combine a lot of different tools, each coming with its own subscription and pricing. Azure DevOps is quite complete and comes at competitive pricing and a generous free tier. Full–disclosure – I am in no way associated with Microsoft, but I really loved Azure DevOps and I think it can be a great alternative for other common software development suites from Atlassian, Jetbrains and others.
To not paint everything pink, there’s one annoying thing about Azure DevOps I want to highlight. For some reason, the login JWT token seems to expire really fast. When creating this presentation I had to re-authenticate almost a dozen times per day. The other thing is about work item types – I have no clue why the Task type doesn’t show up on any boards. I think it renders it quite useless – I’m still not sure what is the purpose of this type.
How a configured pipeline for an Ionic App looks on Azure DevOps?
Finally, we have a short code demo. I don’t want to go too much into the details because I encourage you to open the link that you see above – there is an open-source DevOps project. You can actually go there, clone the repository, create your own free Azure project and just push it.
You’ll be able to set it all up, and if you have problems, you can reach out to me because it’s really simple. Basically, as you can see there this is a project with all different features including boards where you can check out the task that I’ve been working on.
It’s a pretty simple workflow, that you can see, and it basically builds the whole application. Deploys it to Microsoft App Center, both for Android and iOS platforms, and as you can see – it’s 39 lines long so pretty simple, but there is additional configuration. Git goes for it with the Fastlane, so fastfiles, gemfiles, all the stuff that needs to happen there which you can look at in the repository.
As you can see we are using the App Center. We use it for QA purposes because using TestFlight or Google Play Store isn’t the perfect solution. It’s not an easy way to version different builds or name the different build versions and all that. Using App Center is much more convenient and I think that for 2 apps, it’s completely free. Azure actually comes with quite a nice subscription package and good free tier.
I think you can take a look at the code in the different areas to check how you can build it. It’s a pretty simple example, and in this example, we are also using Capacitor so it’s a little bit different than the pipelines we used to set up with Cordova. With Cordova, you have to use the Ionic – well, had to – you could use the Ionic plugin for Fastlane. Before it existed, we had to use manual builds, like dig in really deep into the whole process, and we were using React here – but here is a very basic React app. We just cloned one of the default templates for Ionic.
Step 0: Gather all the distributed knowledge (dependencies, external services, etc.) from the team
Step 1: Setup Fastlane and all external services:
- App Center
- Repo for certificates
Step 2: Azure DevOps pipeline in YAML
- Match certificates repo: SSH key for DevOps, best to have a dedicated account for the build server that shouldn’t be used for development. id_rsa_azure private key file for SSH. Get an entry for your repo (Github or bitbucket) from your knownHostsEntry file.
- Headless testing – add puppeteer to dependencies, karma setting for headless chrome:
- process.env.CHROME_BIN = require(“puppeteer”).executablePath();
- there’s a ticket in Azure support and the hack will no longer be relevant once it’s resolved
- karma JUnit test reporter – to display nice test format
- Conditional builds – ex. if merge commit then don’t build, other logic possible too
- Build parameters (env variables) – ex. build number, etc.
- Scripting – You can use node or bash scripts to prep your build – ex. set the build ID for the QA to be able to identify the build, run cURL commands to publish slack notifications, communicate with APIs (ex. Zapier webhook, JIRA update ticket, etc.)
- FASTLANE_PASSWORD & MATCH_PASSWORD – getting provisioning profiles & decrypting the signing repository
- TOKEN for App Center or whatever
I described some very common bottlenecks in the development process that focus on the last part like testing and making sure that everything fits. I also talked about the ideal continuous delivery process that I like to use so that we can assure and have a bulletproof quality assurance process for application. Finally, I also shared some of the toolsets that I used to do that.
If you have any questions about the IONIC framework you can send a message to Maks
We encourage you to see the video of other presentations that took place during the event: