Gitlab is evolving more and more to be a “one solution” for many tasks, it started as a github clone service to be installed on premise, now it’s focusing on CI/CD. What once could be a github/gitlab/??? + jenkins setup now can be accomplished just by gitlab itself. In my environment we have an Apple Enterprise Subscription and several suppliers developing iOS apps which we distribute with OTA. Certificates to sign the apps are not shared with suppliers so we have, for every app update, to sign the app ourselves before distributing to the final users (in our case through MDM, being apps for company employee use only). Suppliers, our team and some key-users of the company needed a quick way to install test versions of the apps properly signed. There are plenty of solutions to reach this goal of distributing beta to users (Fabric, TestFlight) etc.), but in our context we needed an internal solution, because devices managed by MDM were subject to some restrictions which don’t play well with those kind of services.
Small digression on OTA distribution OTA distribution is quite simple, in the past was a nightmare but now xcode handles quite well al the things. When you build the ipa for Enterprise or AdHoc distribution with xcode, you can ask it to produce a manifest file. The manifest file is a plist (an xml) which contains some information on the app, version, bundle id, an icon and the link where to retrieve the ipa file. To create an install link usable from iOS devices by users, you have to use a custom apple scheme pointing to that manifest plist:
itms-services://?action=download-manifest&url=https://yourdomainname.com/app.plist
note that:
https
connection.ipa application/octet-steam
and .plist text/xml
The goal was to streamline an automation process that enables someone to push code to the gitlab repository and then go, from an iOS device, to an HTML page and directly install the updated app.
The first setup was made with gitlab + Jenkins + a static HTML site generator. The workflow, slightly different for some apps with different level of automation, was something like this:
This approach was good in terms that it did what it had to, but was quite unmaintenable involving several pieces of software completely separated and running on different machines: gitlab to host the sources, jenkins on a OSX machine to build and sign, another machine with a webserver to serve the final website. Also it was involving a lot of “magic by conventions” regarding configurations, scripts and file paths.
When gitlab started to integrate CI/CD we changed this workflow removing Jenkins from the equation and leveraging other gitlab features like the API and its Oauth provider. The software stack was down to gitlab, a simple gitlab runner and a webapp made in python with Flask. The new workflow was:
No publish step were required because everything else was handled by the flask webapp.
The webapp was not so trivial:
user token
to be used with gitlab API
This solution was better since we removed a piece of software, but we still had 3 machines to run everything (one for gitlab, an osx for the runner, another one for the webapp). We removed the “magic by conventions” and all the scripts, embedding the whole logic within the webapp. But we had to do several tricks (aka bad things) to workaround some gitlab API quirks that made the flask app hard to maintain and quite ugly overall.
When gitlab shipped Pages on the Community Edition, we decided to refactor all the things once again. With this setup we could totally kill the annoying flask app, and remove a machine. I will not go inside how gitlab pages works but long story short you can add a job on a CI configuration to publish a folder as a static site, the hostname at which the site is served depends on the repository you configured the CI job. Go through the documentation for more details.
The new flow now goes like this:
The last step could be accomplished in several ways, you can use a full fledged static site generator or just a simple script to generate a single html page.
Based on what you want to accomplish you could have a namespace site
that lists all the projects you want to expose, and then have single project site
with the link to install the build (again RTFM if you don't know what I mean).
A simple .gitlab-ci.yml
for this setup could be like this:
stages: - build - publishbuild_production: stage: build script: - BASE_URL=https://namespace.yourdomain.com/projectname APPNAME=mightyapp fastlane gym artifacts: paths: - public/*.ipa - public/*.plist expire_in: 1 day // we don't need to save this since it is persisted by the pages job laterpages: stage: publish script: - python publish.py "https://namespace.yourdomain.com/projectname" artifacts: paths: - public
I suggest you to use Fastlane gym instead of xcodebuild, to simplify building the ipa and producing the manifest file.
Here is a minimal Gymfile
:
output_directory "./public"export_options( method: "ad-hoc", manifest: { appURL: "#{ENV["BASE_URL"]}/#{ENV["APPNAME"]}.ipa", displayImageURL: "https://placehold.it/600x600", fullSizeImageURL: "https://placehold.it/600x600" })output_name ENV["APPNAME"]
Finally this could be a super quick & dirty but working script for generating an html page:
# -*- coding: utf-8 -*-"""<!doctype html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <title>Install {title}</title></head><body> <div class="main" style="margin:50px; text-align:center;"> <h1>{title}</h1> <p>{bundleid}</p> <p>{version}</p> <a href="itms-services://?action=download-manifest&url={baseurl}/manifest.plist">INSTALL</a> </div></body></html>"""import sysfrom plistlib import readPlistfrom hashlib import md5from os import walk, environfrom datetime import datetimedef main(): baseurl = sys.argv[1] plistfile = "public/manifest.plist" index_html_path = "public/index.html" plist = readPlist(plistfile) metadata = plist['items'][0]['metadata'] info = dict() info["title"] = metadata['title'] info["version"] = metadata['bundle-version'] info["bundleid"] = metadata['bundle-identifier'] info["baseurl"] = baseurl with open(index_html_path, "w+") as f: f.write(__doc__.format(**info))if __name__ == '__main__': main()
The script is really raw and concise, you can go creative by using a template engine or whatever static site generator you want.
Hope to have give you some new idea or at least push you to give gitlab a try ’cause it’s a damned good piece of software!
Full Disclosure: no they don’t pay me unfortunately!
Thanks to Sl3 for proof reading :P
Hacker Noon is how hackers start their afternoons. We’re a part of the @AMIfamily. We are now accepting submissions and happy to discuss advertising & sponsorship opportunities.
To learn more, read our about page, like/message us on Facebook, or simply, tweet/DM @HackerNoon.
If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories. Until next time, don’t take the realities of the world for granted!