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 . 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 , being apps for company employee use only). Suppliers, our team and some 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 ( , ) etc.), but in our context we needed an solution, because devices managed by MDM were subject to some restrictions which don’t play well with those kind of services. Gitlab distribute with OTA MDM key-users Fabric TestFlight internal 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 or distribution with xcode, you can ask it to produce a manifest file. The 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: Small digression on OTA distribution Enterprise AdHoc manifest itms-services://?action=download-manifest&amp;url=https://yourdomainname.com/app.plist note that: the manifest file be served under an connection MUST https the webserver must be able to serve these MIME type and .ipa application/octet-steam .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. First setup The first setup was made with + + . The workflow, slightly different for some apps with different level of automation, was something like this: gitlab Jenkins a static HTML site generator a supplier pushes to gitlab new changes a Job on jenkins is triggered which builds the app, signs and moves the produced on a shared location (accessible by other jobs) build ipa a job on jenkins is triggered manually or by the previous job which, with a mix of scripts and a static site generator (we used just because it uses json files as input for content), produce the final HTML with all Apps pages and correct manifest.plist to handly install them on the devices. publish punch ~AppStore 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 regarding configurations, scripts and file paths. “magic by conventions” Mid setup When gitlab started to integrate we changed this workflow removing Jenkins from the equation and leveraging other gitlab features like the and its . The software stack was down to , a simple and a made in python with . The new workflow was: CI/CD API Oauth provider gitlab gitlab runner webapp Flask push new code to gitlab configured gitlab CI is triggered and builds and stores the signed ipa as an on gitlab. runner artifact No publish step were required because everything else was handled by the flask webapp. The webapp was not so trivial: it uses the oauth features of gitlab to authenticate people that already have access to gitlab and obtain a to be used with gitlab API user token a call to the API with the obtained token retrieves the list of projects visible to the user (filering out the non-iOS ones, to be more specific) going to a specific project page, again through the gitlab API, we retrieve the artifacts of the latest succesful builds we read inside the ipa and generate on the fly a manifest file for the install link (we could have used the manifest generated by xcode and save it along the ipa as artifact) 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 and all the scripts, embedding the whole logic within the webapp. But we had to do several tricks (aka ) to workaround some gitlab API that made the flask app hard to maintain and quite ugly overall. “magic by conventions” bad things quirks Final Setup When gitlab shipped 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 you can add a 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 . Go through the for more details. Pages long story short job job documentation The new flow now goes like this: push new code to gitlab configured gitlab CI is triggered and the build and store the signed ipa and the manifest file as an on gitlab. runner artifact a produces the HTML static files to be published under the project page along with the previous job artifacts (ipa and manifest). pages job 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 that lists all the projects you want to expose, and then have single with the link to install the build (again if you don't know what I mean). namespace site project site RTFM A simple for this setup could be like this: .gitlab-ci.yml 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 instead of xcodebuild, to simplify building the ipa and producing the manifest file. gym 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 you want. static site generator 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 for proof reading :P Sl3 is how hackers start their afternoons. We’re a part of the family. We are now and happy to opportunities. Hacker Noon @AMI accepting submissions discuss advertising & sponsorship To learn more, , , or simply, read our about page like/message us on Facebook tweet/DM @HackerNoon. If you enjoyed this story, we recommend reading our and . Until next time, don’t take the realities of the world for granted! latest tech stories trending tech stories