Writing fastlane scripts in Javascript

3 Posts back to back!? Yes, lot’s of content out there right now.

tldr;

import { Fastlane } from "fastlanejs";
const flane = new Fastlane();

The longer version

I made a recent post about getting an older react native codebase back up on fairly new hardware and the next step was to add fastlane to make sure getting builds for debugging would be easier.

Those who don’t know, fastlane is a very extensible tool written to automate most of the work that you’d do for creating builds and at the end of the day it’s just ruby you can extend it with numerous gem’s available online.

Why write it in Javascript then?

Well, you see there’s a tiny set of issues that need to be addressed to understand the usage of js here.

  1. You cannot split lanes into different files
  2. All helper functions have to be defined in the same Fastfile

Now, this isn’t a problem everyone would face since not everyone has react native apps that use the variant approach for creating dev and prod builds separately. I do and so my Fastfile has quite a bit of code.

These are the functions that my Fastfile has, which are then called by a lane definition

# ios
deploy_ios 
sign_ios 
build_iod
dev
prod

# android 
deploy_android 
sign_android 
build_android
dev
prod

the structure is actually very simple, deploy_ functions call the sign+build functions, then call the dev | prod function based on the params passed.

fastlane ios dev # would create a dev build 
fastlane ios prod # would create an appstore build

Now this is necessary since I deal with apps that aren’t just going to be uploaded to testflight with the prod api, we haves staging servers and the QA needs to test them so dev builds are unstable/untested and can’t be on testflight, someone’s bound to create an accidental release out of it

And when I said multi-variant, there’s 2 bundle identifiers created by com.example.app and com.example.app.dev and the above fastlane lane’s take that into consideration when building. basically each function reads the input parameter.

So, there’s a few more helper functions to find out if the given parameter is for dev or prod

let’s say the app’s ios scheme is named productOne ,then the dev scheme is named productOneDev

Each of those would create a different build, one would trigger a development certification signing and upload itself to diawi and notify slack with the link to the installable app.

The other would upload to testflight and notify slack once the app is out of the processing phase

Similar flow for android and , this means there’s helpers that help with finding out if the given param was for dev or not

def is_ios_dev(scheme)
    if scheme.end_with("Dev")
        true
    else 
        false
    end
end

def is_android_dev(bundle)
    if bundle.end_with(".dev")
        true
    else 
        false
    end
end

Which can be combined into a single function but it’s easier to modify this than having 2 if conditions nested.

Also, yes I know implicit returns are to be avoided in ruby but that’s a pretty simple function!!

Now, all this is easy and ruby is pretty good language to learn and use but fastlane doesn’t really allow importing ruby files so if I need to modularize any of this, I either write custom actions that import my ruby code which adds a lot of glue code but is definitely something I’ll be doing to stay close to the source of the tool.

Till then, JAVASCRIPT

I like the enthusiasm JS community has to move everything into JS.

Ray Deck decided to create a auto generated fastlane API layer for javascript. The concept is pretty simple but even better executed.

  1. Run a fastfile with all the actions to generate all the possible inputs and outputs into a json file
  2. Setup a tool that can talk to the fastlane runner using the socket server that fastlane has
  3. Use the json from the 1st tool to create a typescript api of the same
  4. Wrap this all up in a library and done!

Obviously took a lot of work to get it all working so KUDOS!

Now, I found this library while trying to see if someone had already done it, because I had a more verbose approach that I was going to take which was writing a child_process based wrapper for all the fastlane commands that were documented which would actually be a lot more work than writing something like this. I’m not very smart…

Now, how does this solve my problem?

We get to write smaller functions that are just that, functions and importable. Each function is just like an API call to socket server that’ll pass in the parameters as a serialized payload and get the result back and it’s all promises so you can add in more async code.

Let’s get to how to use the library.

Installation

  1. You still need fastlane so go through their docs to set it up
  2. Creating Appfile and Matchfile will reduce the amount of code you write in lanes so do keep them intact or write them up first.
npm i fastlanejs
# or 
yarn add fastlanejs

Basic Usage

import { Fastlane } from "fastlanejs";

const fastlane = new Fastlane();
(async () => {
  await fastlane.getVersion();
  await fastlane.close();
})();

Real life usage

The API is fully typed so your IDE will help you out a lot with what’s valid and what’s not.

Here’s what the dev version build I mentioned about above would look like

#!/usr/bin/env node

import { Fastlane } from "fastlanejs";
import process from "node:process";
import dotenv from "dotenv";
import { upload } from "diawi-nodejs-uploader";

dotenv.config("../.env");

// dynamic variables to control behaviour over the file
const isDev = true;
const flane = new Fastlane();
const buildtype = "development";
const scheme = "productDev";
const workspace = "ios/product.xcworkspace";
const project = "ios/product.xcodeproj";
const certType = "development";

await run();

async function run() {
  await flane.updateCodeSigningSettings({
    useAutomaticSigning: false,
    path: project,
  });

  await setup();
  await sign();
  await build();
  const lcRes = await flane.laneContext();
  const lc = JSON.parse(lcRes);

  const uploadResponse = await uploadToDiawi(lc.IPA_OUTPUT_PATH);

  if (!uploadResponse.link) {
    return;
  }

  await notifySlack(uploadResponse.link);
  await flane.close();
  return;
}

async function setup() {
  await flane.createKeychain({
    name: process.env.KEYCHAIN_NAME,
    password: process.env.MATCH_PASSWORD,
    unlock: true,
  });

  await flane.match({
    gitUrl: process.env.MATCH_CERTIFICATES_URL,
    teamId: process.env.APPLE_TEAM_ID,
    keychainName: process.env.KEYCHAIN_NAME,
    keychainPassword: process.env.MATCH_PASSWORD,
    readonly: flane.isCi,
    forceForNewDevices: true,
    type: certType,
  });
}

async function notifySlack(link) {
  const gitBranch = await flane.gitBranch();
  await flane.slack({
    message: "Automation Engine: iOS \n" + link,
    success: true,
    payload: { Git: gitBranch },
    useWebhookConfiguredUsernameAndIcon: true,
    slackUrl: process.env.SLACK_HOOK,
  });
}

async function uploadToDiawi(filePath) {
  console.log("Uploading to diawi, please wait...");
  const result = await upload({
    file: filePath,
    token: process.env.DIAWI_TOKEN,
    wall_of_apps: "false",
  });

  return result;
}

async function sign() {
  await flane.registerDevices({
    devicesFile: "./fastlane/devices.txt",
    teamId: process.env.APPLE_TEAM_ID,
  });
  await flane.match({
    gitUrl: process.env.MATCH_CERTIFICATES_URL,
    teamId: process.env.APPLE_TEAM_ID,
    keychainName: process.env.KEYCHAIN_NAME,
    keychainPassword: process.env.MATCH_PASSWORD,
    readonly: flane.isCi,
    forceForNewDevices: true,
    type: certType,
  });
}

async function build() {
  await flane.incrementBuildNumber({
    buildNumber: process.env.BUILD_NUMBER,
    xcodeproj: project,
  });

  await flane.gym({
    configuration: "Debug",
    workspace: workspace,
    scheme: scheme,
    clean: true,
    outputName: scheme,
    silent: true,
    destination: "generic/platform=iOS",
    outputDirectory: "builds",
    exportMethod: buildtype,
  });
}

The above handles the following

  1. Creating keychains
  2. Signing the app
  3. Building the app
  4. Uploading it to a distribution system
  5. Notifying slack

and if you closely observe it’s only the fastlane’s own actions that are available, plugins like the fastlane_diawi has been replaced with a node package instead

I can add parameters to each of these functions and export them from a utils.js file and reuse them to write the prod script with the only things that change to be the parameters on the top since everything else is being read from environment variables.

How do I find the parameters I can pass ?

As mentioned, this is all just generated code from the original fastlane documentation, you can use them and just camelCase the params and you are done.

Overall this is a nice thing for quick fixes and scripts that I would wish to experiment with and while I’ve mentioned that I’d like to stay close to the source, I’ll probably write custom actions that’ll help me with making it easier to move a fastlane configuration from one project to another without having to handle the minor details like bundle identifiers and everything which can be programmatically extracted (which fastlane already does but no clear API for it yet)

Till then, this seems like a viable option, since I’ve got generative javascript code everywhere, writing something similar will be a lot easier.

That’s all for now, Adios!